浏览代码

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/alpinejs/dist/cdn.js" defer></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 }">
         Value: <span x-text="value"></span>

+ 1 - 1
morph.html

@@ -1,7 +1,7 @@
 <html>
     <script src="./packages/morph/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">
 <!-- Before markup goes here: -->

+ 6 - 0
packages/alpinejs/package.json

@@ -2,6 +2,12 @@
     "name": "alpinejs",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

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

@@ -1,6 +1,7 @@
 import { evaluateLater } from '../evaluator'
 import { directive } from '../directives'
 import { mutateDom } from '../mutation'
+import { nextTick } from '../nextTick'
 import bind from '../utils/bind'
 import on from '../utils/on'
 
@@ -34,6 +35,17 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
 
     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.
     let evaluateSetModel = evaluateLater(el, `${expression} = __placeholder`)
     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 { 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)
 
     let target = document.querySelector(expression)
@@ -31,7 +31,16 @@ directive('teleport', (el, { expression }, { cleanup }) => {
     addScopeToNode(clone, {}, el)
 
     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)
 

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

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

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

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

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

@@ -93,6 +93,8 @@ function isNumeric(subject){
 }
 
 function kebabCase(subject) {
+    if ([' ','_'].includes(subject
+    )) return subject
     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)
     }
 
+    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 (keyModifiers.length === 0) return false
 
@@ -149,8 +156,8 @@ function keyToModifiers(key) {
     let modifierToKeyMap = {
         'ctrl': 'control',
         'slash': '/',
-        'space': '-',
-        'spacebar': '-',
+        'space': ' ',
+        'spacebar': ' ',
         'cmd': 'meta',
         'esc': 'escape',
         'up': 'arrow-up',
@@ -159,6 +166,8 @@ function keyToModifiers(key) {
         'right': 'arrow-right',
         'period': '.',
         'equal': '=',
+        'minus': '-',
+        'underscore': '_',
     }
 
     modifierToKeyMap[key] = key

+ 6 - 0
packages/collapse/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/collapse",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "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
         // users as the .space utility will ignore [hidden] elements.
         // 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.
         if (! el._x_isShown && fullyHide) el.hidden = true
         if (! el._x_isShown) el.style.overflow = 'hidden'
@@ -56,7 +56,7 @@ export default function (Alpine) {
                     start: { height: current+'px' },
                     end: { height: full+'px' },
                 }, () => el._x_isShown = true, () => {
-                    if (el.style.height == `${full}px`) {
+                    if (el.getBoundingClientRect().height == full) {
                         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; }
 ```
 
-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
 <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.
 
+## 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:
 
 ```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`:
 
-* `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>
 ## 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.
+
+<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>
     ...
 
-    <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>
   ...
 </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.
 
 ```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.

+ 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 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 -->
-<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

+ 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 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 -->
-<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
@@ -309,7 +309,7 @@ This plugin offers many smaller utilities for managing focus within a page. Thes
 | Property | Description |
 | ---       | --- |
 | `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 |
 | `focused()`   | Get the currently 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 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 -->
-<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
@@ -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`.
 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
-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
 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.
 
 <a name="installation"></a>
+
 ## Installation
 
 <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 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 -->
-<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
@@ -60,6 +61,7 @@ Alpine.plugin(mask)
  </div>
 
 <a name="x-mask"></a>
+
 ## x-mask
 
 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:
 
-| 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>
+
 ## 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.
@@ -128,6 +131,7 @@ function creditCardMask(input) {
 ```
 
 <a name="money-inputs"></a>
+
 ## 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()`.
@@ -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">
 </div>
 <!-- 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.
 
-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!
 
@@ -41,10 +41,10 @@ You can include the CDN build of this plugin as a `<script>` tag, just make sure
 
 ```alpine
 <!-- 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 -->
-<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

+ 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 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 -->
-<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
@@ -204,4 +204,4 @@ Alpine.data('dropdown', function () {
 Alpine.store('darkMode', {
     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
 <html>
 <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>
 <body>
     <h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1>

+ 6 - 0
packages/focus/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/focus",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/history/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/history",
     "version": "3.0.0-alpha.0",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/intersect/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/intersect",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/mask/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/mask",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

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

@@ -163,9 +163,10 @@ export function buildUp(template, input) {
     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 output = ''
@@ -186,17 +187,17 @@ function formatMoney(input, delimeter = '.', thousands) {
         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)
 
-    if (input.includes(delimeter)) template += `${delimeter}99`
+    if (input.includes(delimiter)) template += `${delimiter}99`
 
     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)
         }
     })

+ 6 - 0
packages/morph/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/morph",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/persist/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/persist",
     "version": "3.10.5",
     "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",
     "license": "MIT",
     "main": "dist/module.cjs.js",

+ 6 - 0
packages/ui/package.json

@@ -2,6 +2,12 @@
     "name": "@alpinejs/ui",
     "version": "3.10.5-beta.8",
     "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",
     "license": "MIT",
     "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)
     if (i === 0) return `${bytes} ${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'))
     }
 )
+
+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',
     html`
         <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',
     html`
         <div x-data="{ count: 0 }">
@@ -493,3 +534,17 @@ test('.dot modifier correctly binds event listener with namespace',
         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',
     [html`
         <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',
     [html`<input x-data x-mask:dynamic="$money">`],
     ({ get }) => {
@@ -153,3 +167,14 @@ test('$money works with permenant inserted at beginning',
         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>
     <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">
         <tr>

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

@@ -1,7 +1,7 @@
 <html>
     <script src="/../../packages/collapse/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>
         <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 () => {
     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) 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');
+});