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.

252 lines (251 loc) 9.61 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"; import { event } from "./event.js"; const styles = ".disabled {\n opacity: 0.3;\n}\n"; class ReorderHandle extends WJElement { /** * Creates an instance of ReorderHandle. */ constructor() { super(); /** * The class name for the component. * @type {string} */ __publicField(this, "className", "ReorderHandle"); this.addEventListener("mousedown", this.startDrag.bind(this)); this.addEventListener("touchstart", this.startTouchDrag.bind(this)); } /** * Returns the CSS styles for the component. * @returns {*} */ static get cssStyleSheet() { return styles; } /** * Returns the list of attributes to observe for changes. * @returns {string[]} */ static get observedAttributes() { return ["dropzone", "parent"]; } setupAttributes() { this.isShadowRoot = "open"; this.syncAria(); } /** * Draws the component. * @returns {DocumentFragment} */ draw() { const fragment = document.createDocumentFragment(); const container = document.createElement("div"); container.classList.add("container"); container.setAttribute("part", "native"); const slot = document.createElement("slot"); container.appendChild(slot); fragment.appendChild(container); return fragment; } /** * Draws the component after it is connected to the DOM. */ afterDraw() { this.syncAria(); if (this.hasAttribute("disabled")) { this.style.opacity = ".3"; } } /** * Sync ARIA attributes on host. */ syncAria() { if (!this.hasAttribute("role")) { this.setAriaState({ role: "button" }); } if (!this.hasAttribute("tabindex")) { this.setAttribute("tabindex", "0"); } if (this.hasAttribute("disabled")) { this.setAriaState({ disabled: true }); } const ariaLabel = this.getAttribute("aria-label"); const label = this.getAttribute("label") || "Reorder item"; if (!ariaLabel && label) { this.setAriaState({ label }); } } /** * Handles the attribute changes. * @param {DragEvent} e */ startDrag(e) { if (this.hasAttribute("disabled") || this.hasAttribute("locked")) return; this.startDragAction(e.clientX, e.clientY); event.dispatchCustomEvent(this, "wje-reorder-handle:start", { draggable: this.getDraggable() }); } /** * Handles the touch start event. * @param {TouchEvent} e */ startTouchDrag(e) { if (this.hasAttribute("disabled") || this.hasAttribute("locked")) return; const touch = e.touches[0]; this.startDragAction(touch.clientX, touch.clientY); } /** * Initiates the drag-and-drop action for a sortable element. * @param {number} clientX The x-coordinate of the mouse pointer at the start of the drag action. * @param {number} clientY The y-coordinate of the mouse pointer at the start of the drag action. */ startDragAction(clientX, clientY) { let draggable = this.getDraggable(); const initialContainer = this.getDropzone(draggable); if (!this.getAttribute("dropzone")) { this.setAttribute("dropzone", initialContainer.localName); } const rect = draggable.getBoundingClientRect(); const offsetX = clientX - rect.left; const offsetY = clientY - rect.top; let placeholder = document.createElement("div"); placeholder.classList.add("sortable-item"); placeholder.style.visibility = "hidden"; placeholder.style.height = `${rect.height}px`; draggable.classList.add("dragging"); draggable.style.position = "fixed"; draggable.style.zIndex = "1000"; draggable.style.width = `${rect.width}px`; const moveAt = (pageX, pageY) => { draggable.style.left = `${pageX - offsetX - document.documentElement.scrollLeft}px`; draggable.style.top = `${pageY - offsetY - document.documentElement.scrollTop}px`; }; moveAt(clientX, clientY); const onMouseMove = (e) => { var _a; moveAt(e.pageX, e.pageY); const dropzone = this.getClosestDropzone(e.clientX, e.clientY); if (!dropzone) return; const siblings = Array.from(dropzone.children).filter( (child) => child !== draggable && child !== placeholder ); for (const sibling of siblings) { const siblingRect = sibling.getBoundingClientRect(); if ((_a = sibling.children[0]) == null ? void 0 : _a.hasAttribute("locked")) continue; if (e.clientY > siblingRect.top && e.clientY < siblingRect.bottom) { if (e.clientY < siblingRect.top + siblingRect.height / 2) { dropzone.insertBefore(placeholder, sibling); } else { dropzone.insertBefore(placeholder, sibling.nextSibling); } break; } } }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); draggable.classList.remove("dragging"); draggable.style.position = ""; draggable.style.zIndex = ""; draggable.style.left = ""; draggable.style.top = ""; draggable.style.width = ""; const finalContainer = placeholder.parentElement; finalContainer.insertBefore(draggable, placeholder); finalContainer.removeChild(placeholder); this.reIndexItems(finalContainer); event.dispatchCustomEvent(this, "wje-reorder-handle:change", { finalContainer, draggable }); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); initialContainer.insertBefore(placeholder, draggable); } /** * Retrieves the closest draggable element based on attribute conditions. * If the element has a "parent" attribute, the method attempts to find the closest ancestor * matching the CSS selector specified in the attribute. If no such ancestor exists, * the method defaults to returning the immediate parent element. * @returns {Element|null} The closest matching ancestor or the immediate parent element if no match is found, or null if the element has no parent. */ getDraggable() { if (this.hasAttribute("parent")) { let parent = this.closest(this.getAttribute("parent")); if (parent) return parent; } return this.parentElement; } /** * Retrieves the nearest dropzone element based on the element's attributes or its parent element. * @param {HTMLElement} element The HTML element for which the dropzone is being determined. * @returns {HTMLElement|null} The nearest dropzone element if found; otherwise, the parent element or null. */ getDropzone(element) { this.getAttribute("dropzone"); if (this.hasAttribute("dropzone")) { let dropzone = element.closest(this.getAttribute("dropzone")); if (dropzone) return dropzone; } return element.parentElement; } /** * Retrieves the closest dropzone element at the specified coordinates. * @param {number} clientX The x-coordinate relative to the viewport. * @param {number} clientY The y-coordinate relative to the viewport. * @returns {HTMLElement|null} - The closest dropzone element matching the `dropzone` attribute, or `null` if none is found. */ getClosestDropzone(clientX, clientY) { const elements = this.getElementsFromPointAll(clientX, clientY); for (const element of elements) { if (element.matches(this.getAttribute("dropzone"))) { return element; } } return null; } /** * Retrieves all elements at the specified coordinates, including those within shadow DOMs. * @param {number} x The x-coordinate relative to the viewport. * @param {number} y The y-coordinate relative to the viewport. * @param {Document|ShadowRoot} [root] The root context in which to search. Defaults to the main document. * @param {Set<Node>} [visited] A set of already visited nodes to avoid infinite recursion in nested shadow DOMs. * @returns {HTMLElement[]} An array of all elements found at the specified coordinates, including shadow DOM elements. */ getElementsFromPointAll(x, y, root = document, visited = /* @__PURE__ */ new Set()) { if (visited.has(root)) return []; visited.add(root); const elements = root.elementsFromPoint(x, y); let allElements = [...elements]; for (const element of elements) { if (element.shadowRoot && !visited.has(element.shadowRoot)) { allElements = allElements.concat(this.getElementsFromPointAll(x, y, element.shadowRoot, visited)); } } return allElements; } /** * Re-indexes child elements of the given container by setting their dataset index. * @param {HTMLElement} container The container element whose children are to be re-indexed. * @returns {void} */ reIndexItems(container) { const items = Array.from(container.children); let index = 0; items.forEach((child) => { var _a; if ((_a = child.children[0]) == null ? void 0 : _a.hasAttribute("locked")) { child.dataset.index = index; } else { child.dataset.index = index; } index++; }); } } ReorderHandle.define("wje-reorder-handle", ReorderHandle); export { ReorderHandle as default }; //# sourceMappingURL=wje-reorder-handle.js.map