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) {
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',
......
......@@ -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');
});
}
}
......
......@@ -2,7 +2,7 @@
/**
* @file
* Definition of Drupal\Edit\Ajax\BaseCommand.
* Contains \Drupal\edit\Ajax\BaseCommand.
*/
namespace Drupal\edit\Ajax;
......
......@@ -2,7 +2,7 @@
/**
* @file
* Definition of Drupal\edit\Ajax\FieldFormCommand.
* Contains \Drupal\edit\Ajax\FieldFormCommand.
*/
namespace Drupal\edit\Ajax;
......
......@@ -2,7 +2,7 @@
/**
* @file
* Definition of Drupal\edit\Ajax\FieldFormSavedCommand.
* Contains \Drupal\edit\Ajax\FieldFormSavedCommand.
*/
namespace Drupal\edit\Ajax;
......
......@@ -2,7 +2,7 @@
/**
* @file
* Definition of Drupal\edit\Ajax\FieldFormValidationErrorsCommand.
* Contains \Drupal\edit\Ajax\FieldFormValidationErrorsCommand.
*/
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 @@
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;
}
/**
......
......@@ -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);
}
}
......@@ -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);
}
<?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 @@
// @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,
......
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