Commit 30251ca2 authored by Dries's avatar Dries

Issue #1980744 by Wim Leers: Turn edit/metadata into a JsonResponse again, to...

Issue #1980744 by Wim Leers: Turn edit/metadata into a JsonResponse again, to allow contrib to implement client-side caching of metadata.
parent 8625dfc7
......@@ -52,7 +52,6 @@ function edit_page_build(&$page) {
$page['#attached']['js'][] = array(
'type' => 'setting',
'data' => array('edit' => array(
'metadataURL' => url('edit/metadata'),
'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode'),
'context' => 'body',
)),
......
......@@ -5,6 +5,13 @@ edit_metadata:
requirements:
_permission: 'access in-place editing'
edit_attachments:
pattern: '/edit/attachments'
defaults:
_controller: '\Drupal\edit\EditController::attachments'
requirements:
_permission: 'access in-place editing'
edit_field_form:
pattern: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
......
......@@ -234,36 +234,72 @@ function fetchMissingMetadata (callback) {
var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
fieldsMetadataQueue = [];
$(window).ready(function () {
var id = 'edit-load-metadata';
// Create a temporary element to be able to use Drupal.ajax.
var $el = $('<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,
event: 'edit-internal.edit',
submit: { 'fields[]': fieldIDs },
// No progress indicator.
progress: { type: null }
});
// Implement a scoped editMetaData AJAX command: calls the callback.
Drupal.ajax[id].commands.editMetadata = function (ajax, response, status) {
$.ajax({
url: Drupal.url('edit/metadata'),
type: 'POST',
data: { 'fields[]' : fieldIDs },
dataType: 'json',
success: function(results) {
// Store the metadata.
_.each(response.data, function (fieldMetadata, fieldID) {
_.each(results, function (fieldMetadata, fieldID) {
Drupal.edit.metadata.add(fieldID, fieldMetadata);
});
// Clean-up.
delete Drupal.ajax[id];
$el.remove();
callback(fieldElementsWithoutMetadata);
};
// This will ensure our scoped editMetadata AJAX command gets called.
$el.trigger('edit-internal.edit');
}
});
}
}
/**
* Loads missing in-place editor's attachments (JavaScript and CSS files).
*
* Missing in-place editors are those whose fields are actively being used on
* the page but don't have
*
* @param Function callback
* Callback function to be called when the missing in-place editors (if any)
* have been inserted into the DOM. i.e. they may still be loading.
*/
function loadMissingEditors (callback) {
var loadedEditors = _.keys(Drupal.edit.editors);
var missingEditors = [];
Drupal.edit.collections.fields.each(function (fieldModel) {
var id = fieldModel.id;
var metadata = Drupal.edit.metadata.get(id);
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
missingEditors.push(metadata.editor);
}
});
missingEditors = _.uniq(missingEditors);
if (missingEditors.length === 0) {
callback();
}
// @todo Simplify this once https://drupal.org/node/1533366 lands.
var id = 'edit-load-editors';
// Create a temporary element to be able to use Drupal.ajax.
var $el = $('<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: Drupal.url('edit/attachments'),
event: 'edit-internal.edit',
submit: { 'editors[]': missingEditors },
// No progress indicator.
progress: { type: null }
});
// Implement a scoped insert AJAX command: calls the callback after all AJAX
// command functions have been executed (hence the deferred calling).
var realInsert = Drupal.ajax.prototype.commands.insert;
Drupal.ajax[id].commands.insert = function (ajax, response, status) {
_.defer(function() { callback(); });
realInsert(ajax, response, status);
};
// Trigger the AJAX request, which will should return AJAX commands to insert
// any missing attachments.
$el.trigger('edit-internal.edit');
}
/**
* Attempts to set up a "Quick edit" link and corresponding EntityModel.
*
......@@ -323,14 +359,16 @@ function initializeEntityContextualLink (contextualLink) {
});
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
// Set up contextual link view.
var $links = $(contextualLink.el).find('.contextual-links');
var contextualLinkView = new Drupal.edit.ContextualLinkView($.extend({
el: $('<li class="quick-edit"><a href=""></a></li>').prependTo($links),
model: entityModel,
appModel: Drupal.edit.app.model
}, options));
entityModel.set('contextualLinkView', contextualLinkView);
// Set up contextual link view after loading any missing in-place editors.
loadMissingEditors(function () {
var $links = $(contextualLink.el).find('.contextual-links');
var contextualLinkView = new Drupal.edit.ContextualLinkView($.extend({
el: $('<li class="quick-edit"><a href=""></a></li>').prependTo($links),
model: entityModel,
appModel: Drupal.edit.app.model
}, options));
entityModel.set('contextualLinkView', contextualLinkView);
});
return true;
}
......
<?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);
}
}
......@@ -30,12 +30,10 @@ class EditController extends ContainerAware {
* entity and field level to determine whether the current user may edit them.
* Also retrieves other metadata.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The Ajax response.
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response.
*/
public function metadata(Request $request) {
$response = new AjaxResponse();
$fields = $request->request->get('fields');
if (!isset($fields)) {
throw new NotFoundHttpException();
......@@ -66,15 +64,25 @@ public function metadata(Request $request) {
$metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode);
}
$response->addCommand(new MetaDataCommand($metadata));
return new JsonResponse($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'];
}
/**
* Returns AJAX commands to load in-place editors' attachments.
*
* Given a list of in-place editor IDs as POST parameters, render AJAX
* commands to load those in-place editors.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The Ajax response.
*/
public function attachments(Request $request) {
$response = new AjaxResponse();
$editors = $request->request->get('editors');
if (!isset($editors)) {
throw new NotFoundHttpException();
}
$editorSelector = $this->container->get('edit.editor.selector');
$elements['#attached'] = $editorSelector->getEditorAttachments($editors);
drupal_process_attached($elements);
......
......@@ -110,35 +110,36 @@ function testUserWithPermission() {
$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 useless metadata: AjaxController is dumb
// 2. an insert command that loads the required in-place editors
// 3. a metadata command with correct per-field metadata
// Retrieving the metadata should result in a 200 JSON response.
$htmlPageDrupalSettings = $this->drupalSettings;
$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.');
$expected = array(
'node/1/body/und/full' => array(
'label' => 'Body',
'access' => TRUE,
'editor' => 'form',
'aria' => 'Entity node 1, field Body',
)
);
$this->assertIdentical(drupal_json_decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
// Restore drupalSettings to build the next requests; simpletest wipes them
// after a JSON response.
$this->drupalSettings = $htmlPageDrupalSettings;
// Retrieving the attachments should result in a 200 response, containing:
// 1. a settings command with useless metadata: AjaxController is dumb
// 2. an insert command that loads the required in-place editors
$response = $this->retrieveAttachments(array('form'));
$ajax_commands = drupal_json_decode($response);
$this->assertIdentical(2, count($ajax_commands), 'The attachments HTTP request results in two AJAX commands.');
// First command: settings.
$this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.');
// 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/editors/formEditor.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.');
// Retrieving the form for this field should result in a 200 response,
// containing only an editFieldForm command.
$response = $this->retrieveFieldForm('node/1/body/und/full');
......@@ -187,6 +188,43 @@ protected function retrieveMetadata($ids) {
));
}
/**
* Retrieves AJAX commands to load attachments for the given in-place editors.
*
* @param array $editors
* An array of in-place editor ids.
*
* @return string
* The response body.
*/
protected function retrieveAttachments($editors) {
// Build POST values.
$post = array();
for ($i = 0; $i < count($editors); $i++) {
$post['editors[' . $i . ']'] = $editors[$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);
// Perform HTTP request.
return $this->curlExec(array(
CURLOPT_URL => url('edit/attachments', array('absolute' => TRUE)),
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
CURLOPT_HTTPHEADER => array(
'Accept: application/vnd.drupal-ajax',
'Content-Type: application/x-www-form-urlencoded',
),
));
}
/**
* Retrieve field form from the server. May also result in additional
* JavaScript settings and CSS/JS being loaded.
......@@ -207,7 +245,7 @@ protected function retrieveFieldForm($field_id) {
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $post . $this->getAjaxPageStatePostData(),
CURLOPT_HTTPHEADER => array(
'Accept: application/json',
'Accept: application/vnd.drupal-ajax',
'Content-Type: application/x-www-form-urlencoded',
),
));
......
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