toolshed-8.x-1.x-dev/assets/widgets/Accordion.es6.js

assets/widgets/Accordion.es6.js
(({ theme, t, Toolshed: ts }) => {
  /**
   * Function to create an HTMLElement to serve as the expand/collapse toggle
   * for an AccordionItem.
   *
   * @return {HTMLElement}
   *   An HTMLElement to serve as the expand/collapse toggle for an
   *   AccordionItem.
   */
  theme.accordionToggler = (expandText = 'Expand', collapseText = 'Collapse') => {
    // Create top-level button element.
    const btn = new ts.Element('button', { class: 'toggler'});
    btn.innerHTML = `
      <span class="toggler__collapse-text">${expandText}</span>
      <span class="toggler__expand-text">${collapseText}</span>
    `;

    return btn;
  };


  let idCt = 1;
  function uniqueId() {
    // A unique ID for accordion items to use with "aria-controls" property.
    return `ts-accordion-${idCt++}`;
  }

  /**
   * Defines a collapsible accordion item.
   */
  ts.AccordionItem = class AccordionItem extends ts.Element {
    /**
     * Class representing a single item of an Accordion which is expandable and
     * collapsible. This item maintains its components and the state of the
     * open and close states.
     *
     * @param {Element} el
     *   The whole accordion item.
     * @param {Element} expander
     *   Element to use for toggling the body content.
     * @param {Element} body
     *   The content area of the accordion item which is revealed or hidden
     *   when the accordion item is expanded or collapsed.
     * @param {Accordion|AccordionItem} parent
     *   Parent accordion instance.
     * @param {Object} settings
     *   Object of configurations to use to control the behavior of the
     *   accordion item.
     */
    constructor(el, expander, body, parent = null, settings = {}) {
      super(el, {class: ['accordion-item', 'item--collapsible']});

      this.isExpanded = true;
      this.parent = parent;
      this.body = body;
      this.expander = expander;
      this.useAnimation = settings.animate || true;
      this.animateTime = settings.animateTime || '0.3s';
      this.children = [];

      this.body.addClass('item__body');
      this.expander.setAttrs({
        'aria-expanded': this.isExpanded.toString(),
        'aria-controls': body.id || (body.id = uniqueId()),
      });

      this.onToggleExpand = AccordionItem[parent && settings.exclusive ? 'onClickExclusive' : 'onClickToggle'].bind(this);
      expander.on('click', this.onToggleExpand);
    }

    /**
     * Add a child accordion item.
     *
     * @param {AccordionItem} child
     *   Child accordion item to add.
     */
    addChild(child) {
      this.children.push(child);
    }

    /**
     * Expand the accordion item's content display.
     *
     * @param {bool} allowAnimate
     *   Animate expand animations if an animation is configured.
     */
    expand(allowAnimate = true) {
      this.addClass('item--expanded');
      this.removeClass('item--collapsed');
      this.expander.setAttr('aria-expanded', 'true');
      this.isExpanded = true;
      this.expander.addClass('toggler--expanded');
      this.expander.removeClass('toggler--collapsed');

      if (allowAnimate && this.useAnimation) {
        this.body.expand(this.animateTime);
      }
      else {
        this.body.style.display = '';
        this.body.style.height = '';
      }
    }

    /**
     * Collapse the accordion item's content display.
     *
     * @param {bool} allowAnimate
     *   Should the collapse functionality expect to animate the transitions
     *   if the accordion item is setup to do animations.
     */
    collapse(allowAnimate = true) {
      this.addClass('item--collapsed');
      this.removeClass('item--expanded');
      this.expander.ariaExpanded = 'false';
      this.isExpanded = false;
      this.expander.addClass('toggler--collapsed');
      this.expander.removeClass('toggler--expanded');

      if (allowAnimate && this.useAnimation) {
        this.body.collapse(this.animateTime);
      }
      else {
        this.body.style.display = 'none';
      }

      // Recursively close all the open child accordions.
      this.children.forEach((child) => {
        if (child.isExpanded) child.collapse(false);
      });
    }

    /**
     * Expand and collapse of accordion items, without concern to the state of
     * other accordion items. Multiple items can be open at once.
     *
     * @param {ClickEvent} e
     *   DOM click event object, containing information about the item being
     *   being clicked with the collapse functionality.
     */
    static onClickToggle(e) {
      e.preventDefault();

      if (this.isExpanded) {
        this.collapse();
      }
      else {
        this.expand();
      }
    }

    /**
     * Expand and collapse of accordion items but ensure only one item is open
     * at a time.
     *
     * @param {ClickEvent} e
     *   DOM click event object, containing information about the item being
     *   being clicked with the collapse functionality.
     */
    static onClickExclusive(e) {
      e.preventDefault();

      if (this.isExpanded) {
        this.collapse();
      }
      else {
        // Recursively close all the open child accordions.
        this.parent.children.forEach((child) => {
          if (child !== this && child.isExpanded) child.collapse();
        });

        this.expand();
      }
    }

    /**
     * Teardown any events and alterations this accordion item made to the DOM.
     */
    destroy() {
      this.expander.removeAttrs(['aria-expanded', 'aria-controls']);
      this.expander.removeClass([
        'toggler--expanded',
        'toggler--collapsed',
      ]);
      this.removeClass([
        'accordion-item',
        'item--collapsed',
        'item--expanded',
        'item--collapsible',
      ]);

      // Reset the accordion body display.
      this.body.style.display = '';
      this.body.style.height = '';

      // Clear the expander events.
      this.expander.destroy();
      super.destroy();
    }
  }

  /**
   * Defines a non-collapsible accordion item.
   */
  class NonAccordionItem extends ts.Element {
    /**
     * Class representing a single item of an Accordion which is not expandable
     * or collapsible.
     *
     * @param {Element} el
     *   The whole accordion item.
     * @param {Accordion|AccordionItem} parent
     *   Parent accordion instance.
     */
    constructor(el, parent) {
      super(el, {class: 'accordion-item'});
      this.el = el;
      this.parent = parent;
      this.children = [];
      this.isExpanded = false;
    }

    /**
     * Add a child accordion item.
     *
     * @param {AccordionItem} child
     *   Child accordion item to add.
     */
    addChild(child) {
      this.isExpanded = true;
      this.children.push(child);
    }

    /**
     * This class implements an expand function in order to have the same
     * interface as the AccordionItem class, but since this is a
     * non-collapsible accordion item, it does nothing.
     */
    expand() {} // eslint-disable-line class-methods-use-this

    /**
     * This class implements a collapse function in order to have the same
     * interface as the AccordionItem class, but since this is a
     * non-collapsible accordion item, it just collapses any children.
     */
    collapse() {
      // Recursively close all the open child accordions.
      this.children.forEach((child) => {
        if (child.isExpanded) child.collapse(false);
      });
    }
  }

  /**
   * Accordion object definition.
   */
  ts.Accordion = class Accordion extends ts.Element {
    /**
     * Create an accordion of expandable and collapsible items.
     *
     * @param {Element} el
     *   DOM element to turn into an accordion.
     * @param {Object} configs
     *   A configuration options which determine the options:
     *   {
     *     initOpen: {bool|selector} [true],
     *       // Should accordion panels start of initially open.
     *     exclusive: {bool} [true],
     *       // Accordion only has one item open at a time.
     *     animate: {bool} [true],
     *       // Should the accordions expand and collapse animate.
     *     transitionDuration: {string} ['0.3s']
     *       // How long the expand/collapse transitions should last. Should be
     *       // a duration that a CSS transition can accept (such as '300ms' or
     *       // '0.3s').
     *     toggler: {DOMString|function|object}
     *       // The selector to find the element to toggle the collapse and expand, OR
     *       // A callback function to create the toggler element, OR
     *       // An object with `create` and `attacher` functions to create and
     *       // place the toggler into the DOM.
     *     itemSel: {DOMString}
     *       // The selector to use when locating each of the accordion items.
     *     bodySel: {DOMString}
     *       // The selector to use to find the item body within the item context.
     *   }
     */
    constructor(el, configs) {
      super(el, { class: 'accordion'});

      this.items = new Map();
      this.children = [];

      this.configs = {
        exclusive: true,
        initOpen: true,
        animate: true,
        animateTime: '0.3s',
        ...configs,
      };

      const {
        exclusive,
        initOpen,
        itemSel,
        bodySel,
      } = this.configs;

      let toggleOpt = this.configs.toggler || theme.accordionToggler;

      // If the expand option is just a selector, then the element is expected
      // to already be in the document (otherwise not findable).
      if (ts.isString(toggleOpt)) {
        this.getToggler = (item) => new ts.Element(item.querySelector(toggleOpt)||'button');
      }
      else {
        if (typeof toggleOpt === 'function') toggleOpt = { create: toggleOpt };

        if (!toggleOpt.attach) {
          toggleOpt.attach = (toggler, item) => item.insertBefore(toggler.el, item.querySelector(bodySel));
        }

        this.getToggler = (item) => {
          let toggler = toggleOpt.create(item);

          // If not already in the DOM, use the attach callback to insert it.
          if (toggler && !toggler.parentNode) {
            toggleOpt.attach(toggler, item);
          }

          return toggler;
        };
      }

      // Build the accordion items allowing for a nested tree structure.
      this.find(itemSel).forEach((el) => this.buildTreeItem(el));

      if (!initOpen || exclusive) {
        this.children.forEach((child, i) => {
          if (!initOpen || i !== 0) child.collapse(false);
        });
      }
    }

    /**
     * Add a child accordion item.
     *
     * @param {AccordionItem} child
     *   Child accordion item to add.
     */
    addChild(child) {
      this.children.push(child);
    }

    /**
     * Find the parent accordion item for this element.
     *
     * @param {DOMElement} el
     *   The element to search for the closest matching selector for.
     * @param {string} selector
     *   Selector to use when identifying an accordion item.
     *
     * @return {Element|null}
     *   If an appropriate parent matching the item selector is found within
     *   the confines of the accordion element, it is returned. Otherwise,
     *   return the Element wrapping the accordion.
     */
    findParent(el, selector) {
      let cur = el;

      do {
        cur = cur.parentElement;
      } while (cur && cur !== this.el && !cur.matches(selector));

      return cur;
    }

    /**
     * Recursively add accordion items to the tree structure.
     *
     * @param {DOMElement} el
     *   The HTMLElement tos transform into an accordion item.
     *
     * @return {AccordionItem|NonAccordionItem}
     *   An accordion item or a NonAccordionItem (item that doesn't collapse).
     */
    buildTreeItem(el) {
      let a = this.items.get(el);

      // This element has already been built, don't need to rebuild subtree.
      if (a) return a;

      const { itemSel, bodySel } = this.configs;
      const parent = this.findParent(el, itemSel);

      // Only create an accordion item if it has a parent item which exists
      // within the Accordion scope.
      if (!parent) throw new Error('Unable to find a parent buildTreeItem().');

      const iParent = (parent === this.el) ? this : this.items.get(parent) || this.buildTreeItem(parent);
      if (!iParent) throw new Error('No valid accordion parent available, or not a descendent of the accordion.');

      // Only create a collapsible accordion item if it has a has a body to
      // expand, and an expander (expand/collapse toggle). Otherwise create a
      // non-collapsible accordion item.
      let accord;
      const body = el.querySelector(bodySel);
      if (body) {
        const expander = this.getToggler(el);

        if (expander) {
          // This accordion item should be made collapsible.
          accord = new ts.AccordionItem(el, expander, new ts.Element(body), iParent, this.configs);
        }
      }

      if (!accord) {
        // This accordion item should be made non-collapsible.
        accord = new NonAccordionItem(item, iParent);
      }

      // Add the accordion item to the accordion's 'items' list, and to its
      // parent's list of children.
      this.items.set(el, accord);
      iParent.addChild(accord);

      return accord;
    }

    /**
     * Teardown any changes to the DOM as well as clearing AccordionItems.
     */
    destroy() {
      this.removeClass('accordion');

      // Destroy all the accordion items.
      this.items.forEach((item) => item.destroy());

      delete this.items;
      delete this.children;
      delete this.configs;

      super.destroy();
    }
  };
})(Drupal);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc