Chris@0: /** Chris@0: * @file Chris@0: * Attaches behavior for the Quick Edit module. Chris@0: * Chris@0: * Everything happens asynchronously, to allow for: Chris@0: * - dynamically rendered contextual links Chris@0: * - asynchronously retrieved (and cached) per-field in-place editing metadata Chris@0: * - asynchronous setup of in-place editable field and "Quick edit" link. Chris@0: * Chris@0: * To achieve this, there are several queues: Chris@0: * - fieldsMetadataQueue: fields whose metadata still needs to be fetched. Chris@0: * - fieldsAvailableQueue: queue of fields whose metadata is known, and for Chris@0: * which it has been confirmed that the user has permission to edit them. Chris@0: * However, FieldModels will only be created for them once there's a Chris@0: * contextual link for their entity: when it's possible to initiate editing. Chris@0: * - contextualLinksQueue: queue of contextual links on entities for which it Chris@0: * is not yet known whether the user has permission to edit at >=1 of them. Chris@0: */ Chris@0: Chris@0: (function ($, _, Backbone, Drupal, drupalSettings, JSON, storage) { Chris@0: const options = $.extend(drupalSettings.quickedit, Chris@0: // Merge strings on top of drupalSettings so that they are not mutable. Chris@0: { Chris@0: strings: { Chris@0: quickEdit: Drupal.t('Quick edit'), Chris@0: }, Chris@0: }, Chris@0: ); Chris@0: Chris@0: /** Chris@0: * Tracks fields without metadata. Contains objects with the following keys: Chris@0: * - DOM el Chris@0: * - String fieldID Chris@0: * - String entityID Chris@0: */ Chris@0: let fieldsMetadataQueue = []; Chris@0: Chris@0: /** Chris@0: * Tracks fields ready for use. Contains objects with the following keys: Chris@0: * - DOM el Chris@0: * - String fieldID Chris@0: * - String entityID Chris@0: */ Chris@0: let fieldsAvailableQueue = []; Chris@0: Chris@0: /** Chris@0: * Tracks contextual links on entities. Contains objects with the following Chris@0: * keys: Chris@0: * - String entityID Chris@0: * - DOM el Chris@0: * - DOM region Chris@0: */ Chris@0: let contextualLinksQueue = []; Chris@0: Chris@0: /** Chris@0: * Tracks how many instances exist for each unique entity. Contains key-value Chris@0: * pairs: Chris@0: * - String entityID Chris@0: * - Number count Chris@0: */ Chris@0: const entityInstancesTracker = {}; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: */ Chris@0: Drupal.behaviors.quickedit = { Chris@0: attach(context) { Chris@0: // Initialize the Quick Edit app once per page load. Chris@0: $('body').once('quickedit-init').each(initQuickEdit); Chris@0: Chris@0: // Find all in-place editable fields, if any. Chris@0: const $fields = $(context).find('[data-quickedit-field-id]').once('quickedit'); Chris@0: if ($fields.length === 0) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Process each entity element: identical entities that appear multiple Chris@0: // times will get a numeric identifier, starting at 0. Chris@0: $(context).find('[data-quickedit-entity-id]').once('quickedit').each((index, entityElement) => { Chris@0: processEntity(entityElement); Chris@0: }); Chris@0: Chris@0: // Process each field element: queue to be used or to fetch metadata. Chris@0: // When a field is being rerendered after editing, it will be processed Chris@0: // immediately. New fields will be unable to be processed immediately, Chris@0: // but will instead be queued to have their metadata fetched, which occurs Chris@0: // below in fetchMissingMetaData(). Chris@0: $fields.each((index, fieldElement) => { Chris@0: processField(fieldElement); Chris@0: }); Chris@0: Chris@0: // Entities and fields on the page have been detected, try to set up the Chris@0: // contextual links for those entities that already have the necessary Chris@0: // meta- data in the client-side cache. Chris@0: contextualLinksQueue = _.filter(contextualLinksQueue, contextualLink => !initializeEntityContextualLink(contextualLink)); Chris@0: Chris@0: // Fetch metadata for any fields that are queued to retrieve it. Chris@0: fetchMissingMetadata((fieldElementsWithFreshMetadata) => { Chris@0: // Metadata has been fetched, reprocess fields whose metadata was Chris@0: // missing. Chris@0: _.each(fieldElementsWithFreshMetadata, processField); Chris@0: Chris@0: // Metadata has been fetched, try to set up more contextual links now. Chris@0: contextualLinksQueue = _.filter(contextualLinksQueue, contextualLink => !initializeEntityContextualLink(contextualLink)); Chris@0: }); Chris@0: }, Chris@0: detach(context, settings, trigger) { Chris@0: if (trigger === 'unload') { Chris@0: deleteContainedModelsAndQueues($(context)); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: Drupal.quickedit = { Chris@0: Chris@0: /** Chris@0: * A {@link Drupal.quickedit.AppView} instance. Chris@0: */ Chris@0: app: null, Chris@0: Chris@0: /** Chris@0: * @type {object} Chris@0: * Chris@0: * @prop {Array.} entities Chris@0: * @prop {Array.} fields Chris@0: */ Chris@0: collections: { Chris@0: // All in-place editable entities (Drupal.quickedit.EntityModel) on the Chris@0: // page. Chris@0: entities: null, Chris@0: // All in-place editable fields (Drupal.quickedit.FieldModel) on the page. Chris@0: fields: null, Chris@0: }, Chris@0: Chris@0: /** Chris@0: * In-place editors will register themselves in this object. Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: editors: {}, Chris@0: Chris@0: /** Chris@0: * Per-field metadata that indicates whether in-place editing is allowed, Chris@0: * which in-place editor should be used, etc. Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: metadata: { Chris@0: Chris@0: /** Chris@0: * Check if a field exists in storage. Chris@0: * Chris@0: * @param {string} fieldID Chris@0: * The field id to check. Chris@0: * Chris@0: * @return {bool} Chris@0: * Whether it was found or not. Chris@0: */ Chris@0: has(fieldID) { Chris@0: return storage.getItem(this._prefixFieldID(fieldID)) !== null; Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Add metadata to a field id. Chris@0: * Chris@0: * @param {string} fieldID Chris@0: * The field ID to add data to. Chris@0: * @param {object} metadata Chris@0: * Metadata to add. Chris@0: */ Chris@0: add(fieldID, metadata) { Chris@0: storage.setItem(this._prefixFieldID(fieldID), JSON.stringify(metadata)); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Get a key from a field id. Chris@0: * Chris@0: * @param {string} fieldID Chris@0: * The field ID to check. Chris@0: * @param {string} [key] Chris@0: * The key to check. If empty, will return all metadata. Chris@0: * Chris@0: * @return {object|*} Chris@0: * The value for the key, if defined. Otherwise will return all metadata Chris@0: * for the specified field id. Chris@0: * Chris@0: */ Chris@0: get(fieldID, key) { Chris@0: const metadata = JSON.parse(storage.getItem(this._prefixFieldID(fieldID))); Chris@0: return (typeof key === 'undefined') ? metadata : metadata[key]; Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Prefix the field id. Chris@0: * Chris@0: * @param {string} fieldID Chris@0: * The field id to prefix. Chris@0: * Chris@0: * @return {string} Chris@0: * A prefixed field id. Chris@0: */ Chris@0: _prefixFieldID(fieldID) { Chris@0: return `Drupal.quickedit.metadata.${fieldID}`; Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Unprefix the field id. Chris@0: * Chris@0: * @param {string} fieldID Chris@0: * The field id to unprefix. Chris@0: * Chris@0: * @return {string} Chris@0: * An unprefixed field id. Chris@0: */ Chris@0: _unprefixFieldID(fieldID) { Chris@0: // Strip "Drupal.quickedit.metadata.", which is 26 characters long. Chris@0: return fieldID.substring(26); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Intersection calculation. Chris@0: * Chris@0: * @param {Array} fieldIDs Chris@0: * An array of field ids to compare to prefix field id. Chris@0: * Chris@0: * @return {Array} Chris@0: * The intersection found. Chris@0: */ Chris@0: intersection(fieldIDs) { Chris@0: const prefixedFieldIDs = _.map(fieldIDs, this._prefixFieldID); Chris@0: const intersection = _.intersection(prefixedFieldIDs, _.keys(sessionStorage)); Chris@0: return _.map(intersection, this._unprefixFieldID); Chris@0: }, Chris@0: }, Chris@0: }; Chris@0: Chris@0: // Clear the Quick Edit metadata cache whenever the current user's set of Chris@0: // permissions changes. Chris@0: const permissionsHashKey = Drupal.quickedit.metadata._prefixFieldID('permissionsHash'); Chris@0: const permissionsHashValue = storage.getItem(permissionsHashKey); Chris@0: const permissionsHash = drupalSettings.user.permissionsHash; Chris@0: if (permissionsHashValue !== permissionsHash) { Chris@0: if (typeof permissionsHash === 'string') { Chris@0: _.chain(storage).keys().each((key) => { Chris@0: if (key.substring(0, 26) === 'Drupal.quickedit.metadata.') { Chris@0: storage.removeItem(key); Chris@0: } Chris@0: }); Chris@0: } Chris@0: storage.setItem(permissionsHashKey, permissionsHash); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Detect contextual links on entities annotated by quickedit. Chris@0: * Chris@0: * Queue contextual links to be processed. Chris@0: * Chris@0: * @param {jQuery.Event} event Chris@0: * The `drupalContextualLinkAdded` event. Chris@0: * @param {object} data Chris@0: * An object containing the data relevant to the event. Chris@0: * Chris@0: * @listens event:drupalContextualLinkAdded Chris@0: */ Chris@0: $(document).on('drupalContextualLinkAdded', (event, data) => { Chris@0: if (data.$region.is('[data-quickedit-entity-id]')) { Chris@0: // If the contextual link is cached on the client side, an entity instance Chris@0: // will not yet have been assigned. So assign one. Chris@0: if (!data.$region.is('[data-quickedit-entity-instance-id]')) { Chris@0: data.$region.once('quickedit'); Chris@0: processEntity(data.$region.get(0)); Chris@0: } Chris@0: const contextualLink = { Chris@0: entityID: data.$region.attr('data-quickedit-entity-id'), Chris@0: entityInstanceID: data.$region.attr('data-quickedit-entity-instance-id'), Chris@0: el: data.$el[0], Chris@0: region: data.$region[0], Chris@0: }; Chris@0: // Set up contextual links for this, otherwise queue it to be set up Chris@0: // later. Chris@0: if (!initializeEntityContextualLink(contextualLink)) { Chris@0: contextualLinksQueue.push(contextualLink); Chris@0: } Chris@0: } Chris@0: }); Chris@0: Chris@0: /** Chris@0: * Extracts the entity ID from a field ID. Chris@0: * Chris@0: * @param {string} fieldID Chris@0: * A field ID: a string of the format Chris@0: * `////`. Chris@0: * Chris@0: * @return {string} Chris@0: * An entity ID: a string of the format `/`. Chris@0: */ Chris@0: function extractEntityID(fieldID) { Chris@0: return fieldID.split('/').slice(0, 2).join('/'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Initialize the Quick Edit app. Chris@0: * Chris@0: * @param {HTMLElement} bodyElement Chris@0: * This document's body element. Chris@0: */ Chris@0: function initQuickEdit(bodyElement) { Chris@0: Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection(); Chris@0: Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection(); Chris@0: Chris@0: // Instantiate AppModel (application state) and AppView, which is the Chris@0: // controller of the whole in-place editing experience. Chris@0: Drupal.quickedit.app = new Drupal.quickedit.AppView({ Chris@0: el: bodyElement, Chris@0: model: new Drupal.quickedit.AppModel(), Chris@0: entitiesCollection: Drupal.quickedit.collections.entities, Chris@0: fieldsCollection: Drupal.quickedit.collections.fields, Chris@0: }); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Assigns the entity an instance ID. Chris@0: * Chris@0: * @param {HTMLElement} entityElement Chris@0: * A Drupal Entity API entity's DOM element with a data-quickedit-entity-id Chris@0: * attribute. Chris@0: */ Chris@0: function processEntity(entityElement) { Chris@0: const entityID = entityElement.getAttribute('data-quickedit-entity-id'); Chris@0: if (!entityInstancesTracker.hasOwnProperty(entityID)) { Chris@0: entityInstancesTracker[entityID] = 0; Chris@0: } Chris@0: else { Chris@0: entityInstancesTracker[entityID]++; Chris@0: } Chris@0: Chris@0: // Set the calculated entity instance ID for this element. Chris@0: const entityInstanceID = entityInstancesTracker[entityID]; Chris@0: entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Fetch the field's metadata; queue or initialize it (if EntityModel exists). Chris@0: * Chris@0: * @param {HTMLElement} fieldElement Chris@0: * A Drupal Field API field's DOM element with a data-quickedit-field-id Chris@0: * attribute. Chris@0: */ Chris@0: function processField(fieldElement) { Chris@0: const metadata = Drupal.quickedit.metadata; Chris@0: const fieldID = fieldElement.getAttribute('data-quickedit-field-id'); Chris@0: const entityID = extractEntityID(fieldID); Chris@0: // Figure out the instance ID by looking at the ancestor Chris@0: // [data-quickedit-entity-id] element's data-quickedit-entity-instance-id Chris@0: // attribute. Chris@0: const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`; Chris@0: const $entityElement = $(entityElementSelector); Chris@0: Chris@0: // If there are no elements returned from `entityElementSelector` Chris@0: // throw an error. Check the browser console for this message. Chris@0: if (!$entityElement.length) { Chris@0: throw `Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="${fieldID}"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="${entityID}"]. This is typically caused by the theme's template for this entity type forgetting to print the attributes.`; Chris@0: } Chris@0: let entityElement = $(fieldElement).closest($entityElement); Chris@0: Chris@0: // In the case of a full entity view page, the entity title is rendered Chris@0: // outside of "the entity DOM node": it's rendered as the page title. So in Chris@0: // this case, we find the lowest common parent element (deepest in the tree) Chris@0: // and consider that the entity element. Chris@0: if (entityElement.length === 0) { Chris@0: const $lowestCommonParent = $entityElement.parents().has(fieldElement).first(); Chris@0: entityElement = $lowestCommonParent.find($entityElement); Chris@0: } Chris@0: const entityInstanceID = entityElement Chris@0: .get(0) Chris@0: .getAttribute('data-quickedit-entity-instance-id'); Chris@0: Chris@0: // Early-return if metadata for this field is missing. Chris@0: if (!metadata.has(fieldID)) { Chris@0: fieldsMetadataQueue.push({ Chris@0: el: fieldElement, Chris@0: fieldID, Chris@0: entityID, Chris@0: entityInstanceID, Chris@0: }); Chris@0: return; Chris@0: } Chris@0: // Early-return if the user is not allowed to in-place edit this field. Chris@0: if (metadata.get(fieldID, 'access') !== true) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // If an EntityModel for this field already exists (and hence also a "Quick Chris@0: // edit" contextual link), then initialize it immediately. Chris@0: if (Drupal.quickedit.collections.entities.findWhere({ entityID, entityInstanceID })) { Chris@0: initializeField(fieldElement, fieldID, entityID, entityInstanceID); Chris@0: } Chris@0: // Otherwise: queue the field. It is now available to be set up when its Chris@0: // corresponding entity becomes in-place editable. Chris@0: else { Chris@0: fieldsAvailableQueue.push({ el: fieldElement, fieldID, entityID, entityInstanceID }); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Initialize a field; create FieldModel. Chris@0: * Chris@0: * @param {HTMLElement} fieldElement Chris@0: * The field's DOM element. Chris@0: * @param {string} fieldID Chris@0: * The field's ID. Chris@0: * @param {string} entityID Chris@0: * The field's entity's ID. Chris@0: * @param {string} entityInstanceID Chris@0: * The field's entity's instance ID. Chris@0: */ Chris@0: function initializeField(fieldElement, fieldID, entityID, entityInstanceID) { Chris@0: const entity = Drupal.quickedit.collections.entities.findWhere({ Chris@0: entityID, Chris@0: entityInstanceID, Chris@0: }); Chris@0: Chris@0: $(fieldElement).addClass('quickedit-field'); Chris@0: Chris@0: // The FieldModel stores the state of an in-place editable entity field. Chris@0: const field = new Drupal.quickedit.FieldModel({ Chris@0: el: fieldElement, Chris@0: fieldID, Chris@0: id: `${fieldID}[${entity.get('entityInstanceID')}]`, Chris@0: entity, Chris@0: metadata: Drupal.quickedit.metadata.get(fieldID), Chris@0: acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app), Chris@0: }); Chris@0: Chris@0: // Track all fields on the page. Chris@0: Drupal.quickedit.collections.fields.add(field); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Fetches metadata for fields whose metadata is missing. Chris@0: * Chris@0: * Fields whose metadata is missing are tracked at fieldsMetadataQueue. Chris@0: * Chris@0: * @param {function} callback Chris@0: * A callback function that receives field elements whose metadata will just Chris@0: * have been fetched. Chris@0: */ Chris@0: function fetchMissingMetadata(callback) { Chris@0: if (fieldsMetadataQueue.length) { Chris@0: const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID'); Chris@0: const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el'); Chris@0: let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true); Chris@0: // Ensure we only request entityIDs for which we don't have metadata yet. Chris@0: entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs)); Chris@0: fieldsMetadataQueue = []; Chris@0: Chris@0: $.ajax({ Chris@0: url: Drupal.url('quickedit/metadata'), Chris@0: type: 'POST', Chris@0: data: { Chris@0: 'fields[]': fieldIDs, Chris@0: 'entities[]': entityIDs, Chris@0: }, Chris@0: dataType: 'json', Chris@0: success(results) { Chris@0: // Store the metadata. Chris@0: _.each(results, (fieldMetadata, fieldID) => { Chris@0: Drupal.quickedit.metadata.add(fieldID, fieldMetadata); Chris@0: }); Chris@0: Chris@0: callback(fieldElementsWithoutMetadata); Chris@0: }, Chris@0: }); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Loads missing in-place editor's attachments (JavaScript and CSS files). Chris@0: * Chris@0: * Missing in-place editors are those whose fields are actively being used on Chris@0: * the page but don't have. Chris@0: * Chris@0: * @param {function} callback Chris@0: * Callback function to be called when the missing in-place editors (if any) Chris@0: * have been inserted into the DOM. i.e. they may still be loading. Chris@0: */ Chris@0: function loadMissingEditors(callback) { Chris@0: const loadedEditors = _.keys(Drupal.quickedit.editors); Chris@0: let missingEditors = []; Chris@0: Drupal.quickedit.collections.fields.each((fieldModel) => { Chris@0: const metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID')); Chris@0: if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) { Chris@0: missingEditors.push(metadata.editor); Chris@0: // Set a stub, to prevent subsequent calls to loadMissingEditors() from Chris@0: // loading the same in-place editor again. Loading an in-place editor Chris@0: // requires talking to a server, to download its JavaScript, then Chris@0: // executing its JavaScript, and only then its Drupal.quickedit.editors Chris@0: // entry will be set. Chris@0: Drupal.quickedit.editors[metadata.editor] = false; Chris@0: } Chris@0: }); Chris@0: missingEditors = _.uniq(missingEditors); Chris@0: if (missingEditors.length === 0) { Chris@0: callback(); Chris@0: return; Chris@0: } Chris@0: Chris@0: // @see https://www.drupal.org/node/2029999. Chris@0: // Create a Drupal.Ajax instance to load the form. Chris@0: const loadEditorsAjax = Drupal.ajax({ Chris@0: url: Drupal.url('quickedit/attachments'), Chris@0: submit: { 'editors[]': missingEditors }, Chris@0: }); Chris@0: // Implement a scoped insert AJAX command: calls the callback after all AJAX Chris@0: // command functions have been executed (hence the deferred calling). Chris@0: const realInsert = Drupal.AjaxCommands.prototype.insert; Chris@0: loadEditorsAjax.commands.insert = function (ajax, response, status) { Chris@0: _.defer(callback); Chris@0: realInsert(ajax, response, status); Chris@0: }; Chris@0: // Trigger the AJAX request, which will should return AJAX commands to Chris@0: // insert any missing attachments. Chris@0: loadEditorsAjax.execute(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Attempts to set up a "Quick edit" link and corresponding EntityModel. Chris@0: * Chris@0: * @param {object} contextualLink Chris@0: * An object with the following properties: Chris@0: * - String entityID: a Quick Edit entity identifier, e.g. "node/1" or Chris@0: * "block_content/5". Chris@0: * - String entityInstanceID: a Quick Edit entity instance identifier, Chris@0: * e.g. 0, 1 or n (depending on whether it's the first, second, or n+1st Chris@0: * instance of this entity). Chris@0: * - DOM el: element pointing to the contextual links placeholder for this Chris@0: * entity. Chris@0: * - DOM region: element pointing to the contextual region of this entity. Chris@0: * Chris@0: * @return {bool} Chris@0: * Returns true when a contextual the given contextual link metadata can be Chris@0: * removed from the queue (either because the contextual link has been set Chris@0: * up or because it is certain that in-place editing is not allowed for any Chris@0: * of its fields). Returns false otherwise. Chris@0: */ Chris@0: function initializeEntityContextualLink(contextualLink) { Chris@0: const metadata = Drupal.quickedit.metadata; Chris@0: // Check if the user has permission to edit at least one of them. Chris@0: function hasFieldWithPermission(fieldIDs) { Chris@0: for (let i = 0; i < fieldIDs.length; i++) { Chris@0: const fieldID = fieldIDs[i]; Chris@0: if (metadata.get(fieldID, 'access') === true) { Chris@0: return true; Chris@0: } Chris@0: } Chris@0: return false; Chris@0: } Chris@0: Chris@0: // Checks if the metadata for all given field IDs exists. Chris@0: function allMetadataExists(fieldIDs) { Chris@0: return fieldIDs.length === metadata.intersection(fieldIDs).length; Chris@0: } Chris@0: Chris@0: // Find all fields for this entity instance and collect their field IDs. Chris@0: const fields = _.where(fieldsAvailableQueue, { Chris@0: entityID: contextualLink.entityID, Chris@0: entityInstanceID: contextualLink.entityInstanceID, Chris@0: }); Chris@0: const fieldIDs = _.pluck(fields, 'fieldID'); Chris@0: Chris@0: // No fields found yet. Chris@0: if (fieldIDs.length === 0) { Chris@0: return false; Chris@0: } Chris@0: // The entity for the given contextual link contains at least one field that Chris@0: // the current user may edit in-place; instantiate EntityModel, Chris@0: // EntityDecorationView and ContextualLinkView. Chris@0: else if (hasFieldWithPermission(fieldIDs)) { Chris@0: const entityModel = new Drupal.quickedit.EntityModel({ Chris@0: el: contextualLink.region, Chris@0: entityID: contextualLink.entityID, Chris@0: entityInstanceID: contextualLink.entityInstanceID, Chris@0: id: `${contextualLink.entityID}[${contextualLink.entityInstanceID}]`, Chris@0: label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label'), Chris@0: }); Chris@0: Drupal.quickedit.collections.entities.add(entityModel); Chris@0: // Create an EntityDecorationView associated with the root DOM node of the Chris@0: // entity. Chris@0: const entityDecorationView = new Drupal.quickedit.EntityDecorationView({ Chris@0: el: contextualLink.region, Chris@0: model: entityModel, Chris@0: }); Chris@0: entityModel.set('entityDecorationView', entityDecorationView); Chris@0: Chris@0: // Initialize all queued fields within this entity (creates FieldModels). Chris@0: _.each(fields, (field) => { Chris@0: initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID); Chris@0: }); Chris@0: fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields); Chris@0: Chris@0: // Initialization should only be called once. Use Underscore's once method Chris@0: // to get a one-time use version of the function. Chris@0: const initContextualLink = _.once(() => { Chris@0: const $links = $(contextualLink.el).find('.contextual-links'); Chris@0: const contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({ Chris@0: el: $('
  • ').prependTo($links), Chris@0: model: entityModel, Chris@0: appModel: Drupal.quickedit.app.model, Chris@0: }, options)); Chris@0: entityModel.set('contextualLinkView', contextualLinkView); Chris@0: }); Chris@0: Chris@0: // Set up ContextualLinkView after loading any missing in-place editors. Chris@0: loadMissingEditors(initContextualLink); Chris@0: Chris@0: return true; Chris@0: } Chris@0: // There was not at least one field that the current user may edit in-place, Chris@0: // even though the metadata for all fields within this entity is available. Chris@0: else if (allMetadataExists(fieldIDs)) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Delete models and queue items that are contained within a given context. Chris@0: * Chris@0: * Deletes any contained EntityModels (plus their associated FieldModels and Chris@0: * ContextualLinkView) and FieldModels, as well as the corresponding queues. Chris@0: * Chris@0: * After EntityModels, FieldModels must also be deleted, because it is Chris@0: * possible in Drupal for a field DOM element to exist outside of the entity Chris@0: * DOM element, e.g. when viewing the full node, the title of the node is not Chris@0: * rendered within the node (the entity) but as the page title. Chris@0: * Chris@0: * Note: this will not delete an entity that is actively being in-place Chris@0: * edited. Chris@0: * Chris@0: * @param {jQuery} $context Chris@0: * The context within which to delete. Chris@0: */ Chris@0: function deleteContainedModelsAndQueues($context) { Chris@0: $context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each((index, entityElement) => { Chris@0: // Delete entity model. Chris@0: const entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement }); Chris@0: if (entityModel) { Chris@0: const contextualLinkView = entityModel.get('contextualLinkView'); Chris@0: contextualLinkView.undelegateEvents(); Chris@0: contextualLinkView.remove(); Chris@0: // Remove the EntityDecorationView. Chris@0: entityModel.get('entityDecorationView').remove(); Chris@0: // Destroy the EntityModel; this will also destroy its FieldModels. Chris@0: entityModel.destroy(); Chris@0: } Chris@0: Chris@0: // Filter queue. Chris@0: function hasOtherRegion(contextualLink) { Chris@0: return contextualLink.region !== entityElement; Chris@0: } Chris@0: Chris@0: contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion); Chris@0: }); Chris@0: Chris@0: $context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each((index, fieldElement) => { Chris@0: // Delete field models. Chris@0: Drupal.quickedit.collections.fields.chain() Chris@0: .filter(fieldModel => fieldModel.get('el') === fieldElement) Chris@0: .invoke('destroy'); Chris@0: Chris@0: // Filter queues. Chris@0: function hasOtherFieldElement(field) { Chris@0: return field.el !== fieldElement; Chris@0: } Chris@0: Chris@0: fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement); Chris@0: fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement); Chris@0: }); Chris@0: } Chris@0: }(jQuery, _, Backbone, Drupal, drupalSettings, window.JSON, window.sessionStorage));