UNPKG

x-widget

Version:

Adds the ability to define reusable Widgets (WebComponents) using Alpinejs.

364 lines (354 loc) 12.1 kB
var __defProp = Object.defineProperty; var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); var __export = (target, all) => { __markAsModule(target); for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/index.mjs __export(exports, { default: () => src_default, slotsMagic: () => slotsMagic, xPropDirective: () => xPropDirective, xWidgetData: () => xWidgetData, xWidgetDirective: () => xWidgetDirective }); // src/x-widget-data.mjs var camelToSnake = (name) => name.replace(/[a-z][A-Z]/g, (m) => m[0] + "-" + m[1].toLowerCase()); function xWidgetData(spec) { const Alpine = this; const propDescriptorEntries = Object.entries(Object.getOwnPropertyDescriptors(spec)); return ($el, $data) => { const widgetEl = findWidget($el); console.assert(widgetEl, "widget not found"); const boundProps = Array.from(widgetEl.attributes).filter((attr) => attr.name.startsWith("x-prop:")).map(({ name }) => name.substr(7)); const observer = new MutationObserver((changes) => { changes.forEach(({ attributeName, target }) => { setProp(attributeName, target.getAttribute(attributeName)); }); }); observer.observe(widgetEl, { attributes: true, attributeFilter: Object.keys(spec).map(camelToSnake), attributeOldValue: false }); const proplessDescriptors = Object.fromEntries(propDescriptorEntries.filter(([name]) => !boundProps.includes(name))); const data = Alpine.reactive(Object.assign(Object.create(Object.getPrototypeOf(spec), proplessDescriptors), { destroy() { observer.disconnect(); } })); const attribs = [...widgetEl.attributes]; for (const name of Object.getOwnPropertyNames(spec)) { const attrib = attribs.find((attr) => attr.name.match(new RegExp(`^((x-(bind|prop))?:)?${camelToSnake(name)}$`))); if (!attrib) continue; if (attrib.name.startsWith("x-prop:")) { Object.defineProperty(data, name, { configurable: true, enumerable: true, get() { return $data[name]; }, set(newValue) { $data[name] = newValue; } }); } else { setProp(name, widgetEl.getAttribute(camelToSnake(name))); } } function setProp(name, attribValue) { const defaultValue = spec[name]; if (typeof defaultValue === "boolean") { if (attribValue === "") { data[name] = true; } else if (attribValue === "false") { data[name] = false; } else { data[name] = !!attribValue; } } else if (typeof defaultValue === "number") { data[name] = parseFloat(attribValue); } else if (typeof defaultValue === "string") { data[name] = attribValue; } else { throw new Error("unsupported static attribute: " + name); } } return data; }; } function findWidget(el) { while (el && !el.tagName.includes("-")) el = el.parentElement; return el; } // node_modules/alpinejs/src/scope.js function addScopeToNode(node, data, referenceNode) { node._x_dataStack = [data, ...closestDataStack(referenceNode || node)]; return () => { node._x_dataStack = node._x_dataStack.filter((i) => i !== data); }; } function closestDataStack(node) { if (node._x_dataStack) return node._x_dataStack; if (typeof ShadowRoot === "function" && node instanceof ShadowRoot) { return closestDataStack(node.host); } if (!node.parentNode) { return []; } return closestDataStack(node.parentNode); } function mergeProxies(objects) { let thisProxy = new Proxy({}, { ownKeys: () => { return Array.from(new Set(objects.flatMap((i) => Object.keys(i)))); }, has: (target, name) => { return objects.some((obj) => obj.hasOwnProperty(name)); }, get: (target, name) => { return (objects.find((obj) => { if (obj.hasOwnProperty(name)) { let descriptor = Object.getOwnPropertyDescriptor(obj, name); if (descriptor.get && descriptor.get._x_alreadyBound || descriptor.set && descriptor.set._x_alreadyBound) { return true; } if ((descriptor.get || descriptor.set) && descriptor.enumerable) { let getter = descriptor.get; let setter = descriptor.set; let property = descriptor; getter = getter && getter.bind(thisProxy); setter = setter && setter.bind(thisProxy); if (getter) getter._x_alreadyBound = true; if (setter) setter._x_alreadyBound = true; Object.defineProperty(obj, name, { ...property, get: getter, set: setter }); } return true; } return false; }) || {})[name]; }, set: (target, name, value) => { let closestObjectWithKey = objects.find((obj) => obj.hasOwnProperty(name)); if (closestObjectWithKey) { closestObjectWithKey[name] = value; } else { objects[objects.length - 1][name] = value; } return true; } }); return thisProxy; } // node_modules/alpinejs/src/utils/error.js function tryCatch(el, expression, callback, ...args) { try { return callback(...args); } catch (e) { handleError(e, el, expression); } } function handleError(error, el, expression = void 0) { Object.assign(error, { el, expression }); console.warn(`Alpine Expression Error: ${error.message} ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); setTimeout(() => { throw error; }, 0); } // src/lazy-evaluator.mjs function lazyEvaluator(el, expression) { let dataStack = closestDataStack(el); let evaluator = generateEvaluatorFromString(dataStack, expression, el); return tryCatch.bind(null, el, expression, evaluator); } function generateEvaluatorFromString(dataStack, expression, el) { let func = generateFunctionFromString(expression, el); return (receiver = () => { }, { scope = {} } = {}) => { func.result = void 0; func.finished = false; let completeScope = mergeProxies([scope, ...dataStack]); if (typeof func === "function") { let promise = func(func, completeScope).catch((error) => handleError(error, el, expression)); if (func.finished) { receiver(bindResult(func.result, completeScope)); func.result = void 0; } else { promise.then((result) => bindResult(result, completeScope)).catch((error) => handleError(error, el, expression)).finally(() => func.result = void 0); } } }; } function bindResult(result, scope) { return typeof result === "function" ? result.bind(scope) : result; } var evaluatorMemo = {}; function generateFunctionFromString(expression, el) { if (evaluatorMemo[expression]) { return evaluatorMemo[expression]; } let AsyncFunction = Object.getPrototypeOf(async function() { }).constructor; let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression; const safeAsyncFunction = () => { try { return new AsyncFunction(["__self", "scope"], `with (scope) { __self.result = ${rightSideSafeExpression} }; __self.finished = true; return __self.result;`); } catch (error) { handleError(error, el, expression); return Promise.resolve(); } }; let func = safeAsyncFunction(); evaluatorMemo[expression] = func; return func; } // src/x-prop-directive.mjs var snakeToCamel = (name) => name.replace(/-([a-zA-Z])/g, (m) => m[1].toUpperCase() + m.substr(2)); function xPropDirective(el, { value: attribName, expression }, { cleanup }) { const propName = snakeToCamel(attribName); const read = lazyEvaluator(expression === propName ? el.parentElement : el, expression); let unsafeValue; const setter = safeLeftHandSide(el, expression) ? lazyEvaluator(el.parentElement, `${expression} = __`) : (_, { scope: { __: newValue } }) => unsafeValue = newValue; const propObj = {}; Object.defineProperty(propObj, propName, { get() { let result; if (typeof unsafeValue !== "undefined") { return unsafeValue; } read((newValue) => result = newValue); return result; }, set(newValue) { if (propName !== "value") { el[propName] = newValue; } setter(() => { }, { scope: { __: newValue } }); } }); if (propName !== "value") { el[propName] = propObj[propName]; } const removeScope = addScopeToNode(el, propObj); cleanup(() => removeScope()); } function safeLeftHandSide(el, lhs) { try { let scope = mergeProxies(closestDataStack(el)); new Function("scope", `with(scope) {${lhs} = ${lhs}}`)(scope); return true; } catch { return false; } } // src/x-widget.mjs function slotsMagic(el) { while (el && !el._x_slots) el = el.parentElement; return el?._x_slots; } function xWidgetDirective(el, { expression, modifiers }, { Alpine }) { const tagName = expression; if (window.customElements.get(tagName)) return; if (modifiers[0]) { const style = document.createElement("style"); style.innerHTML = `${tagName} { display: ${modifiers[0]}}`; document.head.appendChild(style); } if (Alpine._widgets) { Alpine._widgets.push(tagName); } else { Alpine._widgets = [tagName]; } const templateContent = el.content.firstElementChild; window.customElements.define(tagName, class extends HTMLElement { constructor() { super(); this._slotFills = null; } connectedCallback() { let slotFills; if (this._slotFills) { slotFills = this._slotFills; } else { slotFills = collectSlotFills(this); this._slotFills = slotFills; } const newEl = templateContent.cloneNode(true); this._x_slots = Object.fromEntries([...slotFills.entries()].map(([name, value]) => [name, value])); const targetSlots = findTargetSlots(newEl); for (const targetSlot of targetSlots) { const slotName = targetSlot.name || "default"; const fills = slotFills.get(slotName); if (fills) { targetSlot.replaceWith(...fills.map((n) => n.cloneNode(true))); } else { targetSlot.replaceWith(...[...targetSlot.childNodes]); } } requestAnimationFrame(() => { while (this.firstChild) { this.removeChild(this.firstChild); } this.appendChild(newEl); }); this.dispatchEvent(new CustomEvent("x-widget:connected", { bubbles: true })); } }); } function findTargetSlots(el) { let slots = [...el.querySelectorAll("slot")]; if (el.tagName === "SLOT") slots.unshift(el); const templates = el.querySelectorAll("template"); for (const template of templates) { if (template.getAttribute("x-widget")) continue; for (const child of template.content.children) { slots.push(...findTargetSlots(child)); } } return slots; } function collectSlotFills(el) { const slots = new Map(); function collectForSlot(slotName, nodes) { if (slots.has(slotName)) { slots.get(slotName).push(...nodes); } else { slots.set(slotName, nodes); } } for (const child of el.childNodes) { if (child.tagName === "TEMPLATE") { const slotName = child.getAttribute("slot"); const isSlotFill = !slotName && (child.getAttribute("x-for") || child.getAttribute("x-if")); collectForSlot(slotName || "default", isSlotFill ? [child] : [...child.content.childNodes]); } else if (child.nodeType !== Node.TEXT_NODE || child.textContent.trim()) { collectForSlot("default", [child]); } } return slots; } // src/index.mjs function src_default(Alpine) { Alpine.magic("slots", slotsMagic); Alpine.directive("widget", xWidgetDirective); Alpine.directive("prop", xPropDirective); Alpine.data("xWidget", xWidgetData.bind(Alpine)); }