Переглянути джерело

Merge branch 'main' of github.com:alpinejs/alpine

Caleb Porzio 2 роки тому
батько
коміт
4aa5f85366
36 змінених файлів з 347 додано та 49 видалено
  1. 1 1
      index.html
  2. 1 1
      morph.html
  3. 6 0
      packages/alpinejs/package.json
  4. 12 0
      packages/alpinejs/src/directives/x-model.js
  5. 11 2
      packages/alpinejs/src/directives/x-teleport.js
  6. 1 1
      packages/alpinejs/src/evaluator.js
  7. 0 1
      packages/alpinejs/src/lifecycle.js
  8. 11 2
      packages/alpinejs/src/utils/on.js
  9. 6 0
      packages/collapse/package.json
  10. 2 2
      packages/collapse/src/index.js
  11. 9 1
      packages/docs/src/en/directives/cloak.md
  12. 24 2
      packages/docs/src/en/directives/for.md
  13. 2 2
      packages/docs/src/en/essentials/installation.md
  14. 2 2
      packages/docs/src/en/plugins/collapse.md
  15. 3 3
      packages/docs/src/en/plugins/focus.md
  16. 3 3
      packages/docs/src/en/plugins/intersect.md
  17. 23 7
      packages/docs/src/en/plugins/mask.md
  18. 3 3
      packages/docs/src/en/plugins/morph.md
  19. 3 3
      packages/docs/src/en/plugins/persist.md
  20. 1 1
      packages/docs/src/en/start-here.md
  21. 6 0
      packages/focus/package.json
  22. 6 0
      packages/history/package.json
  23. 6 0
      packages/intersect/package.json
  24. 6 0
      packages/mask/package.json
  25. 9 8
      packages/mask/src/index.js
  26. 6 0
      packages/morph/package.json
  27. 6 0
      packages/persist/package.json
  28. 6 0
      packages/ui/package.json
  29. 1 1
      scripts/build.js
  30. 19 0
      tests/cypress/integration/directives/x-model.spec.js
  31. 55 0
      tests/cypress/integration/directives/x-on.spec.js
  32. 38 0
      tests/cypress/integration/directives/x-teleport.spec.js
  33. 25 0
      tests/cypress/integration/plugins/mask.spec.js
  34. 1 1
      tests/cypress/manual-memory.html
  35. 1 1
      tests/cypress/manual-transition-test.html
  36. 32 1
      tests/jest/mask.spec.js

+ 1 - 1
index.html

@@ -8,7 +8,7 @@
     <script src="./packages/ui/dist/cdn.js" defer></script>
     <script src="./packages/ui/dist/cdn.js" defer></script>
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
     <script src="//cdn.tailwindcss.com"></script>
     <script src="//cdn.tailwindcss.com"></script>
-    <!-- <script src="https://unpkg.com/alpinejs@3.0.0/dist/cdn.min.js" defer></script> -->
+    <!-- <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.0.0/dist/cdn.min.js" defer></script> -->
 
 
     <!-- <div x-data="{ value: null }">
     <!-- <div x-data="{ value: null }">
         Value: <span x-text="value"></span>
         Value: <span x-text="value"></span>

+ 1 - 1
morph.html

@@ -1,7 +1,7 @@
 <html>
 <html>
     <script src="./packages/morph/dist/cdn.js" defer></script>
     <script src="./packages/morph/dist/cdn.js" defer></script>
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
     <script src="./packages/alpinejs/dist/cdn.js" defer></script>
-    <!-- <script src="https://unpkg.com/alpinejs@3.0.0/dist/cdn.min.js" defer></script> -->
+    <!-- <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.0.0/dist/cdn.min.js" defer></script> -->
 
 
 <div id="before">
 <div id="before">
 <!-- Before markup goes here: -->
 <!-- Before markup goes here: -->

+ 6 - 0
packages/alpinejs/package.json

@@ -2,6 +2,12 @@
     "name": "alpinejs",
     "name": "alpinejs",
     "version": "3.10.5",
     "version": "3.10.5",
     "description": "The rugged, minimal JavaScript framework",
     "description": "The rugged, minimal JavaScript framework",
+    "homepage": "https://alpinejs.dev",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/alpinejs"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 12 - 0
packages/alpinejs/src/directives/x-model.js

@@ -1,6 +1,7 @@
 import { evaluateLater } from '../evaluator'
 import { evaluateLater } from '../evaluator'
 import { directive } from '../directives'
 import { directive } from '../directives'
 import { mutateDom } from '../mutation'
 import { mutateDom } from '../mutation'
+import { nextTick } from '../nextTick'
 import bind from '../utils/bind'
 import bind from '../utils/bind'
 import on from '../utils/on'
 import on from '../utils/on'
 
 
@@ -34,6 +35,17 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
 
 
     cleanup(() => el._x_removeModelListeners['default']())
     cleanup(() => el._x_removeModelListeners['default']())
 
 
+    // If the input/select/textarea element is linked to a form
+    // we listen for the reset event on the parent form (the event
+    // does not trigger on the single inputs) and update
+    // on nextTick so the page doesn't end up out of sync
+    if (el.form) {
+        let removeResetListener = on(el.form, 'reset', [], (e) => {
+            nextTick(() => el._x_model && el._x_model.set(el.value))
+        })
+        cleanup(() => removeResetListener())
+    }
+
     // Allow programmatic overiding of x-model.
     // Allow programmatic overiding of x-model.
     let evaluateSetModel = evaluateLater(el, `${expression} = __placeholder`)
     let evaluateSetModel = evaluateLater(el, `${expression} = __placeholder`)
     el._x_model = {
     el._x_model = {

+ 11 - 2
packages/alpinejs/src/directives/x-teleport.js

@@ -4,7 +4,7 @@ import { mutateDom } from "../mutation"
 import { addScopeToNode } from "../scope"
 import { addScopeToNode } from "../scope"
 import { warn } from "../utils/warn"
 import { warn } from "../utils/warn"
 
 
-directive('teleport', (el, { expression }, { cleanup }) => {
+directive('teleport', (el, { modifiers, expression }, { cleanup }) => {
     if (el.tagName.toLowerCase() !== 'template') warn('x-teleport can only be used on a <template> tag', el)
     if (el.tagName.toLowerCase() !== 'template') warn('x-teleport can only be used on a <template> tag', el)
 
 
     let target = document.querySelector(expression)
     let target = document.querySelector(expression)
@@ -31,7 +31,16 @@ directive('teleport', (el, { expression }, { cleanup }) => {
     addScopeToNode(clone, {}, el)
     addScopeToNode(clone, {}, el)
 
 
     mutateDom(() => {
     mutateDom(() => {
-        target.appendChild(clone)
+        if (modifiers.includes('prepend')) {
+            // insert element before the target
+            target.parentNode.insertBefore(clone, target)
+        } else if (modifiers.includes('append')) {
+            // insert element after the target
+            target.parentNode.insertBefore(clone, target.nextSibling)
+        } else {
+            // origin
+            target.appendChild(clone)
+        }
 
 
         initTree(clone)
         initTree(clone)
 
 

+ 1 - 1
packages/alpinejs/src/evaluator.js

@@ -73,7 +73,7 @@ function generateFunctionFromString(expression, el) {
         || /^[\n\s]*if.*\(.*\)/.test(expression)
         || /^[\n\s]*if.*\(.*\)/.test(expression)
         // Support expressions starting with "let/const" like: "let foo = 'bar'"
         // Support expressions starting with "let/const" like: "let foo = 'bar'"
         || /^(let|const)\s/.test(expression)
         || /^(let|const)\s/.test(expression)
-            ? `(() => { ${expression} })()`
+            ? `(async()=>{ ${expression} })()`
             : expression
             : expression
 
 
     const safeAsyncFunction = () => {
     const safeAsyncFunction = () => {

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

@@ -1,7 +1,6 @@
 import { startObservingMutations, onAttributesAdded, onElAdded, onElRemoved, cleanupAttributes } from "./mutation"
 import { startObservingMutations, onAttributesAdded, onElAdded, onElRemoved, cleanupAttributes } from "./mutation"
 import { deferHandlingDirectives, directives } from "./directives"
 import { deferHandlingDirectives, directives } from "./directives"
 import { dispatch } from './utils/dispatch'
 import { dispatch } from './utils/dispatch'
-import { nextTick } from "./nextTick"
 import { walk } from "./utils/walk"
 import { walk } from "./utils/walk"
 import { warn } from './utils/warn'
 import { warn } from './utils/warn'
 
 

+ 11 - 2
packages/alpinejs/src/utils/on.js

@@ -93,6 +93,8 @@ function isNumeric(subject){
 }
 }
 
 
 function kebabCase(subject) {
 function kebabCase(subject) {
+    if ([' ','_'].includes(subject
+    )) return subject
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
 }
 }
 
 
@@ -110,6 +112,11 @@ function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) {
         keyModifiers.splice(debounceIndex, isNumeric((keyModifiers[debounceIndex+1] || 'invalid-wait').split('ms')[0]) ? 2 : 1)
         keyModifiers.splice(debounceIndex, isNumeric((keyModifiers[debounceIndex+1] || 'invalid-wait').split('ms')[0]) ? 2 : 1)
     }
     }
 
 
+    if (keyModifiers.includes('throttle')) {
+        let debounceIndex = keyModifiers.indexOf('throttle')
+        keyModifiers.splice(debounceIndex, isNumeric((keyModifiers[debounceIndex+1] || 'invalid-wait').split('ms')[0]) ? 2 : 1)
+    }
+
     // If no modifier is specified, we'll call it a press.
     // If no modifier is specified, we'll call it a press.
     if (keyModifiers.length === 0) return false
     if (keyModifiers.length === 0) return false
 
 
@@ -149,8 +156,8 @@ function keyToModifiers(key) {
     let modifierToKeyMap = {
     let modifierToKeyMap = {
         'ctrl': 'control',
         'ctrl': 'control',
         'slash': '/',
         'slash': '/',
-        'space': '-',
-        'spacebar': '-',
+        'space': ' ',
+        'spacebar': ' ',
         'cmd': 'meta',
         'cmd': 'meta',
         'esc': 'escape',
         'esc': 'escape',
         'up': 'arrow-up',
         'up': 'arrow-up',
@@ -159,6 +166,8 @@ function keyToModifiers(key) {
         'right': 'arrow-right',
         'right': 'arrow-right',
         'period': '.',
         'period': '.',
         'equal': '=',
         'equal': '=',
+        'minus': '-',
+        'underscore': '_',
     }
     }
 
 
     modifierToKeyMap[key] = key
     modifierToKeyMap[key] = key

+ 6 - 0
packages/collapse/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/collapse",
     "name": "@alpinejs/collapse",
     "version": "3.10.5",
     "version": "3.10.5",
     "description": "Collapse and expand elements with robust animations",
     "description": "Collapse and expand elements with robust animations",
+    "homepage": "https://alpinejs.dev/plugins/collapse",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/collapse"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 2 - 2
packages/collapse/src/index.js

@@ -19,7 +19,7 @@ export default function (Alpine) {
         // We use the hidden attribute for the benefit of Tailwind
         // We use the hidden attribute for the benefit of Tailwind
         // users as the .space utility will ignore [hidden] elements.
         // users as the .space utility will ignore [hidden] elements.
         // We also use display:none as the hidden attribute has very
         // We also use display:none as the hidden attribute has very
-        // low CSS specificity and could be accidentally overriden
+        // low CSS specificity and could be accidentally overridden
         // by a user.
         // by a user.
         if (! el._x_isShown && fullyHide) el.hidden = true
         if (! el._x_isShown && fullyHide) el.hidden = true
         if (! el._x_isShown) el.style.overflow = 'hidden'
         if (! el._x_isShown) el.style.overflow = 'hidden'
@@ -56,7 +56,7 @@ export default function (Alpine) {
                     start: { height: current+'px' },
                     start: { height: current+'px' },
                     end: { height: full+'px' },
                     end: { height: full+'px' },
                 }, () => el._x_isShown = true, () => {
                 }, () => el._x_isShown = true, () => {
-                    if (el.style.height == `${full}px`) {
+                    if (el.getBoundingClientRect().height == full) {
                         el.style.overflow = null
                         el.style.overflow = null
                     }
                     }
                 })
                 })

+ 9 - 1
packages/docs/src/en/directives/cloak.md

@@ -15,7 +15,13 @@ For `x-cloak` to work however, you must add the following CSS to the page.
 [x-cloak] { display: none !important; }
 [x-cloak] { display: none !important; }
 ```
 ```
 
 
-Now, the following example will hide the `<span>` tag until Alpine has set its text content to the `message` property.
+The following example will hide the `<span>` tag until its `x-show` is specifically set to true, preventing any "blip" of the hidden element onto screen as Alpine loads.
+
+```alpine
+<span x-cloak x-show="false">This will not 'blip' onto screen at any point</span>
+```
+
+`x-cloak` doesn't just work on elements hidden by `x-show` or `x-if`: it also ensures that elements containing data are hidden until the data is correctly set. The following example will hide the `<span>` tag until Alpine has set its text content to the `message` property.
 
 
 ```alpine
 ```alpine
 <span x-cloak x-text="message"></span>
 <span x-cloak x-text="message"></span>
@@ -23,6 +29,8 @@ Now, the following example will hide the `<span>` tag until Alpine has set its t
 
 
 When Alpine loads on the page, it removes all `x-cloak` property from the element, which also removes the `display: none;` applied by CSS, therefore showing the element.
 When Alpine loads on the page, it removes all `x-cloak` property from the element, which also removes the `display: none;` applied by CSS, therefore showing the element.
 
 
+## Alternative to global syntax
+
 If you'd like to achieve this same behavior, but avoid having to include a global style, you can use the following cool, but admittedly odd trick:
 If you'd like to achieve this same behavior, but avoid having to include a global style, you can use the following cool, but admittedly odd trick:
 
 
 ```alpine
 ```alpine

+ 24 - 2
packages/docs/src/en/directives/for.md

@@ -27,8 +27,8 @@ Alpine's `x-for` directive allows you to create DOM elements by iterating throug
 
 
 There are two rules worth noting about `x-for`:
 There are two rules worth noting about `x-for`:
 
 
-* `x-for` MUST be declared on a `<template>` element
-* That `<template>` element MUST have only one root element
+>`x-for` MUST be declared on a `<template>` element
+> That `<template>` element MUST contain only one root element
 
 
 <a name="keys"></a>
 <a name="keys"></a>
 ## Keys
 ## Keys
@@ -85,3 +85,25 @@ If you need to simply loop `n` number of times, rather than iterate through an a
 ```
 ```
 
 
 `i` in this case can be named anything you like.
 `i` in this case can be named anything you like.
+
+<a name="contents-of-a-template"></a>
+## Contents of a `<template>`
+
+As mentioned above, an `<template>` tag must contain only one root element.
+
+For example, the following code will not work:
+
+```alpine
+<template x-for="color in colors">
+    <span>The next color is </span><span x-text="color">
+</template>
+```
+
+but this code will work:
+```alpine
+<template x-for="color in colors">
+    <p>
+        <span>The next color is </span><span x-text="color">
+    </p>
+</template>
+```

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

@@ -22,7 +22,7 @@ This is by far the simplest way to get started with Alpine. Include the followin
   <head>
   <head>
     ...
     ...
 
 
-    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
   </head>
   </head>
   ...
   ...
 </html>
 </html>
@@ -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://unpkg.com/alpinejs@3.10.5/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.10.5/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.

+ 2 - 2
packages/docs/src/en/plugins/collapse.md

@@ -22,10 +22,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 
 ```alpine
 ```alpine
 <!-- Alpine Plugins -->
 <!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
 
 
 <!-- Alpine Core -->
 <!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 ```
 ```
 
 
 ### Via NPM
 ### Via NPM

+ 3 - 3
packages/docs/src/en/plugins/focus.md

@@ -24,10 +24,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 
 ```alpine
 ```alpine
 <!-- Alpine Plugins -->
 <!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
 
 
 <!-- Alpine Core -->
 <!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 ```
 ```
 
 
 ### Via NPM
 ### Via NPM
@@ -309,7 +309,7 @@ This plugin offers many smaller utilities for managing focus within a page. Thes
 | Property | Description |
 | Property | Description |
 | ---       | --- |
 | ---       | --- |
 | `focus(el)`   | Focus the passed element (handling annoyances internally: using nextTick, etc.) |
 | `focus(el)`   | Focus the passed element (handling annoyances internally: using nextTick, etc.) |
-| `focusable(el)`   | Detect weather or not an element is focusable |
+| `focusable(el)`   | Detect whether or not an element is focusable |
 | `focusables()`   | Get all "focusable" elements within the current element |
 | `focusables()`   | Get all "focusable" elements within the current element |
 | `focused()`   | Get the currently focused element on the page |
 | `focused()`   | Get the currently focused element on the page |
 | `lastFocused()`   | Get the last focused element on the page |
 | `lastFocused()`   | Get the last focused element on the page |

+ 3 - 3
packages/docs/src/en/plugins/intersect.md

@@ -22,10 +22,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 
 ```alpine
 ```alpine
 <!-- Alpine Plugins -->
 <!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
 
 
 <!-- Alpine Core -->
 <!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 ```
 ```
 
 
 ### Via NPM
 ### Via NPM
@@ -152,7 +152,7 @@ If you wanted to trigger only when 5% of the element has entered the viewport, y
 Allows you to control the `rootMargin` property of the underlying `IntersectionObserver`.
 Allows you to control the `rootMargin` property of the underlying `IntersectionObserver`.
 This effectively tweaks the size of the viewport boundary. Positive values
 This effectively tweaks the size of the viewport boundary. Positive values
 expand the boundary beyond the viewport, and negative values shrink it inward. The values
 expand the boundary beyond the viewport, and negative values shrink it inward. The values
-work like CSS margin: one value for all sides, two values for top/bottom, left/right, or
+work like CSS margin: one value for all sides; two values for top/bottom, left/right; or
 four values for top, right, bottom, left. You can use `px` and `%` values, or use a bare number to
 four values for top, right, bottom, left. You can use `px` and `%` values, or use a bare number to
 get a pixel value.
 get a pixel value.
 
 

+ 23 - 7
packages/docs/src/en/plugins/mask.md

@@ -12,6 +12,7 @@ Alpine's Mask plugin allows you to automatically format a text input field as a
 This is useful for many different types of inputs: phone numbers, credit cards, dollar amounts, account numbers, dates, etc.
 This is useful for many different types of inputs: phone numbers, credit cards, dollar amounts, account numbers, dates, etc.
 
 
 <a name="installation"></a>
 <a name="installation"></a>
+
 ## Installation
 ## Installation
 
 
 <div x-data="{ expanded: false }">
 <div x-data="{ expanded: false }">
@@ -27,10 +28,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 
 ```alpine
 ```alpine
 <!-- Alpine Plugins -->
 <!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/mask@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.x.x/dist/cdn.min.js"></script>
 
 
 <!-- Alpine Core -->
 <!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 ```
 ```
 
 
 ### Via NPM
 ### Via NPM
@@ -60,6 +61,7 @@ Alpine.plugin(mask)
  </div>
  </div>
 
 
 <a name="x-mask"></a>
 <a name="x-mask"></a>
+
 ## x-mask
 ## x-mask
 
 
 The primary API for using this plugin is the `x-mask` directive.
 The primary API for using this plugin is the `x-mask` directive.
@@ -80,13 +82,14 @@ Notice how the text you type into the input field must adhere to the format prov
 
 
 The following wildcard characters are supported in masks:
 The following wildcard characters are supported in masks:
 
 
-| Wildcard                   | Description                 |
-| -------------------------- | --------------------------- |
-| `*` | Any character |
-| `a` | Only alpha characters (a-z, A-Z) |
-| `9` | Only numeric characters (0-9) |
+| Wildcard | Description                      |
+| -------- | -------------------------------- |
+| `*`      | Any character                    |
+| `a`      | Only alpha characters (a-z, A-Z) |
+| `9`      | Only numeric characters (0-9)    |
 
 
 <a name="mask-functions"></a>
 <a name="mask-functions"></a>
+
 ## Dynamic Masks
 ## Dynamic Masks
 
 
 Sometimes simple mask literals (i.e. `(999) 999-9999`) are not sufficient. In these cases, `x-mask:dynamic` allows you to dynamically generate masks on the fly based on user input.
 Sometimes simple mask literals (i.e. `(999) 999-9999`) are not sufficient. In these cases, `x-mask:dynamic` allows you to dynamically generate masks on the fly based on user input.
@@ -128,6 +131,7 @@ function creditCardMask(input) {
 ```
 ```
 
 
 <a name="money-inputs"></a>
 <a name="money-inputs"></a>
+
 ## Money Inputs
 ## Money Inputs
 
 
 Because writing your own dynamic mask expression for money inputs is fairly complex, Alpine offers a prebuilt one and makes it available as `$money()`.
 Because writing your own dynamic mask expression for money inputs is fairly complex, Alpine offers a prebuilt one and makes it available as `$money()`.
@@ -155,3 +159,15 @@ If you wish to swap the periods for commas and vice versa (as is required in cer
     <input type="text" x-mask:dynamic="$money($input, ',')"  placeholder="0,00">
     <input type="text" x-mask:dynamic="$money($input, ',')"  placeholder="0,00">
 </div>
 </div>
 <!-- END_VERBATIM -->
 <!-- END_VERBATIM -->
+
+You may also choose to override the thousands separator by supplying a third optional argument:
+
+```alpine
+<input x-mask:dynamic="$money($input, '.', ' ')">
+```
+
+<!-- START_VERBATIM -->
+<div class="demo" x-data>
+    <input type="text" x-mask:dynamic="$money($input, '.', ' ')"  placeholder="3 000.00">
+</div>
+<!-- END_VERBATIM -->

+ 3 - 3
packages/docs/src/en/plugins/morph.md

@@ -9,7 +9,7 @@ graph_image: https://alpinejs.dev/social_morph.jpg
 
 
 Alpine's Morph plugin allows you to "morph" an element on the page into the provided HTML template, all while preserving any browser or Alpine state within the "morphed" element.
 Alpine's Morph plugin allows you to "morph" an element on the page into the provided HTML template, all while preserving any browser or Alpine state within the "morphed" element.
 
 
-This is useful for updating HTML from a server request without loosing Alpine's on-page state. A utility like this is at the core of full-stack frameworks like [Laravel Livewire](https://laravel-livewire.com/) and [Phoenix LiveView](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript).
+This is useful for updating HTML from a server request without losing Alpine's on-page state. A utility like this is at the core of full-stack frameworks like [Laravel Livewire](https://laravel-livewire.com/) and [Phoenix LiveView](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript).
 
 
 The best way to understand its purpose is with the following interactive visualization. Give it a try!
 The best way to understand its purpose is with the following interactive visualization. Give it a try!
 
 
@@ -41,10 +41,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 
 ```alpine
 ```alpine
 <!-- Alpine Plugins -->
 <!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/morph@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/cdn.min.js"></script>
 
 
 <!-- Alpine Core -->
 <!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 ```
 ```
 
 
 ### Via NPM
 ### Via NPM

+ 3 - 3
packages/docs/src/en/plugins/persist.md

@@ -22,10 +22,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 
 ```alpine
 ```alpine
 <!-- Alpine Plugins -->
 <!-- Alpine Plugins -->
-<script defer src="https://unpkg.com/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
 
 
 <!-- Alpine Core -->
 <!-- Alpine Core -->
-<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 ```
 ```
 
 
 ### Via NPM
 ### Via NPM
@@ -204,4 +204,4 @@ Alpine.data('dropdown', function () {
 Alpine.store('darkMode', {
 Alpine.store('darkMode', {
     on: Alpine.$persist(true).as('darkMode_on')
     on: Alpine.$persist(true).as('darkMode_on')
 });
 });
-```
+```

+ 1 - 1
packages/docs/src/en/start-here.md

@@ -12,7 +12,7 @@ Using a text editor, fill the file with these contents:
 ```alpine
 ```alpine
 <html>
 <html>
 <head>
 <head>
-    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
+    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
 </head>
 </head>
 <body>
 <body>
     <h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>
     <h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>

+ 6 - 0
packages/focus/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/focus",
     "name": "@alpinejs/focus",
     "version": "3.10.5",
     "version": "3.10.5",
     "description": "Manage focus within a page",
     "description": "Manage focus within a page",
+    "homepage": "https://alpinejs.dev/plugins/focus",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/focus"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/history/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/history",
     "name": "@alpinejs/history",
     "version": "3.0.0-alpha.0",
     "version": "3.0.0-alpha.0",
     "description": "Sync Alpine data with the browser's query string",
     "description": "Sync Alpine data with the browser's query string",
+    "homepage": "https://alpinejs.dev/",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/history"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/intersect/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/intersect",
     "name": "@alpinejs/intersect",
     "version": "3.10.5",
     "version": "3.10.5",
     "description": "Trigger JavaScript when an element enters the viewport",
     "description": "Trigger JavaScript when an element enters the viewport",
+    "homepage": "https://alpinejs.dev/plugins/intersect",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/intersect"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/mask/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/mask",
     "name": "@alpinejs/mask",
     "version": "3.10.5",
     "version": "3.10.5",
     "description": "An Alpine plugin for input masking",
     "description": "An Alpine plugin for input masking",
+    "homepage": "https://alpinejs.dev/plugins/mask",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/mask"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 9 - 8
packages/mask/src/index.js

@@ -163,9 +163,10 @@ export function buildUp(template, input) {
     return output
     return output
 }
 }
 
 
-function formatMoney(input, delimeter = '.', thousands) {
-    thousands = (delimeter === ',' && thousands === undefined)
-        ? '.' : ','
+export function formatMoney(input, delimiter = '.', thousands) {
+    if (/^\D+$/.test(input)) return '9'
+
+    thousands = thousands ?? (delimiter === "," ? "." : ",")
 
 
     let addThousands = (input, thousands) => {
     let addThousands = (input, thousands) => {
         let output = ''
         let output = ''
@@ -186,17 +187,17 @@ function formatMoney(input, delimeter = '.', thousands) {
         return output
         return output
     }
     }
 
 
-    let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimeter}]`, 'g'), '')
-    let template = Array.from({ length: strippedInput.split(delimeter)[0].length }).fill('9').join('')
+    let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimiter}]`, 'g'), '')
+    let template = Array.from({ length: strippedInput.split(delimiter)[0].length }).fill('9').join('')
 
 
     template = addThousands(template, thousands)
     template = addThousands(template, thousands)
 
 
-    if (input.includes(delimeter)) template += `${delimeter}99`
+    if (input.includes(delimiter)) template += `${delimiter}99`
 
 
     queueMicrotask(() => {
     queueMicrotask(() => {
-        if (this.el.value.endsWith(delimeter)) return
+        if (this.el.value.endsWith(delimiter)) return
 
 
-        if (this.el.value[this.el.selectionStart - 1] === delimeter) {
+        if (this.el.value[this.el.selectionStart - 1] === delimiter) {
             this.el.setSelectionRange(this.el.selectionStart - 1, this.el.selectionStart - 1)
             this.el.setSelectionRange(this.el.selectionStart - 1, this.el.selectionStart - 1)
         }
         }
     })
     })

+ 6 - 0
packages/morph/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/morph",
     "name": "@alpinejs/morph",
     "version": "3.10.5",
     "version": "3.10.5",
     "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",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/morph"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/persist/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/persist",
     "name": "@alpinejs/persist",
     "version": "3.10.5",
     "version": "3.10.5",
     "description": "Persist Alpine data across page loads",
     "description": "Persist Alpine data across page loads",
+    "homepage": "https://alpinejs.dev/plugins/persist",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/persist"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/ui/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/ui",
     "name": "@alpinejs/ui",
     "version": "3.10.5-beta.8",
     "version": "3.10.5-beta.8",
     "description": "Headless UI components for Alpine",
     "description": "Headless UI components for Alpine",
+    "homepage": "https://alpinejs.dev/components#headless",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/alpinejs/alpine.git",
+        "directory": "packages/ui"
+    },
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "main": "dist/module.cjs.js",

+ 1 - 1
scripts/build.js

@@ -115,4 +115,4 @@ function bytesToSize(bytes) {
     const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10)
     const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10)
     if (i === 0) return `${bytes} ${sizes[i]}`
     if (i === 0) return `${bytes} ${sizes[i]}`
     return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`
     return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`
-  }
+}

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

@@ -110,3 +110,22 @@ test('x-model can be accessed programmatically',
         get('span').should(haveText('bob'))
         get('span').should(haveText('bob'))
     }
     }
 )
 )
+
+test('x-model updates value when the form is reset',
+    html`
+    <div x-data="{ foo: '' }">
+        <form>
+            <input x-model="foo"></input>
+            <button type="reset">Reset</button>
+        </form>
+        <span x-text="foo"></span>
+    </div>
+    `,
+    ({ get }) => {
+        get('span').should(haveText(''))
+        get('input').type('baz')
+        get('span').should(haveText('baz'))
+        get('button').click()
+        get('span').should(haveText(''))
+    }
+)

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

@@ -275,6 +275,22 @@ test('.debounce modifier',
     }
     }
 )
 )
 
 
+test('.throttle modifier',
+    html`
+        <div x-data="{ count: 0 }">
+            <input x-on:keyup.throttle.504ms="count = count+1">
+            <span x-text="count"></span>
+        </div>
+    `,
+    ({ get }) => {
+        get('span').should(haveText('0'))
+        get('input').type('f')
+        get('span').should(haveText('1'))
+        get('input').type('ffffffffffff')
+        get('span').should(haveText('1'))
+    }
+)
+
 test('keydown modifiers',
 test('keydown modifiers',
     html`
     html`
         <div x-data="{ count: 0 }">
         <div x-data="{ count: 0 }">
@@ -330,6 +346,31 @@ test('keydown modifiers',
     }
     }
 )
 )
 
 
+test('discerns between space minus underscore',
+    html`
+        <div x-data="{ count: 0 }">
+            <input id="space" type="text" x-on:keydown.space="count++" />
+            <input id="minus" type="text" x-on:keydown.-="count++" />
+            <input id="underscore" type="text" x-on:keydown._="count++" />
+            <span x-text="count"></span>
+        </div>
+    `,
+    ({get}) => {
+        get('span').should(haveText('0'))
+        get('#space').type(' ')
+        get('span').should(haveText('1'))
+        get('#space').type('-')
+        get('span').should(haveText('1'))
+        get('#minus').type('-')
+        get('span').should(haveText('2'))
+        get('#minus').type(' ')
+        get('span').should(haveText('2'))
+        get('#underscore').type('_')
+        get('span').should(haveText('3'))
+        get('#underscore').type(' ')
+        get('span').should(haveText('3'))
+    })
+
 test('keydown combo modifiers',
 test('keydown combo modifiers',
     html`
     html`
         <div x-data="{ count: 0 }">
         <div x-data="{ count: 0 }">
@@ -493,3 +534,17 @@ test('.dot modifier correctly binds event listener with namespace',
         get('span').should(haveText('baz'))
         get('span').should(haveText('baz'))
     }
     }
 )
 )
+
+test('handles await in handlers with invalid right hand expressions',
+    html`
+        <div x-data="{ text: 'original' }">
+            <button @click="let value = 'new string'; text = await Promise.resolve(value)"></button>
+            <span x-text="text"></span>
+        </div>
+    `,
+    ({ get }) => {
+        get('span').should(haveText('original'))
+        get('button').click()
+        get('span').should(haveText('new string'))
+    }
+)

+ 38 - 0
tests/cypress/integration/directives/x-teleport.spec.js

@@ -19,6 +19,44 @@ test('can use a x-teleport',
     },
     },
 )
 )
 
 
+test('can use a x-teleport.append',
+    [html`
+        <div x-data="{ count: 1 }" id="a">
+            <button @click="count++">Inc</button>
+
+            <template x-teleport.append="#b">
+                <span x-text="count"></span>
+            </template>
+        </div>
+
+        <div id="b"></div>
+    `],
+    ({ get }) => {
+        get('#b + span').should(haveText('1'))
+        get('button').click()
+        get('#b + span').should(haveText('2'))
+    },
+)
+
+test('can use a x-teleport.prepend',
+    [html`
+        <div x-data="{ count: 1 }" id="a">
+            <button @click="count++">Inc</button>
+
+            <template x-teleport.prepend="#b">
+                <span x-text="count"></span>
+            </template>
+        </div>
+
+        <div id="b"></div>
+    `],
+    ({ get }) => {
+        get('#a + span').should(haveText('1'))
+        get('button').click()
+        get('#a + span').should(haveText('2'))
+    },
+)
+
 test('can teleport multiple',
 test('can teleport multiple',
     [html`
     [html`
         <div x-data="{ count: 1 }" id="a">
         <div x-data="{ count: 1 }" id="a">

+ 25 - 0
tests/cypress/integration/plugins/mask.spec.js

@@ -144,6 +144,20 @@ test('$money swapping commas and periods',
     },
     },
 )
 )
 
 
+test('$money with different thousands separator',
+    [html`<input x-data x-mask:function="$money($input, '.', ' ')" />`],
+    ({ get }) => {
+        get('input').type('3000').should(haveValue('3 000'));
+        get('input').type('{backspace}').blur().should(haveValue('300'));
+        get('input').type('5').should(haveValue('3 005'));
+        get('input').type('{selectAll}{backspace}').should(haveValue(''));
+        get('input').type('123').should(haveValue('123'));
+        get('input').type('4').should(haveValue('1 234'));
+        get('input').type('567').should(haveValue('1 234 567'));
+        get('input').type('.89').should(haveValue('1 234 567.89'));
+    }
+);
+
 test('$money works with permenant inserted at beginning',
 test('$money works with permenant inserted at beginning',
     [html`<input x-data x-mask:dynamic="$money">`],
     [html`<input x-data x-mask:dynamic="$money">`],
     ({ get }) => {
     ({ get }) => {
@@ -153,3 +167,14 @@ test('$money works with permenant inserted at beginning',
         get('input').should(haveValue('40.00'))
         get('input').should(haveValue('40.00'))
     }
     }
 )
 )
+
+test('$money mask should remove letters or non numeric characters',
+    [html`<input x-data x-mask:dynamic="$money">`],
+    ({ get }) => {
+        get('input').type('A').should(haveValue(''))
+        get('input').type('ABC').should(haveValue(''))
+        get('input').type('$').should(haveValue(''))
+        get('input').type('/').should(haveValue(''))
+        get('input').type('40').should(haveValue('40'))
+    }
+)

+ 1 - 1
tests/cypress/manual-memory.html

@@ -1,6 +1,6 @@
 <html>
 <html>
     <script src="/../../packages/alpinejs/dist/cdn.js" defer></script>
     <script src="/../../packages/alpinejs/dist/cdn.js" defer></script>
-    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
+    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
 
 
     <table class="w-dull">
     <table class="w-dull">
         <tr>
         <tr>

+ 1 - 1
tests/cypress/manual-transition-test.html

@@ -1,7 +1,7 @@
 <html>
 <html>
     <script src="/../../packages/collapse/dist/cdn.js" defer></script>
     <script src="/../../packages/collapse/dist/cdn.js" defer></script>
     <script src="/../../packages/alpinejs/dist/cdn.js" defer></script>
     <script src="/../../packages/alpinejs/dist/cdn.js" defer></script>
-    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
+    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
 
 
     <table>
     <table>
         <tr>
         <tr>

+ 32 - 1
tests/jest/mask.spec.js

@@ -1,4 +1,4 @@
-let { stripDown } = require('../../packages/mask/dist/module.cjs')
+let { stripDown, formatMoney } = require('../../packages/mask/dist/module.cjs');
 
 
 test('strip-down functionality', async () => {
 test('strip-down functionality', async () => {
     expect(stripDown('(***) ***-****', '7162256108')).toEqual('7162256108')
     expect(stripDown('(***) ***-****', '7162256108')).toEqual('7162256108')
@@ -12,3 +12,34 @@ test('strip-down functionality', async () => {
     expect(stripDown('(999) 999-9999', '(716) 2256108')).toEqual('7162256108')
     expect(stripDown('(999) 999-9999', '(716) 2256108')).toEqual('7162256108')
     expect(stripDown('(999) 999-9999', '(716) 2-25--6108')).toEqual('7162256108')
     expect(stripDown('(999) 999-9999', '(716) 2-25--6108')).toEqual('7162256108')
 })
 })
+
+test('formatMoney functionality', async () => {
+    // Default arguments implicit and explicit
+    expect(formatMoney('123456')).toEqual('123,456');
+    expect(formatMoney('9900900')).toEqual('9,900,900');
+    expect(formatMoney('5600.40')).toEqual('5,600.40');
+    expect(formatMoney('123456', '.')).toEqual('123,456');
+    expect(formatMoney('9900900', '.')).toEqual('9,900,900');
+    expect(formatMoney('5600.40', '.')).toEqual('5,600.40');
+    expect(formatMoney('123456', '.', ',')).toEqual('123,456');
+    expect(formatMoney('9900900', '.', ',')).toEqual('9,900,900');
+    expect(formatMoney('5600.40', '.', ',')).toEqual('5,600.40');
+
+    // Switch decimal separator
+    expect(formatMoney('123456', ',')).toEqual('123.456');
+    expect(formatMoney('9900900', ',')).toEqual('9.900.900');
+    expect(formatMoney('5600.40', ',')).toEqual('5.600,40');
+    expect(formatMoney('123456', '/')).toEqual('123.456');
+    expect(formatMoney('9900900', '/')).toEqual('9.900.900');
+    expect(formatMoney('5600.40', '/')).toEqual('5.600/40');
+
+    // Switch thousands separator
+    expect(formatMoney('123456', '.', ' ')).toEqual('123 456');
+    expect(formatMoney('9900900', '.', ' ')).toEqual('9 900 900');
+    expect(formatMoney('5600.40', '.', ' ')).toEqual('5 600.40');
+
+    // Switch decimal and thousands separator
+    expect(formatMoney('123456', '#', ' ')).toEqual('123 456');
+    expect(formatMoney('9900900', '#', ' ')).toEqual('9 900 900');
+    expect(formatMoney('5600.40', '#', ' ')).toEqual('5 600#40');
+});