baseframe-js
Version:
A suite of useful Javascript plugins and functions to help with Front-end Development on websites
250 lines (249 loc) • 9.64 kB
JavaScript
import $ from 'cash-dom';
import { isVisible, getDataOptions, noop } from './util/helpers';
import trapFocus from './fn/trapFocus';
import transition from "./fn/transition";
import { KEYS } from "./core/constants";
import Store from "./core/Store";
const VERSION = "3.0.0";
const DATA_NAME = 'NavMobile';
const EVENT_NAME = 'navMobile';
const DEFAULTS = {
enableBtn: '#mobile-nav-btn',
ariaLabel: 'Toggle site navigation',
subMenuText: 'toggle menu for',
insertToggleBtnAfter: 'a',
slideDuration: 400,
outerElement: document.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;
params;
menuOpened;
allowClick;
cssList;
static defaults = DEFAULTS;
static version = VERSION;
static pluginName = DATA_NAME;
#transition = transition();
constructor(element, options) {
const s = this;
const dataOptions = getDataOptions(element, EVENT_NAME);
s.$element = $(element);
s.params = $.extend({}, NavMobile.defaults, options, dataOptions);
const { cssPrefix, menuBtnCss } = s.params;
s.cssList = {
menuOuterOpen: `${cssPrefix}--outer-open`,
menuHasUL: `${cssPrefix}__has-ul`,
menuOpen: `${cssPrefix}--open`,
menuOpening: `${cssPrefix}--is-opening`,
menuClosing: `${cssPrefix}--is-closing`,
menuToggling: `${cssPrefix}--toggling`,
menuBtnCss: `${cssPrefix}__btn-nav ${menuBtnCss}`
};
//run the methods
s.addChildNavCss();
s.handleEvents();
s.checkIfEnabled();
const elemID = element.id || element.className;
$(s.params.enableBtn).attr({
'aria-controls': elemID,
'aria-label': s.params.ariaLabel
});
return s;
}
handleEvents() {
const s = this;
const css = s.cssList;
const $enableBtn = $(s.params.enableBtn);
$enableBtn
.attr({ 'aria-expanded': 'false' })
.on(`click.${EVENT_NAME} ${EVENT_NAME}`, function (e) {
if (!s.allowClick)
return;
s.#menuToggle();
e.stopPropagation();
e.preventDefault();
});
$(document).on(`keydown.${EVENT_NAME}`, (e) => {
if (e.code === KEYS.esc && s.$element.hasClass(css.menuOpen) && s.allowClick) {
s.#menuToggle();
if ($enableBtn.length) {
$enableBtn[0].focus();
}
}
});
s.#navToggle();
s.#outsideClickClose();
}
#menuToggle() {
const s = this;
const p = s.params;
const css = s.cssList;
let trappedFocus = null;
const $enableBtn = $(p.enableBtn);
const $elemParent = s.$element.parent();
const doClose = s.menuOpened;
const cssMenuState = `${doClose ? css.menuClosing : css.menuOpening} ${css.menuToggling}`;
s.#transition(() => {
s.menuOpened = !doClose;
s.$element.addClass(cssMenuState);
$(p.enableBtn).attr({ 'aria-expanded': !doClose + '' });
$(p.outerElement)
.toggleClass(css.menuOuterOpen, !doClose)
.addClass(cssMenuState);
if (doClose) {
$elemParent
.find(`.${css.menuOpen}`)
.removeClass(css.menuOpen)
.find("[style]").css({ 'display': null });
trappedFocus && trappedFocus.remove();
}
else {
if (p.doTrapFocus) {
trappedFocus = trapFocus(p.trapFocusElem || s.$element, { nameSpace: EVENT_NAME });
}
}
}, () => {
$(p.outerElement).removeClass(cssMenuState);
s.$element.removeClass(cssMenuState);
if (!doClose) {
s.$element.addClass(css.menuOpen);
}
s.params[doClose ? 'afterClose' : 'afterOpen'](s.$element, $(p.outerElement), $enableBtn);
}, p.slideDuration);
}
#navToggle() {
const s = this;
const css = s.cssList;
s.$element.on(`click.${EVENT_NAME} ${EVENT_NAME}`, `.${css.menuBtnCss.replace(/\s/g, '.')}`, function (e) {
const p = s.params;
const css = s.cssList;
const $li = $(this).closest(`.${css.menuHasUL}`);
const doClose = $li.hasClass(css.menuOpen);
const $ul = $li.find('ul').first();
//exit because were in desktop view
if (!s.allowClick) {
return;
}
const cssMenuState = `${css.menuToggling} ${doClose ? css.menuClosing : css.menuOpening}`;
s.allowClick = doClose;
s.#transition(() => {
$li.addClass(cssMenuState).toggleClass(css.menuOpen, !doClose);
$ul.addClass(cssMenuState).toggleClass(css.menuOpen, !doClose);
if (p.animateHeight) {
const openHeight = ($ul.length ? $ul[0].scrollHeight : 0);
const height = doClose ? 0 : openHeight;
$ul.css({ height: doClose ? openHeight : 0 });
setTimeout(() => {
$ul.css({ height });
}, 0);
}
}, () => {
$li.removeClass(cssMenuState).toggleClass(css.menuOpen, !doClose);
$ul.removeClass(cssMenuState).toggleClass(css.menuOpen, !doClose);
$ul.css({ height: null });
if (doClose) {
s.params.afterNavItemClose($li);
}
else {
s.params.afterNavItemOpen($li);
}
s.allowClick = true;
}, p.slideDuration);
e.stopPropagation();
});
s.$element.on(`click.${EVENT_NAME} ${EVENT_NAME}`, 'a', function (e) {
//prohibit closing if an anchor is clicked
if (s.params.stopPropagation) {
e.stopPropagation();
}
});
}
#outsideClickClose() {
const s = this;
$(document.body).on(`click.${EVENT_NAME}`, function (e) {
if (s.params.outsideClickClose) {
if (!s.menuOpened) {
return;
} //lets just exit then..
const menuClicked = e.target ? s.$element.has(e.target).length > 0 : false;
//if the menu item is not clicked and its opened
//the menu button shouldn't register because propogation is prevented to the body
if (!menuClicked && s.menuOpened) {
s.#menuToggle();
}
}
});
}
addChildNavCss() {
const s = this;
const p = s.params;
const css = s.cssList;
$('li', s.$element).has('ul').each(function () {
const $this = $(this);
let skipUl = false;
if (typeof p.menuBtnSkip === 'function' &&
// condition in function must return false
p.menuBtnSkip(this)) {
skipUl = true;
}
if (!$this.next('button').length && !skipUl) {
const $el = $this.find(p.insertToggleBtnAfter).first();
$el.addClass(css.menuHasUL);
if ($el.length) {
if ($el[0].parentNode.isSameNode(this)) {
// make sure the <el> is a direct child of <li>
$el.after($('<button>').attr({
class: css.menuBtnCss,
type: 'button',
'aria-label': p.subMenuText + ' ' + $el.text().trim()
}));
}
}
}
});
}
checkIfEnabled() {
const s = this;
let resizeTimer;
//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
$(window).on(`resize.${EVENT_NAME} ${EVENT_NAME}`, function (e) {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const $enableBtn = $(s.params.enableBtn);
s.allowClick = typeof s.params.bkptEnable === 'number' ?
$(window).width() <= s.params.bkptEnable :
($enableBtn.length ? isVisible($enableBtn[0]) : false);
}, e.type === EVENT_NAME ? 0 : 200);
}).trigger(EVENT_NAME);
}
static remove(element, plugin) {
$(element).each(function () {
const s = plugin || Store(this, DATA_NAME);
$(s.params.enableBtn).off(`click.${EVENT_NAME} ${EVENT_NAME}`);
$(document).off(`keydown.${EVENT_NAME}`);
s.$element
.off(`click.${EVENT_NAME} ${EVENT_NAME}`)
.off(`click.${EVENT_NAME} ${EVENT_NAME}`);
$(document.body).off(`click.${EVENT_NAME}`);
$(window).off(`resize.${EVENT_NAME} ${EVENT_NAME}`);
Store(this, DATA_NAME, null);
});
}
}