UNPKG

baseframe-js

Version:

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

253 lines (252 loc) 9.37 kB
import $be from "base-elem-js"; import { getDataOptions, isFunc, noop, reflow, setParams } from './util/helpers'; import focusTrap from './fn/focusTrap'; import { KEYS } from "./core/constants"; import Store from "./core/Store"; import { body } from './util/helpers'; import { debounceResize } from './fn/debounce'; const VERSION = "3.0.0"; const DATA_NAME = 'NavMobile'; const EVENT_NAME = 'navMobile'; const DEFAULTS = { enableBtn: '#mobile-nav-btn', ariaLabel: 'Toggle site navigation', tgBtnText: 'toggle menu', insertBtnAfter: 'a', slideDuration: 400, outerElement: body, outsideClickClose: true, animateHeight: true, cssPrefix: 'menu', menuBtnCss: 'i i-arrow-b', menuBtnSkip: false, afterNavItemOpen: noop, afterNavItemClose: noop, afterOpen: noop, afterClose: noop, doTrapFocus: true, trapFocusElem: null, stopPropagation: true, bkptEnable: null }; export default class NavMobile { $element; element; params; menuOpened; allowClick; cssList; $enableBtn; enableBtn; static defaults = DEFAULTS; static version = VERSION; static pluginName = DATA_NAME; #transition = $be.useTransition(); constructor(element, options) { const s = this; const dataOptions = getDataOptions(element, EVENT_NAME); s.$element = $be(element); s.element = element; s.params = setParams(NavMobile.defaults, options, dataOptions); s.$enableBtn = $be(s.params.enableBtn); s.enableBtn = s.$enableBtn.elem[0]; const { cssPrefix, menuBtnCss } = s.params; s.cssList = { outerOpen: `${cssPrefix}--outer-active`, open: `${cssPrefix}--active`, opening: `${cssPrefix}--starting`, closing: `${cssPrefix}--ending`, toggling: `${cssPrefix}--toggling`, hasUl: `${cssPrefix}__has-ul`, noUl: `${cssPrefix}__no-ul`, btn: `${cssPrefix}__btn-tg ${menuBtnCss}` }; //run the methods s.#addCssAndBtn(); s.#handleEvents(); s.#checkIfEnabled(); const elemID = element.id || element.className; s.$enableBtn.attr({ 'aria-controls': elemID, 'aria-label': s.params.ariaLabel }); return s; } #handleEvents() { const s = this; const p = s.params; const css = s.cssList; const $enableBtn = $be(p.enableBtn); $enableBtn .attr({ 'aria-expanded': 'false' }) .on([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`], function (e) { if (!s.allowClick) return; s.#menuToggle(); e.stopPropagation(); e.preventDefault(); }); $be(document).on(`keydown.${EVENT_NAME}`, (e) => { if (e.code === KEYS.esc && s.$element.hasClass(css.open) && s.allowClick) { s.#menuToggle(); if ($enableBtn.hasEls) { $enableBtn.elem[0].focus(); } } }); s.#navToggle(); s.#outsideClickClose(); } #menuToggle() { const s = this; const p = s.params; const css = s.cssList; let trappedFocus = null; const $elemParent = s.$element.find(elem => elem.parentElement); const doClose = s.menuOpened; const cssMenuState = [doClose ? css.closing : css.opening, css.toggling]; const $outerElem = $be(p.outerElement); s.#transition(() => { s.menuOpened = !doClose; s.$element.addClass(cssMenuState); $be(p.enableBtn).attr({ 'aria-expanded': !doClose + '' }); $outerElem .tgClass(css.outerOpen, !doClose) .addClass(cssMenuState); if (doClose) { $elemParent .find(`.${css.open}`) .rmClass(css.open) .find("[style]").css({ 'display': null }); trappedFocus && trappedFocus.remove(); } else { if (p.doTrapFocus) { trappedFocus = focusTrap(p.trapFocusElem || s.$element, { nameSpace: EVENT_NAME }); } } }, () => { $outerElem.rmClass(cssMenuState); s.$element.rmClass(cssMenuState); if (!doClose) { s.$element.addClass(css.open); } const fn = doClose ? p.afterClose : p.afterOpen; fn(s.element, $outerElem.toArray(), s.enableBtn); }, p.slideDuration); } #navToggle() { const s = this; const p = s.params; const css = s.cssList; s.$element.on([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`], function (ev, elem) { const ulElem = elem.closest(`.${css.hasUl}`); //exit because were in desktop view if (!s.allowClick || !ulElem) return; const $li = $be(ulElem); const $ul = $li.find('ul').get(0); const close = $li.hasClass(css.open); const cssMenuState = [css.toggling, close ? css.closing : css.opening]; s.allowClick = close; s.#transition(() => { $li.addClass(cssMenuState).tgClass(css.open, !close); $ul.addClass(cssMenuState).tgClass(css.open, !close); if (p.animateHeight) { const openHeight = ($ul.hasEls ? $ul.elem[0].scrollHeight : 0); const height = close ? 0 : openHeight; $ul.css({ height: (close ? openHeight : 0) + 'px' }) .each(elem => reflow(elem)) .css({ height: height + 'px' }); } }, () => { $li.rmClass(cssMenuState).tgClass(css.open, !close); $ul.rmClass(cssMenuState).tgClass(css.open, !close).css({ height: null }); const li = $li.toArray(); if (close) p.afterNavItemClose(li); else p.afterNavItemOpen(li); s.allowClick = true; }, p.slideDuration); ev.stopPropagation(); }, `.${css.btn.replace(/\s/g, '.')}`); s.$element.on([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`], (ev, elem) => { //prohibit closing if an anchor is clicked if (s.params.stopPropagation) { ev.stopPropagation(); } }, 'a'); } #outsideClickClose() { const s = this; $be(document.body).on(`click.${EVENT_NAME}`, (e) => { if (s.params.outsideClickClose) { if (!s.menuOpened) return; const menuClicked = s.$element.elem[0].contains(e.target); if (!menuClicked && s.menuOpened) { s.#menuToggle(); } } }); } #addCssAndBtn() { const s = this; const p = s.params; const css = s.cssList; const $lis = $be('li', s.element); $lis.each((elem, i) => { const $li = $be(elem); const $uls = $li.findBy('tag', 'ul'); const ulCss = $uls.hasEls ? css.hasUl : css.noUl; const nextElem = elem.nextElementSibling; $li.addClass(ulCss); if (!$uls.hasEls) return; const menuBtnSkip = isFunc(p.menuBtnSkip) ? p.menuBtnSkip(elem) : p.menuBtnSkip; if (!menuBtnSkip) { const $afterEl = $li.find(p.insertBtnAfter).get(0); const btnAlreadySet = nextElem ? $be(nextElem).hasClass(css.btn) : false; if (!btnAlreadySet) { const btn = $be.make(`button.${css.btn}`, { type: 'button', ariaLabel: p.tgBtnText }); $afterEl.insert(btn, 'after'); } } }); } #checkIfEnabled() { const s = this; const p = s.params; //basically if the navigational button is visible then //we can allow the click to open the navigation //this is so it doesn't clash with any other plugins //and allows for the control of this click via CSS debounceResize(() => { const bkptEnIsNum = typeof p.bkptEnable === 'number'; s.allowClick = bkptEnIsNum ? window.innerWidth <= p.bkptEnable : s.$enableBtn.hasEls ? $be.isVisible(s.$enableBtn.elem[0]) : false; }, EVENT_NAME, true, 200); } static remove(element, plugin) { $be(element).each((elem) => { const s = plugin || Store(elem, DATA_NAME); $be(document).off(`keydown.${EVENT_NAME}`); $be(s.params.enableBtn).off([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`]); s.$element.off([`click.${EVENT_NAME}`, `[${EVENT_NAME}]`]); $be(document.body).off(`click.${EVENT_NAME}`); $be(window).off([`resize.${EVENT_NAME}`, `[${EVENT_NAME}]`]); Store(elem, DATA_NAME, null); }); } }