Unverified Commit 5bce5eeb authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2402103 by nod_, bnjmnm, larowlan, flxa, anmolgoyal74, droplet, xjm,...

Issue #2402103 by nod_, bnjmnm, larowlan, flxa, anmolgoyal74, droplet, xjm, catch, gabesullice, lauriii, Dries, justafish: Add once.js to core
parent daf10a06
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@
    "Modernizr": true,
    "Popper": true,
    "Sortable": true,
    "once": true,
    "CKEDITOR": true,
    "tabbable": true
  },
+387 −0
Original line number Diff line number Diff line
/*! @drupal/once - v1.0.0 - 2021-03-04 */
/**
 * Mark DOM elements as processed to prevent multiple initializations.
 *
 * @module @drupal/once
 *
 * @example <!-- Use as a module -->
 * <script type="module">
 *   import once from 'https://unpkg.com/@drupal/once/src/once.js';
 *   const elements = once('my-once-id', 'div');
 *   // Initialize elements.
 *   elements.forEach(el => el.innerHTML = 'processed');
 * </script>
 *
 * @example <!-- Use as a regular script -->
 * <script src="https://unpkg.com/@drupal/once"></script>
 * <script>
 *   const elements = once('my-once-id', 'div');
 *   // Initialize elements.
 *   elements.forEach(el => el.innerHTML = 'processed');
 * </script>
 * @example <!-- Using a single element as input-->
 * <script src="https://unpkg.com/@drupal/once"></script>
 * <script>
 *   // once methods always return an array, to simplify the use with a single
 *   // element use destructuring or the shift method.
 *   const [myElement] = once('my-once-id', document.body);
 *   const myElement = once('my-once-id', document.body).shift();
 * </script>
 */

/**
 * Illegal spaces in ids.
 *
 * @private
 *
 * @type {RegExp}
 */
const wsRE = /[\11\12\14\15\40]+/;

/**
 * Name of the HTML attribute containing an element's once ids.
 *
 * @private
 *
 * @type {string}
 */
const attrName = 'data-once';

/**
 * Shortcut to access the html element.
 *
 * @private
 *
 * @type {HTMLElement}
 */
const doc = document;

/**
 * Helper to access element attributes.
 *
 * @private
 *
 * @param {Element} element
 *   The Element to access the data-once attribute from.
 * @param {string} op
 *   The action to take on the element.
 * @param {string} [value]
 *   Optional value for setAttribute.
 *
 * @return {string|undefined|null|boolean}
 *   Result of the attribute method.
 */
function attr(element, op, value) {
  return element[`${op}Attribute`](attrName, value);
}

/**
 * Return the attribute selector.
 *
 * @private
 *
 * @param {string} id
 *   The id passed by a call to a once() function.
 *
 * @return {string}
 *   The full CSS attribute selector.
 *
 * @throws {TypeError|RangeError}
 */
function attrSelector(id) {
  // Verify the validity of the once id.
  if (typeof id !== 'string') {
    throw new TypeError('once ID must be a string');
  }
  if (id === '' || wsRE.test(id)) {
    throw new RangeError('once ID must not be empty or contain spaces');
  }
  // The id is valid, return the full CSS selector.
  return `[${attrName}~="${id}"]`;
}

/**
 * Verifies that an item is an instance of Element.
 *
 * This function is used during filtering to ensure only DOM elements are
 * processed. once() makes use of get/setAttribute, which are methods
 * inherited from the Element object, so only of Element can be used.
 *
 * @private
 *
 * @param {*} itemToCheck
 *   The item to check.
 *
 * @return {boolean}
 *   True if the item is an instance of Element
 *
 * @throws {TypeError}
 */
function checkElement(itemToCheck) {
  if (!(itemToCheck instanceof Element)) {
    throw new TypeError('The element must be an instance of Element');
  }
  return true;
}

/**
 * Process arguments, query the DOM if necessary.
 *
 * @private
 *
 * @param {NodeList|Array.<Element>|Element|string} selector
 *   A NodeList or array of elements.
 * @param {Document|Element} [context=document]
 *   An element to use as context for querySelectorAll.
 *
 * @return {Array.<Element>}
 *   An array with the processed Id and the list of elements to process.
 */
function getElements(selector, context = doc) {
  if (!selector) {
    throw new TypeError('Selector must not be empty');
  }
  // Assume selector is an array-like value.
  let elements = selector;

  // This is a selector, query the elements.
  if (
    typeof selector === 'string' &&
    (context === doc || checkElement(context))
  ) {
    elements = context.querySelectorAll(selector);
  }
  // This is a single element.
  else if (selector instanceof Element) {
    elements = [selector];
  }

  // Make sure an array is returned and not a NodeList or an Array-like object.
  return Array.prototype.slice.call(elements);
}

/**
 * A helper for applying DOM changes to a filtered set of elements.
 *
 * This makes it possible to filter items that are not instances of Element,
 * then modify their DOM attributes in a single array traversal.
 *
 * @private
 *
 * @param {string} selector
 *   A CSS selector to check against to each element in the array.
 * @param {Array.<Element>} elements
 *   A NodeList or array of elements passed by a call to a once() function.
 * @param {function} [apply]
 *   An optional function to apply on all matched elements.
 *
 * @return {Array.<Element>}
 *   The array of elements that match the CSS selector.
 */
function filterAndModify(selector, elements, apply) {
  return elements.filter(element => {
    const selected = checkElement(element) && element.matches(selector);
    if (selected && apply) {
      apply(element);
    }
    return selected;
  });
}

/**
 * Add or remove an item from a list of once values.
 *
 * This function removes duplicates while adding or removing a once id in a
 * single array traversal.
 *
 * @private
 *
 * @param {Element} element
 *   A space separated string of once ids from a data-drupal-once attribute.
 * @param {string} [add]
 *   The once id to add to the list of values.
 * @param {string} [remove]
 *   The once id to remove from the list of values.
 *
 * @return {undefined}
 *   Nothing to return this is a callback in a foreach.
 */
function updateAttribute(element, { add, remove }) {
  const result = [];
  if (attr(element, 'has')) {
    attr(element, 'get')
      .trim()
      .split(wsRE)
      .forEach(item => {
        if (result.indexOf(item) < 0 && item !== remove) {
          result.push(item);
        }
      });
  }
  if (add) {
    result.push(add);
  }
  const attribute = result.join(' ');
  attr(element, attribute === '' ? 'remove' : 'set', attribute);
}

/**
 * Ensures a JavaScript callback is only executed once on a set of elements.
 *
 * Filters a NodeList or array of elements, removing those already processed
 * by a callback with a given id.
 * This method adds a `data-once` attribute on DOM elements. The value of
 * this attribute identifies if a given callback has been executed on that
 * element.
 *
 * @global
 *
 * @example <caption>Basic usage</caption>
 * const elements = once('my-once-id', '[data-myelement]');
 * @example <caption>Input parameters accepted</caption>
 * // NodeList.
 * once('my-once-id', document.querySelectorAll('[data-myelement]'));
 * // Array or Array-like of Element.
 * once('my-once-id', jQuery('[data-myelement]'));
 * // A CSS selector without a context.
 * once('my-once-id', '[data-myelement]');
 * // A CSS selector with a context.
 * once('my-once-id', '[data-myelement]', document.head);
 * // Single Element.
 * once('my-once-id', document.querySelector('#some-id'));
 * @example <caption>Using a single element</caption>
 * // Once always returns an array, even when passing a single element. Some
 * // forms that can be used to keep code readable.
 * // Destructuring:
 * const [myElement] = once('my-once-id', document.body);
 * // By changing the resulting array, es5 compatible.
 * const myElement = once('my-once-id', document.body).shift();
 *
 * @param {string} id
 *   The id of the once call.
 * @param {NodeList|Array.<Element>|Element|string} selector
 *   A NodeList or array of elements.
 * @param {Document|Element} [context=document]
 *   An element to use as context for querySelectorAll.
 *
 * @return {Array.<Element>}
 *   An array of elements that have not yet been processed by a once call
 *   with a given id.
 */
function once(id, selector, context) {
  return filterAndModify(
    `:not(${attrSelector(id)})`,
    getElements(selector, context),
    element => updateAttribute(element, { add: id }),
  );
}

/**
 * Removes a once id from an element's data-drupal-once attribute value.
 *
 * If a once id is removed from an element's data-drupal-once attribute value,
 * the JavaScript callback associated with that id can be executed on that
 * element again.
 *
 * @method once.remove
 *
 * @example <caption>Basic usage</caption>
 * const elements = once.remove('my-once-id', '[data-myelement]');
 * @example <caption>Input parameters accepted</caption>
 * // NodeList.
 * once.remove('my-once-id', document.querySelectorAll('[data-myelement]'));
 * // Array or Array-like of Element.
 * once.remove('my-once-id', jQuery('[data-myelement]'));
 * // A CSS selector without a context.
 * once.remove('my-once-id', '[data-myelement]');
 * // A CSS selector with a context.
 * once.remove('my-once-id', '[data-myelement]', document.head);
 * // Single Element.
 * once.remove('my-once-id', document.querySelector('#some-id'));
 *
 * @param {string} id
 *   The id of a once call.
 * @param {NodeList|Array.<Element>|Element|string} selector
 *   A NodeList or array of elements to remove the once id from.
 * @param {Document|Element} [context=document]
 *   An element to use as context for querySelectorAll.
 *
 * @return {Array.<Element>}
 *   A filtered array of elements that had been processed by the provided id,
 *   and are now able to be processed again.
 */
once.remove = (id, selector, context) => {
  return filterAndModify(
    attrSelector(id),
    getElements(selector, context),
    element => updateAttribute(element, { remove: id }),
  );
};

/**
 * Finds elements that have been processed by a given once id.
 *
 * Behaves like {@link once} and {@link once.remove} without changing the DOM.
 * To select all DOM nodes processed by a given id, use {@link once.find}.
 *
 * @method once.filter
 *
 * @example <caption>Basic usage</caption>
 * const filteredElements = once.filter('my-once-id', '[data-myelement]');
 * @example <caption>Input parameters accepted</caption>
 * // NodeList.
 * once.filter('my-once-id', document.querySelectorAll('[data-myelement]'));
 * // Array or Array-like of Element.
 * once.filter('my-once-id', jQuery('[data-myelement]'));
 * // A CSS selector without a context.
 * once.filter('my-once-id', '[data-myelement]');
 * // A CSS selector with a context.
 * once.filter('my-once-id', '[data-myelement]', document.head);
 * // Single Element.
 * once.filter('my-once-id', document.querySelector('#some-id'));
 *
 * @param {string} id
 *   The id of the once call.
 * @param {NodeList|Array.<Element>|Element|string} selector
 *   A NodeList or array of elements to remove the once id from.
 * @param {Document|Element} [context=document]
 *   An element to use as context for querySelectorAll.
 *
 * @return {Array.<Element>}
 *   A filtered array of elements that have already been processed by the
 *   provided once id.
 */
once.filter = (id, selector, context) =>
  filterAndModify(attrSelector(id), getElements(selector, context));

/**
 * Finds elements that have been processed by a given once id.
 *
 * Query the 'context' element for elements that already have the
 * corresponding once id value.
 *
 * @method once.find
 *
 * @example <caption>Basic usage</caption>
 * const oncedElements = once.find('my-once-id');
 * @example <caption>Input parameters accepted</caption>
 * // Call without parameters, return all elements with a `data-once` attribute.
 * once.find();
 * // Call without a context.
 * once.find('my-once-id');
 * // Call with a context.
 * once.find('my-once-id', document.head);
 *
 * @param {string} [id]
 *   The id of the once call.
 * @param {Document|Element} [context=document]
 *   Scope of the search for matching elements.
 *
 * @return {Array.<Element>}
 *   A filtered array of elements that have already been processed by the
 *   provided once id.
 */
once.find = (id, context) =>
  getElements(!id ? `[${attrName}]` : attrSelector(id), context);

export default once;
+3 −0
Original line number Diff line number Diff line
/*! @drupal/once - v1.0.0 - 2021-03-04 */
var once=function(){"use strict";var n=/[\11\12\14\15\40]+/,t="data-once",e=document;function r(n,e,r){return n[e+"Attribute"](t,r)}function o(t){if("string"!=typeof t)throw new TypeError("once ID must be a string");if(""===t||n.test(t))throw new RangeError("once ID must not be empty or contain spaces");return'[data-once~="'+t+'"]'}function u(n){if(!(n instanceof Element))throw new TypeError("The element must be an instance of Element");return!0}function i(n,t){if(void 0===t&&(t=e),!n)throw new TypeError("Selector must not be empty");var r=n;return"string"!=typeof n||t!==e&&!u(t)?n instanceof Element&&(r=[n]):r=t.querySelectorAll(n),Array.prototype.slice.call(r)}function c(n,t,e){return t.filter((function(t){var r=u(t)&&t.matches(n);return r&&e&&e(t),r}))}function f(t,e){var o=e.add,u=e.remove,i=[];r(t,"has")&&r(t,"get").trim().split(n).forEach((function(n){i.indexOf(n)<0&&n!==u&&i.push(n)})),o&&i.push(o);var c=i.join(" ");r(t,""===c?"remove":"set",c)}function a(n,t,e){return c(":not("+o(n)+")",i(t,e),(function(t){return f(t,{add:n})}))}return a.remove=function(n,t,e){return c(o(n),i(t,e),(function(t){return f(t,{remove:n})}))},a.filter=function(n,t,e){return c(o(n),i(t,e))},a.find=function(n,t){return i(n?o(n):"[data-once]",t)},a}();
//# sourceMappingURL=once.min.js.map
+1 −0
Original line number Diff line number Diff line
{"version":3,"file":"once.min.js","sources":["once.js"],"sourcesContent":null,"names":["const","wsRE","attrName","doc","document","attr","element","op","value","attrSelector","id","TypeError","test","RangeError","checkElement","itemToCheck","Element","getElements","selector","context","let","elements","querySelectorAll","Array","prototype","slice","call","filterAndModify","apply","filter","selected","matches","updateAttribute","result","trim","split","forEach","item","indexOf","remove","push","add","attribute","join","once","find"],"mappings":";iCAsCAA,IAAMC,EAAO,qBASPC,EAAW,YASXC,EAAMC,SAiBZ,SAASC,EAAKC,EAASC,EAAIC,GACzB,OAAOF,EAAWC,eAAeL,EAAUM,GAgB7C,SAASC,EAAaC,GAEpB,GAAkB,iBAAPA,EACT,MAAM,IAAIC,UAAU,4BAEtB,GAAW,KAAPD,GAAaT,EAAKW,KAAKF,GACzB,MAAM,IAAIG,WAAW,+CAGvB,sBAAyBH,OAoB3B,SAASI,EAAaC,GACpB,KAAMA,aAAuBC,SAC3B,MAAM,IAAIL,UAAU,8CAEtB,OAAO,EAgBT,SAASM,EAAYC,EAAUC,GAC7B,kBADuChB,IAClCe,EACH,MAAM,IAAIP,UAAU,8BAGtBS,IAAIC,EAAWH,EAef,MAXsB,iBAAbA,GACNC,IAAYhB,IAAOW,EAAaK,GAK1BD,aAAoBF,UAC3BK,EAAW,CAACH,IAJZG,EAAWF,EAAQG,iBAAiBJ,GAQ/BK,MAAMC,UAAUC,MAAMC,KAAKL,GAqBpC,SAASM,EAAgBT,EAAUG,EAAUO,GAC3C,OAAOP,EAASQ,iBAAOvB,GACrBN,IAAM8B,EAAWhB,EAAaR,IAAYA,EAAQyB,QAAQb,GAI1D,OAHIY,GAAYF,GACdA,EAAMtB,GAEDwB,KAsBX,SAASE,EAAgB1B,4BACjB2B,EAAS,GACX5B,EAAKC,EAAS,QAChBD,EAAKC,EAAS,OACX4B,OACAC,MAAMlC,GACNmC,kBAAQC,GACHJ,EAAOK,QAAQD,GAAQ,GAAKA,IAASE,GACvCN,EAAOO,KAAKH,MAIhBI,GACFR,EAAOO,KAAKC,GAEdzC,IAAM0C,EAAYT,EAAOU,KAAK,KAC9BtC,EAAKC,EAAuB,KAAdoC,EAAmB,SAAW,MAAOA,GA8CrD,SAASE,EAAKlC,EAAIQ,EAAUC,GAC1B,OAAOQ,UACGlB,EAAaC,OACrBO,EAAYC,EAAUC,aACtBb,UAAW0B,EAAgB1B,EAAS,CAAEmC,IAAK/B,cAsC/CkC,EAAKL,gBAAU7B,EAAIQ,EAAUC,GAC3B,OAAOQ,EACLlB,EAAaC,GACbO,EAAYC,EAAUC,aACtBb,UAAW0B,EAAgB1B,EAAS,CAAEiC,OAAQ7B,QAqClDkC,EAAKf,gBAAUnB,EAAIQ,EAAUC,UAC3BQ,EAAgBlB,EAAaC,GAAKO,EAAYC,EAAUC,KA6B1DyB,EAAKC,cAAQnC,EAAIS,UACfF,EAAaP,EAAuBD,EAAaC,iBAAKS"}
 No newline at end of file
+18 −1
Original line number Diff line number Diff line
@@ -181,7 +181,7 @@ drupal.date:
  dependencies:
    - core/drupal
    - core/modernizr
    - core/jquery.once
    - core/once

drupal.debounce:
  version: VERSION
@@ -279,6 +279,11 @@ drupal.dropbutton:
    - core/drupalSettings
    - core/jquery.once

drupal.element.matches:
  version: VERSION
  js:
    misc/polyfills/element.matches.js: { weight: -20 }

drupal.entity-form:
  version: VERSION
  js:
@@ -705,6 +710,18 @@ normalize:
      assets/vendor/normalize-css/normalize.css: { weight: -20 }
      misc/normalize-fixes.css: { weight: -19 }

once:
  remote: https://git.drupalcode.org/project/once
  version: "1.0.0"
  license:
    name: GNU-GPL-2.0-or-later
    url: https://git.drupalcode.org/project/once/-/raw/v1.0.0/LICENSE.md
    gpl-compatible: true
  js:
    assets/vendor/once/once.min.js: { weight: -19, minified: true }
  dependencies:
    - core/drupal.element.matches

picturefill:
  remote: https://github.com/scottjehl/picturefill
  version: "3.0.3"
Loading