UNPKG

@ulu/frontend

Version:

A framework-agnostic frontend toolkit providing a modular, tree-shakable library of accessible components and utilities. Designed for seamless integration, it features a highly configurable SCSS system for any environment and vanilla JavaScript modules op

318 lines (317 loc) 12.9 kB
var I = Object.defineProperty; var w = Object.getOwnPropertySymbols; var A = Object.prototype.hasOwnProperty, z = Object.prototype.propertyIsEnumerable; var g = (r, t, s) => t in r ? I(r, t, { enumerable: !0, configurable: !0, writable: !0, value: s }) : r[t] = s, b = (r, t) => { for (var s in t || (t = {})) A.call(t, s) && g(r, s, t[s]); if (w) for (var s of w(t)) z.call(t, s) && g(r, s, t[s]); return r; }; var f = (r, t, s) => g(r, typeof t != "symbol" ? t + "" : t, s); var y = (r, t, s) => new Promise((e, i) => { var n = (c) => { try { o(s.next(c)); } catch (u) { i(u); } }, a = (c) => { try { o(s.throw(c)); } catch (u) { i(u); } }, o = (c) => c.done ? e(c.value) : Promise.resolve(c.value).then(n, a); o((s = s.apply(r, t)).next()); }); import { ComponentInitializer as D } from "../core/component.js"; import { wrapSettingString as T } from "../core/settings.js"; import { ensureId as S } from "../utils/id.js"; import { hasRequiredProps as M } from "@ulu/utils/object.js"; import { trimWhitespace as L } from "@ulu/utils/string.js"; import { debounce as B } from "@ulu/utils/performance.js"; import { logError as k, log as N, logWarning as x } from "../utils/class-logger.js"; import q from "swipe-listener"; const C = new D({ type: "slider", baseAttribute: "data-ulu-slider" }), P = C.attributeSelector("track"), V = C.attributeSelector("track-container"), F = C.attributeSelector("control-context"), H = C.attributeSelector("slide"), O = [], m = { once: !0 }, $ = (r) => `${r}ms`, R = [ "container", "trackContainer", "track", "slides" ]; function _() { C.init({ withData: !0, coreEvents: ["pageModified"], setup({ element: r, data: t, initialize: s }) { j(r, t), s(); } }); } function j(r, t) { const s = Object.assign({}, t), e = { container: r, track: r.querySelector(P), trackContainer: r.querySelector(V), controlContext: r.querySelector(F), slides: r.querySelectorAll(H) }; e.slides.length && O.push(new E(e, s, !1)); } const l = class l { static _initializeGlobals() { l.globalsInitialized || (addEventListener("load", () => { addEventListener("resize", B(() => { l.instances.forEach((t) => t.handleResize()); }, 250)); }), l.reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches, l.globalsInitialized = !0); } constructor(t, s) { l._initializeGlobals(); const e = Object.assign({}, l.defaults, s); this.options = e, this.slide = null, this.index = null, this.swipeInstance = null, this.swipeListener = null, this.swipeImageListener = null, this.transitioning = !1, M(R) || k(this, "Missing a required Element"), t.slides.length || k(this, "Missing slides"), S(t.track), this.trackId = t.track.id, this.slides = [...t.slides].map((i, n) => (S(i), { element: i, index: n, id: i.id, number: n + 1 })), this.elements = b(b(b({}, t), this.createControls(t.controlContext || t.container)), this.createNav(t.navContext || t.container)), this.transition = e.transition ? e.transitionFade || l.reduceMotion ? this.fadeTransition : this.slideTransition : this.noTransition, this.setup(), this.goto(0, null, "init"), N(this, "Slider Instance Created", this), l.instances.push(this); } /** * Sliding mechanism needs translate updated on resize */ handleResize() { const { slide: t, transition: s, slideTransition: e } = this; s === e && t && this.translateTo(t.element.offsetLeft, 0); } /** * Goto to the previous slide */ previous(t) { const { index: s, slides: e } = this, i = e.length - 1, n = s - 1, a = n < 0 ? i : n; this.emit("previous", [t, a]), this.goto(a, t, "previous"); } /** * Goto to the next slide */ next(t) { const { index: s, slides: e } = this, i = s + 1, n = i > e.length - 1 ? 0 : i; this.emit("next", [t, n]), this.goto(n, t, "next"); } /** * Makes sure that no matter what the callback is called if transition event * doesn't start or fails to finish/cancel * @param {number} element * @param {number} duration Duration to wait for complete * @param {Function} beginTransition Css changes to begin/start transition */ ensureTransitionEnds(t, s, e) { return new Promise((i) => { const n = {}, a = () => { clearTimeout(n.start), n.end = setTimeout(o, s + 500); }, o = () => { clearTimeout(n.start), clearTimeout(n.end), t.removeEventListener("transitionrun", a, m), t.removeEventListener("transitionend", o, m), t.removeEventListener("transitioncancel", o, m), i(); }; t.addEventListener("transitionrun", a, m), t.addEventListener("transitionend", o, m), t.addEventListener("transitioncancel", o, m), n.start = setTimeout(o, s + 500), t.style.transitionDuration = $(s), e(), s || o(); }); } /** * Translate the track to X */ translateTo(t, s) { const { track: e } = this.elements, i = () => e.style.transform = `translateX(-${t}px)`; return e.style.willChange = "transform", this.ensureTransitionEnds(e, s, i).then(() => { e.style.willChange = "auto"; }); } /** * Show's a specific slide and hides others, except when passing true to show all * then all slides will visible */ setVisibility(t, s) { s || (t.element.style.visibility = "visible"), this.slides.forEach((e) => { e !== t && (e.element.style.visibility = s ? "visible" : "hidden"); }); } /** * Perform a fade on a single slide */ fadeSlide(t, s) { const { options: e } = this, { element: i } = t, n = s ? e.transitionDuration : e.transitionDurationExit; return this.ensureTransitionEnds(i, n, () => { i.style.opacity = s ? "1" : "0"; }); } /** * Handler for the entire slide transition */ slideTransition(a) { return y(this, arguments, function* ({ slide: t, index: s, old: e, oldIndex: i, triggerType: n }) { const o = this.slides.length, c = n === "previous", u = o - 1, h = s === 0 && i === u, p = s === u && i === 0; let d, v = this.options.transitionDuration; i && !h && !p && (v = v * Math.abs(i - s)), o < 3 ? h && !c ? d = e : p && (d = c ? t : e) : h ? d = e : p && (d = t), this.setVisibility(null, !0), d && (d.element.style.order = "-1", yield this.translateTo(h ? 0 : e.element.offsetLeft, 0)), yield this.translateTo(t.element.offsetLeft, v), d && (d.element.style.order = "0", yield this.translateTo(t.element.offsetLeft, 0)), this.setVisibility(t, !1); }); } /** * Handler for the entire fade transition */ fadeTransition(e) { return y(this, arguments, function* ({ slide: t, old: s }) { this.setVisibility(null, !0), s && (yield this.fadeSlide(s, !1), s.element.style.order = "0"), t.element.style.order = "-1", yield this.fadeSlide(t, !0), this.setVisibility(t, !1); }); } /** * Handler for the entire NO transition */ noTransition({ slide: t, old: s }) { return this.setVisibility(t, !1), s && (s.element.style.order = "0"), t.element.style.order = "-1", Promise.resolve(); } goto(t, s, e) { const { slide: i, index: n, slides: a, elements: o } = this, c = e === "init", u = a[t], h = this.getClass("nav-button--active"), p = this.getClass("transition", !0), d = { slide: u, index: t, old: i, oldIndex: n, triggerType: e }; if (t === n) { x(this, "Could not goto slide, still performing transition"); return; } if (this.transitioning) { x(this, "Cancel goto(), same slide index as current slide"); return; } this.elements.track.inert = !0, this.transitioning = !0, i && i.navButton.classList.remove(h), u.navButton.classList.add(h), o.container.classList.add(p), this.transition(d).then(() => { this.index = t, this.slide = u, this.transitioning = !1, this.elements.track.inert = !1, o.container.classList.remove(p), c || (u.element.focus(), this.emit("goto", [s, t, u])); }); } setup() { const { container: t, track: s, trackContainer: e } = this.elements, i = L(this.trackCss()), n = L(this.trackContainerStyles()), a = L(this.slideCss()); s.setAttribute("style", i), e.setAttribute("style", n), s.setAttribute("aria-live", "polite"), this.slides.forEach((o) => { o.element.setAttribute("style", a), o.element.setAttribute("tabindex", "-1"), o.element.setAttribute("role", "group"), o.element.setAttribute("aria-roledescription", "slide"), o.element.setAttribute("aria-label", `Slide ${o.number} of ${this.slides.length}`); }), t.classList.add(this.getClass()), this.options.swipeEnabled && this.setupSwipe(); } setupSwipe() { const t = this.elements.track.querySelectorAll("img"); this.swipeListener = (s) => { this.onSwipe(s); }, this.swipeImageListener = (s) => { s.preventDefault(); }, this.slides.forEach((s) => { const { element: e } = s; s.swipeInstance = q(e, this.options.swipeOptions), e.addEventListener("swipe", this.swipeListener); }), t.forEach((s) => { s.addEventListener("dragstart", this.swipeImageListener); }); } onSwipe(t) { const { directions: s } = t.detail, e = s.left ? "next" : s.right ? "previous" : null; e && this[e](t); } trackContainerStyles() { return ` overflow: hidden; `; } transitionCss(t) { const { transitionTimingFunction: s, transitionDuration: e } = this.options; return ` transition-property: ${t}; transition-duration: ${$(e)}; transition-timing-function: ${s}; `; } trackCss() { return ` display: flex; position: relative; list-style: none; ${this.transition === this.slideTransition ? this.transitionCss("transform") : ""} `; } slideCss() { const t = this.transition === this.fadeTransition; return ` width: 100%; flex: 0 0 100%; ${t ? this.transitionCss("opacity") : ""} opacity: ${t ? "0" : "1"} `; } getClass(t, s) { const { namespace: e } = this.options; return s ? `${e}--${t}` : t ? `${e}__${t}` : e; } createControlButton(t) { const s = document.createElement("button"); return s.classList.add(this.getClass("control-button")), s.classList.add(this.getClass(`control-button--${t}`)), s.classList.add(...this.options.buttonClasses), s.setAttribute("data-slider-control", t), s.setAttribute("type", "button"), s.setAttribute("aria-controls", this.trackId), s.innerHTML = this.getControlContent(t), s; } createControls(t) { const s = document.createElement("ul"), e = document.createElement("li"), i = document.createElement("li"), n = this.createControlButton("previous"), a = this.createControlButton("next"); return s.classList.add(this.getClass("controls")), e.appendChild(n), i.appendChild(a), s.appendChild(e), s.appendChild(i), n.addEventListener("click", this.previous.bind(this)), a.addEventListener("click", this.next.bind(this)), t.appendChild(s), { controls: s, previousItem: e, nextItem: i, previous: n, next: a }; } createNav(t) { const s = document.createElement("ul"), e = this.slides.map(this.createNavButton.bind(this)), i = e.map((n) => { const a = document.createElement("li"); return a.appendChild(n), s.appendChild(a), a; }); return s.classList.add(this.getClass("nav")), t.appendChild(s), { nav: s, navButtons: e, navItems: i }; } createNavButton(t, s) { const e = document.createElement("button"); return e.classList.add(this.getClass("nav-button")), e.setAttribute("type", "button"), e.setAttribute("aria-controls", t.id), e.innerHTML = this.getNavContent(t), t.navButton = e, e.addEventListener("click", this.goto.bind(this, s)), e; } getControlContent(t) { const s = this.options[t === "next" ? "iconClassNext" : "iconClassPrevious"]; return ` <span class="${this.options.classAccessiblyHidden}">${t}</span> <span class="${this.getClass("control-icon")} ${s}" aria-hidden="true"></span> `; } getNavContent(t) { return `<span class="${this.options.classAccessiblyHidden}">Item ${t.number}</span>`; } emit(t, s) { this.options.events[t] && this.options.events[t].apply(this, s); } }; f(l, "instances", []), f(l, "globalsInitialized", !1), f(l, "reduceMotion", !1), /** * Default options for slider */ f(l, "defaults", { classAccessiblyHidden: "hidden-visually", namespace: "Slider", events: {}, transition: !0, transitionFade: !1, transitionDuration: 700, transitionDurationExit: 400, transitionTimingFunction: "ease-in-out", buttonClasses: ["button", "button--icon"], iconClassPrevious: T("iconClassPrevious"), iconClassNext: T("iconClassNext"), swipeEnabled: !0, swipeOptions: { preventScroll: !0 } }); let E = l; export { E as Slider, _ as init, C as initializer, j as setupSlider };