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
JavaScript
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);
});
}
}