Drupal investigation

quickedit.js 24KB

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