浏览代码

Merge branch 'main' into feature/ui/combobox

Caleb Porzio 1 年之前
父节点
当前提交
b9753e8da3
共有 34 个文件被更改,包括 726 次插入333 次删除
  1. 1 1
      README.md
  2. 361 251
      package-lock.json
  3. 0 3
      package.json
  4. 1 1
      packages/alpinejs/package.json
  5. 14 4
      packages/alpinejs/src/directives/x-for.js
  6. 7 8
      packages/alpinejs/src/directives/x-model.js
  7. 6 6
      packages/alpinejs/src/directives/x-transition.js
  8. 6 0
      packages/alpinejs/src/lifecycle.js
  9. 13 6
      packages/alpinejs/src/magics.js
  10. 4 2
      packages/alpinejs/src/plugin.js
  11. 2 10
      packages/alpinejs/src/scope.js
  12. 19 0
      packages/alpinejs/src/utils/bind.js
  13. 17 14
      packages/alpinejs/src/utils/on.js
  14. 1 1
      packages/collapse/package.json
  15. 1 1
      packages/docs/package.json
  16. 1 1
      packages/docs/src/en/essentials/installation.md
  17. 0 6
      packages/docs/src/en/ui.md
  18. 1 1
      packages/focus/package.json
  19. 8 3
      packages/focus/src/index.js
  20. 1 1
      packages/intersect/package.json
  21. 1 1
      packages/mask/package.json
  22. 1 1
      packages/morph/package.json
  23. 31 0
      packages/morph/src/morph.js
  24. 4 1
      packages/navigate/package.json
  25. 1 1
      packages/persist/package.json
  26. 1 1
      packages/ui/package.json
  27. 3 3
      tests/cypress/integration/directives/x-bind-style.spec.js
  28. 23 4
      tests/cypress/integration/directives/x-bind.spec.js
  29. 46 0
      tests/cypress/integration/directives/x-for.spec.js
  30. 35 0
      tests/cypress/integration/directives/x-model.spec.js
  31. 33 0
      tests/cypress/integration/directives/x-on.spec.js
  32. 60 0
      tests/cypress/integration/directives/x-transition.spec.js
  33. 20 0
      tests/cypress/integration/plugins/morph.spec.js
  34. 3 1
      tests/cypress/utils.js

+ 1 - 1
README.md

@@ -8,7 +8,7 @@ Stay here for contribution-related information.
 
 
 > Looking for V2 docs? [here they are](https://github.com/alpinejs/alpine/tree/v2.8.2)
 > Looking for V2 docs? [here they are](https://github.com/alpinejs/alpine/tree/v2.8.2)
 
 
-<p align="center"><a href="https://alpinejs.dev/patterns"><img src="/hero.jpg" alt="Alpine Compoenent Patterns"></a></p>
+<p align="center"><a href="https://alpinejs.dev/patterns"><img src="/hero.jpg" alt="Alpine Component Patterns"></a></p>
 
 
 ## Contribution Guide:
 ## Contribution Guide:
 
 

文件差异内容过多而无法显示
+ 361 - 251
package-lock.json


+ 0 - 3
package.json

@@ -20,8 +20,5 @@
         "jest": "jest test",
         "jest": "jest test",
         "update-docs": "node ./scripts/update-docs.js",
         "update-docs": "node ./scripts/update-docs.js",
         "release": "node ./scripts/release.js"
         "release": "node ./scripts/release.js"
-    },
-    "dependencies": {
-        "nprogress": "^0.2.0"
     }
     }
 }
 }

+ 1 - 1
packages/alpinejs/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "alpinejs",
     "name": "alpinejs",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "The rugged, minimal JavaScript framework",
     "description": "The rugged, minimal JavaScript framework",
     "homepage": "https://alpinejs.dev",
     "homepage": "https://alpinejs.dev",
     "repository": {
     "repository": {

+ 14 - 4
packages/alpinejs/src/directives/x-for.js

@@ -1,4 +1,4 @@
-import { addScopeToNode, refreshScope } from '../scope'
+import { addScopeToNode } from '../scope'
 import { evaluateLater } from '../evaluator'
 import { evaluateLater } from '../evaluator'
 import { directive } from '../directives'
 import { directive } from '../directives'
 import { reactive } from '../reactivity'
 import { reactive } from '../reactivity'
@@ -163,6 +163,8 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
             let marker = document.createElement('div')
             let marker = document.createElement('div')
 
 
             mutateDom(() => {
             mutateDom(() => {
+                if (! elForSpot) warn(`x-for ":key" is undefined or invalid`, templateEl)
+
                 elForSpot.after(marker)
                 elForSpot.after(marker)
                 elInSpot.after(elForSpot)
                 elInSpot.after(elForSpot)
                 elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
                 elForSpot._x_currentIfEl && elForSpot.after(elForSpot._x_currentIfEl)
@@ -171,7 +173,7 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
                 marker.remove()
                 marker.remove()
             })
             })
 
 
-            refreshScope(elForSpot, scopes[keys.indexOf(keyForSpot)])
+            elForSpot._x_refreshXForScope(scopes[keys.indexOf(keyForSpot)])
         }
         }
 
 
         // We can now create and add new elements.
         // We can now create and add new elements.
@@ -188,7 +190,15 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
 
 
             let clone = document.importNode(templateEl.content, true).firstElementChild
             let clone = document.importNode(templateEl.content, true).firstElementChild
 
 
-            addScopeToNode(clone, reactive(scope), templateEl)
+            let reactiveScope = reactive(scope)
+
+            addScopeToNode(clone, reactiveScope, templateEl)
+
+            clone._x_refreshXForScope = (newScope) => {
+                Object.entries(newScope).forEach(([key, value]) => {
+                    reactiveScope[key] = value
+                })
+            }
 
 
             mutateDom(() => {
             mutateDom(() => {
                 lastEl.after(clone)
                 lastEl.after(clone)
@@ -207,7 +217,7 @@ function loop(el, iteratorNames, evaluateItems, evaluateKey) {
         // data it depends on in case the data has changed in an
         // data it depends on in case the data has changed in an
         // "unobservable" way.
         // "unobservable" way.
         for (let i = 0; i < sames.length; i++) {
         for (let i = 0; i < sames.length; i++) {
-            refreshScope(lookup[sames[i]], scopes[keys.indexOf(sames[i])])
+            lookup[sames[i]]._x_refreshXForScope(scopes[keys.indexOf(sames[i])])
         }
         }
 
 
         // Now we'll log the keys (and the order they're in) for comparing
         // Now we'll log the keys (and the order they're in) for comparing

+ 7 - 8
packages/alpinejs/src/directives/x-model.js

@@ -46,10 +46,6 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
             })
             })
         }
         }
     }
     }
-
-    if (modifiers.includes('fill') && el.hasAttribute('value') && (getValue() === null || getValue() === '')) {
-        setValue(el.value)
-    }
     
     
     if (typeof expression === 'string' && el.type === 'radio') {
     if (typeof expression === 'string' && el.type === 'radio') {
         // Radio buttons only work properly when they share a name attribute.
         // Radio buttons only work properly when they share a name attribute.
@@ -73,7 +69,10 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
     let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
         setValue(getInputValue(el, modifiers, e, getValue()))
         setValue(getInputValue(el, modifiers, e, getValue()))
     })
     })
-
+    
+    if (modifiers.includes('fill') && [null, ''].includes(getValue())) {
+        el.dispatchEvent(new Event(event, {}));
+    }
     // Register the listener removal callback on the element, so that
     // Register the listener removal callback on the element, so that
     // in addition to the cleanup function, x-modelable may call it.
     // in addition to the cleanup function, x-modelable may call it.
     // Also, make this a keyed object if we decide to reintroduce
     // Also, make this a keyed object if we decide to reintroduce
@@ -134,9 +133,9 @@ function getInputValue(el, modifiers, event, currentValue) {
         // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
         // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
         // Safari autofill triggers event as CustomEvent and assigns value to target
         // Safari autofill triggers event as CustomEvent and assigns value to target
         // so we return event.target.value instead of event.detail
         // so we return event.target.value instead of event.detail
-        if (event instanceof CustomEvent && event.detail !== undefined) {
-            return typeof event.detail != 'undefined' ? event.detail : event.target.value
-        } else if (el.type === 'checkbox') {
+        if (event instanceof CustomEvent && event.detail !== undefined)
+            return event.detail ?? event.target.value
+        else if (el.type === 'checkbox') {
             // If the data we are binding to is an array, toggle its value inside the array.
             // If the data we are binding to is an array, toggle its value inside the array.
             if (Array.isArray(currentValue)) {
             if (Array.isArray(currentValue)) {
                 let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value
                 let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value

+ 6 - 6
packages/alpinejs/src/directives/x-transition.js

@@ -7,8 +7,8 @@ import { once } from '../utils/once'
 
 
 directive('transition', (el, { value, modifiers, expression }, { evaluate }) => {
 directive('transition', (el, { value, modifiers, expression }, { evaluate }) => {
     if (typeof expression === 'function') expression = evaluate(expression)
     if (typeof expression === 'function') expression = evaluate(expression)
-
-    if (! expression) {
+    if (expression === false) return
+    if (!expression || typeof expression === 'boolean') {
         registerTransitionsFromHelper(el, modifiers, value)
         registerTransitionsFromHelper(el, modifiers, value)
     } else {
     } else {
         registerTransitionsFromClassString(el, expression, value)
         registerTransitionsFromClassString(el, expression, value)
@@ -50,7 +50,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     let wantsScale = wantsAll || modifiers.includes('scale')
     let wantsScale = wantsAll || modifiers.includes('scale')
     let opacityValue = wantsOpacity ? 0 : 1
     let opacityValue = wantsOpacity ? 0 : 1
     let scaleValue = wantsScale ? modifierValue(modifiers, 'scale', 95) / 100 : 1
     let scaleValue = wantsScale ? modifierValue(modifiers, 'scale', 95) / 100 : 1
-    let delay = modifierValue(modifiers, 'delay', 0)
+    let delay = modifierValue(modifiers, 'delay', 0) / 1000
     let origin = modifierValue(modifiers, 'origin', 'center')
     let origin = modifierValue(modifiers, 'origin', 'center')
     let property = 'opacity, transform'
     let property = 'opacity, transform'
     let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
     let durationIn = modifierValue(modifiers, 'duration', 150) / 1000
@@ -60,7 +60,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     if (transitioningIn) {
     if (transitioningIn) {
         el._x_transition.enter.during = {
         el._x_transition.enter.during = {
             transformOrigin: origin,
             transformOrigin: origin,
-            transitionDelay: delay,
+            transitionDelay: `${delay}s`,
             transitionProperty: property,
             transitionProperty: property,
             transitionDuration: `${durationIn}s`,
             transitionDuration: `${durationIn}s`,
             transitionTimingFunction: easing,
             transitionTimingFunction: easing,
@@ -80,7 +80,7 @@ function registerTransitionsFromHelper(el, modifiers, stage) {
     if (transitioningOut) {
     if (transitioningOut) {
         el._x_transition.leave.during = {
         el._x_transition.leave.during = {
             transformOrigin: origin,
             transformOrigin: origin,
-            transitionDelay: delay,
+            transitionDelay: `${delay}s`,
             transitionProperty: property,
             transitionProperty: property,
             transitionDuration: `${durationOut}s`,
             transitionDuration: `${durationOut}s`,
             transitionTimingFunction: easing,
             transitionTimingFunction: easing,
@@ -318,7 +318,7 @@ export function modifierValue(modifiers, key, fallback) {
         if (isNaN(rawValue)) return fallback
         if (isNaN(rawValue)) return fallback
     }
     }
 
 
-    if (key === 'duration') {
+    if (key === 'duration' || key === 'delay') {
         // Support x-transition.duration.500ms && duration.500
         // Support x-transition.duration.500ms && duration.500
         let match = rawValue.match(/([0-9]+)ms/)
         let match = rawValue.match(/([0-9]+)ms/)
         if (match) return match[1]
         if (match) return match[1]

+ 6 - 0
packages/alpinejs/src/lifecycle.js

@@ -4,7 +4,13 @@ import { dispatch } from './utils/dispatch'
 import { walk } from "./utils/walk"
 import { walk } from "./utils/walk"
 import { warn } from './utils/warn'
 import { warn } from './utils/warn'
 
 
+let started = false
+
 export function start() {
 export function start() {
+    if (started) warn('Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems.')
+
+    started = true
+
     if (! document.body) warn('Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine\'s `<script>` tag?')
     if (! document.body) warn('Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine\'s `<script>` tag?')
 
 
     dispatch(document, 'alpine:init')
     dispatch(document, 'alpine:init')

+ 13 - 6
packages/alpinejs/src/magics.js

@@ -11,17 +11,24 @@ export function magic(name, callback) {
 
 
 export function injectMagics(obj, el) {
 export function injectMagics(obj, el) {
     Object.entries(magics).forEach(([name, callback]) => {
     Object.entries(magics).forEach(([name, callback]) => {
-        Object.defineProperty(obj, `$${name}`, {
-            get() {
+        let memoizedUtilities = null;
+        function getUtilities() {
+            if (memoizedUtilities) {
+                return memoizedUtilities;
+            } else {
                 let [utilities, cleanup] = getElementBoundUtilities(el)
                 let [utilities, cleanup] = getElementBoundUtilities(el)
                 
                 
-                utilities = {interceptor, ...utilities}
+                memoizedUtilities = {interceptor, ...utilities}
                 
                 
                 onElRemoved(el, cleanup)
                 onElRemoved(el, cleanup)
-
-                return callback(el, utilities)
+                return memoizedUtilities;
+            }
+        }
+        
+        Object.defineProperty(obj, `$${name}`, {
+            get() {
+                return callback(el, getUtilities());
             },
             },
-
             enumerable: false,
             enumerable: false,
         })
         })
     })
     })

+ 4 - 2
packages/alpinejs/src/plugin.js

@@ -1,5 +1,7 @@
-import Alpine from './alpine'
+import Alpine from "./alpine";
 
 
 export function plugin(callback) {
 export function plugin(callback) {
-    callback(Alpine)
+    let callbacks = Array.isArray(callback) ? callback : [callback]
+
+    callbacks.forEach(i => i(Alpine))
 }
 }

+ 2 - 10
packages/alpinejs/src/scope.js

@@ -15,14 +15,6 @@ export function hasScope(node) {
     return !! node._x_dataStack
     return !! node._x_dataStack
 }
 }
 
 
-export function refreshScope(element, scope) {
-    let existingScope = element._x_dataStack[0]
-
-    Object.entries(scope).forEach(([key, value]) => {
-        existingScope[key] = value
-    })
-}
-
 export function closestDataStack(node) {
 export function closestDataStack(node) {
     if (node._x_dataStack) return node._x_dataStack
     if (node._x_dataStack) return node._x_dataStack
 
 
@@ -60,7 +52,7 @@ export function mergeProxies(objects) {
                     if ((descriptor.get && descriptor.get._x_alreadyBound) || (descriptor.set && descriptor.set._x_alreadyBound)) {
                     if ((descriptor.get && descriptor.get._x_alreadyBound) || (descriptor.set && descriptor.set._x_alreadyBound)) {
                         return true
                         return true
                     }
                     }
-                    
+
                     // Properly bind getters and setters to this wrapper Proxy.
                     // Properly bind getters and setters to this wrapper Proxy.
                     if ((descriptor.get || descriptor.set) && descriptor.enumerable) {
                     if ((descriptor.get || descriptor.set) && descriptor.enumerable) {
                         // Only bind user-defined getters, not our magic properties.
                         // Only bind user-defined getters, not our magic properties.
@@ -81,7 +73,7 @@ export function mergeProxies(objects) {
                         })
                         })
                     }
                     }
 
 
-                    return true 
+                    return true
                 }
                 }
 
 
                 return false
                 return false

+ 19 - 0
packages/alpinejs/src/utils/bind.js

@@ -23,6 +23,14 @@ export default function bind(el, name, value, modifiers = []) {
         case 'class':
         case 'class':
             bindClasses(el, value)
             bindClasses(el, value)
             break;
             break;
+        
+        // 'selected' and 'checked' are special attributes that aren't necessarily
+        // synced with their corresponding properties when updated, so both the 
+        // attribute and property need to be updated when bound.
+        case 'selected':
+        case 'checked':
+            bindAttributeAndProperty(el, name, value)
+            break;
 
 
         default:
         default:
             bindAttribute(el, name, value)
             bindAttribute(el, name, value)
@@ -79,6 +87,11 @@ function bindStyles(el, value) {
     el._x_undoAddedStyles = setStyles(el, value)
     el._x_undoAddedStyles = setStyles(el, value)
 }
 }
 
 
+function bindAttributeAndProperty(el, name, value) {
+    bindAttribute(el, name, value)
+    setPropertyIfChanged(el, name, value)
+}
+
 function bindAttribute(el, name, value) {
 function bindAttribute(el, name, value) {
     if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
     if ([null, undefined, false].includes(value) && attributeShouldntBePreservedIfFalsy(name)) {
         el.removeAttribute(name)
         el.removeAttribute(name)
@@ -95,6 +108,12 @@ function setIfChanged(el, attrName, value) {
     }
     }
 }
 }
 
 
+function setPropertyIfChanged(el, propName, value) {
+    if (el[propName] !== value) {
+        el[propName] = value
+    }
+}
+
 function updateSelect(el, value) {
 function updateSelect(el, value) {
     const arrayWrappedValue = [].concat(value).map(value => { return value + '' })
     const arrayWrappedValue = [].concat(value).map(value => { return value + '' })
 
 

+ 17 - 14
packages/alpinejs/src/utils/on.js

@@ -18,6 +18,23 @@ export default function on (el, event, modifiers, callback) {
     if (modifiers.includes('capture')) options.capture = true
     if (modifiers.includes('capture')) options.capture = true
     if (modifiers.includes('window')) listenerTarget = window
     if (modifiers.includes('window')) listenerTarget = window
     if (modifiers.includes('document')) listenerTarget = document
     if (modifiers.includes('document')) listenerTarget = document
+
+    // By wrapping the handler with debounce & throttle first, we ensure that the wrapping logic itself is not
+    // throttled/debounced, only the user's callback is. This way, if the user expects
+    // `e.preventDefault()` to happen, it'll still happen even if their callback gets throttled.
+    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
+
+        handler = debounce(handler, 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
+
+        handler = throttle(handler, wait)
+    }
+
     if (modifiers.includes('prevent')) handler = wrapHandler(handler, (next, e) => { e.preventDefault(); next(e) })
     if (modifiers.includes('prevent')) handler = wrapHandler(handler, (next, e) => { e.preventDefault(); next(e) })
     if (modifiers.includes('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); next(e) })
     if (modifiers.includes('stop')) handler = wrapHandler(handler, (next, e) => { e.stopPropagation(); next(e) })
     if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) })
     if (modifiers.includes('self')) handler = wrapHandler(handler, (next, e) => { e.target === el && next(e) })
@@ -59,20 +76,6 @@ export default function on (el, event, modifiers, callback) {
         next(e)
         next(e)
     })
     })
 
 
-    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
-
-        handler = debounce(handler, 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
-
-        handler = throttle(handler, wait)
-    }
-
     listenerTarget.addEventListener(event, handler, options)
     listenerTarget.addEventListener(event, handler, options)
 
 
     return () => {
     return () => {

+ 1 - 1
packages/collapse/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/collapse",
     "name": "@alpinejs/collapse",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Collapse and expand elements with robust animations",
     "description": "Collapse and expand elements with robust animations",
     "homepage": "https://alpinejs.dev/plugins/collapse",
     "homepage": "https://alpinejs.dev/plugins/collapse",
     "repository": {
     "repository": {

+ 1 - 1
packages/docs/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/docs",
     "name": "@alpinejs/docs",
-    "version": "3.12.0-revision.1",
+    "version": "3.12.2-revision.1",
     "description": "The documentation for Alpine",
     "description": "The documentation for Alpine",
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT"
     "license": "MIT"

+ 1 - 1
packages/docs/src/en/essentials/installation.md

@@ -33,7 +33,7 @@ This is by far the simplest way to get started with Alpine. Include the followin
 Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
 Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
 
 
 ```alpine
 ```alpine
-<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.0/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.12.2/dist/cdn.min.js"></script>
 ```
 ```
 
 
 That's it! Alpine is now available for use inside your page.
 That's it! Alpine is now available for use inside your page.

+ 0 - 6
packages/docs/src/en/ui.md

@@ -1,6 +0,0 @@
----
-order: 5
-title: UI
-font-type: mono
-type: sub-directory
----

+ 1 - 1
packages/focus/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/focus",
     "name": "@alpinejs/focus",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Manage focus within a page",
     "description": "Manage focus within a page",
     "homepage": "https://alpinejs.dev/plugins/focus",
     "homepage": "https://alpinejs.dev/plugins/focus",
     "repository": {
     "repository": {

+ 8 - 3
packages/focus/src/index.js

@@ -102,12 +102,17 @@ export default function (Alpine) {
 
 
             let oldValue = false
             let oldValue = false
 
 
-            let trap = createFocusTrap(el, {
+            let options = {
                 escapeDeactivates: false,
                 escapeDeactivates: false,
                 allowOutsideClick: true,
                 allowOutsideClick: true,
                 fallbackFocus: () => el,
                 fallbackFocus: () => el,
-                initialFocus: el.querySelector('[autofocus]')
-            })
+            }
+
+            let autofocusEl = el.querySelector('[autofocus]')
+
+            if (autofocusEl) options.initialFocus = autofocusEl
+
+            let trap = createFocusTrap(el, options)
 
 
             let undoInert = () => {}
             let undoInert = () => {}
             let undoDisableScrolling = () => {}
             let undoDisableScrolling = () => {}

+ 1 - 1
packages/intersect/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/intersect",
     "name": "@alpinejs/intersect",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Trigger JavaScript when an element enters the viewport",
     "description": "Trigger JavaScript when an element enters the viewport",
     "homepage": "https://alpinejs.dev/plugins/intersect",
     "homepage": "https://alpinejs.dev/plugins/intersect",
     "repository": {
     "repository": {

+ 1 - 1
packages/mask/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/mask",
     "name": "@alpinejs/mask",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "An Alpine plugin for input masking",
     "description": "An Alpine plugin for input masking",
     "homepage": "https://alpinejs.dev/plugins/mask",
     "homepage": "https://alpinejs.dev/plugins/mask",
     "repository": {
     "repository": {

+ 1 - 1
packages/morph/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/morph",
     "name": "@alpinejs/morph",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Diff and patch a block of HTML on a page with an HTML template",
     "description": "Diff and patch a block of HTML on a page with an HTML template",
     "homepage": "https://alpinejs.dev/plugins/morph",
     "homepage": "https://alpinejs.dev/plugins/morph",
     "repository": {
     "repository": {

+ 31 - 0
packages/morph/src/morph.js

@@ -5,6 +5,8 @@ let resolveStep = () => {}
 let logger = () => {}
 let logger = () => {}
 
 
 export function morph(from, toHtml, options) {
 export function morph(from, toHtml, options) {
+    monkeyPatchDomSetAttributeToAllowAtSymbols()
+
     // We're defining these globals and methods inside this function (instead of outside)
     // We're defining these globals and methods inside this function (instead of outside)
     // because it's an async function and if run twice, they would overwrite
     // because it's an async function and if run twice, they would overwrite
     // each other.
     // each other.
@@ -384,3 +386,32 @@ function initializeAlpineOnTo(from, to, childrenOnly) {
         window.Alpine.clone(from, to)
         window.Alpine.clone(from, to)
     }
     }
 }
 }
+
+let patched = false
+
+function monkeyPatchDomSetAttributeToAllowAtSymbols() {
+    if (patched) return
+
+    patched = true
+
+    // Because morphdom may add attributes to elements containing "@" symbols
+    // like in the case of an Alpine `@click` directive, we have to patch
+    // the standard Element.setAttribute method to allow this to work.
+    let original = Element.prototype.setAttribute
+
+    let hostDiv = document.createElement('div')
+
+    Element.prototype.setAttribute = function newSetAttribute(name, value) {
+        if (! name.includes('@')) {
+            return original.call(this, name, value)
+        }
+
+        hostDiv.innerHTML = `<span ${name}="${value}"></span>`
+
+        let attr = hostDiv.firstElementChild.getAttributeNode(name)
+
+        hostDiv.firstElementChild.removeAttributeNode(attr)
+
+        this.setAttributeNode(attr)
+    }
+}

+ 4 - 1
packages/navigate/package.json

@@ -5,5 +5,8 @@
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",
-    "module": "dist/module.esm.js"
+    "module": "dist/module.esm.js",
+    "dependencies": {
+        "nprogress": "^0.2.0"
+    }
 }
 }

+ 1 - 1
packages/persist/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/persist",
     "name": "@alpinejs/persist",
-    "version": "3.12.0",
+    "version": "3.12.2",
     "description": "Persist Alpine data across page loads",
     "description": "Persist Alpine data across page loads",
     "homepage": "https://alpinejs.dev/plugins/persist",
     "homepage": "https://alpinejs.dev/plugins/persist",
     "repository": {
     "repository": {

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/ui",
     "name": "@alpinejs/ui",
-    "version": "3.12.0-beta.0",
+    "version": "3.12.1-beta.0",
     "description": "Headless UI components for Alpine",
     "description": "Headless UI components for Alpine",
     "homepage": "https://alpinejs.dev/components#headless",
     "homepage": "https://alpinejs.dev/components#headless",
     "repository": {
     "repository": {

+ 3 - 3
tests/cypress/integration/directives/x-bind-style.spec.js

@@ -40,7 +40,7 @@ test('style attribute object binding with CSS variable',
         </div>
         </div>
     `,
     `,
     ({ get }) => {
     ({ get }) => {
-        get('div').should(haveAttribute('style', '--MyCSS-Variable:0.25;'))
+        get('div').should(haveAttribute('style', '--MyCSS-Variable: 0.25;'))
     }
     }
 )
 )
 
 
@@ -62,7 +62,7 @@ test('CSS custom properties are set',
         </div>
         </div>
     `,
     `,
     ({ get }) => {
     ({ get }) => {
-        get('span').should(haveAttribute('style', 'color: var(--custom-prop); --custom-prop:#f00;'))
+        get('span').should(haveAttribute('style', 'color: var(--custom-prop); --custom-prop: #f00;'))
     }
     }
 )
 )
 
 
@@ -73,6 +73,6 @@ test('existing CSS custom properties are preserved',
         </div>
         </div>
     `,
     `,
     ({ get }) => {
     ({ get }) => {
-        get('span').should(haveAttribute('style', 'color: var(--custom-prop-b); --custom-prop-a: red; --custom-prop-b:var(--custom-prop-a);'))
+        get('span').should(haveAttribute('style', 'color: var(--custom-prop-b); --custom-prop-a: red; --custom-prop-b: var(--custom-prop-a);'))
     }
     }
 )
 )

+ 23 - 4
tests/cypress/integration/directives/x-bind.spec.js

@@ -1,4 +1,4 @@
-import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils'
+import { beHidden, beVisible, haveText, beChecked, haveAttribute, haveClasses, haveProperty, haveValue, notBeChecked, notHaveAttribute, notHaveClasses, test, html } from '../../utils';
 
 
 test('sets attribute bindings on initialize',
 test('sets attribute bindings on initialize',
     html`
     html`
@@ -391,8 +391,8 @@ test('x-bind object syntax event handlers defined as functions receive the event
         <script>
         <script>
             window.data = () => { return {
             window.data = () => { return {
                 button: {
                 button: {
-                    ['@click']() {
-                        this.$refs.span.innerText = this.$el.id
+                    ['@click'](event) {
+                        this.$refs.span.innerText = event.currentTarget.id
                     }
                     }
                 }
                 }
             }}
             }}
@@ -410,7 +410,7 @@ test('x-bind object syntax event handlers defined as functions receive the event
     }
     }
 )
 )
 
 
-test('x-bind object syntax event handlers defined as functions receive the event object as their first argument',
+test('x-bind object syntax event handlers defined as functions receive element bound magics',
     html`
     html`
         <script>
         <script>
             window.data = () => { return {
             window.data = () => { return {
@@ -467,3 +467,22 @@ test('Can extract Alpine bound data as a data prop',
         get('#2').should(haveAttribute('foo', 'bar'))
         get('#2').should(haveAttribute('foo', 'bar'))
     }
     }
 )
 )
+
+test('x-bind updates checked attribute and property after user interaction',
+    html`
+        <div x-data="{ checked: true }">
+            <button @click="checked = !checked">toggle</button>
+            <input type="checkbox" x-bind:checked="checked" @change="checked = $event.target.checked" />
+        </div>
+    `,
+    ({ get }) => {
+        get('input').should(haveAttribute('checked', 'checked'))
+        get('input').should(haveProperty('checked', true))
+        get('input').click()
+        get('input').should(notHaveAttribute('checked'))
+        get('input').should(haveProperty('checked', false))
+        get('button').click()
+        get('input').should(haveAttribute('checked', 'checked'))
+        get('input').should(haveProperty('checked', true))
+    }
+)

+ 46 - 0
tests/cypress/integration/directives/x-for.spec.js

@@ -559,3 +559,49 @@ test('renders children using directives injected by x-html correctly',
         get('p:nth-of-type(2) span').should(haveText('bar'))
         get('p:nth-of-type(2) span').should(haveText('bar'))
     }
     }
 )
 )
+
+test(
+    'handles x-data directly inside x-for',
+    html`
+        <div x-data="{ items: [{x:0, k:1},{x:1, k:2}] }">
+            <button x-on:click="items = [{x:3, k:1},{x:4, k:2}]">update</button>
+            <template x-for="item in items" :key="item.k">
+                <div :id="'item-' + item.k" x-data="{ inner: true }">
+                    <span x-text="item.x.toString()"></span>:
+                    <span x-text="item.k"></span>
+                </div>
+            </template>
+        </div>
+    `,
+    ({ get }) => {
+        get('#item-1 span:nth-of-type(1)').should(haveText('0'))
+        get('#item-2 span:nth-of-type(1)').should(haveText('1'))
+        get('button').click()
+        get('#item-1 span:nth-of-type(1)').should(haveText('3'))
+        get('#item-2 span:nth-of-type(1)').should(haveText('4'))
+})
+
+test('x-for throws descriptive error when key is undefined',
+    html`
+        <div x-data="{ items: [
+            {
+                id: 1,
+                name: 'foo',
+            },
+            {
+                id: 2,
+                name: 'bar',
+            },
+            {
+                id: 3,
+                name: 'baz',
+            },
+        ]}">
+            <template x-for="item in items" :key="item.doesntExist">
+                <span x-text="i"></span>
+            </template>
+        </div>
+    `,
+    ({ get }) => {},
+    true
+)

+ 35 - 0
tests/cypress/integration/directives/x-model.spec.js

@@ -151,5 +151,40 @@ test('x-model with fill modifier takes input value on null or empty string',
     }
     }
 )
 )
 
 
+test('x-model with fill modifier works with select/radio elements',
+    html`
+        <div x-data="{ a: null, b: null, c: null, d: null }">
+            <select x-model.fill="a">
+                <option value="123">123</option>
+                <option value="456" selected>456</option>
+            </select>
+            <select x-model.fill="b" multiple>
+                <option value="123" selected>123</option>
+                <option value="456" selected>456</option>
+            </select>
+        </div>
+    `,
+    ({ get }) => {
+        get('[x-data]').should(haveData('a', '456'));
+        get('[x-data]').should(haveData('b', ['123', '456']));
+    }
+);
+
+test('x-model with fill modifier respects number modifier',
+    html`
+        <div x-data="{ a: null, b: null, c: null, d: null }">
+            <input type="text" x-model.fill.number="a" value="456" / >
+            <select x-model.fill.number="b" multiple>
+                <option value="123" selected>123</option>
+                <option value="456" selected>456</option>
+            </select>
+        </div>
+    `,
+    ({ get }) => {
+        get('[x-data]').should(haveData('a', 456));
+        get('[x-data]').should(haveData('b', [123,456]));
+    }
+);
+
 
 
 
 

+ 33 - 0
tests/cypress/integration/directives/x-on.spec.js

@@ -97,6 +97,26 @@ test('.stop modifier',
     }
     }
 )
 )
 
 
+
+test('.stop modifier with a .throttle',
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <button x-on:click="foo = 'baz'">
+                <h1>h1</h1>
+                <h2 @click.stop.throttle>h2</h2>
+            </button>
+        </div>
+    `,
+    ({ get }) => {
+        get('div').should(haveData('foo', 'bar'))
+        get('h2').click()
+        get('h2').click()
+        get('div').should(haveData('foo', 'bar'))
+        get('h1').click()
+        get('div').should(haveData('foo', 'baz'))
+    }
+)
+
 test('.capture modifier',
 test('.capture modifier',
     html`
     html`
         <div x-data="{ foo: 'bar', count: 0 }">
         <div x-data="{ foo: 'bar', count: 0 }">
@@ -178,6 +198,19 @@ test('.prevent modifier',
     }
     }
 )
 )
 
 
+test('.prevent modifier with a .debounce',
+    html`
+        <div x-data="{}">
+            <input type="checkbox" x-on:click.prevent.debounce>
+        </div>
+    `,
+    ({ get }) => {
+        get('input').check()
+        get('input').check()
+        get('input').should(notBeChecked())
+    }
+)
+
 test('.window modifier',
 test('.window modifier',
     html`
     html`
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">

+ 60 - 0
tests/cypress/integration/directives/x-transition.spec.js

@@ -80,3 +80,63 @@ test('transition:enter in nested x-show visually runs',
         get('h1').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
         get('h1').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
     }
     }
 )
 )
+
+test('transition duration and delay with and without ms syntax',
+    html`
+        <div x-data="{ showMs: false, showBlank: false }">
+
+            <span class="ms"
+                  x-show="showMs"
+                  x-transition.delay.80ms.duration.120ms>ms syntax</span>
+            <span class="blank"
+                  x-show="showBlank"
+                  x-transition.delay.80.duration.120>blank syntax</span>
+
+            <button class="ms"    x-on:click="showMs = true"></button>
+            <button class="blank" x-on:click="showBlank = true"></button>
+        </div>
+    `,
+    ({ get }) => {
+        get('span.ms').should(notBeVisible())
+        get('button.ms').click()
+        get('span.ms').should(notBeVisible()) // Not visible due to delay
+        get('span.ms').should(beVisible())
+        get('span.ms').should(notHaveComputedStyle('opacity', '1')) // We expect a value between 0 and 1
+        get('span.ms').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
+
+        get('span.blank').should(notBeVisible())
+        get('button.blank').click()
+        get('span.blank').should(notBeVisible()) // Not visible due to delay
+        get('span.blank').should(beVisible())
+        get('span.blank').should(notHaveComputedStyle('opacity', '1')) // We expect a value between 0 and 1
+        get('span.blank').should(haveComputedStyle('opacity', '1')) // Eventually opacity will be 1
+    }
+)
+
+test(
+    'bound x-transition can handle empty string and true values',
+    html`
+        <script>
+            window.transitions = () => {
+                return {
+                    withEmptyString: {
+                        ["x-transition.opacity"]: "",
+                    },
+                    withBoolean: {
+                        ["x-transition.opacity"]: true,
+                    },
+                };
+            };
+        </script>
+        <div x-data="transitions()">
+            <button x-bind="withEmptyString"></button>
+            <span x-bind="withBoolean">thing</span>
+        </div>
+    `,
+    ({ get }) => 
+        {
+            get('span').should(beVisible())
+            get('span').should(beVisible())
+        }
+    
+);

+ 20 - 0
tests/cypress/integration/plugins/morph.spec.js

@@ -421,3 +421,23 @@ test('can morph with flat-nested conditional markers',
         get('div:nth-of-type(3) input').should(haveValue('bar'))
         get('div:nth-of-type(3) input').should(haveValue('bar'))
     },
     },
 )
 )
+
+// '@event' handlers cannot be assigned directly on the element without Alpine's internl monkey patching...
+test('can morph @event handlers', [
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <button x-text="foo"></button>
+        </div>
+    `],
+    ({ get, click }, reload, window, document) => {
+        let toHtml = html`
+            <button @click="foo = 'buzz'" x-text="foo"></button>
+        `;
+
+        get('button').should(haveText('bar'));
+
+        get('button').then(([el]) => window.Alpine.morph(el, toHtml));
+        get('button').click();
+        get('button').should(haveText('buzz'));
+    }
+);

+ 3 - 1
tests/cypress/utils.js

@@ -87,7 +87,7 @@ function injectHtmlAndBootAlpine(cy, templateAndPotentiallyScripts, callback, pa
     })
     })
 }
 }
 
 
-export let haveData = (key, value) => ([ el ]) => expect(root(el)._x_dataStack[0][key]).to.equal(value)
+export let haveData = (key, value) => ([el]) => expect(root(el)._x_dataStack[0][key]).to.deep.equal(value);
 
 
 export let haveFocus = () => el => expect(el).to.have.focus
 export let haveFocus = () => el => expect(el).to.have.focus
 
 
@@ -97,6 +97,8 @@ export let haveAttribute = (name, value) => el => expect(el).to.have.attr(name,
 
 
 export let notHaveAttribute = (name, value) => el => expect(el).not.to.have.attr(name, value)
 export let notHaveAttribute = (name, value) => el => expect(el).not.to.have.attr(name, value)
 
 
+export let haveProperty = (name, value) => el => expect(el).to.have.prop(name, value)
+
 export let haveText = text => el => expect(el).to.have.text(text)
 export let haveText = text => el => expect(el).to.have.text(text)
 
 
 export let notHaveText = text => el => expect(el).not.to.have.text(text)
 export let notHaveText = text => el => expect(el).not.to.have.text(text)

部分文件因为文件数量过多而无法显示