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
JavaScript
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