UNPKG

baseframe-js

Version:

Baseframe JS is a comprehensive suite of modular plugins and utilities designed for front-end development

176 lines (175 loc) 7.12 kB
import $be from "base-elem-js"; import { getDataOptions, noop, reflow, setParams } from './util/helpers'; import smoothScroll from './fn/smoothScroll'; import UrlState from "./core/UrlState"; import Store from "./core/Store"; ; const VERSION = "4.0.0", DATA_NAME = 'Collapse', EVENT_NAME = 'collapse', DEFAULTS = { cssPrefix: 'collapse', toggleDuration: 500, toggleGroup: false, moveToTopOnOpen: false, moveToTopOffset: 0, moveToTopDuration: 500, urlFilterType: 'hash', historyType: 'replace', locationFilter: null, loadLocation: true, afterOpen: noop, afterClose: noop, afterInit: noop }; export default class Collapse { $element; element; params; toggling; $btnElems; btnElems; $activeItem; initLoaded; static defaults = DEFAULTS; static version = VERSION; static pluginName = DATA_NAME; #transition = $be.useTransition(); constructor(element, options, index) { const s = this; s.element = element; s.$element = $be(element); const dataOptions = getDataOptions(element, EVENT_NAME); s.params = setParams(Collapse.defaults, options, dataOptions); s.toggling = false; s.$btnElems = s.$element.find(`.${s.params.cssPrefix}__btn`).attr({ 'aria-expanded': 'false' }); s.btnElems = s.$btnElems.toArray(); s.$activeItem = null; s.initLoaded = false; // init s.#loadFromUrl(); s.#handleEvents(); s.params.afterInit(element); Store(element, DATA_NAME, s); return s; } static remove(element, plugin) { $be(element).each((elem) => { const s = plugin || Store(elem, DATA_NAME); s.$element.off([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`]); $be(window).off(`popstate.${EVENT_NAME}`); // delete the Store item Store(elem, DATA_NAME, null); }); } #handleEvents() { const s = this; const { cssPrefix } = s.params; s.$element.on([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`], function (e, elem) { const elemId = $be(elem).attr('aria-controls') || elem.hash.substring(1); s.toggle(elemId); e.preventDefault(); }, `.${cssPrefix}__btn`); $be(window).on(`popstate.${EVENT_NAME}`, (e) => { if (s.params.historyType === 'push') { s.#loadFromUrl(); s.initLoaded = true; e.preventDefault(); } }); } #loadFromUrl() { const s = this; const p = s.params; const loadElem = (filterEl) => { const cssOpen = `${p.cssPrefix}--open`; const $tryElem = s.$element.find('#' + filterEl); if ($tryElem.elem.length) { s.$activeItem = $tryElem; s.$activeItem.addClass(cssOpen); s.$btnElems .filter(el => $be(el).attr('aria-controls') === filterEl) .attr({ 'aria-expanded': 'true' }); } }; if (p.locationFilter !== null && p.loadLocation && p.urlFilterType !== 'none') { const filterEl = UrlState.get(p.urlFilterType, p.locationFilter); const cssOpen = `${p.cssPrefix}--open`; s.$element.find(`.${p.cssPrefix}__body.${cssOpen}`).rmClass(cssOpen); s.$btnElems.attr({ 'aria-expanded': 'false' }); if (filterEl) { if (Array.isArray(filterEl)) { filterEl.forEach(loadElem); } else { loadElem(filterEl); } } } } #moveToTopOnOpen() { const s = this; const p = s.params; if (s.$activeItem) { const item = s.$activeItem.map(el => el.closest(`.${p.cssPrefix}__item`)); const offsetTop = typeof p.moveToTopOffset === 'function' ? p.moveToTopOffset() : p.moveToTopOffset; if (item.length && p.moveToTopOnOpen) { const top = (item[0].getBoundingClientRect().top + window.scrollY) - offsetTop; smoothScroll(top, p.moveToTopDuration); } } } toggle(currElemID, allAtOnce = false) { const s = this; const p = s.params; const activeItem = $be.findOne('#' + currElemID, s.element); if ((s.toggling && !allAtOnce) || currElemID === null || !activeItem) return; s.$activeItem = s.$element.find('#' + currElemID); const cssPrefix = `${p.cssPrefix}--`, cssOpen = `${cssPrefix}open`, cssToggling = `${cssPrefix}toggling`, cssOpening = `${cssPrefix}opening`, cssClosing = `${cssPrefix}closing`, cssBodyOpen = `.${p.cssPrefix}__body.${cssOpen}`, $currOpenItems = s.$element.find(cssBodyOpen), $itemsToClose = $currOpenItems.filter(el => p.toggleGroup || el.id === currElemID), activeAlreadyOpen = s.$activeItem.hasClass(cssOpen); $itemsToClose.each((elem) => $be.css(elem, { height: elem.scrollHeight + 'px' })); const start = () => { s.toggling = true; s.$btnElems.each((elem) => { const $btn = $be(elem); const current = $btn.attr('aria-controls') === currElemID; const expanded = $btn.attr('aria-expanded') === 'false'; if (p.toggleGroup) { $btn.attr({ 'aria-expanded': (current && expanded) + '' }); } else if (current) { $btn.attr({ 'aria-expanded': expanded + '' }); } }); $itemsToClose .rmClass(cssOpen) .addClass([cssToggling, cssClosing]) .each(elem => reflow(elem)) .css({ height: '0px' }); if (!activeAlreadyOpen) { const height = activeItem.scrollHeight + 'px'; s.$activeItem .addClass([cssToggling, cssOpening]) .css({ height }); } }, end = () => { s.toggling = false; $itemsToClose .rmClass([cssToggling, cssClosing]) .css({ height: null }); s.params.afterClose(s.btnElems, $itemsToClose.toArray()); if (!activeAlreadyOpen) { s.$activeItem .addClass(cssOpen) .rmClass([cssToggling, cssOpening]) .css({ height: null }); } // Update History in URL if (p.urlFilterType !== 'none') { const paramList = s.$element.find(cssBodyOpen).map((el) => el.id); const paramVal = paramList.length === 1 ? paramList[0] : paramList.length > 0 ? paramList : null; UrlState.set(p.urlFilterType, p.locationFilter, paramVal, p.historyType); } s.params.afterOpen(s.btnElems, s.$activeItem.toArray()); s.#moveToTopOnOpen(); }; s.#transition(start, end, s.params.toggleDuration); } }