From bceb85e8d469f14b211d9c8c8a9ff8388d696d2f Mon Sep 17 00:00:00 2001 From: webchick <webchick@24967.no-reply.drupal.org> Date: Fri, 26 Apr 2013 08:52:01 -0700 Subject: [PATCH] Issue #1943776 by Wim Leers, nod_: In-place editors (Create.js PropertyEditor widgets) should be loaded lazily. --- core/modules/edit/edit.module | 5 +- core/modules/edit/js/edit.js | 45 ++-- .../edit/lib/Drupal/edit/Ajax/BaseCommand.php | 2 +- .../lib/Drupal/edit/Ajax/FieldFormCommand.php | 2 +- .../edit/Ajax/FieldFormSavedCommand.php | 2 +- .../Ajax/FieldFormValidationErrorsCommand.php | 2 +- .../lib/Drupal/edit/Ajax/MetadataCommand.php | 27 +++ .../edit/lib/Drupal/edit/EditController.php | 22 +- .../edit/lib/Drupal/edit/EditorSelector.php | 17 +- .../Drupal/edit/EditorSelectorInterface.php | 5 +- .../lib/Drupal/edit/Tests/EditLoadingTest.php | 198 ++++++++++++++++++ core/modules/editor/js/editor.createjs.js | 2 +- 12 files changed, 290 insertions(+), 39 deletions(-) create mode 100644 core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php create mode 100644 core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 0125d45f709c..1b94b913bf84 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -53,9 +53,7 @@ function edit_contextual_links_view_alter(&$element, $items) { return; } - // Include the attachments and settings for all available editors. - $attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments(); - $element['#attached'] = NestedArray::mergeDeep($element['#attached'], $attachments); + $element['#attached']['library'][] = array('edit', 'edit'); } /** @@ -65,7 +63,6 @@ function edit_library_info() { $path = drupal_get_path('module', 'edit'); $options = array( 'scope' => 'footer', - 'attributes' => array('defer' => TRUE), ); $libraries['edit'] = array( 'title' => 'Edit: in-place editing', diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index f924e7b9b864..bf181ede15ea 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -57,24 +57,37 @@ Drupal.behaviors.edit = { if (remainingFieldsToAnnotate.length) { $(window).ready(function() { - $.ajax({ + var id = 'edit-load-metadata'; + // Create a temporary element to be able to use Drupal.ajax. + var $el = jQuery('<div id="' + id + '" class="element-hidden"></div>').appendTo('body'); + // Create a Drupal.ajax instance to load the form. + Drupal.ajax[id] = new Drupal.ajax(id, $el, { url: drupalSettings.edit.metadataURL, - type: 'POST', - data: { 'fields[]' : _.pluck(remainingFieldsToAnnotate, 'editID') }, - dataType: 'json', - success: function(results) { - // Update the metadata cache. - _.each(results, function(metadata, editID) { - Drupal.edit.metadataCache[editID] = metadata; - }); - - // Annotate the remaining fields based on the updated access cache. - _.each(remainingFieldsToAnnotate, annotateField); - - // Find editable fields, make them editable. - Drupal.edit.app.findEditableProperties($context); - } + event: 'edit-internal.edit', + submit: { 'fields[]' : _.pluck(remainingFieldsToAnnotate, 'editID') }, + progress: { type : null } // No progress indicator. }); + // Implement a scoped editMetaData AJAX command: calls the callback. + Drupal.ajax[id].commands.editMetadata = function(ajax, response, status) { + // Update the metadata cache. + _.each(response.data, function(metadata, editID) { + Drupal.edit.metadataCache[editID] = metadata; + }); + + // Annotate the remaining fields based on the updated access cache. + _.each(remainingFieldsToAnnotate, annotateField); + + // Find editable fields, make them editable. + Drupal.edit.app.findEditableProperties($context); + + // Delete the Drupal.ajax instance that called this very function. + delete Drupal.ajax[id]; + + // Also delete the temporary element. + // $el.remove(); + }; + // This will ensure our scoped editMetadata AJAX command gets called. + $el.trigger('edit-internal.edit'); }); } } diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php index 32d325d656cf..3a07b7f98123 100644 --- a/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php +++ b/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\Edit\Ajax\BaseCommand. + * Contains \Drupal\edit\Ajax\BaseCommand. */ namespace Drupal\edit\Ajax; diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php index 76b01c529922..781e2ffe7bb6 100644 --- a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php +++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\edit\Ajax\FieldFormCommand. + * Contains \Drupal\edit\Ajax\FieldFormCommand. */ namespace Drupal\edit\Ajax; diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php index d2a963060a68..bdee56ec4dc5 100644 --- a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php +++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\edit\Ajax\FieldFormSavedCommand. + * Contains \Drupal\edit\Ajax\FieldFormSavedCommand. */ namespace Drupal\edit\Ajax; diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php index a70372533ae3..a44be4b07d46 100644 --- a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php +++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\edit\Ajax\FieldFormValidationErrorsCommand. + * Contains \Drupal\edit\Ajax\FieldFormValidationErrorsCommand. */ namespace Drupal\edit\Ajax; diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php new file mode 100644 index 000000000000..5f291cacaf0f --- /dev/null +++ b/core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php @@ -0,0 +1,27 @@ +<?php + +/** + * @file + * Contains \Drupal\edit\Ajax\MetadataCommand. + */ + +namespace Drupal\edit\Ajax; + +use Drupal\Core\Ajax\CommandInterface; + +/** + * AJAX command for passing fields metadata to Edit's JavaScript app. + */ +class MetadataCommand extends BaseCommand { + + /** + * Constructs a MetadataCommand object. + * + * @param string $metadata + * The metadata to pass on to the client side. + */ + public function __construct($metadata) { + parent::__construct('editMetadata', $metadata); + } + +} diff --git a/core/modules/edit/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php index a9b051bec963..2138a70ee9a2 100644 --- a/core/modules/edit/lib/Drupal/edit/EditController.php +++ b/core/modules/edit/lib/Drupal/edit/EditController.php @@ -16,6 +16,7 @@ use Drupal\edit\Ajax\FieldFormCommand; use Drupal\edit\Ajax\FieldFormSavedCommand; use Drupal\edit\Ajax\FieldFormValidationErrorsCommand; +use Drupal\edit\Ajax\MetadataCommand; /** * Returns responses for Edit module routes. @@ -29,10 +30,12 @@ class EditController extends ContainerAware { * entity and field level to determine whether the current user may edit them. * Also retrieves other metadata. * - * @return \Symfony\Component\HttpFoundation\JsonResponse - * The JSON response. + * @return \Drupal\Core\Ajax\AjaxResponse + * The Ajax response. */ public function metadata(Request $request) { + $response = new AjaxResponse(); + $fields = $request->request->get('fields'); if (!isset($fields)) { throw new NotFoundHttpException(); @@ -63,7 +66,20 @@ public function metadata(Request $request) { $metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode); } - return new JsonResponse($metadata); + $response->addCommand(new MetaDataCommand($metadata)); + + // Determine in-place editors and ensure their attachments are loaded. + $editors = array(); + foreach ($metadata as $edit_id => $field_metadata) { + if (isset($field_metadata['editor'])) { + $editors[] = $field_metadata['editor']; + } + } + $editorSelector = $this->container->get('edit.editor.selector'); + $elements['#attached'] = $editorSelector->getEditorAttachments($editors); + drupal_process_attached($elements); + + return $response; } /** diff --git a/core/modules/edit/lib/Drupal/edit/EditorSelector.php b/core/modules/edit/lib/Drupal/edit/EditorSelector.php index fe713400412b..ce80defc925f 100644 --- a/core/modules/edit/lib/Drupal/edit/EditorSelector.php +++ b/core/modules/edit/lib/Drupal/edit/EditorSelector.php @@ -41,7 +41,7 @@ public function __construct(PluginManagerInterface $editor_manager) { } /** - * Implements \Drupal\edit\EditorSelectorInterface::getEditor(). + * {@inheritdoc} */ public function getEditor($formatter_type, FieldInstance $instance, array $items) { // Build a static cache of the editors that have registered themselves as @@ -90,24 +90,20 @@ public function getEditor($formatter_type, FieldInstance $instance, array $items } /** - * Implements \Drupal\edit\EditorSelectorInterface::getAllEditorAttachments(). - * - * @todo Instead of loading all JS/CSS for all editors, load them lazily when - * needed. - * @todo The NestedArray stuff is wonky. + * {@inheritdoc} */ - public function getAllEditorAttachments() { + public function getEditorAttachments(array $editor_ids) { $attachments = array(); - $definitions = $this->editorManager->getDefinitions(); + $editor_ids = array_unique($editor_ids); // Editor plugins' attachments. - $editor_ids = array_keys($definitions); foreach ($editor_ids as $editor_id) { $editor = $this->editorManager->createInstance($editor_id); - $attachments[] = $editor->getAttachments();; + $attachments[] = $editor->getAttachments(); } // JavaScript settings for Edit. + $definitions = $this->editorManager->getDefinitions(); foreach ($definitions as $definition) { $attachments[] = array( // This will be used in Create.js' propertyEditorWidgetsConfiguration. @@ -124,4 +120,5 @@ public function getAllEditorAttachments() { return NestedArray::mergeDeepArray($attachments); } + } diff --git a/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php b/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php index e09dd1287bb9..990c207ac650 100644 --- a/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php +++ b/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php @@ -32,10 +32,13 @@ public function getEditor($formatter_type, FieldInstance $instance, array $items /** * Returns the attachments for all editors. * + * @param array $editor_ids + * A list of all in-place editor IDs that should be attached. + * * @return array * An array of attachments, for use with #attached. * * @see drupal_process_attached() */ - public function getAllEditorAttachments(); + public function getEditorAttachments(array $editor_ids); } diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php new file mode 100644 index 000000000000..e1ce8514c37c --- /dev/null +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php @@ -0,0 +1,198 @@ +<?php + +/** + * @file + * Contains \Drupal\edit\Tests\EditLoadingTest. + */ + +namespace Drupal\edit\Tests; + +use Drupal\simpletest\WebTestBase; +use Drupal\edit\Ajax\MetadataCommand; +use Drupal\Core\Ajax\AppendCommand; + +/** + * Tests loading of Edit and lazy-loading of in-place editors. + */ +class EditLoadingTest extends WebTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('contextual', 'edit', 'filter', 'node'); + + public static function getInfo() { + return array( + 'name' => 'In-place editing loading', + 'description' => 'Tests loading of in-place editing functionality and lazy loading of its in-place editors.', + 'group' => 'Edit', + ); + } + + function setUp() { + parent::setUp(); + + // Create a text format. + $filtered_html_format = entity_create('filter_format', array( + 'format' => 'filtered_html', + 'name' => 'Filtered HTML', + 'weight' => 0, + 'filters' => array(), + )); + $filtered_html_format->save(); + + // Create a node type. + $this->drupalCreateContentType(array( + 'type' => 'article', + 'name' => 'Article', + )); + + // Create one node of the above node type using the above text format. + $this->drupalCreateNode(array( + 'type' => 'article', + 'body' => array( + 0 => array( + 'value' => '<p>How are you?</p>', + 'format' => 'filtered_html', + ) + ) + )); + + // Create 2 users, the only difference being the ability to use in-place + // editing + $basic_permissions = array('access content', 'create article content', 'edit any article content', 'use text format filtered_html', 'access contextual links'); + $this->author_user = $this->drupalCreateUser($basic_permissions); + $this->editor_user = $this->drupalCreateUser(array_merge($basic_permissions, array('access in-place editing'))); + } + + /** + * Test the loading of Edit when a user doesn't have access to it. + */ + function testUserWithoutPermission() { + $this->drupalLogin($this->author_user); + $this->drupalGet('node/1'); + + // Settings, library and in-place editors. + $settings = $this->drupalGetSettings(); + $this->assertFalse(isset($settings['edit']), 'Edit settings do not exist.'); + $this->assertFalse(isset($settings['ajaxPageState']['js']['core/modules/edit/js/edit.js']), 'Edit library not loaded.'); + $this->assertFalse(isset($settings['ajaxPageState']['js']['core/modules/edit/js/createjs/editingWidgets/formwidget.js']), "'form' in-place editor not loaded."); + + // HTML annotation must always exist (to not break the render cache). + $this->assertRaw('data-edit-entity="node/1"'); + $this->assertRaw('data-edit-id="node/1/body/und/full"'); + + // Retrieving the metadata should result in an empty 403 response. + $response = $this->retrieveMetadata(array('node/1/body/und/full')); + $this->assertIdentical('{}', $response); + $this->assertResponse(403); + } + + /** + * Tests the loading of Edit when a user does have access to it. + * + * Also ensures lazy loading of in-place editors works. + */ + function testUserWithPermission() { + $this->drupalLogin($this->editor_user); + $this->drupalGet('node/1'); + + // Settings, library and in-place editors. + $settings = $this->drupalGetSettings(); + $this->assertTrue(isset($settings['edit']), 'Edit settings exist.'); + $this->assertTrue(isset($settings['ajaxPageState']['js']['core/modules/edit/js/edit.js']), 'Edit library loaded.'); + $this->assertFalse(isset($settings['ajaxPageState']['js']['core/modules/edit/js/createjs/editingWidgets/formwidget.js']), "'form' in-place editor not loaded."); + + // HTML annotation must always exist (to not break the render cache). + $this->assertRaw('data-edit-entity="node/1"'); + $this->assertRaw('data-edit-id="node/1/body/und/full"'); + + // Retrieving the metadata should result in a 200 response, containing: + // 1. a settings command with correct in-place editor metadata + // 2. an insert command that loads the required in-place editors + // 3. a metadata command with correct per-field metadata + $response = $this->retrieveMetadata(array('node/1/body/und/full')); + $this->assertResponse(200); + $ajax_commands = drupal_json_decode($response); + $this->assertIdentical(3, count($ajax_commands), 'The metadata HTTP request results in three AJAX commands.'); + + // First command: settings. + $this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.'); + $edit_editors = array( + 'direct' => array('widget' => 'direct'), + 'form' => array('widget' => 'formEditEditor'), + ); + $this->assertIdentical($edit_editors, $ajax_commands[0]['settings']['edit']['editors'], 'The settings command contains the expected settings.'); + + // Second command: insert libraries into DOM. + $this->assertIdentical('insert', $ajax_commands[1]['command'], 'The second AJAX command is an append command.'); + $command = new AppendCommand('body', '<script src="' . file_create_url('core/modules/edit/js/createjs/editingWidgets/formwidget.js') . '?v=' . VERSION . '"></script>' . "\n"); + $this->assertIdentical($command->render(), $ajax_commands[1], 'The append command contains the expected data.'); + + // Third command: actual metadata. + $this->assertIdentical('editMetadata', $ajax_commands[2]['command'], 'The third AJAX command is an Edit metadata command.'); + $command = new MetadataCommand(array( + 'node/1/body/und/full' => array( + 'label' => 'Body', + 'access' => TRUE, + 'editor' => 'form', + 'aria' => 'Entity node 1, field Body' + ) + )); + $this->assertIdentical($command->render(), $ajax_commands[2], 'The Edit metadata command contains the expected metadata.'); + } + + /** + * Retrieve Edit metadata from the server. May also result in additional + * JavaScript settings and CSS/JS being loaded. + * + * @param array $ids + * An array of edit ids. + * + * @return string + * The response body. + */ + protected function retrieveMetadata($ids) { + // Build POST values. + $post = array(); + for ($i = 0; $i < count($ids); $i++) { + $post['fields[' . $i . ']'] = $ids[$i]; + } + + // Serialize POST values. + foreach ($post as $key => $value) { + // Encode according to application/x-www-form-urlencoded + // Both names and values needs to be urlencoded, according to + // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 + $post[$key] = urlencode($key) . '=' . urlencode($value); + } + $post = implode('&', $post); + + // Add extra information to the POST data as ajax.js does. + $extra_post = ''; + $drupal_settings = $this->drupalSettings; + if (isset($drupal_settings['ajaxPageState'])) { + $extra_post .= '&' . urlencode('ajax_page_state[theme]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme']); + $extra_post .= '&' . urlencode('ajax_page_state[theme_token]') . '=' . urlencode($drupal_settings['ajaxPageState']['theme_token']); + foreach ($drupal_settings['ajaxPageState']['css'] as $key => $value) { + $extra_post .= '&' . urlencode("ajax_page_state[css][$key]") . '=1'; + } + foreach ($drupal_settings['ajaxPageState']['js'] as $key => $value) { + $extra_post .= '&' . urlencode("ajax_page_state[js][$key]") . '=1'; + } + } + + // Perform HTTP request. + return $this->curlExec(array( + CURLOPT_URL => url('edit/metadata', array('absolute' => TRUE)), + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => $post . $extra_post, + CURLOPT_HTTPHEADER => array( + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded', + ), + )); + } +} diff --git a/core/modules/editor/js/editor.createjs.js b/core/modules/editor/js/editor.createjs.js index de15c7796c00..693d9d91fcf0 100644 --- a/core/modules/editor/js/editor.createjs.js +++ b/core/modules/editor/js/editor.createjs.js @@ -18,7 +18,7 @@ // @todo D8: use jQuery UI Widget bridging. // @see http://drupal.org/node/1874934#comment-7124904 -jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { +jQuery.widget('Midgard.editor', jQuery.Midgard.editWidget, { textFormat: null, textFormatHasTransformations: null, -- GitLab