quickedit.es6.js 25 KB

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