hlfcoding/hlf-dom-extensions

View on GitHub
src/js/accordion.js

Summary

Maintainability
A
0 mins
Test Coverage
//
// HLF Accordion Extension
// =======================
// [Tests](../../tests/js/accordion.html)
//
// The `Accordion` extension provides a simple but flexible accordion behavior.
// Folding a section only folds its items. `autoCollapse` is an option. And
// `cursorItemClass` and `featureCount` options allow certain items to remain
// visible upon folding. A `triggerSelector` option allows any trigger within
// the section to be used.
//
(function(root, attach) {
  if (typeof define === 'function' && define.amd) {
    define(['hlf/core'], attach);
  } else if (typeof exports === 'object') {
    module.exports = attach(require('hlf/core'));
  } else {
    attach(HLF);
  }
})(this, function(HLF) {
  'use strict';
  //
  // Accordion
  // ---------
  //
  // - __autoCollapse__ can be turned off to prevent other sections from folding
  //   when a section unfolds. `true` by default.
  //
  // - __cursorItemClass__ when on a section's item, marks it as a cursor, and
  //   effectively preventing its section from folding. `'active'` by default.
  //
  // - __featureCount__ limits the number of starting items for a section to
  //   show even when folded. `1` by default.
  //
  // - __itemsSelector__ points to all elements in a section, to be toggled
  //   during folding and unfolding. `'li:not(:first-child)'` by default.
  //
  // - __sectionSelector__ points to all elements considered to be folding
  //   sections, which can be modified with the `js-ac-folded` class. `'ul'`
  //   by default.
  //
  // - __triggerSelector__ points to existing UI that when clicked toggles its
  //   section's folding'. `'.accordion-trigger'` by default.
  //
  // To summarize the implementation, given existing section elements in the
  // extended `element`, populate `_sections` with elements (`sectionElement`,
  // its `itemElements`, its `triggerElement`) and state (`hasCursor` and
  // `isFolded`, respectively per `cursorItemClass` and `false`). User input is
  // handled by `_onTriggerClick` and transformed into calls to
  // `_toggleSectionFolding`.
  //
  // `_toggleSectionFolding` at its core changes `display` on the `itemElements`
  // past the `featureCount` option for the section, while toggling the `js-ac-
  // folded` class. It also implements the `autoCollapse` option and recurses
  // to collapse the other sections if needed. It has a couple guards,
  // including not folding if the section `hasCursor`, or if section's
  // `isFolded` is already set.
  //
  class Accordion {
    static get defaults() {
      return {
        autoCollapse: true,
        cursorItemClass: 'active',
        featureCount: 1,
        itemsSelector: 'li:not(:first-child)',
        sectionSelector: 'ul',
        triggerSelector: '.accordion-trigger',
      };
    }
    static toPrefix(context) {
      switch (context) {
        case 'event': return 'hlfac';
        case 'data': return 'hlf-ac';
        case 'class': return 'ac';
        case 'var': return 'ac';
        default: return 'hlf-ac';
      }
    }
    init() {
      this._sections = [];
      [...this.element.querySelectorAll(this.sectionSelector)]
        .forEach(this._setUpSection);
    }
    deinit() {
      this._sections.forEach(this._tearDownSection);
      this._sections = [];
    }
    _onTriggerClick(event) {
      let section = this._sections.find(section => section.triggerElement === event.currentTarget);
      this._toggleSectionFolding(section);
    }
    _setUpSection(sectionElement) {
      let itemElements = [...sectionElement.querySelectorAll(this.itemsSelector)];
      let section = {
        hasCursor: itemElements.some(el => el.classList.contains(this.cursorItemClass)),
        isFolded: false,
        itemElements,
        sectionElement,
        triggerElement: sectionElement.querySelector(this.triggerSelector),
      };
      this._sections.push(section);
      this._toggleSectionFolding(section, !section.hasCursor);
      this._toggleSectionEventListeners(true, section);
    }
    _tearDownSection(section) {
      this._toggleSectionEventListeners(false, section);
      this.autoCollapse = false;
      this._toggleSectionFolding(section, false);
    }
    _toggleSectionFolding(section, folded) {
      const { hasCursor, isFolded } = section;
      if (hasCursor && folded) { return; }
      if (folded == null) { folded = !isFolded; }
      else if (isFolded === folded) { return; }
      if (this.autoCollapse && !folded) {
        this._sections.filter(s => s !== section)
          .forEach(s => this._toggleSectionFolding(s, true));
      }
      let { itemElements, sectionElement, triggerElement } = section;
      let featureCount = this.featureCount;
      if (triggerElement === itemElements[0].previousElementSibling) {
        featureCount -= 1;
      }
      itemElements.slice(featureCount)
        .forEach(el => el.style.display = folded ? 'none' : 'block');
      sectionElement.classList.toggle(this.className('folded'), folded);
      section.isFolded = folded;
    }
    _toggleSectionEventListeners(on, section) {
      let { triggerElement } = section;
      this.toggleEventListeners(on, {
        'click': this._onTriggerClick,
      }, triggerElement);
    }
  }
  Accordion.debug = false;
  HLF.buildExtension(Accordion, {
    autoBind: true,
    compactOptions: true,
    mixinNames: ['event'],
  });
  Object.assign(HLF, { Accordion });
  return Accordion;
});