UNPKG

wj-elements

Version:

WebJET Elements is a modern set of user interface tools harnessing the power of web components designed to simplify web application development.

336 lines (335 loc) 13.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import WJElement from "./wje-element.js"; const styles = "/*\n[ WJ Toolbar ]\n*/\n\n:host {\n width: 100%;\n height: var(--wje-toolbar-height);\n}\n\n.native-toolbar {\n background-color: var(--wje-toolbar-background);\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n justify-content: flex-start;\n border-bottom: 1px solid var(--wje-toolbar-border-color);\n padding-inline: var(--wje-toolbar-padding-inline);\n padding-block: var(--wje-toolbar-padding-block);\n box-shadow: var(--wje-toolbar-shadow);\n gap: var(--wje-toolbar-action-gap);\n overflow: hidden;\n}\n\n::slotted {\n grid-column: span 4;\n}\n\n::slotted([slot='start']) {\n min-width: 0;\n margin-right: auto;\n}\n\n::slotted([slot='end']) {\n flex: 0 0 auto;\n}\n\n:host([sticky]) {\n position: sticky;\n top: var(--wje-toolbar-top);\n z-index: 99;\n}\n"; class Toolbar extends WJElement { /** * Creates an instance of Toolbar. */ constructor() { super(); /** * The class name for the component. * @type {string} */ __publicField(this, "className", "Toolbar"); this._breadcrumbState = /* @__PURE__ */ new WeakMap(); this._responsiveFrame = null; } /** * Returns the CSS stylesheet for the component. * @static * @returns {CSSStyleSheet} The CSS stylesheet */ static get cssStyleSheet() { return styles; } /** * Returns the list of observed attributes. * @static * @returns {Array} An empty array */ static get observedAttributes() { return []; } /** * Sets up the attributes for the component. */ setupAttributes() { this.isShadowRoot = "open"; this.syncAria(); } /** * Draws the component for the toolbar. * @returns {object} Document fragment */ draw() { let fragment = document.createDocumentFragment(); let native = document.createElement("div"); native.setAttribute("part", "native"); native.classList.add("native-toolbar"); let start = document.createElement("slot"); start.setAttribute("name", "start"); let end = document.createElement("slot"); end.setAttribute("name", "end"); native.appendChild(start); native.appendChild(end); fragment.appendChild(native); this.native = native; this.startSlot = start; this.endSlot = end; return fragment; } /** * Initializes responsive layout observers. */ afterDraw() { var _a, _b; this.onSlotChange = () => { this._breadcrumbState = /* @__PURE__ */ new WeakMap(); this.scheduleResponsiveLayout(); }; (_a = this.startSlot) == null ? void 0 : _a.addEventListener("slotchange", this.onSlotChange); (_b = this.endSlot) == null ? void 0 : _b.addEventListener("slotchange", this.onSlotChange); if (typeof ResizeObserver === "function") { this._resizeObserver = new ResizeObserver(() => this.scheduleResponsiveLayout()); this._resizeObserver.observe(this.native || this); } this.scheduleResponsiveLayout(); } /** * Cleans up responsive layout observers. */ afterDisconnect() { var _a, _b, _c; (_a = this.startSlot) == null ? void 0 : _a.removeEventListener("slotchange", this.onSlotChange); (_b = this.endSlot) == null ? void 0 : _b.removeEventListener("slotchange", this.onSlotChange); (_c = this._resizeObserver) == null ? void 0 : _c.disconnect(); if (this._responsiveFrame) { cancelAnimationFrame(this._responsiveFrame); this._responsiveFrame = null; } } /** * Sync ARIA attributes on host. */ syncAria() { if (!this.hasAttribute("role")) { this.setAriaState({ role: "toolbar" }); } const ariaLabel = this.getAttribute("aria-label"); const label = this.getAttribute("label"); if (!ariaLabel && label) { this.setAriaState({ label }); } } /** * Schedules responsive layout recalculation. */ scheduleResponsiveLayout() { if (this._responsiveFrame) return; this._responsiveFrame = requestAnimationFrame(() => { this._responsiveFrame = null; this.updateResponsiveLayout(); }); } /** * Updates slotted breadcrumbs and actions to fit the toolbar width. * @returns {Promise<void>} */ async updateResponsiveLayout() { var _a; const action = this.getToolbarAction(); const breadcrumbs = this.getBreadcrumbs(); const isSelfManagedAction = this.isSelfManagedAction(action); const isSelfManagedBreadcrumbs = this.isSelfManagedBreadcrumbs(breadcrumbs, action); if (!this.native || !action && !breadcrumbs) return; const toolbarWidth = this.native.getBoundingClientRect().width; if (!toolbarWidth) return; const actionMetrics = action && !isSelfManagedAction ? (_a = action.measureActionMetrics) == null ? void 0 : _a.call(action) : null; const breadcrumbMetrics = breadcrumbs && !isSelfManagedBreadcrumbs ? await this.measureBreadcrumbs(breadcrumbs) : null; let visibleActions = (actionMetrics == null ? void 0 : actionMetrics.count) || 0; let compactBreadcrumbs = false; if (actionMetrics && breadcrumbMetrics) { for (let count = actionMetrics.count; count >= 0; count--) { const actionWidth = actionMetrics.getWidthForCount(count); const availableBreadcrumbWidth = toolbarWidth - actionWidth; if (breadcrumbMetrics.fullWidth <= availableBreadcrumbWidth) { visibleActions = count; compactBreadcrumbs = false; break; } if (breadcrumbMetrics.compactWidth <= availableBreadcrumbWidth) { visibleActions = count; compactBreadcrumbs = true; break; } if (count === 0) { visibleActions = 0; compactBreadcrumbs = true; } } } else if (actionMetrics) { visibleActions = this.getVisibleActionsForWidth(actionMetrics, toolbarWidth); } else if (breadcrumbMetrics) { compactBreadcrumbs = breadcrumbMetrics.fullWidth > toolbarWidth; } if (isSelfManagedAction) { this.clearVisibleActions(action); } else { this.setVisibleActions(action, visibleActions); } if (!isSelfManagedBreadcrumbs) { this.setBreadcrumbCompactState(breadcrumbs, compactBreadcrumbs); } } /** * Measures breadcrumbs in their full state. * @param {HTMLElement} breadcrumbs Breadcrumbs component. * @returns {Promise<{count: number, fullWidth: number, compactWidth: number}>} */ async measureBreadcrumbs(breadcrumbs) { var _a; const items = ((_a = breadcrumbs.getBreadcrumbs) == null ? void 0 : _a.call(breadcrumbs)) || []; const count = items.length; if (count === 0) { return { count: 0, fullWidth: 0, compactWidth: 0 }; } const cachedState = this.ensureBreadcrumbState(breadcrumbs, count); if (cachedState.count === count && cachedState.fullWidth) { return { count, fullWidth: cachedState.fullWidth, compactWidth: cachedState.compactWidth }; } this.setBreadcrumbMaxItems(breadcrumbs, count); await breadcrumbs.updateComplete; await this.nextFrame(); const itemWidths = items.map((item) => item.getBoundingClientRect().width); const fullWidth = itemWidths.reduce((sum, width) => sum + width, 0); const state = this.ensureBreadcrumbState(breadcrumbs, count); const before = Math.max(1, breadcrumbs.itemsBeforeCollapse || 1); const after = Math.max(1, breadcrumbs.itemsAfterCollapse || 1); const visibleBefore = itemWidths.slice(0, before).reduce((sum, width) => sum + width, 0); const visibleAfter = itemWidths.slice(Math.max(count - after, before)).reduce((sum, width) => sum + width, 0); const compactWidth = count > state.compactMaxItems ? visibleBefore + visibleAfter + 48 : fullWidth; state.count = count; state.fullWidth = fullWidth; state.compactWidth = compactWidth; return { count, fullWidth, compactWidth }; } /** * Stores original breadcrumb settings used as responsive compact target. * @param {HTMLElement} breadcrumbs Breadcrumbs component. * @param {number} count Number of breadcrumb elements currently in the trail. * @returns {{compactMaxItems: number}} */ ensureBreadcrumbState(breadcrumbs, count) { if (!this._breadcrumbState.has(breadcrumbs)) { const attr = breadcrumbs.getAttribute("max-items"); const parsed = attr === null ? NaN : Number(attr); const compactMaxItems = Number.isFinite(parsed) && parsed > 0 ? parsed : Math.min(3, count); this._breadcrumbState.set(breadcrumbs, { compactMaxItems: Math.max(1, Math.min(compactMaxItems, count)) }); } return this._breadcrumbState.get(breadcrumbs); } /** * Applies the compact or full breadcrumb state. * @param {HTMLElement|null} breadcrumbs Breadcrumbs component. * @param {boolean} compact Whether compact mode should be used. */ setBreadcrumbCompactState(breadcrumbs, compact) { var _a; if (!breadcrumbs) return; const count = ((_a = breadcrumbs.getBreadcrumbs) == null ? void 0 : _a.call(breadcrumbs).length) || 0; const state = this.ensureBreadcrumbState(breadcrumbs, count); const nextMaxItems = compact ? state.compactMaxItems : count; this.setBreadcrumbMaxItems(breadcrumbs, nextMaxItems); } /** * Sets breadcrumb max-items only when it changed. * @param {HTMLElement} breadcrumbs Breadcrumbs component. * @param {number} value The max item count. */ setBreadcrumbMaxItems(breadcrumbs, value) { var _a, _b; if (!breadcrumbs || !value) return; if (+breadcrumbs.getAttribute("max-items") === value) { (_a = breadcrumbs.updateCollapse) == null ? void 0 : _a.call(breadcrumbs); return; } breadcrumbs.setAttribute("max-items", value); (_b = breadcrumbs.updateCollapse) == null ? void 0 : _b.call(breadcrumbs); } /** * Finds how many actions fit into the available width. * @param {object} actionMetrics Measured action metrics. * @param {number} width Available width. * @returns {number} */ getVisibleActionsForWidth(actionMetrics, width) { for (let count = actionMetrics.count; count >= 0; count--) { if (actionMetrics.getWidthForCount(count) <= width) return count; } return 0; } /** * Applies visible action count. * @param {HTMLElement|null} action Toolbar action component. * @param {number} count Visible action count. */ setVisibleActions(action, count) { var _a; if (!action) return; if (+action.getAttribute("visible-items") === count) { return; } action.setAttribute("visible-items", count); (_a = action.applyOverflow) == null ? void 0 : _a.call(action); } /** * Clears toolbar-managed visible action state when actions manage themselves. * @param {HTMLElement|null} action Toolbar action component. */ clearVisibleActions(action) { var _a; if (!((_a = action == null ? void 0 : action.hasAttribute) == null ? void 0 : _a.call(action, "visible-items"))) return; action.removeAttribute("visible-items"); } /** * Returns the slotted toolbar action. * @returns {HTMLElement|null} */ getToolbarAction() { var _a, _b; return ((_b = (_a = this.endSlot) == null ? void 0 : _a.assignedElements) == null ? void 0 : _b.call(_a).find((el) => { var _a2; return ((_a2 = el.tagName) == null ? void 0 : _a2.toLowerCase()) === "wje-toolbar-action"; })) || null; } /** * Returns the slotted breadcrumbs. * @returns {HTMLElement|null} */ getBreadcrumbs() { var _a, _b; return ((_b = (_a = this.startSlot) == null ? void 0 : _a.assignedElements) == null ? void 0 : _b.call(_a).find((el) => { var _a2; return ((_a2 = el.tagName) == null ? void 0 : _a2.toLowerCase()) === "wje-breadcrumbs"; })) || null; } /** * Returns whether toolbar actions are managed by their own breakpoint logic. * @param {HTMLElement|null} action Toolbar action component. * @returns {boolean} */ isSelfManagedAction(action) { var _a; return !!((_a = action == null ? void 0 : action.getAttribute) == null ? void 0 : _a.call(action, "breakpoint")); } /** * Returns whether breadcrumb collapse is managed by its own breakpoint logic. * @param {HTMLElement|null} breadcrumbs Breadcrumbs component. * @returns {boolean} */ isSelfManagedBreadcrumbs(breadcrumbs, action) { var _a, _b; return !!(breadcrumbs == null ? void 0 : breadcrumbs.getAttribute("breakpoint")) || ((_a = breadcrumbs == null ? void 0 : breadcrumbs.hasAttribute) == null ? void 0 : _a.call(breadcrumbs, "max-items")) && !!((_b = action == null ? void 0 : action.getExistingDropdown) == null ? void 0 : _b.call(action)); } /** * Waits for one animation frame. * @returns {Promise<void>} */ nextFrame() { return new Promise((resolve) => requestAnimationFrame(resolve)); } } Toolbar.define("wje-toolbar", Toolbar); export { Toolbar as default }; //# sourceMappingURL=wje-toolbar.js.map