core/modules/edit/edit.module | 1 - core/modules/edit/edit.routing.yml | 7 ++ core/modules/edit/js/edit.js | 96 ++++++++++++++------ .../edit/lib/Drupal/edit/Ajax/MetadataCommand.php | 27 ------ .../edit/lib/Drupal/edit/EditController.php | 30 +++--- .../edit/lib/Drupal/edit/Tests/EditLoadingTest.php | 78 ++++++++++++---- 6 files changed, 151 insertions(+), 88 deletions(-) diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 5b11560..c29672e 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -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', )), diff --git a/core/modules/edit/edit.routing.yml b/core/modules/edit/edit.routing.yml index b2c0b43..50aa2c9 100644 --- a/core/modules/edit/edit.routing.yml +++ b/core/modules/edit/edit.routing.yml @@ -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: diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index 36c884f..102fef7 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -234,37 +234,73 @@ 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 = $('
').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 = $('
').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. * * @param Object contextualLink @@ -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: $('
  • ').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: $('
  • ').prependTo($links), + model: entityModel, + appModel: Drupal.edit.app.model + }, options)); + entityModel.set('contextualLinkView', contextualLinkView); + }); return true; } diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php deleted file mode 100644 index 5f291ca..0000000 --- a/core/modules/edit/lib/Drupal/edit/Ajax/MetadataCommand.php +++ /dev/null @@ -1,27 +0,0 @@ -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); diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php index 3d83d83..df7f51a 100644 --- a/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php +++ b/core/modules/edit/lib/Drupal/edit/Tests/EditLoadingTest.php @@ -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', '' . "\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'); @@ -188,6 +189,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', ), ));