quickedit.es6.js 24 KB

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