core/modules/edit/js/edit.js | 76 ++++++++++++++++---- core/modules/edit/js/editors/formEditor.js | 4 +- core/modules/edit/js/models/EntityModel.js | 14 ++-- core/modules/edit/js/models/FieldModel.js | 6 +- core/modules/edit/js/views/EditorView.js | 4 +- core/modules/edit/js/views/FieldToolbarView.js | 2 +- .../editor/js/editor.formattedTextEditor.js | 4 +- 7 files changed, 83 insertions(+), 27 deletions(-) diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index b8afb6c..ab1f265 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -54,11 +54,35 @@ var fieldsAvailableQueue = []; */ var contextualLinksQueue = []; +/** + * Tracks how many instances exist for each unique entity. Contains key-value + * pairs: + * - String entityID + * - Number count + */ +var entityInstancesTracker = {}; + Drupal.behaviors.edit = { attach: function (context) { // Initialize the Edit app once per page load. $('body').once('edit-init', initEdit); + // Process each entity element: identical entities that appear multiple + // times will get a numeric identifier, starting at 0. + $(context).find('[data-edit-entity-id]').once('edit').each(function (index, entityElement) { + var entityID = entityElement.getAttribute('data-edit-entity-id'); + if (!entityInstancesTracker.hasOwnProperty(entityID)) { + entityInstancesTracker[entityID] = 0; + } + else { + entityInstancesTracker[entityID]++; + } + + // Set the calculated entity instance ID for this element. + var entityInstanceID = entityInstancesTracker[entityID]; + entityElement.setAttribute('data-edit-entity-instance-id', entityInstanceID); + }); + // Process each field element: queue to be used or to fetch metadata. // When a field is being rerendered after editing, it will be processed // immediately. New fields will be unable to be processed immediately, but @@ -122,6 +146,7 @@ $(document).on('drupalContextualLinkAdded', function (event, data) { if (data.$region.is('[data-edit-entity-id]')) { var contextualLink = { entityID: data.$region.attr('data-edit-entity-id'), + entityInstanceID: data.$region.attr('data-edit-entity-instance-id'), el: data.$el[0], region: data.$region[0] }; @@ -175,13 +200,20 @@ function processField (fieldElement) { var metadata = Drupal.edit.metadata; var fieldID = fieldElement.getAttribute('data-edit-field-id'); var entityID = extractEntityID(fieldID); + // Figure out the instance ID by looking at the ancestor [data-edit-entity-id] + // element's data-edit-entity-instance-id attribute. + var entityInstanceID = $(fieldElement) + .closest('[data-edit-entity-id="' + entityID + '"]') + .get(0) + .getAttribute('data-edit-entity-instance-id'); // Early-return if metadata for this field is missing. if (!metadata.has(fieldID)) { fieldsMetadataQueue.push({ el: fieldElement, fieldID: fieldID, - entityID: entityID + entityID: entityID, + entityInstanceID: entityInstanceID }); return; } @@ -192,13 +224,13 @@ function processField (fieldElement) { // If an EntityModel for this field already exists (and hence also a "Quick // edit" contextual link), then initialize it immediately. - if (Drupal.edit.collections.entities.where({ id: entityID }).length > 0) { - initializeField(fieldElement, fieldID); + if (Drupal.edit.collections.entities.where({ entityID: entityID, entityInstanceID: entityInstanceID }).length > 0) { + initializeField(fieldElement, fieldID, entityID, entityInstanceID); } // Otherwise: queue the field. It is now available to be set up when its // corresponding entity becomes in-place editable. else { - fieldsAvailableQueue.push({ el: fieldElement, fieldID: fieldID, entityID: entityID }); + fieldsAvailableQueue.push({ el: fieldElement, fieldID: fieldID, entityID: entityID, entityInstanceID: entityInstanceID }); } } @@ -209,17 +241,24 @@ function processField (fieldElement) { * The field's DOM element. * @param String fieldID * The field's ID. + * @param String entityID + * The field's entity's ID. + * @param String entityInstanceID + * The field's entity's instance ID. */ -function initializeField (fieldElement, fieldID) { - var entityId = extractEntityID(fieldID); - var entity = Drupal.edit.collections.entities.where({ id: entityId })[0]; +function initializeField (fieldElement, fieldID, entityID, entityInstanceID) { + var entity = Drupal.edit.collections.entities.where({ + entityID: entityID, + entityInstanceID: entityInstanceID + })[0]; $(fieldElement).addClass('edit-field'); // The FieldModel stores the state of an in-place editable entity field. var field = new Drupal.edit.FieldModel({ el: fieldElement, - id: fieldID, + fieldID: fieldID, + id: fieldID + '[' + entity.get('entityInstanceID') + ']', entity: entity, metadata: Drupal.edit.metadata.get(fieldID), acceptStateChange: _.bind(Drupal.edit.app.acceptEditorStateChange, Drupal.edit.app) @@ -281,8 +320,7 @@ 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); + var metadata = Drupal.edit.metadata.get(fieldModel.get('fieldID')); if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) { missingEditors.push(metadata.editor); } @@ -324,8 +362,11 @@ function loadMissingEditors (callback) { * * @param Object contextualLink * An object with the following properties: - * - String entity: an Edit entity identifier, e.g. "node/1" or + * - String entityID: an Edit entity identifier, e.g. "node/1" or * "custom_block/5". + * - String entityInstanceID: an Edit entity instance identifier, e.g. 0, 1 + * or n (depending on whether it's the first, second, or n+1st instance of + * this entity). * - DOM el: element pointing to the contextual links placeholder for this * entity. * - DOM region: element pointing to the contextual region for this entity. @@ -354,8 +395,11 @@ function initializeEntityContextualLink (contextualLink) { return fieldIDs.length === metadata.intersection(fieldIDs).length; } - // Find all fields for this entity and collect their field IDs. - var fields = _.where(fieldsAvailableQueue, { entityID: contextualLink.entityID }); + // Find all fields for this entity instance and collect their field IDs. + var fields = _.where(fieldsAvailableQueue, { + entityID: contextualLink.entityID, + entityInstanceID: contextualLink.entityInstanceID + }); var fieldIDs = _.pluck(fields, 'fieldID'); // No fields found yet. @@ -368,7 +412,9 @@ function initializeEntityContextualLink (contextualLink) { else if (hasFieldWithPermission(fieldIDs)) { var entityModel = new Drupal.edit.EntityModel({ el: contextualLink.region, - id: contextualLink.entityID, + entityID: contextualLink.entityID, + entityInstanceID: contextualLink.entityInstanceID, + id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']', label: Drupal.edit.metadata.get(contextualLink.entityID, 'label') }); Drupal.edit.collections.entities.add(entityModel); @@ -381,7 +427,7 @@ function initializeEntityContextualLink (contextualLink) { // Initialize all queued fields within this entity (creates FieldModels). _.each(fields, function (field) { - initializeField(field.el, field.fieldID); + initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID); }); fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields); diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js index 48827e1..10f380b 100644 --- a/core/modules/edit/js/editors/formEditor.js +++ b/core/modules/edit/js/editors/formEditor.js @@ -65,7 +65,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ var fieldModel = this.fieldModel; // Generate a DOM-compatible ID for the form container DOM element. - var id = 'edit-form-for-' + fieldModel.id.replace(/\//g, '_'); + var id = 'edit-form-for-' + fieldModel.id.replace(/[\/\[\]]/g, '_'); // Render form container. var $formContainer = this.$formContainer = $(Drupal.theme('editFormContainer', { @@ -90,7 +90,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({ // Load form, insert it into the form container and attach event handlers. var formOptions = { - fieldID: fieldModel.id, + fieldID: fieldModel.get('fieldID'), $el: this.$el, nocssjs: false, // Reset an existing entry for this entity in the TempStore (if any) when diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js index 6fefaf3..bbb623a 100644 --- a/core/modules/edit/js/models/EntityModel.js +++ b/core/modules/edit/js/models/EntityModel.js @@ -13,6 +13,12 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ // entities in the DOM to EntityModels in memory. el: null, // An entity ID, of the form "/", e.g. "node/1". + entityID: null, + // An entity instance ID. The first intance of a specific entity (i.e. with + // a given entity ID) is assigned 0, the second 1, and so on. + entityInstanceID: null, + // The unique ID of this entity instance on the page, of the form "/[entity instance ID]", e.g. "node/1[0]". id: null, // The label of the entity. label: null, @@ -248,7 +254,7 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ // signifying just that may be rendered. fieldModel.set('inTempStore', true); // Remember that this field is in TempStore, restore when rerendered. - fieldsInTempStore.push(fieldModel.id); + fieldsInTempStore.push(fieldModel.get('fieldID')); fieldsInTempStore = _.uniq(fieldsInTempStore); entityModel.set('fieldsInTempStore', fieldsInTempStore); } @@ -256,7 +262,7 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ // 'inactive' state, then this is a field for this entity that got // rerendered. Restore its previous 'inTempStore' attribute value. else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') { - fieldModel.set('inTempStore', _.intersection([fieldModel.id], fieldsInTempStore).length > 0); + fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0); } break; @@ -273,7 +279,7 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ // 'inactive' state, then this is a field for this entity that got // rerendered. Restore its previous 'inTempStore' attribute value. else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') { - fieldModel.set('inTempStore', _.intersection([fieldModel.id], fieldsInTempStore).length > 0); + fieldModel.set('inTempStore', _.intersection([fieldModel.get('fieldID')], fieldsInTempStore).length > 0); } // Attempt to save the entity. If the entity's fields are not yet all in @@ -335,7 +341,7 @@ Drupal.edit.EntityModel = Backbone.Model.extend({ var $el = $('#edit-entity-toolbar').find('.action-save'); // This is the span element inside the button. // Create a Drupal.ajax instance to save the entity. var entitySaverAjax = new Drupal.ajax(id, $el, { - url: Drupal.url('edit/entity/' + entityModel.id), + url: Drupal.url('edit/entity/' + entityModel.get('entityID')), event: 'edit-save.edit', progress: { type: 'none' } }); diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js index 159a473..ba5b1be 100644 --- a/core/modules/edit/js/models/FieldModel.js +++ b/core/modules/edit/js/models/FieldModel.js @@ -15,6 +15,10 @@ Drupal.edit.FieldModel = Backbone.Model.extend({ // A field ID, of the form // "////", e.g. // "node/1/field_tags/und/full". + fieldID: null, + // The unique ID of this field within its entity instance on the page, of + // the form "////[entity instance ID]", + // e.g. "node/1/field_tags/und/full[0]". id: null, // A Drupal.edit.EntityModel. Its "fields" attribute, which is a // FieldCollection, is automatically updated to include this FieldModel. @@ -102,7 +106,7 @@ Drupal.edit.FieldModel = Backbone.Model.extend({ * An entity ID: a string of the format `/`. */ getEntityID: function () { - return this.id.split('/').slice(0, 2).join('/'); + return this.get('fieldID').split('/').slice(0, 2).join('/'); } }, { diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js index 408989b..def53ab 100644 --- a/core/modules/edit/js/views/EditorView.js +++ b/core/modules/edit/js/views/EditorView.js @@ -172,7 +172,7 @@ Drupal.edit.EditorView = Backbone.View.extend({ save: function () { var fieldModel = this.fieldModel; var editorModel = this.model; - var backstageId = 'edit_backstage-' + this.fieldModel.id.replace(/[\/\_\s]/g, '-'); + var backstageId = 'edit_backstage-' + this.fieldModel.id.replace(/[\/\[\]\_\s]/g, '-'); function fillAndSubmitForm (value) { var $form = $('#' + backstageId).find('form'); @@ -186,7 +186,7 @@ Drupal.edit.EditorView = Backbone.View.extend({ } var formOptions = { - fieldID: this.fieldModel.id, + fieldID: this.fieldModel.get('fieldID'), $el: this.$el, nocssjs: true, // Reset an existing entry for this entity in the TempStore (if any) when diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js index 497ac67..7b487de 100644 --- a/core/modules/edit/js/views/FieldToolbarView.js +++ b/core/modules/edit/js/views/FieldToolbarView.js @@ -25,7 +25,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ this.$root = this.$el; // Generate a DOM-compatible ID for the form container DOM element. - this._id = 'edit-toolbar-for-' + this.model.id.replace(/\//g, '_'); + this._id = 'edit-toolbar-for-' + this.model.id.replace(/[\/\[\]]/g, '_'); this.model.on('change:state', this.stateChange, this); }, diff --git a/core/modules/editor/js/editor.formattedTextEditor.js b/core/modules/editor/js/editor.formattedTextEditor.js index 9756349..51d0c29 100644 --- a/core/modules/editor/js/editor.formattedTextEditor.js +++ b/core/modules/editor/js/editor.formattedTextEditor.js @@ -34,7 +34,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ initialize: function (options) { Drupal.edit.EditorView.prototype.initialize.call(this, options); - var metadata = Drupal.edit.metadata.get(this.fieldModel.id, 'custom'); + var metadata = Drupal.edit.metadata.get(this.fieldModel.get('fieldID'), 'custom'); this.textFormat = drupalSettings.editor.formats[metadata.format]; this.textFormatHasTransformations = metadata.formatHasTransformations; this.textEditor = Drupal.editors[this.textFormat.editor]; @@ -160,7 +160,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ * @see \Drupal\editor\Ajax\GetUntransformedTextCommand */ _getUntransformedText: function (callback) { - var fieldID = this.fieldModel.id; + var fieldID = this.fieldModel.get('fieldID'); // Create a Drupal.ajax instance to load the form. var textLoaderAjax = new Drupal.ajax(fieldID, this.$el, {