Commit bceb85e8 authored by webchick's avatar webchick

Issue #1943776 by Wim Leers, nod_: In-place editors (Create.js PropertyEditor...

Issue #1943776 by Wim Leers, nod_: In-place editors (Create.js PropertyEditor widgets) should be loaded lazily.
parent 74493174
...@@ -53,9 +53,7 @@ function edit_contextual_links_view_alter(&$element, $items) { ...@@ -53,9 +53,7 @@ function edit_contextual_links_view_alter(&$element, $items) {
return; return;
} }
// Include the attachments and settings for all available editors. $element['#attached']['library'][] = array('edit', 'edit');
$attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments();
$element['#attached'] = NestedArray::mergeDeep($element['#attached'], $attachments);
} }
/** /**
...@@ -65,7 +63,6 @@ function edit_library_info() { ...@@ -65,7 +63,6 @@ function edit_library_info() {
$path = drupal_get_path('module', 'edit'); $path = drupal_get_path('module', 'edit');
$options = array( $options = array(
'scope' => 'footer', 'scope' => 'footer',
'attributes' => array('defer' => TRUE),
); );
$libraries['edit'] = array( $libraries['edit'] = array(
'title' => 'Edit: in-place editing', 'title' => 'Edit: in-place editing',
......
...@@ -57,24 +57,37 @@ Drupal.behaviors.edit = { ...@@ -57,24 +57,37 @@ Drupal.behaviors.edit = {
if (remainingFieldsToAnnotate.length) { if (remainingFieldsToAnnotate.length) {
$(window).ready(function() { $(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, url: drupalSettings.edit.metadataURL,
type: 'POST', event: 'edit-internal.edit',
data: { 'fields[]' : _.pluck(remainingFieldsToAnnotate, 'editID') }, submit: { 'fields[]' : _.pluck(remainingFieldsToAnnotate, 'editID') },
dataType: 'json', progress: { type : null } // No progress indicator.
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);
}
}); });
// 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');
}); });
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\Edit\Ajax\BaseCommand. * Contains \Drupal\edit\Ajax\BaseCommand.
*/ */
namespace Drupal\edit\Ajax; namespace Drupal\edit\Ajax;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\edit\Ajax\FieldFormCommand. * Contains \Drupal\edit\Ajax\FieldFormCommand.
*/ */
namespace Drupal\edit\Ajax; namespace Drupal\edit\Ajax;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\edit\Ajax\FieldFormSavedCommand. * Contains \Drupal\edit\Ajax\FieldFormSavedCommand.
*/ */
namespace Drupal\edit\Ajax; namespace Drupal\edit\Ajax;
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\edit\Ajax\FieldFormValidationErrorsCommand. * Contains \Drupal\edit\Ajax\FieldFormValidationErrorsCommand.
*/ */
namespace Drupal\edit\Ajax; namespace Drupal\edit\Ajax;
......
<?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);
}
}
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
use Drupal\edit\Ajax\FieldFormCommand; use Drupal\edit\Ajax\FieldFormCommand;
use Drupal\edit\Ajax\FieldFormSavedCommand; use Drupal\edit\Ajax\FieldFormSavedCommand;
use Drupal\edit\Ajax\FieldFormValidationErrorsCommand; use Drupal\edit\Ajax\FieldFormValidationErrorsCommand;
use Drupal\edit\Ajax\MetadataCommand;
/** /**
* Returns responses for Edit module routes. * Returns responses for Edit module routes.
...@@ -29,10 +30,12 @@ class EditController extends ContainerAware { ...@@ -29,10 +30,12 @@ class EditController extends ContainerAware {
* entity and field level to determine whether the current user may edit them. * entity and field level to determine whether the current user may edit them.
* Also retrieves other metadata. * Also retrieves other metadata.
* *
* @return \Symfony\Component\HttpFoundation\JsonResponse * @return \Drupal\Core\Ajax\AjaxResponse
* The JSON response. * The Ajax response.
*/ */
public function metadata(Request $request) { public function metadata(Request $request) {
$response = new AjaxResponse();
$fields = $request->request->get('fields'); $fields = $request->request->get('fields');
if (!isset($fields)) { if (!isset($fields)) {
throw new NotFoundHttpException(); throw new NotFoundHttpException();
...@@ -63,7 +66,20 @@ public function metadata(Request $request) { ...@@ -63,7 +66,20 @@ public function metadata(Request $request) {
$metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode); $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;
} }
/** /**
......
...@@ -41,7 +41,7 @@ public function __construct(PluginManagerInterface $editor_manager) { ...@@ -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) { public function getEditor($formatter_type, FieldInstance $instance, array $items) {
// Build a static cache of the editors that have registered themselves as // 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 ...@@ -90,24 +90,20 @@ public function getEditor($formatter_type, FieldInstance $instance, array $items
} }
/** /**
* Implements \Drupal\edit\EditorSelectorInterface::getAllEditorAttachments(). * {@inheritdoc}
*
* @todo Instead of loading all JS/CSS for all editors, load them lazily when
* needed.
* @todo The NestedArray stuff is wonky.
*/ */
public function getAllEditorAttachments() { public function getEditorAttachments(array $editor_ids) {
$attachments = array(); $attachments = array();
$definitions = $this->editorManager->getDefinitions(); $editor_ids = array_unique($editor_ids);
// Editor plugins' attachments. // Editor plugins' attachments.
$editor_ids = array_keys($definitions);
foreach ($editor_ids as $editor_id) { foreach ($editor_ids as $editor_id) {
$editor = $this->editorManager->createInstance($editor_id); $editor = $this->editorManager->createInstance($editor_id);
$attachments[] = $editor->getAttachments();; $attachments[] = $editor->getAttachments();
} }
// JavaScript settings for Edit. // JavaScript settings for Edit.
$definitions = $this->editorManager->getDefinitions();
foreach ($definitions as $definition) { foreach ($definitions as $definition) {
$attachments[] = array( $attachments[] = array(
// This will be used in Create.js' propertyEditorWidgetsConfiguration. // This will be used in Create.js' propertyEditorWidgetsConfiguration.
...@@ -124,4 +120,5 @@ public function getAllEditorAttachments() { ...@@ -124,4 +120,5 @@ public function getAllEditorAttachments() {
return NestedArray::mergeDeepArray($attachments); return NestedArray::mergeDeepArray($attachments);
} }
} }
...@@ -32,10 +32,13 @@ public function getEditor($formatter_type, FieldInstance $instance, array $items ...@@ -32,10 +32,13 @@ public function getEditor($formatter_type, FieldInstance $instance, array $items
/** /**
* Returns the attachments for all editors. * Returns the attachments for all editors.
* *
* @param array $editor_ids
* A list of all in-place editor IDs that should be attached.
*
* @return array * @return array
* An array of attachments, for use with #attached. * An array of attachments, for use with #attached.
* *
* @see drupal_process_attached() * @see drupal_process_attached()
*/ */
public function getAllEditorAttachments(); public function getEditorAttachments(array $editor_ids);
} }
<?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',
),
));
}
}
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
// @todo D8: use jQuery UI Widget bridging. // @todo D8: use jQuery UI Widget bridging.
// @see http://drupal.org/node/1874934#comment-7124904 // @see http://drupal.org/node/1874934#comment-7124904
jQuery.widget('Midgard.editor', jQuery.Midgard.direct, { jQuery.widget('Midgard.editor', jQuery.Midgard.editWidget, {
textFormat: null, textFormat: null,
textFormatHasTransformations: null, textFormatHasTransformations: null,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment