Mercurial > hg > isophonics-drupal-site
comparison core/modules/quickedit/js/quickedit.es6.js @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 /** | |
2 * @file | |
3 * Attaches behavior for the Quick Edit module. | |
4 * | |
5 * Everything happens asynchronously, to allow for: | |
6 * - dynamically rendered contextual links | |
7 * - asynchronously retrieved (and cached) per-field in-place editing metadata | |
8 * - asynchronous setup of in-place editable field and "Quick edit" link. | |
9 * | |
10 * To achieve this, there are several queues: | |
11 * - fieldsMetadataQueue: fields whose metadata still needs to be fetched. | |
12 * - fieldsAvailableQueue: queue of fields whose metadata is known, and for | |
13 * which it has been confirmed that the user has permission to edit them. | |
14 * However, FieldModels will only be created for them once there's a | |
15 * contextual link for their entity: when it's possible to initiate editing. | |
16 * - contextualLinksQueue: queue of contextual links on entities for which it | |
17 * is not yet known whether the user has permission to edit at >=1 of them. | |
18 */ | |
19 | |
20 (function ($, _, Backbone, Drupal, drupalSettings, JSON, storage) { | |
21 const options = $.extend(drupalSettings.quickedit, | |
22 // Merge strings on top of drupalSettings so that they are not mutable. | |
23 { | |
24 strings: { | |
25 quickEdit: Drupal.t('Quick edit'), | |
26 }, | |
27 }, | |
28 ); | |
29 | |
30 /** | |
31 * Tracks fields without metadata. Contains objects with the following keys: | |
32 * - DOM el | |
33 * - String fieldID | |
34 * - String entityID | |
35 */ | |
36 let fieldsMetadataQueue = []; | |
37 | |
38 /** | |
39 * Tracks fields ready for use. Contains objects with the following keys: | |
40 * - DOM el | |
41 * - String fieldID | |
42 * - String entityID | |
43 */ | |
44 let fieldsAvailableQueue = []; | |
45 | |
46 /** | |
47 * Tracks contextual links on entities. Contains objects with the following | |
48 * keys: | |
49 * - String entityID | |
50 * - DOM el | |
51 * - DOM region | |
52 */ | |
53 let contextualLinksQueue = []; | |
54 | |
55 /** | |
56 * Tracks how many instances exist for each unique entity. Contains key-value | |
57 * pairs: | |
58 * - String entityID | |
59 * - Number count | |
60 */ | |
61 const entityInstancesTracker = {}; | |
62 | |
63 /** | |
64 * | |
65 * @type {Drupal~behavior} | |
66 */ | |
67 Drupal.behaviors.quickedit = { | |
68 attach(context) { | |
69 // Initialize the Quick Edit app once per page load. | |
70 $('body').once('quickedit-init').each(initQuickEdit); | |
71 | |
72 // Find all in-place editable fields, if any. | |
73 const $fields = $(context).find('[data-quickedit-field-id]').once('quickedit'); | |
74 if ($fields.length === 0) { | |
75 return; | |
76 } | |
77 | |
78 // Process each entity element: identical entities that appear multiple | |
79 // times will get a numeric identifier, starting at 0. | |
80 $(context).find('[data-quickedit-entity-id]').once('quickedit').each((index, entityElement) => { | |
81 processEntity(entityElement); | |
82 }); | |
83 | |
84 // Process each field element: queue to be used or to fetch metadata. | |
85 // When a field is being rerendered after editing, it will be processed | |
86 // immediately. New fields will be unable to be processed immediately, | |
87 // but will instead be queued to have their metadata fetched, which occurs | |
88 // below in fetchMissingMetaData(). | |
89 $fields.each((index, fieldElement) => { | |
90 processField(fieldElement); | |
91 }); | |
92 | |
93 // Entities and fields on the page have been detected, try to set up the | |
94 // contextual links for those entities that already have the necessary | |
95 // meta- data in the client-side cache. | |
96 contextualLinksQueue = _.filter(contextualLinksQueue, contextualLink => !initializeEntityContextualLink(contextualLink)); | |
97 | |
98 // Fetch metadata for any fields that are queued to retrieve it. | |
99 fetchMissingMetadata((fieldElementsWithFreshMetadata) => { | |
100 // Metadata has been fetched, reprocess fields whose metadata was | |
101 // missing. | |
102 _.each(fieldElementsWithFreshMetadata, processField); | |
103 | |
104 // Metadata has been fetched, try to set up more contextual links now. | |
105 contextualLinksQueue = _.filter(contextualLinksQueue, contextualLink => !initializeEntityContextualLink(contextualLink)); | |
106 }); | |
107 }, | |
108 detach(context, settings, trigger) { | |
109 if (trigger === 'unload') { | |
110 deleteContainedModelsAndQueues($(context)); | |
111 } | |
112 }, | |
113 }; | |
114 | |
115 /** | |
116 * | |
117 * @namespace | |
118 */ | |
119 Drupal.quickedit = { | |
120 | |
121 /** | |
122 * A {@link Drupal.quickedit.AppView} instance. | |
123 */ | |
124 app: null, | |
125 | |
126 /** | |
127 * @type {object} | |
128 * | |
129 * @prop {Array.<Drupal.quickedit.EntityModel>} entities | |
130 * @prop {Array.<Drupal.quickedit.FieldModel>} fields | |
131 */ | |
132 collections: { | |
133 // All in-place editable entities (Drupal.quickedit.EntityModel) on the | |
134 // page. | |
135 entities: null, | |
136 // All in-place editable fields (Drupal.quickedit.FieldModel) on the page. | |
137 fields: null, | |
138 }, | |
139 | |
140 /** | |
141 * In-place editors will register themselves in this object. | |
142 * | |
143 * @namespace | |
144 */ | |
145 editors: {}, | |
146 | |
147 /** | |
148 * Per-field metadata that indicates whether in-place editing is allowed, | |
149 * which in-place editor should be used, etc. | |
150 * | |
151 * @namespace | |
152 */ | |
153 metadata: { | |
154 | |
155 /** | |
156 * Check if a field exists in storage. | |
157 * | |
158 * @param {string} fieldID | |
159 * The field id to check. | |
160 * | |
161 * @return {bool} | |
162 * Whether it was found or not. | |
163 */ | |
164 has(fieldID) { | |
165 return storage.getItem(this._prefixFieldID(fieldID)) !== null; | |
166 }, | |
167 | |
168 /** | |
169 * Add metadata to a field id. | |
170 * | |
171 * @param {string} fieldID | |
172 * The field ID to add data to. | |
173 * @param {object} metadata | |
174 * Metadata to add. | |
175 */ | |
176 add(fieldID, metadata) { | |
177 storage.setItem(this._prefixFieldID(fieldID), JSON.stringify(metadata)); | |
178 }, | |
179 | |
180 /** | |
181 * Get a key from a field id. | |
182 * | |
183 * @param {string} fieldID | |
184 * The field ID to check. | |
185 * @param {string} [key] | |
186 * The key to check. If empty, will return all metadata. | |
187 * | |
188 * @return {object|*} | |
189 * The value for the key, if defined. Otherwise will return all metadata | |
190 * for the specified field id. | |
191 * | |
192 */ | |
193 get(fieldID, key) { | |
194 const metadata = JSON.parse(storage.getItem(this._prefixFieldID(fieldID))); | |
195 return (typeof key === 'undefined') ? metadata : metadata[key]; | |
196 }, | |
197 | |
198 /** | |
199 * Prefix the field id. | |
200 * | |
201 * @param {string} fieldID | |
202 * The field id to prefix. | |
203 * | |
204 * @return {string} | |
205 * A prefixed field id. | |
206 */ | |
207 _prefixFieldID(fieldID) { | |
208 return `Drupal.quickedit.metadata.${fieldID}`; | |
209 }, | |
210 | |
211 /** | |
212 * Unprefix the field id. | |
213 * | |
214 * @param {string} fieldID | |
215 * The field id to unprefix. | |
216 * | |
217 * @return {string} | |
218 * An unprefixed field id. | |
219 */ | |
220 _unprefixFieldID(fieldID) { | |
221 // Strip "Drupal.quickedit.metadata.", which is 26 characters long. | |
222 return fieldID.substring(26); | |
223 }, | |
224 | |
225 /** | |
226 * Intersection calculation. | |
227 * | |
228 * @param {Array} fieldIDs | |
229 * An array of field ids to compare to prefix field id. | |
230 * | |
231 * @return {Array} | |
232 * The intersection found. | |
233 */ | |
234 intersection(fieldIDs) { | |
235 const prefixedFieldIDs = _.map(fieldIDs, this._prefixFieldID); | |
236 const intersection = _.intersection(prefixedFieldIDs, _.keys(sessionStorage)); | |
237 return _.map(intersection, this._unprefixFieldID); | |
238 }, | |
239 }, | |
240 }; | |
241 | |
242 // Clear the Quick Edit metadata cache whenever the current user's set of | |
243 // permissions changes. | |
244 const permissionsHashKey = Drupal.quickedit.metadata._prefixFieldID('permissionsHash'); | |
245 const permissionsHashValue = storage.getItem(permissionsHashKey); | |
246 const permissionsHash = drupalSettings.user.permissionsHash; | |
247 if (permissionsHashValue !== permissionsHash) { | |
248 if (typeof permissionsHash === 'string') { | |
249 _.chain(storage).keys().each((key) => { | |
250 if (key.substring(0, 26) === 'Drupal.quickedit.metadata.') { | |
251 storage.removeItem(key); | |
252 } | |
253 }); | |
254 } | |
255 storage.setItem(permissionsHashKey, permissionsHash); | |
256 } | |
257 | |
258 /** | |
259 * Detect contextual links on entities annotated by quickedit. | |
260 * | |
261 * Queue contextual links to be processed. | |
262 * | |
263 * @param {jQuery.Event} event | |
264 * The `drupalContextualLinkAdded` event. | |
265 * @param {object} data | |
266 * An object containing the data relevant to the event. | |
267 * | |
268 * @listens event:drupalContextualLinkAdded | |
269 */ | |
270 $(document).on('drupalContextualLinkAdded', (event, data) => { | |
271 if (data.$region.is('[data-quickedit-entity-id]')) { | |
272 // If the contextual link is cached on the client side, an entity instance | |
273 // will not yet have been assigned. So assign one. | |
274 if (!data.$region.is('[data-quickedit-entity-instance-id]')) { | |
275 data.$region.once('quickedit'); | |
276 processEntity(data.$region.get(0)); | |
277 } | |
278 const contextualLink = { | |
279 entityID: data.$region.attr('data-quickedit-entity-id'), | |
280 entityInstanceID: data.$region.attr('data-quickedit-entity-instance-id'), | |
281 el: data.$el[0], | |
282 region: data.$region[0], | |
283 }; | |
284 // Set up contextual links for this, otherwise queue it to be set up | |
285 // later. | |
286 if (!initializeEntityContextualLink(contextualLink)) { | |
287 contextualLinksQueue.push(contextualLink); | |
288 } | |
289 } | |
290 }); | |
291 | |
292 /** | |
293 * Extracts the entity ID from a field ID. | |
294 * | |
295 * @param {string} fieldID | |
296 * A field ID: a string of the format | |
297 * `<entity type>/<id>/<field name>/<language>/<view mode>`. | |
298 * | |
299 * @return {string} | |
300 * An entity ID: a string of the format `<entity type>/<id>`. | |
301 */ | |
302 function extractEntityID(fieldID) { | |
303 return fieldID.split('/').slice(0, 2).join('/'); | |
304 } | |
305 | |
306 /** | |
307 * Initialize the Quick Edit app. | |
308 * | |
309 * @param {HTMLElement} bodyElement | |
310 * This document's body element. | |
311 */ | |
312 function initQuickEdit(bodyElement) { | |
313 Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection(); | |
314 Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection(); | |
315 | |
316 // Instantiate AppModel (application state) and AppView, which is the | |
317 // controller of the whole in-place editing experience. | |
318 Drupal.quickedit.app = new Drupal.quickedit.AppView({ | |
319 el: bodyElement, | |
320 model: new Drupal.quickedit.AppModel(), | |
321 entitiesCollection: Drupal.quickedit.collections.entities, | |
322 fieldsCollection: Drupal.quickedit.collections.fields, | |
323 }); | |
324 } | |
325 | |
326 /** | |
327 * Assigns the entity an instance ID. | |
328 * | |
329 * @param {HTMLElement} entityElement | |
330 * A Drupal Entity API entity's DOM element with a data-quickedit-entity-id | |
331 * attribute. | |
332 */ | |
333 function processEntity(entityElement) { | |
334 const entityID = entityElement.getAttribute('data-quickedit-entity-id'); | |
335 if (!entityInstancesTracker.hasOwnProperty(entityID)) { | |
336 entityInstancesTracker[entityID] = 0; | |
337 } | |
338 else { | |
339 entityInstancesTracker[entityID]++; | |
340 } | |
341 | |
342 // Set the calculated entity instance ID for this element. | |
343 const entityInstanceID = entityInstancesTracker[entityID]; | |
344 entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID); | |
345 } | |
346 | |
347 /** | |
348 * Fetch the field's metadata; queue or initialize it (if EntityModel exists). | |
349 * | |
350 * @param {HTMLElement} fieldElement | |
351 * A Drupal Field API field's DOM element with a data-quickedit-field-id | |
352 * attribute. | |
353 */ | |
354 function processField(fieldElement) { | |
355 const metadata = Drupal.quickedit.metadata; | |
356 const fieldID = fieldElement.getAttribute('data-quickedit-field-id'); | |
357 const entityID = extractEntityID(fieldID); | |
358 // Figure out the instance ID by looking at the ancestor | |
359 // [data-quickedit-entity-id] element's data-quickedit-entity-instance-id | |
360 // attribute. | |
361 const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`; | |
362 const $entityElement = $(entityElementSelector); | |
363 | |
364 // If there are no elements returned from `entityElementSelector` | |
365 // throw an error. Check the browser console for this message. | |
366 if (!$entityElement.length) { | |
367 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.`; | |
368 } | |
369 let entityElement = $(fieldElement).closest($entityElement); | |
370 | |
371 // In the case of a full entity view page, the entity title is rendered | |
372 // outside of "the entity DOM node": it's rendered as the page title. So in | |
373 // this case, we find the lowest common parent element (deepest in the tree) | |
374 // and consider that the entity element. | |
375 if (entityElement.length === 0) { | |
376 const $lowestCommonParent = $entityElement.parents().has(fieldElement).first(); | |
377 entityElement = $lowestCommonParent.find($entityElement); | |
378 } | |
379 const entityInstanceID = entityElement | |
380 .get(0) | |
381 .getAttribute('data-quickedit-entity-instance-id'); | |
382 | |
383 // Early-return if metadata for this field is missing. | |
384 if (!metadata.has(fieldID)) { | |
385 fieldsMetadataQueue.push({ | |
386 el: fieldElement, | |
387 fieldID, | |
388 entityID, | |
389 entityInstanceID, | |
390 }); | |
391 return; | |
392 } | |
393 // Early-return if the user is not allowed to in-place edit this field. | |
394 if (metadata.get(fieldID, 'access') !== true) { | |
395 return; | |
396 } | |
397 | |
398 // If an EntityModel for this field already exists (and hence also a "Quick | |
399 // edit" contextual link), then initialize it immediately. | |
400 if (Drupal.quickedit.collections.entities.findWhere({ entityID, entityInstanceID })) { | |
401 initializeField(fieldElement, fieldID, entityID, entityInstanceID); | |
402 } | |
403 // Otherwise: queue the field. It is now available to be set up when its | |
404 // corresponding entity becomes in-place editable. | |
405 else { | |
406 fieldsAvailableQueue.push({ el: fieldElement, fieldID, entityID, entityInstanceID }); | |
407 } | |
408 } | |
409 | |
410 /** | |
411 * Initialize a field; create FieldModel. | |
412 * | |
413 * @param {HTMLElement} fieldElement | |
414 * The field's DOM element. | |
415 * @param {string} fieldID | |
416 * The field's ID. | |
417 * @param {string} entityID | |
418 * The field's entity's ID. | |
419 * @param {string} entityInstanceID | |
420 * The field's entity's instance ID. | |
421 */ | |
422 function initializeField(fieldElement, fieldID, entityID, entityInstanceID) { | |
423 const entity = Drupal.quickedit.collections.entities.findWhere({ | |
424 entityID, | |
425 entityInstanceID, | |
426 }); | |
427 | |
428 $(fieldElement).addClass('quickedit-field'); | |
429 | |
430 // The FieldModel stores the state of an in-place editable entity field. | |
431 const field = new Drupal.quickedit.FieldModel({ | |
432 el: fieldElement, | |
433 fieldID, | |
434 id: `${fieldID}[${entity.get('entityInstanceID')}]`, | |
435 entity, | |
436 metadata: Drupal.quickedit.metadata.get(fieldID), | |
437 acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app), | |
438 }); | |
439 | |
440 // Track all fields on the page. | |
441 Drupal.quickedit.collections.fields.add(field); | |
442 } | |
443 | |
444 /** | |
445 * Fetches metadata for fields whose metadata is missing. | |
446 * | |
447 * Fields whose metadata is missing are tracked at fieldsMetadataQueue. | |
448 * | |
449 * @param {function} callback | |
450 * A callback function that receives field elements whose metadata will just | |
451 * have been fetched. | |
452 */ | |
453 function fetchMissingMetadata(callback) { | |
454 if (fieldsMetadataQueue.length) { | |
455 const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID'); | |
456 const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el'); | |
457 let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true); | |
458 // Ensure we only request entityIDs for which we don't have metadata yet. | |
459 entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs)); | |
460 fieldsMetadataQueue = []; | |
461 | |
462 $.ajax({ | |
463 url: Drupal.url('quickedit/metadata'), | |
464 type: 'POST', | |
465 data: { | |
466 'fields[]': fieldIDs, | |
467 'entities[]': entityIDs, | |
468 }, | |
469 dataType: 'json', | |
470 success(results) { | |
471 // Store the metadata. | |
472 _.each(results, (fieldMetadata, fieldID) => { | |
473 Drupal.quickedit.metadata.add(fieldID, fieldMetadata); | |
474 }); | |
475 | |
476 callback(fieldElementsWithoutMetadata); | |
477 }, | |
478 }); | |
479 } | |
480 } | |
481 | |
482 /** | |
483 * Loads missing in-place editor's attachments (JavaScript and CSS files). | |
484 * | |
485 * Missing in-place editors are those whose fields are actively being used on | |
486 * the page but don't have. | |
487 * | |
488 * @param {function} callback | |
489 * Callback function to be called when the missing in-place editors (if any) | |
490 * have been inserted into the DOM. i.e. they may still be loading. | |
491 */ | |
492 function loadMissingEditors(callback) { | |
493 const loadedEditors = _.keys(Drupal.quickedit.editors); | |
494 let missingEditors = []; | |
495 Drupal.quickedit.collections.fields.each((fieldModel) => { | |
496 const metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID')); | |
497 if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) { | |
498 missingEditors.push(metadata.editor); | |
499 // Set a stub, to prevent subsequent calls to loadMissingEditors() from | |
500 // loading the same in-place editor again. Loading an in-place editor | |
501 // requires talking to a server, to download its JavaScript, then | |
502 // executing its JavaScript, and only then its Drupal.quickedit.editors | |
503 // entry will be set. | |
504 Drupal.quickedit.editors[metadata.editor] = false; | |
505 } | |
506 }); | |
507 missingEditors = _.uniq(missingEditors); | |
508 if (missingEditors.length === 0) { | |
509 callback(); | |
510 return; | |
511 } | |
512 | |
513 // @see https://www.drupal.org/node/2029999. | |
514 // Create a Drupal.Ajax instance to load the form. | |
515 const loadEditorsAjax = Drupal.ajax({ | |
516 url: Drupal.url('quickedit/attachments'), | |
517 submit: { 'editors[]': missingEditors }, | |
518 }); | |
519 // Implement a scoped insert AJAX command: calls the callback after all AJAX | |
520 // command functions have been executed (hence the deferred calling). | |
521 const realInsert = Drupal.AjaxCommands.prototype.insert; | |
522 loadEditorsAjax.commands.insert = function (ajax, response, status) { | |
523 _.defer(callback); | |
524 realInsert(ajax, response, status); | |
525 }; | |
526 // Trigger the AJAX request, which will should return AJAX commands to | |
527 // insert any missing attachments. | |
528 loadEditorsAjax.execute(); | |
529 } | |
530 | |
531 /** | |
532 * Attempts to set up a "Quick edit" link and corresponding EntityModel. | |
533 * | |
534 * @param {object} contextualLink | |
535 * An object with the following properties: | |
536 * - String entityID: a Quick Edit entity identifier, e.g. "node/1" or | |
537 * "block_content/5". | |
538 * - String entityInstanceID: a Quick Edit entity instance identifier, | |
539 * e.g. 0, 1 or n (depending on whether it's the first, second, or n+1st | |
540 * instance of this entity). | |
541 * - DOM el: element pointing to the contextual links placeholder for this | |
542 * entity. | |
543 * - DOM region: element pointing to the contextual region of this entity. | |
544 * | |
545 * @return {bool} | |
546 * Returns true when a contextual the given contextual link metadata can be | |
547 * removed from the queue (either because the contextual link has been set | |
548 * up or because it is certain that in-place editing is not allowed for any | |
549 * of its fields). Returns false otherwise. | |
550 */ | |
551 function initializeEntityContextualLink(contextualLink) { | |
552 const metadata = Drupal.quickedit.metadata; | |
553 // Check if the user has permission to edit at least one of them. | |
554 function hasFieldWithPermission(fieldIDs) { | |
555 for (let i = 0; i < fieldIDs.length; i++) { | |
556 const fieldID = fieldIDs[i]; | |
557 if (metadata.get(fieldID, 'access') === true) { | |
558 return true; | |
559 } | |
560 } | |
561 return false; | |
562 } | |
563 | |
564 // Checks if the metadata for all given field IDs exists. | |
565 function allMetadataExists(fieldIDs) { | |
566 return fieldIDs.length === metadata.intersection(fieldIDs).length; | |
567 } | |
568 | |
569 // Find all fields for this entity instance and collect their field IDs. | |
570 const fields = _.where(fieldsAvailableQueue, { | |
571 entityID: contextualLink.entityID, | |
572 entityInstanceID: contextualLink.entityInstanceID, | |
573 }); | |
574 const fieldIDs = _.pluck(fields, 'fieldID'); | |
575 | |
576 // No fields found yet. | |
577 if (fieldIDs.length === 0) { | |
578 return false; | |
579 } | |
580 // The entity for the given contextual link contains at least one field that | |
581 // the current user may edit in-place; instantiate EntityModel, | |
582 // EntityDecorationView and ContextualLinkView. | |
583 else if (hasFieldWithPermission(fieldIDs)) { | |
584 const entityModel = new Drupal.quickedit.EntityModel({ | |
585 el: contextualLink.region, | |
586 entityID: contextualLink.entityID, | |
587 entityInstanceID: contextualLink.entityInstanceID, | |
588 id: `${contextualLink.entityID}[${contextualLink.entityInstanceID}]`, | |
589 label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label'), | |
590 }); | |
591 Drupal.quickedit.collections.entities.add(entityModel); | |
592 // Create an EntityDecorationView associated with the root DOM node of the | |
593 // entity. | |
594 const entityDecorationView = new Drupal.quickedit.EntityDecorationView({ | |
595 el: contextualLink.region, | |
596 model: entityModel, | |
597 }); | |
598 entityModel.set('entityDecorationView', entityDecorationView); | |
599 | |
600 // Initialize all queued fields within this entity (creates FieldModels). | |
601 _.each(fields, (field) => { | |
602 initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID); | |
603 }); | |
604 fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields); | |
605 | |
606 // Initialization should only be called once. Use Underscore's once method | |
607 // to get a one-time use version of the function. | |
608 const initContextualLink = _.once(() => { | |
609 const $links = $(contextualLink.el).find('.contextual-links'); | |
610 const contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({ | |
611 el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links), | |
612 model: entityModel, | |
613 appModel: Drupal.quickedit.app.model, | |
614 }, options)); | |
615 entityModel.set('contextualLinkView', contextualLinkView); | |
616 }); | |
617 | |
618 // Set up ContextualLinkView after loading any missing in-place editors. | |
619 loadMissingEditors(initContextualLink); | |
620 | |
621 return true; | |
622 } | |
623 // There was not at least one field that the current user may edit in-place, | |
624 // even though the metadata for all fields within this entity is available. | |
625 else if (allMetadataExists(fieldIDs)) { | |
626 return true; | |
627 } | |
628 | |
629 return false; | |
630 } | |
631 | |
632 /** | |
633 * Delete models and queue items that are contained within a given context. | |
634 * | |
635 * Deletes any contained EntityModels (plus their associated FieldModels and | |
636 * ContextualLinkView) and FieldModels, as well as the corresponding queues. | |
637 * | |
638 * After EntityModels, FieldModels must also be deleted, because it is | |
639 * possible in Drupal for a field DOM element to exist outside of the entity | |
640 * DOM element, e.g. when viewing the full node, the title of the node is not | |
641 * rendered within the node (the entity) but as the page title. | |
642 * | |
643 * Note: this will not delete an entity that is actively being in-place | |
644 * edited. | |
645 * | |
646 * @param {jQuery} $context | |
647 * The context within which to delete. | |
648 */ | |
649 function deleteContainedModelsAndQueues($context) { | |
650 $context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each((index, entityElement) => { | |
651 // Delete entity model. | |
652 const entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement }); | |
653 if (entityModel) { | |
654 const contextualLinkView = entityModel.get('contextualLinkView'); | |
655 contextualLinkView.undelegateEvents(); | |
656 contextualLinkView.remove(); | |
657 // Remove the EntityDecorationView. | |
658 entityModel.get('entityDecorationView').remove(); | |
659 // Destroy the EntityModel; this will also destroy its FieldModels. | |
660 entityModel.destroy(); | |
661 } | |
662 | |
663 // Filter queue. | |
664 function hasOtherRegion(contextualLink) { | |
665 return contextualLink.region !== entityElement; | |
666 } | |
667 | |
668 contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion); | |
669 }); | |
670 | |
671 $context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each((index, fieldElement) => { | |
672 // Delete field models. | |
673 Drupal.quickedit.collections.fields.chain() | |
674 .filter(fieldModel => fieldModel.get('el') === fieldElement) | |
675 .invoke('destroy'); | |
676 | |
677 // Filter queues. | |
678 function hasOtherFieldElement(field) { | |
679 return field.el !== fieldElement; | |
680 } | |
681 | |
682 fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement); | |
683 fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement); | |
684 }); | |
685 } | |
686 }(jQuery, _, Backbone, Drupal, drupalSettings, window.JSON, window.sessionStorage)); |