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.

490 lines (489 loc) 17.2 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 Action ]\n*/\n\n:host {\n --wje-toolbar-action-breakpoint-sm: 576px;\n --wje-toolbar-action-breakpoint-md: 768px;\n --wje-toolbar-action-breakpoint-lg: 992px;\n --wje-toolbar-action-breakpoint-xl: 1200px;\n --wje-toolbar-action-breakpoint-2xl: 1450px;\n --wje-toolbar-action-breakpoint-xxl: 1450px;\n\n .native-toolbar-action {\n display: flex;\n align-items: center;\n gap: var(--wje-toolbar-action-gap);\n }\n\n .toolbar-action-more {\n flex: 0 0 auto;\n }\n}\n"; const _ToolbarAction = class _ToolbarAction extends WJElement { /** * Creates an instance of ToolbarAction. */ constructor() { super(); /** * The class name for the component. * @type {string} */ __publicField(this, "className", "ToolbarAction"); this._managedHiddenActions = /* @__PURE__ */ new WeakSet(); this._isCollapsedByBreakpoint = null; this._managedOverflowNodes = /* @__PURE__ */ new WeakSet(); this._overflowRetryFrame = null; this._applyOverflowFrame = null; this._observedDropdown = 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 ["breakpoint", "max-items", "visible-items"]; } /** * Sets the collapse breakpoint token or value. * @param {string} value Breakpoint token or CSS size. */ set breakpoint(value) { if (value) this.setAttribute("breakpoint", value); else this.removeAttribute("breakpoint"); } /** * Gets the collapse breakpoint token or value. * @returns {string} */ get breakpoint() { return this.getAttribute("breakpoint") || ""; } /** * Sets the maximum number of visible actions. * @param {number|string} value The maximum number of visible actions. */ set maxItems(value) { this.setAttribute("max-items", value || 0); } /** * Gets the maximum number of visible actions. * @returns {number} */ get maxItems() { return +this.getAttribute("max-items") || 0; } /** * Sets the responsive visible action count. * @param {number|string} value The visible action count. */ set visibleItems(value) { if (value === null || value === void 0 || value === "") { this.removeAttribute("visible-items"); return; } this.setAttribute("visible-items", value); } /** * Gets the responsive visible action count. * @returns {number|null} */ get visibleItems() { if (!this.hasAttribute("visible-items")) return null; const value = +this.getAttribute("visible-items"); return Number.isFinite(value) ? value : null; } /** * Sets up the attributes for the component. */ setupAttributes() { this.isShadowRoot = "open"; } /** * Draws the component for the toolbar action. * @returns {object} Document fragment */ draw() { let fragment = document.createDocumentFragment(); let slot = document.createElement("slot"); let element = document.createElement("div"); element.setAttribute("part", "native"); element.classList.add("native-toolbar-action"); let dropdown = document.createElement("wje-dropdown"); dropdown.setAttribute("placement", "bottom-end"); dropdown.setAttribute("collapsible", ""); dropdown.classList.add("toolbar-action-more"); dropdown.hidden = true; let trigger = document.createElement("wje-button"); trigger.setAttribute("slot", "trigger"); trigger.setAttribute("fill", "link"); trigger.setAttribute("aria-label", "Show more actions"); trigger.innerHTML = '<wje-icon name="dots"></wje-icon>'; let menu = document.createElement("wje-menu"); menu.setAttribute("variant", "context"); dropdown.append(trigger, menu); element.appendChild(slot); element.appendChild(dropdown); fragment.appendChild(element); this.defaultSlot = slot; this.native = element; this.moreDropdown = dropdown; this.moreMenu = menu; return fragment; } /** * Applies the current visible action limit after the component is drawn. */ afterDraw() { var _a; this.onSlotChange = () => { this.observeExistingDropdown(); this.scheduleOverflow(); }; (_a = this.defaultSlot) == null ? void 0 : _a.addEventListener("slotchange", this.onSlotChange); this.handleResize = () => this.handleBreakpointResize(); if (this.getBreakpointWidth()) { window.addEventListener("resize", this.handleResize); } this.observeExistingDropdown(); this.scheduleOverflow(true); } /** * Removes listeners after disconnect. */ afterDisconnect() { var _a, _b; (_a = this.defaultSlot) == null ? void 0 : _a.removeEventListener("slotchange", this.onSlotChange); window.removeEventListener("resize", this.handleResize); this._isCollapsedByBreakpoint = null; if (this._overflowRetryFrame) { cancelAnimationFrame(this._overflowRetryFrame); this._overflowRetryFrame = null; } if (this._applyOverflowFrame) { cancelAnimationFrame(this._applyOverflowFrame); this._applyOverflowFrame = null; } (_b = this._dropdownObserver) == null ? void 0 : _b.disconnect(); this._dropdownObserver = null; this._observedDropdown = null; } /** * Schedules overflow application after layout settles. * @param {boolean} [doubleFrame] Wait an extra frame for initial render/hydration. */ scheduleOverflow(doubleFrame = false) { if (this._applyOverflowFrame) return; this._applyOverflowFrame = requestAnimationFrame(() => { this._applyOverflowFrame = null; if (doubleFrame) { this.scheduleOverflow(false); return; } this.applyOverflow(); }); } /** * Returns the actions for the toolbar action. * @returns {Array<HTMLElement>} Managed toolbar actions. */ getActions() { return this.getAssignedElements().filter( (element) => { var _a; return element.getAttribute("slot") !== "trigger" && (((_a = element.tagName) == null ? void 0 : _a.toLowerCase()) === "wje-button" || element.hasAttribute("data-toolbar-action")); } ); } /** * Returns direct children assigned to the default slot. * @returns {Array<HTMLElement>} */ getAssignedElements() { var _a, _b; return ((_b = (_a = this.defaultSlot) == null ? void 0 : _a.assignedElements) == null ? void 0 : _b.call(_a)) || Array.from(this.children); } /** * Returns an existing top-level dropdown if present. * @returns {HTMLElement|null} */ getExistingDropdown() { return this.getAssignedElements().find((element) => { var _a; return ((_a = element.tagName) == null ? void 0 : _a.toLowerCase()) === "wje-dropdown"; }) || null; } /** * Observes the external dropdown for late menu/content changes. */ observeExistingDropdown() { var _a; const dropdown = this.getExistingDropdown(); if (dropdown === this._observedDropdown) return; (_a = this._dropdownObserver) == null ? void 0 : _a.disconnect(); this._dropdownObserver = null; this._observedDropdown = dropdown; if (!dropdown || typeof MutationObserver !== "function") return; this._dropdownObserver = new MutationObserver(() => this.scheduleOverflow()); this._dropdownObserver.observe(dropdown, { childList: true, subtree: false }); } /** * Returns the dropdown that should receive overflow items. * @returns {HTMLElement} */ getOverflowDropdown() { return this.getExistingDropdown() || this.moreDropdown; } /** * Returns the menu used for overflow items. * @returns {HTMLElement|null} */ getOverflowMenu() { const existingDropdown = this.getExistingDropdown(); if (existingDropdown) { return Array.from(existingDropdown.children).find((element) => { var _a; return ((_a = element.tagName) == null ? void 0 : _a.toLowerCase()) === "wje-menu"; }) || null; } return this.moreMenu; } /** * Gets the number of actions that should stay visible. * @returns {number} */ getVisibleLimit() { const actions = this.getActions(); if (this.isCollapsedByBreakpoint()) { return 0; } const maxItems = this.maxItems > 0 ? this.maxItems : actions.length; const visibleItems = this.visibleItems; const limit = visibleItems === null ? maxItems : Math.min(visibleItems, maxItems); return Math.max(0, Math.min(limit, actions.length)); } /** * Returns whether the toolbar action should collapse based on the configured breakpoint. * @returns {boolean} */ shouldCollapseByBreakpoint() { const breakpointWidth = this.getBreakpointWidth(); if (!breakpointWidth) return false; return window.innerWidth < breakpointWidth; } /** * Returns the cached breakpoint collapse state. * @returns {boolean} */ isCollapsedByBreakpoint() { if (!this.getBreakpointWidth()) { this._isCollapsedByBreakpoint = false; return false; } const nextState = this.shouldCollapseByBreakpoint(); this._isCollapsedByBreakpoint = nextState; return nextState; } /** * Reacts to viewport resize only when the breakpoint mode actually changes. * @returns {void} */ handleBreakpointResize() { if (!this.getBreakpointWidth()) return; const nextState = this.shouldCollapseByBreakpoint(); if (this._isCollapsedByBreakpoint === nextState) return; this._isCollapsedByBreakpoint = nextState; this.scheduleOverflow(); } /** * Resolves the configured breakpoint to a pixel width. * @returns {number|null} */ getBreakpointWidth() { if (!this.breakpoint) return null; const token = this.breakpoint.trim().toLowerCase(); const cssValue = getComputedStyle(this).getPropertyValue(`--wje-toolbar-action-breakpoint-${token}`).trim(); const namedBreakpoint = _ToolbarAction.BREAKPOINTS[token]; if (cssValue) { const cssNumber = parseFloat(cssValue); if (Number.isFinite(cssNumber)) return cssNumber; } if (Number.isFinite(namedBreakpoint)) { return namedBreakpoint; } const directNumber = parseFloat(token); return Number.isFinite(directNumber) ? directNumber : null; } /** * Updates visible actions and the overflow dropdown. * @returns {void} */ applyOverflow() { var _a; const actions = this.getActions(); const visibleLimit = this.getVisibleLimit(); const overflowActions = actions.slice(visibleLimit); const existingDropdown = this.getExistingDropdown(); const overflowMenu = this.getOverflowMenu(); if (this._overflowRetryFrame) { cancelAnimationFrame(this._overflowRetryFrame); this._overflowRetryFrame = null; } this.restoreManagedActions(actions); this.restoreManagedOverflowContent(); if (existingDropdown && overflowActions.length > 0 && !overflowMenu) { this._overflowRetryFrame = requestAnimationFrame(() => { this._overflowRetryFrame = null; this.scheduleOverflow(); }); return; } if (!existingDropdown) { (_a = this.moreMenu) == null ? void 0 : _a.replaceChildren(); } overflowActions.forEach((action) => { action.hidden = true; action.style.display = "none"; this._managedHiddenActions.add(action); }); if (overflowMenu && existingDropdown && overflowActions.length > 0 && overflowMenu.children.length > 0) { overflowMenu.append(this.createOverflowDivider()); } overflowActions.forEach((action) => { overflowMenu == null ? void 0 : overflowMenu.append(this.createMenuItem(action)); }); if (!existingDropdown && this.moreDropdown) { this.moreDropdown.hidden = overflowActions.length === 0; } else if (existingDropdown && this.moreDropdown) { this.moreDropdown.hidden = true; } } /** * Restores buttons hidden by this component. * @param {Array<HTMLElement>} actions Toolbar buttons. */ restoreManagedActions(actions = this.getActions()) { actions.forEach((action) => { if (this._managedHiddenActions.has(action)) { action.hidden = false; action.style.removeProperty("display"); this._managedHiddenActions.delete(action); } }); } /** * Removes overflow menu nodes that were previously injected by this component. */ restoreManagedOverflowContent() { const dropdown = this.getExistingDropdown(); if (!dropdown) return; const menu = this.getOverflowMenu(); if (!menu) return; Array.from(menu.children).forEach((child) => { if (this._managedOverflowNodes.has(child)) { child.remove(); this._managedOverflowNodes.delete(child); } }); } /** * Creates a dropdown menu proxy for an overflowed button. * @param {HTMLElement} action The original action button. * @returns {HTMLElement} */ createMenuItem(action) { let menuItem = document.createElement("wje-menu-item"); menuItem.innerHTML = action.innerHTML; if (action.hasAttribute("disabled") || action.getAttribute("aria-disabled") === "true") { menuItem.setAttribute("disabled", ""); } menuItem.addEventListener("wje-menu-item:click", (e) => this.handleMenuItemClick(e, action)); menuItem.addEventListener("click", (e) => this.handleMenuItemClick(e, action)); this._managedOverflowNodes.add(menuItem); return menuItem; } /** * Creates a divider separating existing dropdown actions from responsive overflow actions. * @returns {HTMLElement} */ createOverflowDivider() { const divider = document.createElement("wje-divider"); this._managedOverflowNodes.add(divider); return divider; } /** * Forwards menu item activation to the original button. * @param {Event} e The menu event. * @param {HTMLElement} action The original action button. */ handleMenuItemClick(e, action) { var _a; (_a = e.preventDefault) == null ? void 0 : _a.call(e); e.stopPropagation(); if (!action || action.hasAttribute("disabled") || action.getAttribute("aria-disabled") === "true") return; action.click(); } /** * Measures action widths while preserving current overflow state. * @returns {{count: number, widths: number[], gap: number, moreWidth: number, getWidthForCount: Function}} */ measureActionMetrics() { const actions = this.getActions(); const hasExistingDropdown = !!this.getExistingDropdown(); this.restoreManagedActions(actions); const widths = actions.map((action) => action.getBoundingClientRect().width); const style = this.native ? getComputedStyle(this.native) : null; const gap = style ? parseFloat(style.columnGap || style.gap || "0") || 0 : 0; const moreWidth = this.measureMoreWidth(); this.applyOverflow(); return { count: actions.length, widths, gap, moreWidth, getWidthForCount: (visibleCount) => { const count = Math.max(0, Math.min(visibleCount, actions.length)); const overflowCount = actions.length - count; const visibleWidth = widths.slice(0, count).reduce((sum, width) => sum + width, 0); const visibleGaps = Math.max(count - 1, 0) * gap; const usesDropdown = hasExistingDropdown || overflowCount > 0; const moreGap = count > 0 && usesDropdown ? gap : 0; return visibleWidth + visibleGaps + (usesDropdown ? moreWidth + moreGap : 0); } }; } /** * Measures the overflow dropdown trigger. * @returns {number} */ measureMoreWidth() { const dropdown = this.getOverflowDropdown(); if (!dropdown) return 48; const isInternalDropdown = dropdown === this.moreDropdown; const wasHidden = dropdown.hidden; const previousVisibility = dropdown.style.visibility; if (isInternalDropdown && wasHidden) { dropdown.hidden = false; dropdown.style.visibility = "hidden"; } const width = dropdown.getBoundingClientRect().width || 48; if (isInternalDropdown && wasHidden) { dropdown.hidden = true; dropdown.style.visibility = previousVisibility; } if (dropdown === this.moreDropdown && this.getExistingDropdown()) { this.moreDropdown.hidden = true; } return width; } }; __publicField(_ToolbarAction, "BREAKPOINTS", { sm: 576, md: 768, lg: 992, xl: 1200, "2xl": 1450, xxl: 1450 }); let ToolbarAction = _ToolbarAction; ToolbarAction.define("wje-toolbar-action", ToolbarAction); export { ToolbarAction as default }; //# sourceMappingURL=wje-toolbar-action.js.map