From 319091cf9d9f796cf7e46dae2985c76caa682af6 Mon Sep 17 00:00:00 2001 From: rameel Date: Wed, 28 Oct 2026 18:37:12 +0500 Subject: [PATCH] all: require Alpine.js 3.15.12 or later --- README.md | 2 + package.json | 2 +- pnpm-lock.yaml | 2 +- src/utilities/anchor_block.js | 2 - tests/playwright/anchor-block.spec.js | 3 +- .../{alpine.3.14.1.js => alpine.3.15.12.js} | 649 +++++++++++------- tests/playwright/assets/page.html | 2 +- 7 files changed, 400 insertions(+), 262 deletions(-) rename tests/playwright/assets/{alpine.3.14.1.js => alpine.3.15.12.js} (88%) diff --git a/README.md b/README.md index 0c039d3..efee773 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ **`Ramstack.AlpineGear.js`** provides useful and convenient plugins for [Alpine.js](https://alpinejs.dev/). +The plugins require Alpine.js 3.15.12 or later. + ## Included Plugins **[@ramstack/alpinegear-bound](https://www.npmjs.com/package/@ramstack/alpinegear-bound)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/bound))
diff --git a/package.json b/package.json index 834c730..36fca59 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test:vitest": "vitest run" }, "peerDependencies": { - "alpinejs": "^3.13.0" + "alpinejs": "^3.15.12" }, "devDependencies": { "@conventional-changelog/git-client": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2927607..a43a7cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: .: dependencies: alpinejs: - specifier: ^3.13.0 + specifier: ^3.15.12 version: 3.16.2 devDependencies: '@conventional-changelog/git-client': diff --git a/src/utilities/anchor_block.js b/src/utilities/anchor_block.js index 46bb920..d07b10b 100644 --- a/src/utilities/anchor_block.js +++ b/src/utilities/anchor_block.js @@ -46,8 +46,6 @@ export function anchor_block(el, template, { addScopeToNode, cleanup, destroyTre } }, delete() { - // Not in mutateDom: Alpine before 3.14.2 removes nested x-if content - // and x-for items without destroying them, leaving that to its observer el._r_block = null; delete el._x_currentIfEl; delete el._x_lastRenderedEl; diff --git a/tests/playwright/anchor-block.spec.js b/tests/playwright/anchor-block.spec.js index f37ae31..395bdfc 100644 --- a/tests/playwright/anchor-block.spec.js +++ b/tests/playwright/anchor-block.spec.js @@ -56,7 +56,7 @@ for (const [directive, block] of Object.entries(blocks)) { await expect(page.locator("dt, dd")).toHaveText(["Term", "Description"]); expect(await probe(1)).toEqual({ destroyed: [], effects: [0, 1], events: 1 }); - // Alpine 3.14.2+ removes x-if and x-for clones this way, with its observer paused + // Alpine removes x-if and x-for clones this way, with its observer paused await page.evaluate(() => Alpine.mutateDom(() => { const anchor = document.querySelector("dl > template"); Alpine.destroyTree(anchor); @@ -85,7 +85,6 @@ test("anchor block: removes nested x-if and x-for content", async ({ page }) => await expect(page.locator("b, i")).toHaveText(["1", "2", "3", "4"]); - // Alpine before 3.14.2 leaves destroying the removed clones to its observer await set_data(page.locator("div"), { show: false }); await expect(page.locator("b, i")).toHaveCount(0); }); diff --git a/tests/playwright/assets/alpine.3.14.1.js b/tests/playwright/assets/alpine.3.15.12.js similarity index 88% rename from tests/playwright/assets/alpine.3.14.1.js rename to tests/playwright/assets/alpine.3.15.12.js index d25ad8f..3813721 100644 --- a/tests/playwright/assets/alpine.3.14.1.js +++ b/tests/playwright/assets/alpine.3.15.12.js @@ -4,9 +4,17 @@ var flushing = false; var queue = []; var lastFlushedIndex = -1; + var transactionActive = false; function scheduler(callback) { queueJob(callback); } + function startTransaction() { + transactionActive = true; + } + function commitTransaction() { + transactionActive = false; + queueFlush(); + } function queueJob(job) { if (!queue.includes(job)) queue.push(job); @@ -19,6 +27,8 @@ } function queueFlush() { if (!flushing && !flushPending) { + if (transactionActive) + return; flushPending = true; queueMicrotask(flushJobs); } @@ -88,21 +98,33 @@ function watch(getter, callback) { let firstTime = true; let oldValue; + let oldValueJSON; let effectReference = effect(() => { let value = getter(); - JSON.stringify(value); + let newJSON = JSON.stringify(value); if (!firstTime) { - queueMicrotask(() => { - callback(value, oldValue); - oldValue = value; - }); - } else { - oldValue = value; + if (typeof value === "object" || value !== oldValue) { + let previousValue = typeof oldValue === "object" ? JSON.parse(oldValueJSON) : oldValue; + queueMicrotask(() => { + callback(value, previousValue); + }); + } } + oldValue = value; + oldValueJSON = newJSON; firstTime = false; }); return () => release(effectReference); } + async function transaction(callback) { + startTransaction(); + try { + await callback(); + await Promise.resolve(); + } finally { + commitTransaction(); + } + } // packages/alpinejs/src/mutation.js var onAttributeAddeds = []; @@ -142,10 +164,9 @@ }); } function cleanupElement(el) { - if (el._x_cleanups) { - while (el._x_cleanups.length) - el._x_cleanups.pop()(); - } + el._x_effects?.forEach(dequeueJob); + while (el._x_cleanups?.length) + el._x_cleanups.pop()(); } var observer = new MutationObserver(onMutate); var currentlyObserving = false; @@ -193,7 +214,7 @@ deferredMutations = deferredMutations.concat(mutations); return; } - let addedNodes = /* @__PURE__ */ new Set(); + let addedNodes = []; let removedNodes = /* @__PURE__ */ new Set(); let addedAttributes = /* @__PURE__ */ new Map(); let removedAttributes = /* @__PURE__ */ new Map(); @@ -201,8 +222,24 @@ if (mutations[i].target._x_ignoreMutationObserver) continue; if (mutations[i].type === "childList") { - mutations[i].addedNodes.forEach((node) => node.nodeType === 1 && addedNodes.add(node)); - mutations[i].removedNodes.forEach((node) => node.nodeType === 1 && removedNodes.add(node)); + mutations[i].removedNodes.forEach((node) => { + if (node.nodeType !== 1) + return; + if (!node._x_marker) + return; + removedNodes.add(node); + }); + mutations[i].addedNodes.forEach((node) => { + if (node.nodeType !== 1) + return; + if (removedNodes.has(node)) { + removedNodes.delete(node); + return; + } + if (node._x_marker) + return; + addedNodes.push(node); + }); } if (mutations[i].type === "attributes") { let el = mutations[i].target; @@ -235,29 +272,15 @@ onAttributeAddeds.forEach((i) => i(el, attrs)); }); for (let node of removedNodes) { - if (addedNodes.has(node)) + if (addedNodes.some((i) => i.contains(node))) continue; onElRemoveds.forEach((i) => i(node)); } - addedNodes.forEach((node) => { - node._x_ignoreSelf = true; - node._x_ignore = true; - }); for (let node of addedNodes) { - if (removedNodes.has(node)) - continue; if (!node.isConnected) continue; - delete node._x_ignoreSelf; - delete node._x_ignore; onElAddeds.forEach((i) => i(node)); - node._x_ignore = true; - node._x_ignoreSelf = true; } - addedNodes.forEach((node) => { - delete node._x_ignoreSelf; - delete node._x_ignore; - }); addedNodes = null; removedNodes = null; addedAttributes = null; @@ -288,6 +311,13 @@ function mergeProxies(objects) { return new Proxy({ objects }, mergeProxyTrap); } + function keyInPrototypeChain(obj, key) { + if (obj === null || obj === Object.prototype) + return null; + if (Object.prototype.hasOwnProperty.call(obj, key)) + return obj; + return keyInPrototypeChain(Object.getPrototypeOf(obj), key); + } var mergeProxyTrap = { ownKeys({ objects }) { return Array.from( @@ -313,9 +343,14 @@ ); }, set({ objects }, name, value, thisProxy) { - const target = objects.find( - (obj) => Object.prototype.hasOwnProperty.call(obj, name) - ) || objects[objects.length - 1]; + let target; + for (const obj of objects) { + target = keyInPrototypeChain(obj, name); + if (target) + break; + } + if (!target) + target = objects[objects.length - 1]; const descriptor = Object.getOwnPropertyDescriptor(target, name); if (descriptor?.set && descriptor?.get) return descriptor.set.call(thisProxy, value) || true; @@ -332,7 +367,7 @@ // packages/alpinejs/src/interceptor.js function initInterceptors(data2) { - let isObject2 = (val) => typeof val === "object" && !Array.isArray(val) && val !== null; + let isObject3 = (val) => typeof val === "object" && !Array.isArray(val) && val !== null; let recurse = (obj, basePath = "") => { Object.entries(Object.getOwnPropertyDescriptors(obj)).forEach(([key, { value, enumerable }]) => { if (enumerable === false || value === void 0) @@ -343,7 +378,7 @@ if (typeof value === "object" && value !== null && value._x_interceptor) { obj[key] = value.initialize(data2, path, key); } else { - if (isObject2(value) && value !== obj && !(value instanceof Element)) { + if (isObject3(value) && value !== obj && !(value instanceof Element)) { recurse(value, path); } } @@ -401,27 +436,23 @@ magics[name] = callback; } function injectMagics(obj, el) { + let memoizedUtilities = getUtilities(el); Object.entries(magics).forEach(([name, callback]) => { - let memoizedUtilities = null; - function getUtilities() { - if (memoizedUtilities) { - return memoizedUtilities; - } else { - let [utilities, cleanup2] = getElementBoundUtilities(el); - memoizedUtilities = { interceptor, ...utilities }; - onElRemoved(el, cleanup2); - return memoizedUtilities; - } - } Object.defineProperty(obj, `$${name}`, { get() { - return callback(el, getUtilities()); + return callback(el, memoizedUtilities); }, enumerable: false }); }); return obj; } + function getUtilities(el) { + let [utilities, cleanup2] = getElementBoundUtilities(el); + let utils = { interceptor, ...utilities }; + onElRemoved(el, cleanup2); + return utils; + } // packages/alpinejs/src/utils/error.js function tryCatch(el, expression, callback, ...args) { @@ -431,7 +462,14 @@ handleError(e, el, expression); } } - function handleError(error2, el, expression = void 0) { + function handleError(...args) { + return errorHandler(...args); + } + var errorHandler = normalErrorHandler; + function setErrorHandler(handler4) { + errorHandler = handler4; + } + function normalErrorHandler(error2, el, expression = void 0) { error2 = Object.assign( error2 ?? { message: "No error message given." }, { el, expression } @@ -461,10 +499,15 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); function evaluateLater(...args) { return theEvaluatorFunction(...args); } - var theEvaluatorFunction = normalEvaluator; + var theEvaluatorFunction = () => { + }; function setEvaluator(newEvaluator) { theEvaluatorFunction = newEvaluator; } + var theRawEvaluatorFunction; + function setRawEvaluator(newEvaluator) { + theRawEvaluatorFunction = newEvaluator; + } function normalEvaluator(el, expression) { let overriddenMagics = {}; injectMagics(overriddenMagics, el); @@ -474,7 +517,11 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } function generateEvaluatorFromFunction(dataStack, func) { return (receiver = () => { - }, { scope: scope2 = {}, params = [] } = {}) => { + }, { scope: scope2 = {}, params = [], context } = {}) => { + if (!shouldAutoEvaluateFunctions) { + runIfTypeOfFunction(receiver, func, mergeProxies([scope2, ...dataStack]), params); + return; + } let result = func.apply(mergeProxies([scope2, ...dataStack]), params); runIfTypeOfFunction(receiver, result); }; @@ -509,12 +556,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); function generateEvaluatorFromString(dataStack, expression, el) { let func = generateFunctionFromString(expression, el); return (receiver = () => { - }, { scope: scope2 = {}, params = [] } = {}) => { + }, { scope: scope2 = {}, params = [], context } = {}) => { func.result = void 0; func.finished = false; let completeScope = mergeProxies([scope2, ...dataStack]); if (typeof func === "function") { - let promise = func(func, completeScope).catch((error2) => handleError(error2, el, expression)); + let promise = func.call(context, func, completeScope).catch((error2) => handleError(error2, el, expression)); if (func.finished) { runIfTypeOfFunction(receiver, func.result, completeScope, params, el); func.result = void 0; @@ -540,6 +587,38 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); receiver(value); } } + function evaluateRaw(...args) { + return theRawEvaluatorFunction(...args); + } + function normalRawEvaluator(el, expression, extras = {}) { + let overriddenMagics = {}; + injectMagics(overriddenMagics, el); + let dataStack = [overriddenMagics, ...closestDataStack(el)]; + let scope2 = mergeProxies([extras.scope ?? {}, ...dataStack]); + let params = extras.params ?? []; + if (expression.includes("await")) { + let AsyncFunction = Object.getPrototypeOf(async function() { + }).constructor; + let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression.trim()) || /^(let|const)\s/.test(expression.trim()) ? `(async()=>{ ${expression} })()` : expression; + let func = new AsyncFunction( + ["scope"], + `with (scope) { let __result = ${rightSideSafeExpression}; return __result }` + ); + let result = func.call(extras.context, scope2); + return result; + } else { + let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression.trim()) || /^(let|const)\s/.test(expression.trim()) ? `(()=>{ ${expression} })()` : expression; + let func = new Function( + ["scope"], + `with (scope) { let __result = ${rightSideSafeExpression}; return __result }` + ); + let result = func.call(extras.context, scope2); + if (typeof result === "function" && shouldAutoEvaluateFunctions) { + return result.apply(scope2, params); + } + return result; + } + } // packages/alpinejs/src/directives.js var prefixAsString = "x-"; @@ -669,6 +748,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); var alpineAttributeRegex = () => new RegExp(`^${prefixAsString}([^:^.]+)\\b`); function toParsedDirectives(transformedAttributeMap, originalAttributeOverride) { return ({ name, value }) => { + if (name === value) + value = ""; let typeMatch = name.match(alpineAttributeRegex()); let valueMatch = name.match(/:([a-zA-Z0-9\-_:]+)/); let modifiers = name.match(/\.[^.\]]+(?=[^\]]*$)/g) || []; @@ -686,8 +767,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); var directiveOrder = [ "ignore", "ref", - "data", "id", + "data", "anchor", "bind", "init", @@ -707,14 +788,16 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } // packages/alpinejs/src/utils/dispatch.js - function dispatch(el, name, detail = {}) { - el.dispatchEvent( + function dispatch(el, name, detail = {}, options = {}) { + return el.dispatchEvent( new CustomEvent(name, { detail, bubbles: true, // Allows events to pass the shadow DOM barrier. composed: true, - cancelable: true + cancelable: true, + // Allows overriding the default event options. + ...options }) ); } @@ -793,7 +876,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); if (callback(el)) return el; if (el._x_teleportBack) - el = el._x_teleportBack; + return findClosest(el._x_teleportBack, callback); + if (el.parentNode instanceof ShadowRoot) { + return findClosest(el.parentNode.host, callback); + } if (!el.parentElement) return; return findClosest(el.parentElement, callback); @@ -805,21 +891,29 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); function interceptInit(callback) { initInterceptors2.push(callback); } + var markerDispenser = 1; function initTree(el, walker = walk, intercept = () => { }) { + if (findClosest(el, (i) => i._x_ignore)) + return; deferHandlingDirectives(() => { walker(el, (el2, skip) => { + if (el2._x_marker) + return; intercept(el2, skip); initInterceptors2.forEach((i) => i(el2, skip)); directives(el2, el2.attributes).forEach((handle) => handle()); + if (!el2._x_ignore) + el2._x_marker = markerDispenser++; el2._x_ignore && skip(); }); }); } function destroyTree(root, walker = walk) { walker(root, (el) => { - cleanupAttributes(el); cleanupElement(el); + cleanupAttributes(el); + delete el._x_marker; }); } function warnAboutMissingPlugins() { @@ -877,9 +971,11 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } return setClassesFromString(el, value); } + function splitClasses(classString) { + return classString.split(/\s/).filter(Boolean); + } function setClassesFromString(el, classString) { - let split = (classString2) => classString2.split(" ").filter(Boolean); - let missingClasses = (classString2) => classString2.split(" ").filter((i) => !el.classList.contains(i)).filter(Boolean); + let missingClasses = (classString2) => splitClasses(classString2).filter((i) => !el.classList.contains(i)).filter(Boolean); let addClassesAndReturnUndo = (classes) => { el.classList.add(...classes); return () => { @@ -890,9 +986,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); return addClassesAndReturnUndo(missingClasses(classString)); } function setClassesFromObject(el, classObject) { - let split = (classString) => classString.split(" ").filter(Boolean); - let forAdd = Object.entries(classObject).flatMap(([classString, bool]) => bool ? split(classString) : false).filter(Boolean); - let forRemove = Object.entries(classObject).flatMap(([classString, bool]) => !bool ? split(classString) : false).filter(Boolean); + let forAdd = Object.entries(classObject).flatMap(([classString, bool]) => bool ? splitClasses(classString) : false).filter(Boolean); + let forRemove = Object.entries(classObject).flatMap(([classString, bool]) => !bool ? splitClasses(classString) : false).filter(Boolean); let added = []; let removed = []; forRemove.forEach((i) => { @@ -1326,18 +1421,11 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } } function bindInputValue(el, value) { - if (el.type === "radio") { + if (isRadio(el)) { if (el.attributes.value === void 0) { el.value = value; } - if (window.fromModel) { - if (typeof value === "boolean") { - el.checked = safeParseBoolean(el.value) === value; - } else { - el.checked = checkedAttrLooseCompare(el.value, value); - } - } - } else if (el.type === "checkbox") { + } else if (isCheckbox(el)) { if (Number.isInteger(value)) { el.value = value; } else if (!Array.isArray(value) && typeof value !== "boolean" && ![null, void 0].includes(value)) { @@ -1413,34 +1501,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } return rawValue ? Boolean(rawValue) : null; } + var booleanAttributes = /* @__PURE__ */ new Set([ + "allowfullscreen", + "async", + "autofocus", + "autoplay", + "checked", + "controls", + "default", + "defer", + "disabled", + "formnovalidate", + "inert", + "ismap", + "itemscope", + "loop", + "multiple", + "muted", + "nomodule", + "novalidate", + "open", + "playsinline", + "readonly", + "required", + "reversed", + "selected", + "shadowrootclonable", + "shadowrootdelegatesfocus", + "shadowrootserializable" + ]); function isBooleanAttr(attrName) { - const booleanAttributes = [ - "disabled", - "checked", - "required", - "readonly", - "open", - "selected", - "autofocus", - "itemscope", - "multiple", - "novalidate", - "allowfullscreen", - "allowpaymentrequest", - "formnovalidate", - "autoplay", - "controls", - "loop", - "muted", - "playsinline", - "default", - "ismap", - "reversed", - "async", - "defer", - "nomodule" - ]; - return booleanAttributes.includes(attrName); + return booleanAttributes.has(attrName); } function attributeShouldntBePreservedIfFalsy(name) { return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name); @@ -1473,13 +1564,19 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } return attr; } + function isCheckbox(el) { + return el.type === "checkbox" || el.localName === "ui-checkbox" || el.localName === "ui-switch"; + } + function isRadio(el) { + return el.type === "radio" || el.localName === "ui-radio"; + } // packages/alpinejs/src/utils/debounce.js function debounce(func, wait) { - var timeout; + let timeout; return function() { - var context = this, args = arguments; - var later = function() { + const context = this, args = arguments; + const later = function() { timeout = null; func.apply(context, args); }; @@ -1551,10 +1648,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); return stores[name]; } stores[name] = value; + initInterceptors(stores[name]); if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") { stores[name].init(); } - initInterceptors(stores[name]); } function getStores() { return stores; @@ -1642,7 +1739,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); get raw() { return raw; }, - version: "3.14.1", + get transaction() { + return transaction; + }, + version: "3.15.12", flushAndStopDeferringMutations, dontAutoEvaluateFunctions, disableEffectScheduling, @@ -1656,13 +1756,17 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); onlyDuringClone, addRootSelector, addInitSelector, + setErrorHandler, interceptClone, addScopeToNode, deferMutations, mapAttributes, evaluateLater, interceptInit, + initInterceptors, + injectMagics, setEvaluator, + setRawEvaluator, mergeProxies, extractProp, findClosest, @@ -1681,6 +1785,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); throttle, debounce, evaluate, + evaluateRaw, initTree, nextTick, prefixed: prefix, @@ -2528,7 +2633,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); return; el._x_removeModelListeners["default"](); let outerGet = el._x_model.get; - let outerSet = el._x_model.set; + let outerSet = el._x_model.setWithModifiers; let releaseEntanglement = entangle( { get() { @@ -2580,10 +2685,9 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } }; mutateDom(() => { - placeInDom(clone2, target, modifiers); skipDuringClone(() => { + placeInDom(clone2, target, modifiers); initTree(clone2); - clone2._x_ignore = true; })(); }); el._x_teleportPutBack = () => { @@ -2592,7 +2696,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); placeInDom(el._x_teleport, target2, modifiers); }); }; - cleanup2(() => clone2.remove()); + cleanup2( + () => mutateDom(() => { + clone2.remove(); + destroyTree(clone2); + }) + ); }); var teleportContainerDuringClone = document.createElement("div"); function getTarget(expression) { @@ -2632,24 +2741,16 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); event = dotSyntax(event); if (modifiers.includes("camel")) event = camelCase2(event); - if (modifiers.includes("passive")) - options.passive = true; if (modifiers.includes("capture")) options.capture = true; if (modifiers.includes("window")) listenerTarget = window; if (modifiers.includes("document")) listenerTarget = document; - if (modifiers.includes("debounce")) { - let nextModifier = modifiers[modifiers.indexOf("debounce") + 1] || "invalid-wait"; - let wait = isNumeric(nextModifier.split("ms")[0]) ? Number(nextModifier.split("ms")[0]) : 250; - handler4 = debounce(handler4, wait); - } - if (modifiers.includes("throttle")) { - let nextModifier = modifiers[modifiers.indexOf("throttle") + 1] || "invalid-wait"; - let wait = isNumeric(nextModifier.split("ms")[0]) ? Number(nextModifier.split("ms")[0]) : 250; - handler4 = throttle(handler4, wait); + if (modifiers.includes("passive")) { + options.passive = modifiers[modifiers.indexOf("passive") + 1] !== "false"; } + handler4 = addDebounceOrThrottle(modifiers, handler4); if (modifiers.includes("prevent")) handler4 = wrapHandler(handler4, (next, e) => { e.preventDefault(); @@ -2684,6 +2785,14 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); handler4 = wrapHandler(handler4, (next, e) => { e.target === el && next(e); }); + if (event === "submit") { + handler4 = wrapHandler(handler4, (next, e) => { + if (e.target._x_pendingModelUpdates) { + e.target._x_pendingModelUpdates.forEach((fn) => fn()); + } + next(e); + }); + } if (isKeyEvent(event) || isClickEvent(event)) { handler4 = wrapHandler(handler4, (next, e) => { if (isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers)) { @@ -2697,6 +2806,19 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); listenerTarget.removeEventListener(event, handler4, options); }; } + function addDebounceOrThrottle(modifiers, handler4) { + if (modifiers.includes("debounce")) { + let nextModifier = modifiers[modifiers.indexOf("debounce") + 1] || "invalid-wait"; + let wait = isNumeric(nextModifier.split("ms")[0]) ? Number(nextModifier.split("ms")[0]) : 250; + handler4 = debounce(handler4, wait); + } + if (modifiers.includes("throttle")) { + let nextModifier = modifiers[modifiers.indexOf("throttle") + 1] || "invalid-wait"; + let wait = isNumeric(nextModifier.split("ms")[0]) ? Number(nextModifier.split("ms")[0]) : 250; + handler4 = throttle(handler4, wait); + } + return handler4; + } function dotSyntax(subject) { return subject.replace(/-/g, "."); } @@ -2721,7 +2843,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) { let keyModifiers = modifiers.filter((i) => { - return !["window", "document", "prevent", "stop", "once", "capture", "self", "away", "outside", "passive"].includes(i); + return !["window", "document", "prevent", "stop", "once", "capture", "self", "away", "outside", "passive", "preserve-scroll", "blur", "change", "lazy"].includes(i); }); if (keyModifiers.includes("debounce")) { let debounceIndex = keyModifiers.indexOf("debounce"); @@ -2785,7 +2907,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); directive("model", (el, { modifiers, expression }, { effect: effect3, cleanup: cleanup2 }) => { let scopeTarget = el; if (modifiers.includes("parent")) { - scopeTarget = el.parentNode; + scopeTarget = findClosest(el, (element) => element !== el); } let evaluateGet = evaluateLater(scopeTarget, expression); let evaluateSet; @@ -2820,13 +2942,50 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); el.setAttribute("name", expression); }); } - var event = el.tagName.toLowerCase() === "select" || ["checkbox", "radio"].includes(el.type) || modifiers.includes("lazy") ? "change" : "input"; - let removeListener = isCloning ? () => { - } : on(el, event, modifiers, (e) => { - setValue(getInputValue(el, modifiers, e, getValue())); - }); + let hasChangeModifier = modifiers.includes("change") || modifiers.includes("lazy"); + let hasBlurModifier = modifiers.includes("blur"); + let hasEnterModifier = modifiers.includes("enter"); + let hasExplicitEventModifiers = hasChangeModifier || hasBlurModifier || hasEnterModifier; + let removeListener; + if (isCloning) { + removeListener = () => { + }; + } else if (hasExplicitEventModifiers) { + let listeners = []; + let syncValue = (e) => setValue(getInputValue(el, modifiers, e, getValue())); + if (hasChangeModifier) { + listeners.push(on(el, "change", modifiers, syncValue)); + } + if (hasBlurModifier) { + listeners.push(on(el, "blur", modifiers, syncValue)); + if (el.form) { + let form = el.form; + let syncCallback = () => syncValue({ target: el }); + if (!form._x_pendingModelUpdates) + form._x_pendingModelUpdates = []; + form._x_pendingModelUpdates.push(syncCallback); + cleanup2(() => { + if (form._x_pendingModelUpdates) { + form._x_pendingModelUpdates.splice(form._x_pendingModelUpdates.indexOf(syncCallback), 1); + } + }); + } + } + if (hasEnterModifier) { + listeners.push(on(el, "keydown", modifiers, (e) => { + if (e.key === "Enter") + syncValue(e); + })); + } + removeListener = () => listeners.forEach((remove) => remove()); + } else { + let event = el.tagName.toLowerCase() === "select" || ["checkbox", "radio"].includes(el.type) ? "change" : "input"; + removeListener = on(el, event, modifiers, (e) => { + setValue(getInputValue(el, modifiers, e, getValue())); + }); + } if (modifiers.includes("fill")) { - if ([void 0, null, ""].includes(getValue()) || el.type === "checkbox" && Array.isArray(getValue()) || el.tagName.toLowerCase() === "select" && el.multiple) { + if ([void 0, null, ""].includes(getValue()) || isCheckbox(el) && Array.isArray(getValue()) || el.tagName.toLowerCase() === "select" && el.multiple) { setValue( getInputValue(el, modifiers, { target: el }, getValue()) ); @@ -2848,14 +3007,29 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); }, set(value) { setValue(value); - } + }, + setWithModifiers: addDebounceOrThrottle(modifiers, setValue) }; el._x_forceModelUpdate = (value) => { if (value === void 0 && typeof expression === "string" && expression.match(/\./)) value = ""; - window.fromModel = true; - mutateDom(() => bind(el, "value", value)); - delete window.fromModel; + mutateDom(() => { + if (isCheckbox(el)) { + if (Array.isArray(value)) { + el.checked = value.some((val) => val == el.value); + } else { + el.checked = !!value; + } + } else if (isRadio(el)) { + if (typeof value === "boolean") { + el.checked = safeParseBoolean(el.value) === value; + } else { + el.checked = el.value == value; + } + } else { + bind(el, "value", value); + } + }); }; effect3(() => { let value = getValue(); @@ -2868,7 +3042,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); return mutateDom(() => { if (event instanceof CustomEvent && event.detail !== void 0) return event.detail !== null && event.detail !== void 0 ? event.detail : event.target.value; - else if (el.type === "checkbox") { + else if (isCheckbox(el)) { if (Array.isArray(currentValue)) { let newValue = null; if (modifiers.includes("number")) { @@ -2899,7 +3073,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); }); } else { let newValue; - if (el.type === "radio") { + if (isRadio(el)) { if (event.target.checked) { newValue = event.target.value; } else { @@ -2964,7 +3138,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); effect3(() => { evaluate2((value) => { mutateDom(() => { - el.innerHTML = value; + el.innerHTML = value ?? ""; el._x_ignoreSelf = true; initTree(el); delete el._x_ignoreSelf; @@ -3111,131 +3285,92 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); // the x-bind:key expression is stored for our use instead of evaluated. el._x_keyExpression || "index" ); - el._x_prevKeys = []; - el._x_lookup = {}; + el._x_lookup = /* @__PURE__ */ new Map(); effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey)); cleanup2(() => { - Object.values(el._x_lookup).forEach((el2) => el2.remove()); - delete el._x_prevKeys; + el._x_lookup.forEach( + (el2) => mutateDom(() => { + destroyTree(el2); + el2.remove(); + }) + ); delete el._x_lookup; }); }); - function loop(el, iteratorNames, evaluateItems, evaluateKey) { - let isObject2 = (i) => typeof i === "object" && !Array.isArray(i); - let templateEl = el; + function refreshScope(scope2) { + return (newScope) => { + Object.entries(newScope).forEach(([key, value]) => { + scope2[key] = value; + }); + }; + } + function loop(templateEl, iteratorNames, evaluateItems, evaluateKey) { evaluateItems((items) => { - if (isNumeric3(items) && items >= 0) { - items = Array.from(Array(items).keys(), (i) => i + 1); - } - if (items === void 0) + if (isNumeric3(items)) + items = Array.from({ length: items }, (_, i) => i + 1); + if (items === void 0 || items === null) items = []; - let lookup = el._x_lookup; - let prevKeys = el._x_prevKeys; - let scopes = []; - let keys = []; - if (isObject2(items)) { - items = Object.entries(items).map(([key, value]) => { - let scope2 = getIterationScopeVariables(iteratorNames, value, key, items); - evaluateKey((value2) => { - if (keys.includes(value2)) - warn("Duplicate key on x-for", el); - keys.push(value2); - }, { scope: { index: key, ...scope2 } }); - scopes.push(scope2); - }); - } else { - for (let i = 0; i < items.length; i++) { - let scope2 = getIterationScopeVariables(iteratorNames, items[i], i, items); - evaluateKey((value) => { - if (keys.includes(value)) - warn("Duplicate key on x-for", el); - keys.push(value); - }, { scope: { index: i, ...scope2 } }); - scopes.push(scope2); - } - } - let adds = []; - let moves = []; - let removes = []; - let sames = []; - for (let i = 0; i < prevKeys.length; i++) { - let key = prevKeys[i]; - if (keys.indexOf(key) === -1) - removes.push(key); - } - prevKeys = prevKeys.filter((key) => !removes.includes(key)); - let lastKey = "template"; - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - let prevIndex = prevKeys.indexOf(key); - if (prevIndex === -1) { - prevKeys.splice(i, 0, key); - adds.push([lastKey, i]); - } else if (prevIndex !== i) { - let keyInSpot = prevKeys.splice(i, 1)[0]; - let keyForSpot = prevKeys.splice(prevIndex - 1, 1)[0]; - prevKeys.splice(i, 0, keyForSpot); - prevKeys.splice(prevIndex, 0, keyInSpot); - moves.push([keyInSpot, keyForSpot]); - } else { - sames.push(key); - } - lastKey = key; - } - for (let i = 0; i < removes.length; i++) { - let key = removes[i]; - if (!!lookup[key]._x_effects) { - lookup[key]._x_effects.forEach(dequeueJob); - } - lookup[key].remove(); - lookup[key] = null; - delete lookup[key]; - } - for (let i = 0; i < moves.length; i++) { - let [keyInSpot, keyForSpot] = moves[i]; - let elInSpot = lookup[keyInSpot]; - let elForSpot = lookup[keyForSpot]; - let marker = document.createElement("div"); - mutateDom(() => { - if (!elForSpot) - warn(`x-for ":key" is undefined or invalid`, templateEl, keyForSpot, lookup); - elForSpot.after(marker); - elInSpot.after(elForSpot); - elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl); - marker.before(elInSpot); - elInSpot._x_currentIfEl && elInSpot.after(elInSpot._x_currentIfEl); - marker.remove(); + if (items instanceof Set) + items = Array.from(items); + if (items instanceof Map) + items = Array.from(items); + let oldLookup = templateEl._x_lookup; + let lookup = /* @__PURE__ */ new Map(); + templateEl._x_lookup = lookup; + let hasStringKeys = isObject2(items); + let scopeEntries = Object.entries(items).map(([index, item]) => { + if (!hasStringKeys) + index = parseInt(index); + let scope2 = getIterationScopeVariables(iteratorNames, item, index, items); + let key; + evaluateKey((innerKey) => { + if (typeof innerKey === "object") + warn("x-for key cannot be an object, it must be a string or an integer", templateEl); + if (oldLookup.has(innerKey)) { + lookup.set(innerKey, oldLookup.get(innerKey)); + oldLookup.delete(innerKey); + } + key = innerKey; + }, { scope: { index, ...scope2 } }); + return [key, scope2]; + }); + mutateDom(() => { + oldLookup.forEach((el) => { + destroyTree(el); + el.remove(); }); - elForSpot._x_refreshXForScope(scopes[keys.indexOf(keyForSpot)]); - } - for (let i = 0; i < adds.length; i++) { - let [lastKey2, index] = adds[i]; - let lastEl = lastKey2 === "template" ? templateEl : lookup[lastKey2]; - if (lastEl._x_currentIfEl) - lastEl = lastEl._x_currentIfEl; - let scope2 = scopes[index]; - let key = keys[index]; - let clone2 = document.importNode(templateEl.content, true).firstElementChild; - let reactiveScope = reactive(scope2); - addScopeToNode(clone2, reactiveScope, templateEl); - clone2._x_refreshXForScope = (newScope) => { - Object.entries(newScope).forEach(([key2, value]) => { - reactiveScope[key2] = value; - }); - }; - mutateDom(() => { - lastEl.after(clone2); - skipDuringClone(() => initTree(clone2))(); + let added = /* @__PURE__ */ new Set(); + let prev = templateEl; + scopeEntries.forEach(([key, scope2]) => { + if (lookup.has(key)) { + let el = lookup.get(key); + el._x_refreshXForScope(scope2); + if (prev.nextElementSibling !== el) { + if (prev.nextElementSibling) + el.replaceWith(prev.nextElementSibling); + prev.after(el); + } + prev = el; + if (el._x_currentIfEl) { + if (el.nextElementSibling !== el._x_currentIfEl) + prev.after(el._x_currentIfEl); + prev = el._x_currentIfEl; + } + return; + } + if (templateEl.content.children.length > 1) + warn("x-for templates require a single root element, additional elements will be ignored.", templateEl); + let clone2 = document.importNode(templateEl.content, true).firstElementChild; + let reactiveScope = reactive(scope2); + addScopeToNode(clone2, reactiveScope, templateEl); + clone2._x_refreshXForScope = refreshScope(reactiveScope); + lookup.set(key, clone2); + added.add(clone2); + prev.after(clone2); + prev = clone2; }); - if (typeof key === "object") { - warn("x-for key cannot be an object, it must be a string or an integer", templateEl); - } - lookup[key] = clone2; - } - for (let i = 0; i < sames.length; i++) { - lookup[sames[i]]._x_refreshXForScope(scopes[keys.indexOf(sames[i])]); - } - templateEl._x_prevKeys = keys; + skipDuringClone(() => added.forEach((clone2) => initTree(clone2)))(); + }); }); } function parseForExpression(expression) { @@ -3282,7 +3417,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); return scopeVariables; } function isNumeric3(subject) { - return !Array.isArray(subject) && !isNaN(subject); + return typeof subject !== "object" && !isNaN(subject); + } + function isObject2(subject) { + return typeof subject === "object" && !Array.isArray(subject); } // packages/alpinejs/src/directives/x-ref.js @@ -3290,6 +3428,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); } handler3.inline = (el, { expression }, { cleanup: cleanup2 }) => { let root = closestRoot(el); + if (!root) + return; if (!root._x_refs) root._x_refs = {}; root._x_refs[expression] = el; @@ -3313,12 +3453,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); }); el._x_currentIfEl = clone2; el._x_undoIf = () => { - walk(clone2, (node) => { - if (!!node._x_effects) { - node._x_effects.forEach(dequeueJob); - } + mutateDom(() => { + destroyTree(clone2); + clone2.remove(); }); - clone2.remove(); delete el._x_currentIfEl; }; return clone2; @@ -3375,6 +3513,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el); // packages/alpinejs/src/index.js alpine_default.setEvaluator(normalEvaluator); + alpine_default.setRawEvaluator(normalRawEvaluator); alpine_default.setReactivityEngine({ reactive: reactive2, effect: effect2, release: stop, raw: toRaw }); var src_default = alpine_default; diff --git a/tests/playwright/assets/page.html b/tests/playwright/assets/page.html index e39714e..33a850d 100644 --- a/tests/playwright/assets/page.html +++ b/tests/playwright/assets/page.html @@ -21,6 +21,6 @@ - +