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.
363 lines (362 loc) • 14 kB
JavaScript
var __defProp = Object.defineProperty;
var __typeError = (msg) => {
throw TypeError(msg);
};
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);
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
var _onMenuItemCustom;
import WJElement from "./wje-element.js";
import { P as Popup } from "./popup.element-C8-g3WLs.js";
import { event } from "./event.js";
const _Dropdown = class _Dropdown extends WJElement {
/**
* Creates an instance of Dropdown.
* @class
*/
constructor() {
super();
/**
* The placement of the dropdown.
* @type {{"wje-popup": Popup}}
*/
__publicField(this, "dependencies", {
"wje-popup": Popup
});
/**
* Sets the placement of the dropdown.
* @type {string}
*/
__publicField(this, "className", "Dropdown");
/**
* Callback function to handle other dropdowns being opened. Close the dropdown if it is not the target and collapse is enabled.
* @param {Event} e The event object.
*/
__publicField(this, "otherDropdownOpennedCallback", (e) => {
if (e.detail.detail.target !== this) {
this.classList.remove("active");
this.popup.hide();
}
});
/**
* Handles popup hide events and closes only the dropdown that owns the popup.
* This prevents nested dropdowns from collapsing their parent dropdown when the
* child popup is hidden.
* @param {Event} e The popup hide event.
*/
__publicField(this, "popupHideCallback", (e) => {
if (this.classList.contains("active") && e.target === this) {
this.toggleCallback(e);
}
});
/**
* Handles delegated clicks from inside the popup and closes the dropdown when a leaf menu item is selected.
* This works even when the menu is portaled, because we rely on the composed path.
*/
__publicField(this, "onMenuItemClick", (e) => {
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
const item = path.find((n) => n && n.tagName === "WJE-MENU-ITEM");
if (!item) return;
const owner = this.getMenuItemOwner(path, item);
if (owner && owner !== this) return;
if (item.hasAttribute("disabled") || item.getAttribute("aria-disabled") === "true") return;
if (typeof item.querySelector === "function" && item.querySelector("wje-menu")) return;
this.onClose();
});
/**
* @summary Toggles the dropdown element between active and inactive states.
* Calls the `onOpen` method if the element is currently inactive,
* and calls the `onClose` method if the element is currently active.
* @param {Event} e The event object.
*/
__publicField(this, "toggleCallback", (e) => {
if (this.classList.contains("active")) {
e.stopPropagation();
this.onClose();
} else {
e.stopPropagation();
this.onOpen(e);
}
});
/**
* Open the popup element.
* @param {object} e
*/
__publicField(this, "onOpen", (e) => {
this.classList.add("active");
this.syncAria();
Promise.resolve(this.beforeShow(this)).then((res) => {
if (!this.classList.contains("active")) {
throw new Error("beforeShow method returned false or not string");
}
this.popup.show(true);
this.syncPopupOwner();
event.addListener(document, "wje-menu-item:click", null, __privateGet(this, _onMenuItemCustom), false);
event.dispatchCustomEvent(this, "wje-dropdown:open", {
bubbles: true,
detail: { target: this }
});
Promise.resolve(this.afterShow(this));
}).catch((error) => {
this.classList.remove("active");
this.popup.hide(true);
});
});
__publicField(this, "beforeClose", () => {
});
__publicField(this, "afterClose", () => {
});
__publicField(this, "onClose", () => {
this.classList.remove("active");
this.syncAria();
Promise.resolve(this.beforeClose(this)).then((res) => {
if (this.classList.contains("active")) {
throw new Error("beforeShow method returned false or not string");
}
this.popup.hide(true);
event.removeListener(document, "wje-menu-item:click", null, __privateGet(this, _onMenuItemCustom), false);
event.dispatchCustomEvent(this, "wje-dropdown:close", {
bubbles: true,
detail: { target: this }
});
Promise.resolve(this.afterClose(this));
}).catch((error) => {
this.classList.add("active");
this.popup.show(true);
});
});
__privateAdd(this, _onMenuItemCustom, (e) => {
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
if (!this.popup || !this.popup.floatingEl || !path.includes(this.popup.floatingEl)) return;
const item = path.find((n) => n && n.tagName === "WJE-MENU-ITEM");
if (!item) return;
const owner = this.getMenuItemOwner(path, item);
if (owner && owner !== this) return;
if (item.hasAttribute("disabled") || item.getAttribute("aria-disabled") === "true") return;
if (item.hasAttribute("has-submenu")) return;
this.classList.remove("active");
this.popup.hide(true);
});
this._instanceId = ++_Dropdown._instanceId;
}
/**
* Sets or removes the 'portaled' attribute on the element.
* When the value is truthy, the attribute 'portaled' is added to the element.
* When the value is falsy, the attribute 'portaled' is removed from the element.
* @param {boolean} value Determines whether to add or remove the 'portaled' attribute.
*/
set portaled(value) {
if (value) {
this.setAttribute("portaled", value);
} else {
this.removeAttribute("portaled");
}
}
/**
* Getter method for the `portaled` property.
* Checks if the `portaled` attribute is present on the element.
* @returns {boolean} Returns `true` if the `portaled` attribute exists, otherwise `false`.
*/
get portaled() {
return this.getAttribute("portaled") || "";
}
/**
* Checks whether the element has the 'portaled' attribute.
* @returns {boolean} True if the element has the 'portaled' attribute, otherwise false.
*/
get isPortaled() {
return this.hasAttribute("portaled");
}
/**
* Sets the placement of the dropdown.
* @param value
*/
set trigger(value) {
this.setAttribute("trigger", value);
}
/**
* Gets the placement of the dropdown.
* @returns {string|string}
*/
get trigger() {
return this.getAttribute("trigger") || "click";
}
/**
* Getter for the CSS stylesheet.
* @returns {string[]}
*/
static get observedAttributes() {
return ["active"];
}
/**
* Sets up the attributes for the dropdown.
*/
setupAttributes() {
this.isShadowRoot = "open";
}
/**
* Removes the popup element.
*/
beforeDraw() {
var _a;
(_a = this.popup) == null ? void 0 : _a.remove();
this.popup = null;
}
/**
* Draws the dropdown element and returns the created document fragment.
* @returns {DocumentFragment}
*/
draw() {
let fragment = document.createDocumentFragment();
this.classList.add("wje-placement", "wje-" + this.placement || "wje-start");
let native = document.createElement("div");
native.setAttribute("part", "native");
native.classList.add("native-dropdown");
let tooltip = document.createElement("wje-tooltip");
tooltip.setAttribute("content", this.tooltip);
let anchorSlot = document.createElement("slot");
anchorSlot.setAttribute("name", "trigger");
anchorSlot.setAttribute("slot", "anchor");
let slot = document.createElement("slot");
let popup = document.createElement("wje-popup");
popup.setAttribute("placement", this.placement);
popup.setAttribute("offset", this.offset);
popup.setAttribute("part", "popup");
if (this.isPortaled)
popup.setAttribute("portal", this.portaled);
popup.append(anchorSlot, slot);
popup.setAttribute("manual", "");
native.appendChild(popup);
fragment.appendChild(native);
this.popup = popup;
this.anchorSlot = anchorSlot;
return fragment;
}
/**
* Adds event listeners for the mouseenter and mouseleave events.
*/
afterDraw() {
this.syncPopupOwner();
this.syncOwnedContentOwner();
event.addListener(this, "wje-popup:hide", null, this.popupHideCallback);
event.addListener(this.popup, "click", null, this.onMenuItemClick, { capture: true });
if (this.trigger !== "click") {
event.addListener(this, "mouseenter", null, this.onOpen);
event.addListener(this, "mouseleave", null, this.onClose);
} else {
event.addListener(this.anchorSlot, "click", null, this.toggleCallback, { capture: true });
}
if (this.hasAttribute("collapsible")) {
event.addListener(
Array.from(this.querySelectorAll("wje-menu-item")),
"click",
"wje-menu-item:click",
this.onClose
);
}
this.onSlotChange = () => this.syncAria();
this.anchorSlot.addEventListener("slotchange", this.onSlotChange);
this.syncAria();
}
/**
* Adds event listeners for the mouseenter and mouseleave events.
*/
afterDisconnect() {
var _a;
event.removeListener(this, "mouseenter", null, this.onOpen);
event.removeListener(this, "mouseleave", null, this.onClose);
event.removeListener(this.anchorSlot, "click", null, this.toggleCallback, { capture: true });
event.removeListener(this, "wje-popup:hide", null, this.popupHideCallback);
event.removeListener(this.popup, "click", null, this.onMenuItemClick, { capture: true });
event.removeListener(document, "wje-menu-item:click", null, __privateGet(this, _onMenuItemCustom), false);
(_a = this.anchorSlot) == null ? void 0 : _a.removeEventListener("slotchange", this.onSlotChange);
}
/**
* Assigns the current dropdown instance as the owner of its popup layers.
* Owner metadata is later used to resolve which dropdown should react to
* delegated menu-item clicks, including portaled popup content.
*/
syncPopupOwner() {
if (!this.popup) return;
this.popup.ownerDropdown = this;
if (this.popup.native) {
this.popup.native.ownerDropdown = this;
}
if (this.popup.floatingEl) {
this.popup.floatingEl.ownerDropdown = this;
}
}
/**
* Recursively assigns owner metadata to the dropdown content subtree while
* leaving nested dropdown roots untouched, so each nested dropdown can keep
* its own ownership boundary.
* @param {HTMLElement} [root] The subtree root whose children should inherit this dropdown owner. Defaults to the current dropdown.
*/
syncOwnedContentOwner(root = this) {
const children = Array.from(root.children || []);
for (const child of children) {
if (child.tagName === "WJE-DROPDOWN" && child !== this) {
continue;
}
child.ownerDropdown = this;
this.syncOwnedContentOwner(child);
}
}
/**
* Resolves the dropdown that owns a clicked menu item. The lookup prefers
* explicit owner metadata and falls back to DOM traversal so both regular
* and portaled dropdown content can be scoped correctly.
* @param {EventTarget[]} path The composed event path.
* @param {HTMLElement} item The clicked menu item element.
* @returns {HTMLElement|null} The owning dropdown element or null when it cannot be resolved.
*/
getMenuItemOwner(path, item) {
var _a;
if (item == null ? void 0 : item.ownerDropdown) return item.ownerDropdown;
if (item == null ? void 0 : item.closest) {
const closestDropdown = item.closest("wje-dropdown");
if (closestDropdown) return closestDropdown;
}
const ownerFromPath = (_a = path.find((node) => node == null ? void 0 : node.ownerDropdown)) == null ? void 0 : _a.ownerDropdown;
if (ownerFromPath) return ownerFromPath;
const dropdownFromPath = path.find((node) => (node == null ? void 0 : node.tagName) === "WJE-DROPDOWN");
if (dropdownFromPath) return dropdownFromPath;
return null;
}
/**
* @summary Returns the content to be displayed before showing the dropdown.
* @returns {any} The content to be displayed.
*/
beforeShow() {
return this.content;
}
/**
* This method is called after the dropdown is shown.
*/
afterShow() {
}
/**
* Syncs ARIA attributes for the trigger element.
*/
syncAria() {
var _a, _b, _c, _d;
const triggerEl = (_c = (_b = (_a = this.anchorSlot) == null ? void 0 : _a.assignedElements) == null ? void 0 : _b.call(_a, { flatten: true })) == null ? void 0 : _c[0];
if (!triggerEl) return;
const popupId = ((_d = this.popup) == null ? void 0 : _d.id) || `wje-dropdown-popup-${this._instanceId}`;
if (this.popup && !this.popup.id) this.popup.id = popupId;
const hasMenu = !!this.querySelector("wje-menu");
triggerEl.setAttribute("aria-haspopup", hasMenu ? "menu" : "dialog");
triggerEl.setAttribute("aria-expanded", this.classList.contains("active") ? "true" : "false");
triggerEl.setAttribute("aria-controls", popupId);
}
};
_onMenuItemCustom = new WeakMap();
__publicField(_Dropdown, "_instanceId", 0);
let Dropdown = _Dropdown;
Dropdown.define("wje-dropdown", Dropdown);
export {
Dropdown as default
};
//# sourceMappingURL=wje-dropdown.js.map