baseframe-js
Version:
Baseframe JS is a comprehensive suite of modular plugins and utilities designed for front-end development
238 lines (237 loc) • 8.6 kB
JavaScript
import $be from "base-elem-js";
import Store from "./core/Store";
import UrlState from "./core/UrlState";
import focusTrap from './fn/focusTrap';
import { camelCase, getDataOptions, reflow, setParams } from './util/helpers';
import { noop } from './util/helpers';
import loadFromUrl from './util/loadfromUrl';
const VERSION = '1.2.0';
const EVENT_NAME = 'modal';
const DATA_NAME = 'Modal';
const DEFAULTS = {
enableEvent: 'click',
appendTo: document.body,
ariaLabelledby: null,
ariaLabel: null,
cssPrefix: 'modal',
closeBtnIconCss: 'ico i-close',
closeBtnLabel: 'Close',
closeOutDelay: 250,
focusInDelay: 0,
backDropClose: true,
fromDOM: false,
modalCss: null,
modalID: null,
src: '',
urlFilterType: 'hash',
historyType: 'replace',
locationFilter: 'modal',
loadLocation: true,
onOpenOnce: noop,
onOpen: noop,
onClose: noop,
afterClose: noop
};
const hasCb = (cb, components) => {
if (cb && typeof cb === 'function') {
cb(components);
}
};
export default class Modal {
element;
params;
modalID;
components;
modalEvent;
focusTrapped;
enabledElem;
openedOnce;
domMarker;
cssList;
static defaults = DEFAULTS;
static version = VERSION;
static pluginName = DATA_NAME;
constructor(element, options) {
const s = this;
s.element = element;
const dataOptions = getDataOptions(element, EVENT_NAME);
const params = setParams(Modal.defaults, options, dataOptions);
const { cssPrefix } = params;
s.params = params;
s.modalID = s.params.modalID;
s.modalEvent = EVENT_NAME + '_' + s.modalID;
s.focusTrapped;
s.enabledElem;
s.openedOnce = false;
s.cssList = {
show: cssPrefix + '--show',
dismissing: cssPrefix + '--dismissing',
open: cssPrefix + '-open',
btnDismiss: cssPrefix + '__btn-dismiss',
dialog: cssPrefix + '__dialog',
dialogContent: cssPrefix + '__dialog-content',
backdrop: cssPrefix + '__backdrop',
marker: cssPrefix + '-content-placemarker'
};
s.components = s.#createComponents();
s.domMarker = $be.make(`span.${s.cssList.marker}-content-placemarker#${s.modalID}__marker`);
s.#handleEvents();
loadFromUrl(s.params, (id) => {
if (id === s.modalID) {
s.components.show ? s.close() : s.#setDisplayAndEvents();
}
});
Store(element, DATA_NAME, s);
return s;
}
static remove(element, plugin) {
$be(element).each((elem) => {
const s = plugin || Store(elem, DATA_NAME);
const params = s.params;
const { backdrop, closeBtn } = s.components;
s.components.show && s.components.close();
$be(s.element).off(`${s.params.enableEvent}.${s.modalEvent}`);
$be(closeBtn).off(`click.${s.modalEvent}Dismiss`);
if (params.backDropClose)
$be(backdrop).off(`click.${s.modalEvent}Dismiss`);
$be(document).off([`keydown.${s.modalEvent}Dismiss`, `[${s.modalEvent}Dismiss]`]);
$be(window).off([`popstate.${s.modalEvent}`, `[${s.modalEvent}]`]);
Store(elem, DATA_NAME, null);
});
}
#handleEvents() {
const s = this;
$be(s.element).on(`${s.params.enableEvent}.${s.modalEvent}`, (e) => {
s.#setDisplayAndEvents();
e.preventDefault();
});
}
#setDisplayAndEvents() {
const s = this;
s.#enableModal();
$be(document).on(`keydown.${s.modalEvent}Dismiss`, function (e) {
const ekey = e.code || e.originalEvent.key; //cash-dom || jquery
if (ekey === 'Escape') {
s.close();
e.preventDefault();
}
});
$be(document).on(`[${s.modalEvent}Dismiss]`, s.close);
}
#createComponents() {
const { make } = $be;
const s = this, p = s.params, css = s.cssList, rootCss = p.cssPrefix + (p.modalCss ? ' ' + p.modalCss : ''), ariaLabelledby = (p.ariaLabelledby || s.element.dataset.ariaLabelledby) || null, modal = make(`div.${rootCss}`, {
id: s.modalID,
ariaLabel: (p.ariaLabel || s.element.dataset.ariaLabel) || null
}), closeBtn = make(`button.${css.btnDismiss}`, {
type: 'button',
ariaLabel: p.closeBtnLabel
}, `<i class="${p.closeBtnIconCss}"></i>`), dialogContent = make(`div.${css.dialogContent}`), dialog = make(`div.${css.dialog}`), backdrop = make(`div.${css.backdrop}`), $content = $be(s.params.src ||
s.element.hash ||
s.element.dataset.modalSrc);
;
// typescript doesn't like setting this in the object above
modal.setAttribute('aria-labelledby', ariaLabelledby);
$be(dialog).insert([closeBtn, dialogContent]);
$be(modal).insert([backdrop, dialog]);
return {
backdrop,
content: $content.elem,
contentAppended: false,
dialog,
dialogContent,
closeBtn,
id: s.modalID,
modal,
close: () => s.close(),
show: false
};
}
#enableModal() {
const s = this;
const { backdrop, closeBtn, content, modal } = s.components;
const p = s.params;
const $modal = $be(modal);
s.enabledElem = document.activeElement;
if (p.fromDOM) {
$be(content).insert(s.domMarker, 'after');
}
if (!s.components.contentAppended) {
$be(s.components.dialogContent).insert(content);
$be.oa(s.components, { contentAppended: true });
}
$be(p.appendTo).insert(modal);
// attach events after appended to DOM
$be(closeBtn).on(`click.${s.modalEvent}Dismiss`, () => s.close());
if (p.backDropClose)
$be(backdrop).on(`click.${s.modalEvent}Dismiss`, () => s.close());
hasCb(p.onOpen, s.components);
if (!s.openedOnce) {
hasCb(p.onOpenOnce, s.components);
s.openedOnce = true;
}
$modal
.attr({ role: 'dialog', 'aria-modal': 'true' })
.each(elem => reflow(elem))
.addClass(s.cssList.show);
$be.oa(s.components, { show: true });
if (p.focusInDelay !== null) {
setTimeout(() => {
s.focusTrapped = focusTrap($modal, { nameSpace: camelCase(s.modalID) });
}, p.focusInDelay);
}
$be(p.appendTo).addClass(s.cssList.open).css({
overflow: 'hidden',
paddingRight: '0px'
});
if (p.urlFilterType !== 'none') {
UrlState.set(p.urlFilterType, p.locationFilter, s.modalID, p.historyType);
}
}
close() {
const s = this;
const p = s.params;
const { backdrop, closeBtn, content, modal } = s.components;
const $modal = $be(modal);
hasCb(p.onClose, s.components);
$modal
.addClass(s.cssList.dismissing)
.rmClass(s.cssList.show);
// detach events
$be(closeBtn).off(`click.${s.modalEvent}Dismiss`);
if (p.backDropClose)
$be(backdrop).off(`click.${s.modalEvent}Dismiss`);
$be(document).off([
`keydown.${s.modalEvent}Dismiss`,
`${s.modalEvent}Dismiss`
]);
if (p.urlFilterType !== 'none') {
UrlState.set(p.urlFilterType, p.locationFilter, null, p.historyType);
}
setTimeout(() => {
$modal.attr({
role: 'dialog',
'aria-modal': ''
}).rmClass(s.cssList.dismissing).css({
display: ''
});
$be(p.appendTo).rmClass(s.cssList.open).css({
overflow: null,
paddingRight: null
});
if (p.focusInDelay !== null) {
s.focusTrapped.remove();
}
if (s.enabledElem && s.enabledElem instanceof HTMLElement) {
s.enabledElem.focus();
}
if (p.fromDOM) {
$be(s.domMarker).insert(content, 'after').remove();
$be.oa(s.components, { contentAppended: false });
}
hasCb(p.afterClose, s.components);
$modal.remove();
$be.oa(s.components, { show: false });
}, p.closeOutDelay);
}
}