diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index bf181ede15ea..61157c4df969 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -6,22 +6,22 @@ "use strict"; -Drupal.edit = Drupal.edit || {}; -Drupal.edit.metadataCache = Drupal.edit.metadataCache || {}; +Drupal.edit = { metadataCache: {}, contextualLinksQueue: [] }; /** * Attach toggling behavior and in-place editing. */ Drupal.behaviors.edit = { - attach: function(context) { + attach: function (context) { var $context = $(context); var $fields = $context.find('[data-edit-id]'); // Initialize the Edit app. $('body').once('edit-init', Drupal.edit.init); - var annotateField = function(field) { - if (_.has(Drupal.edit.metadataCache, field.editID)) { + function annotateField (field) { + var hasField = _.has(Drupal.edit.metadataCache, field.editID); + if (hasField) { var meta = Drupal.edit.metadataCache[field.editID]; field.$el.addClass((meta.access) ? 'edit-allowed' : 'edit-disallowed'); @@ -31,21 +31,19 @@ Drupal.behaviors.edit = { .attr('aria-label', meta.aria) .addClass('edit-field edit-type-' + ((meta.editor === 'form') ? 'form' : 'direct')); } - - return true; } - return false; - }; + return hasField; + } // Find all fields in the context without metadata. - var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function(el) { + var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function (el) { var $el = $(el); return { $el: $el, editID: $el.attr('data-edit-id') }; }); // Fields whose metadata is known (typically when they were just modified) // can be annotated immediately, those remaining must be requested. - var remainingFieldsToAnnotate = _.reduce(fieldsToAnnotate, function(result, field) { + var remainingFieldsToAnnotate = _.reduce(fieldsToAnnotate, function (result, field) { if (!annotateField(field)) { result.push(field); } @@ -56,7 +54,7 @@ Drupal.behaviors.edit = { Drupal.edit.app.findEditableProperties($context); if (remainingFieldsToAnnotate.length) { - $(window).ready(function() { + $(window).ready(function () { var id = 'edit-load-metadata'; // Create a temporary element to be able to use Drupal.ajax. var $el = jQuery('
').appendTo('body'); @@ -64,13 +62,14 @@ Drupal.behaviors.edit = { Drupal.ajax[id] = new Drupal.ajax(id, $el, { url: drupalSettings.edit.metadataURL, event: 'edit-internal.edit', - submit: { 'fields[]' : _.pluck(remainingFieldsToAnnotate, 'editID') }, - progress: { type : null } // No progress indicator. + submit: { 'fields[]': _.pluck(remainingFieldsToAnnotate, 'editID') }, + // No progress indicator. + progress: { type: null } }); // Implement a scoped editMetaData AJAX command: calls the callback. - Drupal.ajax[id].commands.editMetadata = function(ajax, response, status) { + Drupal.ajax[id].commands.editMetadata = function (ajax, response, status) { // Update the metadata cache. - _.each(response.data, function(metadata, editID) { + _.each(response.data, function (metadata, editID) { Drupal.edit.metadataCache[editID] = metadata; }); @@ -80,6 +79,12 @@ Drupal.behaviors.edit = { // Find editable fields, make them editable. Drupal.edit.app.findEditableProperties($context); + // Metadata cache has been updated, try to set up more contextual + // links now. + Drupal.edit.contextualLinksQueue = _.filter(Drupal.edit.contextualLinksQueue, function (data) { + return !Drupal.edit.setUpContextualLink(data); + }); + // Delete the Drupal.ajax instance that called this very function. delete Drupal.ajax[id]; @@ -93,34 +98,98 @@ Drupal.behaviors.edit = { } }; -Drupal.edit.init = function() { +/** + * Detect contextual links on entities annotated by Edit; queue these to be + * processed. + */ +$(document).on('drupalContextualLinkAdded', function (event, data) { + if (data.$region.is('[data-edit-entity]')) { + var contextualLink = { + entity: data.$region.attr('data-edit-entity'), + $el: data.$el, + $region: data.$region + }; + // Set up contextual links for this, otherwise queue it to be set up later. + if (!Drupal.edit.setUpContextualLink(contextualLink)) { + Drupal.edit.contextualLinksQueue.push(contextualLink); + } + } +}); + +/** + * Attempts to set up a "Quick edit" contextual link. + * + * @param Object contextualLink + * An object with the following properties: + * - entity: an Edit entity identifier, e.g. "node/1" or "custom_block/5". + * - $el: a jQuery element pointing to the contextual links for this entity. + * - $region: a jQuery element pointing to the contextual region for this + * entity. + * + * @return Boolean + * Returns true when a contextual the given contextual link metadata can be + * removed from the queue (either because the contextual link has been set up + * or because it is certain that in-place editing is not allowed for any of + * its fields). + * Returns false otherwise. + */ +Drupal.edit.setUpContextualLink = function (contextualLink) { + // Check if the user has permission to edit at least one of them. + function hasFieldWithPermission (editIDs) { + var i, meta = Drupal.edit.metadataCache; + for (i = 0; i < editIDs.length; i++) { + var editID = editIDs[i]; + if (_.has(meta, editID) && meta[editID].access === true) { + return true; + } + } + return false; + } + + // Checks if the metadata for all given editIDs exists. + function allMetadataExists (editIDs) { + var editIDsWithMetadata = _.intersection(editIDs, _.keys(Drupal.edit.metadataCache)); + return editIDs.length === editIDsWithMetadata.length; + } + + // Find the Edit IDs of all fields within this entity. + var editIDs = []; + contextualLink.$region + .find('[data-edit-id^="' + contextualLink.entity + '/"]') + .each(function () { + editIDs.push($(this).attr('data-edit-id')); + }); + + // The entity for the given contextual link contains at least one field that + // the current user may edit in-place; instantiate ContextualLinkView. + if (hasFieldWithPermission(editIDs)) { + new Drupal.edit.views.ContextualLinkView({ + el: $('
  • ').prependTo(contextualLink.$el), + model: Drupal.edit.app.model, + entity: contextualLink.entity + }); + return true; + } + // There was not at least one field that the current user may edit in-place, + // even though the metadata for all fields within this entity is available. + else if (allMetadataExists(editIDs)) { + return true; + } + + return false; +}; + +Drupal.edit.init = function () { // Instantiate EditAppView, which is the controller of it all. EditAppModel // instance tracks global state (viewing/editing in-place). var appModel = new Drupal.edit.models.EditAppModel(); - var app = new Drupal.edit.EditAppView({ - el: $('body'), - model: appModel - }); - - // Add "Quick edit" links to all contextual menus where editing the full - // node is possible. - // @todo Generalize this to work for all entities. - $('ul.contextual-links li.node-edit') - .before('
  • ') - .each(function() { - // Instantiate ContextualLinkView. - var $editContextualLink = $(this).prev(); - var editContextualLinkView = new Drupal.edit.views.ContextualLinkView({ - el: $editContextualLink.get(0), - model: appModel, - entity: $editContextualLink.parents('[data-edit-entity]').attr('data-edit-entity') - }); - }); - // For now, we work with a singleton app, because for Drupal.behaviors to be // able to discover new editable properties that get AJAXed in, it must know // with which app instance they should be associated. - Drupal.edit.app = app; + Drupal.edit.app = new Drupal.edit.EditAppView({ + el: $('body'), + model: appModel + }); }; })(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/edit/js/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js index efe8ddd3c5d1..472bd6efcfb3 100644 --- a/core/modules/edit/js/views/contextuallink-view.js +++ b/core/modules/edit/js/views/contextuallink-view.js @@ -12,8 +12,16 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ entity: null, - events: { - 'click': 'onClick' + events: function () { + // Prevents delay and simulated mouse events. + function touchEndToClick (event) { + event.preventDefault(); + event.target.click(); + } + return { + 'click a': 'onClick', + 'touchEnd a': touchEndToClick + }; }, /** @@ -46,7 +54,7 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ event.preventDefault(); var that = this; - var updateActiveEntity = function() { + function updateActiveEntity () { // The active entity is the current entity, i.e. stop editing the current // entity. if (that.model.get('activeEntity') === that.entity) { @@ -57,7 +65,7 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ else { that.model.set('activeEntity', that.entity); } - }; + } // If there's an active editor, attempt to set its state to 'candidate', and // only then do what the user asked. @@ -67,7 +75,7 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ if (activeEditor) { var editableEntity = activeEditor.options.widget; var predicate = activeEditor.options.property; - editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function(accepted) { + editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function (accepted) { if (accepted) { updateActiveEntity(); } @@ -88,7 +96,7 @@ Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ render: function () { var activeEntity = this.model.get('activeEntity'); var string = (activeEntity !== this.entity) ? Drupal.t('Quick edit') : Drupal.t('Stop quick edit'); - this.$el.html('' + string + ''); + this.$el.find('a').text(string); return this; },