UNPKG

smdm-slide-menu

Version:

A library agnostic multilevel page menu with a smooth slide effect based on CSS transitions, focused on accessibility.

641 lines (639 loc) 21.3 kB
var E = ((o) => ((o.Left = 'left'), (o.Right = 'right'), o))(E || {}), c = ((o) => ( (o.Back = 'back'), (o.Close = 'close'), (o.Forward = 'forward'), (o.Navigate = 'navigate'), (o.NavigateTo = 'navigateTo'), (o.Open = 'open'), (o.Initialize = 'init'), o ))(c || {}); const r = 'slide-menu', l = { active: `${r}__submenu--active`, backlink: `${r}__backlink`, control: `${r}__control`, controls: `${r}__controls`, decorator: `${r}__decorator`, slider: `${r}__slider`, item: `${r}__item`, submenu: `${r}__submenu`, sliderWrapper: `${r}__slider__wrapper`, foldableWrapper: `${r}__foldable__wrapper`, hasSubMenu: `${r}__item--has-submenu`, activeItem: `${r}__item--active`, hasFoldableSubmenu: `${r}__item--has-foldable-submenu`, foldableSubmenu: `${r}__submenu--foldable`, foldableSubmenuAlignTop: `${r}__submenu--foldable-align-top`, foldOpen: `${r}--fold-open`, slideIn: `${r}--slide-in`, slideOut: `${r}--slide-out`, open: `${r}--open`, left: `${r}--left`, right: `${r}--right`, title: `${r}__title`, hiddenOnRoot: `${r}--hidden-on-root-level`, invisibleOnRoot: `${r}--invisible-on-root-level`, }; function L(o, t, e) { const i = []; for (; o && o.parentElement !== null && i.length < e; ) o instanceof HTMLElement && o.matches(t) && i.push(o), (o = o.parentElement); return i; } function k(o, t) { const e = L(o, t, 1); return e.length ? e[0] : null; } const g = 'a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'; function A(o) { const t = Array.from((o == null ? void 0 : o.querySelectorAll(g)) ?? []).find((e) => S(e)); t == null || t.focus(); } function S(o) { var t; for (let e = o; e && e !== document; e = e.parentNode) if ( ((t = e.style) == null ? void 0 : t.display) === 'none' || getComputedStyle(e).display === 'none' ) return !1; return !0; } function F(o, t, e, i) { const s = t.querySelectorAll(g), a = e ?? s[0], n = s[s.length - 1]; (o.key === 'Tab' || o.keyCode === 9) && (o.shiftKey ? document.activeElement === a && (n.focus(), o.preventDefault()) : document.activeElement === n && (a.focus(), o.preventDefault())); } let v = 0; class p { constructor(t, e, i) { var s, a, n; (this.menuElem = t), (this.options = e), (this.anchorElem = i), (this.isFoldable = !1), (this.active = !1), (this.id = 'smdm-' + v), v++, (this.name = ((s = this.anchorElem) == null ? void 0 : s.textContent) ?? ''), (this.parentMenuElem = ((a = i == null ? void 0 : i.parentElement) == null ? void 0 : a.closest('ul')) ?? void 0), (this.parent = (n = this.parentMenuElem) == null ? void 0 : n._slide), i && (i == null || i.classList.add(l.hasSubMenu), this.options.onlyNavigateDecorator || ((i.dataset.action = c.NavigateTo), (i.dataset.target = this.options.id), (i.dataset.arg = this.id))), t.classList.add(l.submenu), (t.dataset.smdmId = this.id), t.querySelectorAll('a').forEach((d) => { d.classList.add(l.item); }), (this.isFoldable = !!(i != null && i.classList.contains(l.hasFoldableSubmenu))), this.isFoldable && t.classList.add(l.foldableSubmenu), e.showBackLink && this.addBackLink(e), this.addLinkDecorator(e), (t._slide = this); } get isActive() { return this.active; } addBackLink(t = this.options) { var a; const e = ((a = this.anchorElem) == null ? void 0 : a.textContent) ?? '', i = document.createElement('a'); (i.innerHTML = (t.backLinkBefore ?? '') + e + (t.backLinkAfter ?? '')), i.classList.add(l.backlink, l.control, l.item), (i.dataset.action = c.Back), i.setAttribute('href', '#'); const s = document.createElement('li'); return s.appendChild(i), this.menuElem.insertBefore(s, this.menuElem.firstChild), i; } addLinkDecorator(t) { var i, s, a; const e = 'span'; if (t.submenuLinkBefore) { const n = document.createElement(e); n.classList.add(l.decorator), (n.innerHTML = t.submenuLinkBefore), (n.dataset.action = c.NavigateTo), (n.dataset.target = this.options.id), (n.dataset.arg = this.id), this.options.onlyNavigateDecorator && n.setAttribute('tabindex', '0'), (s = this.anchorElem) == null || s.insertBefore(n, (i = this.anchorElem) == null ? void 0 : i.firstChild); } if (t.submenuLinkAfter) { const n = document.createElement(e); n.classList.add(l.decorator), (n.innerHTML = t.submenuLinkAfter), (n.dataset.action = c.NavigateTo), (n.dataset.target = this.options.id), (n.dataset.arg = this.id), this.options.onlyNavigateDecorator && n.setAttribute('tabindex', '0'), (a = this.anchorElem) == null || a.appendChild(n); } return this.anchorElem; } deactivate() { return (this.active = !1), this.menuElem.classList.remove(l.active), this; } activate() { return (this.active = !0), this.menuElem.classList.add(l.active), this; } enableTabbing() { var t; (t = this.menuElem) == null || t.querySelectorAll('[tabindex="-1"]').forEach((e) => { e.setAttribute('tabindex', '0'); }); } disableTabbing() { this.menuElem.querySelectorAll(g).forEach((t) => { t.setAttribute('tabindex', '-1'); }); } appendTo(t) { return t.appendChild(this.menuElem), this; } postionTop(t) { return (this.menuElem.style.top = t + 'px'), this; } getClosestNotFoldableSlide() { return this.isFoldable ? this.getAllParents().find((t) => !t.isFoldable) : this; } getAllParents() { const t = []; let e = this.parent; for (; e; ) t.push(e), (e = e == null ? void 0 : e.parent); return t; } focusFirstElem() { A(this.menuElem); } canFold() { return this.isFoldable && window.innerWidth >= this.options.minWidthFold; } matches(t) { var e, i, s; return ( this.id === t || this.menuElem.id === t || ((e = this.anchorElem) == null ? void 0 : e.id) === t || ((i = this.anchorElem) == null ? void 0 : i.href) === t || ((s = this.anchorElem) == null ? void 0 : s.matches(t)) || this.menuElem.matches(t) ); } contains(t) { return this.anchorElem === t || this.menuElem.contains(t); } focus() { this.focusFirstElem(); } } const _ = { backLinkAfter: '', backLinkBefore: '', showBackLink: !0, keyClose: 'Escape', keyOpen: '', position: E.Right, submenuLinkAfter: '', submenuLinkBefore: '', closeOnClickOutside: !1, onlyNavigateDecorator: !1, menuWidth: 320, minWidthFold: 640, transitionDuration: 300, dynamicOpenTarget: !1, debug: !1, id: '', }; let y = 0; class w { constructor(t, e) { var i, s; if ( ((this.lastFocusedElement = null), (this.isOpen = !1), (this.isAnimating = !1), (this.lastAction = null), (this.slides = []), (this.menuTitleDefaultText = 'Menu'), t === null) ) throw new Error('Argument `elem` must be a valid HTML node'); for ( this.options = Object.assign({}, _, e), this.menuElem = t, this.options.id = this.menuElem.id ?? 'smdm-slide-menu-' + y, y++, this.menuElem.id = this.options.id, this.menuElem.classList.add(r), this.menuElem.classList.add(this.options.position), this.menuElem._slideMenu = this, document.documentElement.style.setProperty( '--smdm-sm-menu-width', `${this.options.menuWidth}px`, ), document.documentElement.style.setProperty( '--smdm-sm-min-width-fold', `${this.options.minWidthFold}px`, ), document.documentElement.style.setProperty( '--smdm-sm-transition-duration', `${this.options.transitionDuration}ms`, ), this.sliderElem = document.createElement('div'), this.sliderElem.classList.add(l.slider); this.menuElem.firstChild; ) this.sliderElem.appendChild(this.menuElem.firstChild); this.menuElem.appendChild(this.sliderElem), (this.sliderWrapperElem = document.createElement('div')), this.sliderWrapperElem.classList.add(l.sliderWrapper), this.sliderElem.appendChild(this.sliderWrapperElem), (this.foldableWrapperElem = document.createElement('div')), this.foldableWrapperElem.classList.add(l.foldableWrapper), this.sliderElem.after(this.foldableWrapperElem), (this.menuTitle = this.menuElem.querySelector(`.${l.title}`)), (this.menuTitleDefaultText = ((s = (i = this.menuTitle) == null ? void 0 : i.textContent) == null ? void 0 : s.trim()) ?? this.menuTitleDefaultText), this.options.onlyNavigateDecorator && (!this.options.submenuLinkAfter || !this.options.submenuLinkBefore) && this.debugLog( 'Make sure to provide navigation decorators manually! Otherwise `onlyNavigateDecorator` only works with `submenuLinkAfter` and `submenuLinkBefore` options!', ), this.initMenu(), this.initSlides(), this.initEventHandlers(), (this.menuElem.style.display = 'flex'), this.triggerEvent(c.Initialize), (this.activeSubmenu = this.slides[0].activate()), this.navigateTo(this.defaultOpenTarget ?? this.slides[0], !1); } get defaultOpenTarget() { const t = this.menuElem.dataset.openTarget ?? 'smdm-sm-no-default-provided'; return this.getTargetMenuFromIdentifier(t); } debugLog(...t) { this.options.debug && console.log(...t); } toggleVisibility(t, e = !0) { let i; if (t === void 0) { this.isOpen ? this.close(e) : this.show(e); return; } t ? ((i = 0), (this.lastFocusedElement = document.activeElement), setTimeout(() => { var s; (s = this.activeSubmenu) == null || s.focusFirstElem(); }, this.options.transitionDuration)) : ((i = this.options.position === E.Left ? '-100%' : '100%'), setTimeout(() => { var s; this.slides.forEach((a) => !a.isActive && a.deactivate()), (s = this.lastFocusedElement) == null || s.focus(), this.menuElem.classList.remove(l.foldOpen); }, this.options.transitionDuration)), (this.isOpen = !!t), this.moveElem(this.menuElem, i); } toggle(t = !0) { if (this.isOpen) { this.close(t); return; } this.open(t); } show(t = !0) { var e; this.triggerEvent(c.Open), this.toggleVisibility(!0, t), (e = document.querySelector('body')) == null || e.classList.add(l.open); } close(t = !0) { var e; this.triggerEvent(c.Close), this.toggleVisibility(!1, t), (e = document.querySelector('body')) == null || e.classList.remove(l.open); } back(t = !1) { var s, a, n; const e = this.slides[0]; let i = ((s = this.activeSubmenu) == null ? void 0 : s.parent) ?? e; t && ((this.activeSubmenu = ((a = this.activeSubmenu) == null ? void 0 : a.getClosestNotFoldableSlide()) ?? e), (i = ((n = this.activeSubmenu) == null ? void 0 : n.parent) ?? e), this.closeFold()), this.navigateTo(i); } closeFold() { this.slides.forEach((t) => { t.appendTo(this.sliderWrapperElem); }), this.menuElem.classList.remove(l.foldOpen); } openFold() { this.slides.forEach((t) => { t.isFoldable && t.appendTo(this.foldableWrapperElem); }), this.menuElem.classList.add(l.foldOpen); } navigateTo(t, e = !0) { e && !this.isOpen && this.show(); const i = this.findNextMenu(t), s = this.activeSubmenu, a = i.getAllParents(), n = a.find((b) => !b.canFold()), d = s == null ? void 0 : s .getAllParents() .map((b) => b.id) .includes(i.id), h = i == null ? void 0 : i .getAllParents() .map((b) => b.id) .includes((s == null ? void 0 : s.id) ?? ''); e && (this.triggerEvent(c.Navigate), d ? this.triggerEvent(c.Back) : h ? this.triggerEvent(c.Forward) : this.triggerEvent(c.NavigateTo)), this.updateMenuTitle(i, n), this.setTabbingForFold(i, n, s, a); const u = [i, ...a]; this.activateVisibleMenus(u, d, s, i); const m = this.getSlideLevel(i, d), T = -this.options.menuWidth * m; this.moveElem(this.sliderWrapperElem, T, 'px'), this.hideControlsIfOnRootLevel(m), this.setBodyTagSlideLevel(m), this.setActiveSubmenu(i), setTimeout(() => { e && i.focusFirstElem(), d && (s == null || s.deactivate()); }, this.options.transitionDuration); } setActiveSubmenu(t) { this.activeSubmenu = t; } setBodyTagSlideLevel(t) { var e; (e = document.querySelector('body')) == null || e.setAttribute('data-slide-menu-level', t.toString()); } setTabbingForFold(t, e, i, s) { t.canFold() ? (this.openFold(), t.getAllParents().forEach((a) => { a.canFold() && a.enableTabbing(); }), e == null || e.getAllParents().forEach((a) => { a.disableTabbing(); })) : i != null && i.canFold() && !t.canFold() ? (this.closeFold(), s.forEach((a) => { a.disableTabbing(); })) : !(i != null && i.canFold()) && !t.canFold() && s.forEach((a) => { a.disableTabbing(); }); } activateVisibleMenus(t, e, i, s) { const a = t.map((n) => (n == null ? void 0 : n.id)); this.slides.forEach((n) => { if (!a.includes(n.id)) { if (e && n.id === (i == null ? void 0 : i.id)) return; n.deactivate(), n.disableTabbing(); } }), t.forEach((n) => { (n != null && n.isActive) || n == null || n.activate(); }), s.enableTabbing(); } findNextMenu(t) { if (typeof t == 'string') { const e = this.getTargetMenuFromIdentifier(t); if (e instanceof p) return e; throw new Error('Invalid parameter `target`. A valid query selector is required.'); } if (t instanceof HTMLElement) { const e = this.slides.find((i) => i.contains(t)); if (e instanceof p) return e; throw new Error('Invalid parameter `target`. Not found in slide menu'); } if (t instanceof p) return t; throw new Error('No valid next slide fund'); } hideControlsIfOnRootLevel(t) { const e = document.querySelectorAll( `.${l.control}.${l.hiddenOnRoot}, .${l.control}.${l.invisibleOnRoot}`, ); t === 0 ? e.forEach((i) => { i.setAttribute('tabindex', '-1'); }) : e.forEach((i) => { i.removeAttribute('tabindex'); }); } getSlideLevel(t, e) { const i = Array.from(this.sliderWrapperElem.querySelectorAll('.' + l.active)).length, s = t.canFold() ? 0 : Number(e); return Math.max(1, i) - 1 - s; } updateMenuTitle(t, e) { var i, s, a, n; if (this.menuTitle) { let d = ((i = t == null ? void 0 : t.anchorElem) == null ? void 0 : i.textContent) ?? this.menuTitleDefaultText; const h = ((s = this.options) == null ? void 0 : s.submenuLinkAfter) ?? '', u = ((a = this.options) == null ? void 0 : a.submenuLinkBefore) ?? ''; t.canFold() && e && (d = ((n = e.anchorElem) == null ? void 0 : n.textContent) ?? d), h && (d = d.replace(h, '')), u && (d = d.replace(u, '')), (this.menuTitle.innerText = d.trim()); } } getTargetMenuFromIdentifier(t) { return ( this.slides.find((e) => e.matches(t)) ?? this.slides.find((e) => e.menuElem.querySelector(t)) ); } getTargetMenuDynamically() { const t = location.pathname, e = location.hash, i = this.slides.find((a) => a.matches(e)); return this.slides.find((a) => a.matches(t)) ?? i; } open(t = !0) { const e = this.options.dynamicOpenTarget ? this.getTargetMenuDynamically() : this.defaultOpenTarget; e && this.navigateTo(e), this.show(t); } initEventHandlers() { this.menuElem.addEventListener('transitionend', this.onTransitionEnd.bind(this)), this.sliderElem.addEventListener('transitionend', this.onTransitionEnd.bind(this)), this.options.closeOnClickOutside && document.addEventListener('click', (t) => { var e; this.isOpen && !this.isAnimating && !this.menuElem.contains(t.target) && !((e = t.target) != null && e.closest('.' + l.control)) && this.close(); }), this.initKeybindings(); } onTransitionEnd(t) { (t.target !== this.menuElem && t.target !== this.sliderElem && t.target !== this.foldableWrapperElem && t.target !== this.sliderWrapperElem) || ((this.isAnimating = !1), this.lastAction && (this.triggerEvent(this.lastAction, !0), (this.lastAction = null))); } initKeybindings() { document.addEventListener('keydown', (t) => { const e = document.activeElement; switch (t.key) { case this.options.keyClose: t.preventDefault(), this.close(); break; case this.options.keyOpen: t.preventDefault(), this.show(); break; case 'Enter': e != null && e.classList.contains(l.decorator) && e.click(); break; } }); } triggerEvent(t, e = !1) { this.lastAction = t; const i = `sm.${t}${e ? '-after' : ''}`, s = new CustomEvent(i); this.menuElem.dispatchEvent(s); } markSelectedItem(t) { this.menuElem.querySelectorAll('.' + l.activeItem).forEach((e) => { e.classList.remove(l.activeItem); }), t.classList.add(l.activeItem); } moveElem(t, e, i = '%') { setTimeout(() => { e.toString().includes(i) || (e += i); const s = `translateX(${e})`; t.style.transform !== s && ((this.isAnimating = !0), (t.style.transform = s)); }, 0); } initMenu() { this.runWithoutAnimation(() => { switch (this.options.position) { case E.Left: Object.assign(this.menuElem.style, { left: 0, right: 'auto', transform: 'translateX(-100%)', }); break; default: Object.assign(this.menuElem.style, { left: 'auto', right: 0 }); break; } }), this.menuElem.classList.add(this.options.position); const t = this.menuElem.querySelector('ul'); t && this.slides.push(new p(t, this.options)), this.menuElem.addEventListener('keydown', (e) => { var s; const i = this.menuElem.querySelector( `.${l.controls} .${l.control}:not([disabled]):not([tabindex="-1"])`, ); F(e, ((s = this.activeSubmenu) == null ? void 0 : s.menuElem) ?? this.menuElem, i); }); } runWithoutAnimation(t) { const e = [this.menuElem, this.sliderElem]; e.forEach((i) => (i.style.transition = 'none')), t(), this.menuElem.offsetHeight, e.forEach((i) => i.style.removeProperty('transition')), (this.isAnimating = !1); } initSlides() { this.menuElem.querySelectorAll('a').forEach((t, e) => { if (t.parentElement === null) return; const i = t.parentElement.querySelector('ul'); if (!i) return; const s = new p(i, this.options, t); this.slides.push(s); }), this.slides.forEach((t) => { t.appendTo(this.sliderWrapperElem); }); } get onlyNavigateDecorator() { return this.options.onlyNavigateDecorator; } } document.addEventListener('click', (o) => { var m; const t = (f) => f ? f.classList.contains(l.control) || f.classList.contains(l.hasSubMenu) || f.classList.contains(l.decorator) : !1, e = t(o.target) ? o.target : (m = o.target) == null ? void 0 : m.closest( `.${l.decorator}[data-action], .${l.control}[data-action], .${l.hasSubMenu}[data-action]`, ); if (!e || !t(e)) return; const i = e.getAttribute('data-target'), s = !i || i === 'this' ? k(e, `.${r}`) : document.getElementById(i) ?? document.querySelector(i); if (!s) throw new Error(`Unable to find menu ${i}`); const a = s._slideMenu; a && !a.onlyNavigateDecorator && o.preventDefault(), a && a.onlyNavigateDecorator && e.matches(`.${l.decorator}`) && o.preventDefault(); const n = e.getAttribute('data-action'), d = e.getAttribute('data-arg'), h = { false: !1, true: !0, null: null, undefined: void 0 }, u = Object.keys(h).includes((d == null ? void 0 : d.toString()) ?? '') ? h[d] : d; a && n && typeof a[n] == 'function' && (u ? a[n](u) : a[n]()); }); window.SlideMenu = w; window.dispatchEvent(new Event('sm.ready'));