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