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
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 ]\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