Bladeren bron

Merge branch 'feature/ui/combobox' of github.com:alpinejs/alpine into feature/ui/combobox

Caleb Porzio 2 jaren geleden
bovenliggende
commit
95598e996f
50 gewijzigde bestanden met toevoegingen van 1303 en 215 verwijderingen
  1. 3 3
      .github/workflows/run-tests.yml
  2. 111 1
      index.html
  3. 573 40
      package-lock.json
  4. 1 2
      package.json
  5. 1 1
      packages/alpinejs/package.json
  6. 0 2
      packages/alpinejs/src/alpine.js
  7. 1 1
      packages/alpinejs/src/clone.js
  8. 2 16
      packages/alpinejs/src/directives.js
  9. 1 1
      packages/alpinejs/src/directives/x-data.js
  10. 10 2
      packages/alpinejs/src/directives/x-model.js
  11. 1 1
      packages/alpinejs/src/entangle.js
  12. 3 5
      packages/alpinejs/src/evaluator.js
  13. 4 1
      packages/alpinejs/src/scheduler.js
  14. 1 1
      packages/alpinejs/src/utils/on.js
  15. 1 1
      packages/collapse/package.json
  16. 1 1
      packages/docs/package.json
  17. 2 2
      packages/docs/src/en/advanced/csp.md
  18. 1 1
      packages/docs/src/en/advanced/extending.md
  19. 5 5
      packages/docs/src/en/directives/bind.md
  20. 3 3
      packages/docs/src/en/directives/data.md
  21. 11 0
      packages/docs/src/en/directives/model.md
  22. 15 2
      packages/docs/src/en/directives/on.md
  23. 6 6
      packages/docs/src/en/essentials/installation.md
  24. 1 1
      packages/docs/src/en/essentials/lifecycle.md
  25. 14 1
      packages/docs/src/en/plugins/mask.md
  26. 1 1
      packages/focus/package.json
  27. 1 1
      packages/intersect/package.json
  28. 1 1
      packages/mask/package.json
  29. 48 38
      packages/mask/src/index.js
  30. 1 1
      packages/morph/package.json
  31. 1 1
      packages/persist/package.json
  32. 1 1
      packages/ui/package.json
  33. 40 37
      packages/ui/src/combobox.js
  34. 1 1
      packages/ui/src/disclosure.js
  35. 5 0
      packages/ui/src/list-context.js
  36. 1 1
      packages/ui/src/listbox.js
  37. 1 1
      packages/ui/src/menu.js
  38. 1 1
      packages/ui/src/radio.js
  39. 1 1
      packages/ui/src/switch.js
  40. 1 1
      packages/ui/src/tabs.js
  41. 6 17
      scripts/build.js
  42. 31 0
      tests/cypress/integration/clone.spec.js
  43. 37 0
      tests/cypress/integration/directives/x-if.spec.js
  44. 24 0
      tests/cypress/integration/directives/x-model.spec.js
  45. 35 2
      tests/cypress/integration/directives/x-on.spec.js
  46. 2 2
      tests/cypress/integration/entangle.spec.js
  47. 45 0
      tests/cypress/integration/plugins/mask.spec.js
  48. 1 1
      tests/cypress/integration/plugins/navigate.spec.js
  49. 244 5
      tests/cypress/integration/plugins/ui/combobox.spec.js
  50. 1 1
      tests/cypress/integration/plugins/ui/popover.spec.js

+ 3 - 3
.github/workflows/run-tests.yml

@@ -4,10 +4,10 @@ jobs:
   build:
   build:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
-      - uses: actions/checkout@v1
-      - uses: actions/setup-node@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-node@v3
         with:
         with:
-          node-version: '15'
+          node-version: '18'
       - run: npm install
       - run: npm install
       - run: npm run build
       - run: npm run build
       - run: npm run test
       - run: npm run test

+ 111 - 1
index.html

@@ -9,6 +9,113 @@
 <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>
 
 
+<div
+    x-data="{
+        query: '',
+        people: [
+            { id: 1, name: 'Wade Cooper' },
+            { id: 2, name: 'Arlene Mccoy' },
+            { id: 3, name: 'Devon Webb' },
+            { id: 4, name: 'Tom Cook' },
+            { id: 5, name: 'Tanya Fox' },
+            { id: 6, name: 'Hellen Schmidt' },
+            { id: 7, name: 'Caroline Schultz' },
+            { id: 8, name: 'Mason Heaney' },
+            { id: 9, name: 'Claudie Smitham' },
+            { id: 10, name: 'Emil Schaefer' },
+        ],
+        activePersons: [],
+        get queryPerson() {
+            if (! this.query) return null
+
+            return {
+                id: 11, name: this.query,
+            }
+        },
+        onSubmit(e) {
+            e.preventDefault()
+            console.log([...new FormData(e.currentTarget).entries()])
+        },
+        removePerson(person) {
+            this.activePersons = this.activePersons.filter((p) => p !== person)
+        }
+    }"
+    class="flex h-full w-screen justify-center space-x-4 bg-gray-50 p-12"
+>
+    <div class="w-full max-w-4xl">
+        <div class="space-y-1">
+            <form @submit="onSubmit">
+                <div x-combobox x-model="activePersons" name="people" multiple>
+                    <label x-combobox:label class="block text-sm font-medium leading-5 text-gray-700">
+                        Assigned to
+                    </label>
+
+                    <div class="relative">
+                        <div>Query: <span x-text="query"></span></div>
+                        <span class="inline-block w-full rounded-md shadow-sm">
+                            <div class="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-2 pr-10 text-left transition duration-150 ease-in-out focus-within:border-blue-700 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-700 sm:text-sm sm:leading-5">
+                                <span class="block flex flex-wrap gap-2">
+                                    <span x-show="activePersons.length === 0" class="p-0.5">Empty</span>
+                                    <template x-for="person in activePersons" :key="person.id">
+                                        <span class="flex items-center gap-1 rounded bg-blue-50 px-2 py-0.5">
+                                            <span x-text="person.name"></span>
+                                            <svg class="h-4 w-4 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" @click.stop.prevent="removePerson(person)">
+                                                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
+                                            </svg>
+                                        </span>
+                                    </template>
+                                    <input x-combobox:input @change="query = $event.target.value" class="border-none p-0 focus:ring-0" placeholder="Search..." />
+                                </span>
+                                <button x-combobox:button class="absolute inset-y-0 right-0 flex items-center pr-2">
+                                    <svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
+                                        <path d="M7 7l3-3 3 3m0 6l-3 3-3-3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
+                                    </svg>
+                                </button>
+                            </div>
+                        </span>
+
+                        <div class="absolute mt-1 w-full rounded-md bg-white shadow-lg">
+                            <ul x-combobox:options hold class="shadow-xs max-h-60 overflow-auto rounded-md py-1 text-base leading-6 focus:outline-none sm:text-sm sm:leading-5">
+                                <template
+                                    x-for="person in people.filter((person) =>
+                                        person.name.toLowerCase().includes(query.toLowerCase())
+                                    )"
+                                    :key="person.id"
+                                >
+                                    <li x-combobox:option :value="person" class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
+                                        <span x-text="person.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
+                                        </span>
+                                        <span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
+                                            <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                                                <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
+                                            </svg>
+                                        </span>
+                                    </li>
+                                </template>
+
+                                <!-- <template x-if="queryPerson">
+                                    <li x-combobox:option :value="queryPerson" class="relative cursor-default select-none py-2 pl-3 pr-9 focus:outline-none" :class="$comboboxOption.isActive ? 'bg-indigo-600 text-white' : 'text-gray-900'">
+                                        <span x-text="'Create ' + queryPerson.name" class="block truncate" :class="{ 'font-semibold': $comboboxOption.isSelected, 'font-normal': !$comboboxOption.isSelected }">
+                                        </span>
+                                        <span x-show="$comboboxOption.isSelected" class="absolute inset-y-0 right-0 flex items-center pr-4" :class="{ 'text-white': $comboboxOption.isActive, 'text-indigo-600': !$comboboxOption.isActive }">
+                                            <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                                                <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
+                                            </svg>
+                                        </span>
+                                    </li>
+                                </template> -->
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+                <button class="mt-2 inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
+                    Submit
+                </button>
+            </form>
+        </div>
+    </div>
+</div>
+
 <div
 <div
     x-data="{
     x-data="{
         query: '',
         query: '',
@@ -97,7 +204,7 @@
                                 'text-gray-600': ! $comboboxOption.isActive,
                                 'text-gray-600': ! $comboboxOption.isActive,
                                 'opacity-50 cursor-not-allowed': $comboboxOption.isDisabled,
                                 'opacity-50 cursor-not-allowed': $comboboxOption.isDisabled,
                             }"
                             }"
-                            class="flex items-center cursor-default justify-between gap-2 w-full px-4 py-2 text-sm transition-colors"
+                            class="flex items-center cursor-default justify-between gap-2 w-full px-4 py-2 text-sm"
                         >
                         >
                             <span x-text="framework.name"></span>
                             <span x-text="framework.name"></span>
 
 
@@ -111,8 +218,11 @@
         </div>
         </div>
         <div>local selected: <span x-text="selected?.name"></span></div>
         <div>local selected: <span x-text="selected?.name"></span></div>
         <div>internal selected: <span x-text="$combobox.value?.name"></span></div>
         <div>internal selected: <span x-text="$combobox.value?.name"></span></div>
+            <article x-text="$combobox.activeIndex"></article>
     </div>
     </div>
 </div>
 </div>
 
 
 
 
+
+
 </html>
 </html>

+ 573 - 40
package-lock.json

@@ -12,12 +12,11 @@
             },
             },
             "devDependencies": {
             "devDependencies": {
                 "axios": "^0.21.1",
                 "axios": "^0.21.1",
-                "brotli-size": "^4.0.0",
                 "chalk": "^4.1.1",
                 "chalk": "^4.1.1",
                 "cypress": "^7.0.0",
                 "cypress": "^7.0.0",
                 "cypress-plugin-tab": "^1.0.5",
                 "cypress-plugin-tab": "^1.0.5",
                 "dot-json": "^1.2.2",
                 "dot-json": "^1.2.2",
-                "esbuild": "^0.8.39",
+                "esbuild": "~0.16.17",
                 "jest": "^26.6.3"
                 "jest": "^26.6.3"
             }
             }
         },
         },
@@ -644,6 +643,358 @@
                 "ms": "^2.1.1"
                 "ms": "^2.1.1"
             }
             }
         },
         },
+        "node_modules/@esbuild/android-arm": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
+            "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
+            "cpu": [
+                "arm"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "android"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/android-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
+            "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "android"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/android-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
+            "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "android"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/darwin-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
+            "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/darwin-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
+            "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "darwin"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/freebsd-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
+            "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "freebsd"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/freebsd-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
+            "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "freebsd"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-arm": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
+            "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
+            "cpu": [
+                "arm"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
+            "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-ia32": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
+            "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
+            "cpu": [
+                "ia32"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-loong64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
+            "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
+            "cpu": [
+                "loong64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-mips64el": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
+            "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
+            "cpu": [
+                "mips64el"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-ppc64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
+            "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
+            "cpu": [
+                "ppc64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-riscv64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
+            "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
+            "cpu": [
+                "riscv64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-s390x": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
+            "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
+            "cpu": [
+                "s390x"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/linux-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
+            "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "linux"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/netbsd-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
+            "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "netbsd"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/openbsd-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
+            "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "openbsd"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/sunos-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
+            "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "sunos"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/win32-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
+            "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
+            "cpu": [
+                "arm64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/win32-ia32": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
+            "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
+            "cpu": [
+                "ia32"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@esbuild/win32-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
+            "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
+            "cpu": [
+                "x64"
+            ],
+            "dev": true,
+            "optional": true,
+            "os": [
+                "win32"
+            ],
+            "engines": {
+                "node": ">=12"
+            }
+        },
         "node_modules/@istanbuljs/load-nyc-config": {
         "node_modules/@istanbuljs/load-nyc-config": {
             "version": "1.1.0",
             "version": "1.1.0",
             "dev": true,
             "dev": true,
@@ -1486,17 +1837,6 @@
                 "node": ">=8"
                 "node": ">=8"
             }
             }
         },
         },
-        "node_modules/brotli-size": {
-            "version": "4.0.0",
-            "dev": true,
-            "license": "MIT",
-            "dependencies": {
-                "duplexer": "0.1.1"
-            },
-            "engines": {
-                "node": ">= 10.16.0"
-            }
-        },
         "node_modules/browser-process-hrtime": {
         "node_modules/browser-process-hrtime": {
             "version": "1.0.0",
             "version": "1.0.0",
             "dev": true,
             "dev": true,
@@ -2234,10 +2574,6 @@
                 "dot-json": "bin/dot-json.js"
                 "dot-json": "bin/dot-json.js"
             }
             }
         },
         },
-        "node_modules/duplexer": {
-            "version": "0.1.1",
-            "dev": true
-        },
         "node_modules/ecc-jsbn": {
         "node_modules/ecc-jsbn": {
             "version": "0.1.2",
             "version": "0.1.2",
             "dev": true,
             "dev": true,
@@ -2296,12 +2632,40 @@
             }
             }
         },
         },
         "node_modules/esbuild": {
         "node_modules/esbuild": {
-            "version": "0.8.57",
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
+            "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
             "dev": true,
             "dev": true,
             "hasInstallScript": true,
             "hasInstallScript": true,
-            "license": "MIT",
             "bin": {
             "bin": {
                 "esbuild": "bin/esbuild"
                 "esbuild": "bin/esbuild"
+            },
+            "engines": {
+                "node": ">=12"
+            },
+            "optionalDependencies": {
+                "@esbuild/android-arm": "0.16.17",
+                "@esbuild/android-arm64": "0.16.17",
+                "@esbuild/android-x64": "0.16.17",
+                "@esbuild/darwin-arm64": "0.16.17",
+                "@esbuild/darwin-x64": "0.16.17",
+                "@esbuild/freebsd-arm64": "0.16.17",
+                "@esbuild/freebsd-x64": "0.16.17",
+                "@esbuild/linux-arm": "0.16.17",
+                "@esbuild/linux-arm64": "0.16.17",
+                "@esbuild/linux-ia32": "0.16.17",
+                "@esbuild/linux-loong64": "0.16.17",
+                "@esbuild/linux-mips64el": "0.16.17",
+                "@esbuild/linux-ppc64": "0.16.17",
+                "@esbuild/linux-riscv64": "0.16.17",
+                "@esbuild/linux-s390x": "0.16.17",
+                "@esbuild/linux-x64": "0.16.17",
+                "@esbuild/netbsd-x64": "0.16.17",
+                "@esbuild/openbsd-x64": "0.16.17",
+                "@esbuild/sunos-x64": "0.16.17",
+                "@esbuild/win32-arm64": "0.16.17",
+                "@esbuild/win32-ia32": "0.16.17",
+                "@esbuild/win32-x64": "0.16.17"
             }
             }
         },
         },
         "node_modules/escalade": {
         "node_modules/escalade": {
@@ -6682,7 +7046,7 @@
             }
             }
         },
         },
         "packages/alpinejs": {
         "packages/alpinejs": {
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT",
             "license": "MIT",
             "dependencies": {
             "dependencies": {
                 "@vue/reactivity": "~3.1.1"
                 "@vue/reactivity": "~3.1.1"
@@ -6690,7 +7054,7 @@
         },
         },
         "packages/collapse": {
         "packages/collapse": {
             "name": "@alpinejs/collapse",
             "name": "@alpinejs/collapse",
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/csp": {
         "packages/csp": {
@@ -6703,12 +7067,12 @@
         },
         },
         "packages/docs": {
         "packages/docs": {
             "name": "@alpinejs/docs",
             "name": "@alpinejs/docs",
-            "version": "3.10.5-revision.1",
+            "version": "3.11.1-revision.1",
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/focus": {
         "packages/focus": {
             "name": "@alpinejs/focus",
             "name": "@alpinejs/focus",
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT",
             "license": "MIT",
             "dependencies": {
             "dependencies": {
                 "focus-trap": "^6.6.1"
                 "focus-trap": "^6.6.1"
@@ -6724,17 +7088,17 @@
         },
         },
         "packages/intersect": {
         "packages/intersect": {
             "name": "@alpinejs/intersect",
             "name": "@alpinejs/intersect",
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/mask": {
         "packages/mask": {
             "name": "@alpinejs/mask",
             "name": "@alpinejs/mask",
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/morph": {
         "packages/morph": {
             "name": "@alpinejs/morph",
             "name": "@alpinejs/morph",
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/navigate": {
         "packages/navigate": {
@@ -6744,7 +7108,7 @@
         },
         },
         "packages/persist": {
         "packages/persist": {
             "name": "@alpinejs/persist",
             "name": "@alpinejs/persist",
-            "version": "3.10.5",
+            "version": "3.11.1",
             "license": "MIT"
             "license": "MIT"
         },
         },
         "packages/ui": {
         "packages/ui": {
@@ -7186,6 +7550,160 @@
                 }
                 }
             }
             }
         },
         },
+        "@esbuild/android-arm": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz",
+            "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/android-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz",
+            "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/android-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz",
+            "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/darwin-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz",
+            "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/darwin-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz",
+            "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/freebsd-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz",
+            "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/freebsd-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz",
+            "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-arm": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz",
+            "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz",
+            "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-ia32": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz",
+            "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-loong64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz",
+            "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-mips64el": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz",
+            "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-ppc64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz",
+            "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-riscv64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz",
+            "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-s390x": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz",
+            "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/linux-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz",
+            "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/netbsd-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz",
+            "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/openbsd-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz",
+            "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/sunos-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz",
+            "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/win32-arm64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz",
+            "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/win32-ia32": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz",
+            "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==",
+            "dev": true,
+            "optional": true
+        },
+        "@esbuild/win32-x64": {
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz",
+            "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==",
+            "dev": true,
+            "optional": true
+        },
         "@istanbuljs/load-nyc-config": {
         "@istanbuljs/load-nyc-config": {
             "version": "1.1.0",
             "version": "1.1.0",
             "dev": true,
             "dev": true,
@@ -7788,13 +8306,6 @@
                 "fill-range": "^7.0.1"
                 "fill-range": "^7.0.1"
             }
             }
         },
         },
-        "brotli-size": {
-            "version": "4.0.0",
-            "dev": true,
-            "requires": {
-                "duplexer": "0.1.1"
-            }
-        },
         "browser-process-hrtime": {
         "browser-process-hrtime": {
             "version": "1.0.0",
             "version": "1.0.0",
             "dev": true
             "dev": true
@@ -8288,10 +8799,6 @@
                 "underscore-keypath": "~0.0.22"
                 "underscore-keypath": "~0.0.22"
             }
             }
         },
         },
-        "duplexer": {
-            "version": "0.1.1",
-            "dev": true
-        },
         "ecc-jsbn": {
         "ecc-jsbn": {
             "version": "0.1.2",
             "version": "0.1.2",
             "dev": true,
             "dev": true,
@@ -8334,8 +8841,34 @@
             }
             }
         },
         },
         "esbuild": {
         "esbuild": {
-            "version": "0.8.57",
-            "dev": true
+            "version": "0.16.17",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz",
+            "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==",
+            "dev": true,
+            "requires": {
+                "@esbuild/android-arm": "0.16.17",
+                "@esbuild/android-arm64": "0.16.17",
+                "@esbuild/android-x64": "0.16.17",
+                "@esbuild/darwin-arm64": "0.16.17",
+                "@esbuild/darwin-x64": "0.16.17",
+                "@esbuild/freebsd-arm64": "0.16.17",
+                "@esbuild/freebsd-x64": "0.16.17",
+                "@esbuild/linux-arm": "0.16.17",
+                "@esbuild/linux-arm64": "0.16.17",
+                "@esbuild/linux-ia32": "0.16.17",
+                "@esbuild/linux-loong64": "0.16.17",
+                "@esbuild/linux-mips64el": "0.16.17",
+                "@esbuild/linux-ppc64": "0.16.17",
+                "@esbuild/linux-riscv64": "0.16.17",
+                "@esbuild/linux-s390x": "0.16.17",
+                "@esbuild/linux-x64": "0.16.17",
+                "@esbuild/netbsd-x64": "0.16.17",
+                "@esbuild/openbsd-x64": "0.16.17",
+                "@esbuild/sunos-x64": "0.16.17",
+                "@esbuild/win32-arm64": "0.16.17",
+                "@esbuild/win32-ia32": "0.16.17",
+                "@esbuild/win32-x64": "0.16.17"
+            }
         },
         },
         "escalade": {
         "escalade": {
             "version": "3.1.1",
             "version": "3.1.1",

+ 1 - 2
package.json

@@ -5,12 +5,11 @@
     ],
     ],
     "devDependencies": {
     "devDependencies": {
         "axios": "^0.21.1",
         "axios": "^0.21.1",
-        "brotli-size": "^4.0.0",
         "chalk": "^4.1.1",
         "chalk": "^4.1.1",
         "cypress": "^7.0.0",
         "cypress": "^7.0.0",
         "cypress-plugin-tab": "^1.0.5",
         "cypress-plugin-tab": "^1.0.5",
         "dot-json": "^1.2.2",
         "dot-json": "^1.2.2",
-        "esbuild": "^0.8.39",
+        "esbuild": "~0.16.17",
         "jest": "^26.6.3"
         "jest": "^26.6.3"
     },
     },
     "scripts": {
     "scripts": {

+ 1 - 1
packages/alpinejs/package.json

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

+ 0 - 2
packages/alpinejs/src/alpine.js

@@ -11,7 +11,6 @@ import { getBinding as bound, extractProp } from './utils/bind'
 import { debounce } from './utils/debounce'
 import { debounce } from './utils/debounce'
 import { throttle } from './utils/throttle'
 import { throttle } from './utils/throttle'
 import { setStyles } from './utils/styles'
 import { setStyles } from './utils/styles'
-import { entangle } from './entangle'
 import { nextTick } from './nextTick'
 import { nextTick } from './nextTick'
 import { walk } from './utils/walk'
 import { walk } from './utils/walk'
 import { plugin } from './plugin'
 import { plugin } from './plugin'
@@ -53,7 +52,6 @@ let Alpine = {
     setStyles, // INTERNAL
     setStyles, // INTERNAL
     mutateDom,
     mutateDom,
     directive,
     directive,
-    entangle,
     throttle,
     throttle,
     debounce,
     debounce,
     evaluate,
     evaluate,

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

@@ -2,7 +2,7 @@ import { effect, release, overrideEffect } from "./reactivity"
 import { initTree, isRoot } from "./lifecycle"
 import { initTree, isRoot } from "./lifecycle"
 import { walk } from "./utils/walk"
 import { walk } from "./utils/walk"
 
 
-let isCloning = false
+export let isCloning = false
 
 
 export function skipDuringClone(callback, fallback = () => {}) {
 export function skipDuringClone(callback, fallback = () => {}) {
     return (...args) => isCloning ? fallback(...args) : callback(...args)
     return (...args) => isCloning ? fallback(...args) : callback(...args)

+ 2 - 16
packages/alpinejs/src/directives.js

@@ -27,11 +27,8 @@ export function directive(name, callback) {
                 );
                 );
                 return;
                 return;
             }
             }
-            const pos = directiveOrder.indexOf(directive)
-                ?? directiveOrder.indexOf('DEFAULT');
-            if (pos >= 0) {
-                directiveOrder.splice(pos, 0, name);
-            }
+            const pos = directiveOrder.indexOf(directive);
+            directiveOrder.splice(pos >= 0 ? pos : directiveOrder.indexOf('DEFAULT'), 0, name);
         }
         }
     }
     }
 }
 }
@@ -206,20 +203,9 @@ let directiveOrder = [
     'ref',
     'ref',
     'data',
     'data',
     'id',
     'id',
-    // @todo: provide better directive ordering mechanisms so
-    // that I don't have to manually add things like "tabs"
-    // to the order list...
-    'radio',
-    'tabs',
-    'switch',
-    'disclosure',
-    'menu',
-    'listbox',
-    'combobox',
     'bind',
     'bind',
     'init',
     'init',
     'for',
     'for',
-    'mask',
     'model',
     'model',
     'modelable',
     'modelable',
     'transition',
     'transition',

+ 1 - 1
packages/alpinejs/src/directives/x-data.js

@@ -21,7 +21,7 @@ directive('data', skipDuringClone((el, { expression }, { cleanup }) => {
 
 
     let data = evaluate(el, expression, { scope: dataProviderContext })
     let data = evaluate(el, expression, { scope: dataProviderContext })
 
 
-    if (data === undefined) data = {}
+    if (data === undefined || data === true) data = {}
 
 
     injectMagics(data, el)
     injectMagics(data, el)
 
 

+ 10 - 2
packages/alpinejs/src/directives/x-model.js

@@ -5,6 +5,7 @@ import { nextTick } from '../nextTick'
 import bind from '../utils/bind'
 import bind from '../utils/bind'
 import on from '../utils/on'
 import on from '../utils/on'
 import { warn } from '../utils/warn'
 import { warn } from '../utils/warn'
+import { isCloning } from '../clone'
 
 
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     let scopeTarget = el
     let scopeTarget = el
@@ -46,6 +47,10 @@ 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.
         // People might assume we take care of that for them, because
         // People might assume we take care of that for them, because
@@ -62,7 +67,10 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
         || modifiers.includes('lazy')
         || modifiers.includes('lazy')
             ? 'change' : 'input'
             ? 'change' : 'input'
 
 
-    let removeListener = on(el, event, modifiers, (e) => {
+    // We only want to register the event listener when we're not cloning, since the
+    // mutation observer handles initializing the x-model directive already when
+    // the element is inserted into the DOM. Otherwise we register it twice.
+    let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
         setValue(getInputValue(el, modifiers, e, getValue()))
         setValue(getInputValue(el, modifiers, e, getValue()))
     })
     })
 
 
@@ -127,7 +135,7 @@ function getInputValue(el, modifiers, event, currentValue) {
         // 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) {
         if (event instanceof CustomEvent && event.detail !== undefined) {
-            return event.detail || event.target.value
+            return typeof event.detail != 'undefined' ? event.detail : event.target.value
         } else if (el.type === 'checkbox') {
         } 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)) {

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

@@ -2,7 +2,7 @@ import { effect, release } from './reactivity'
 
 
 export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set: innerSet }) {
 export function entangle({ get: outerGet, set: outerSet }, { get: innerGet, set: innerSet }) {
     let firstRun = true
     let firstRun = true
-    let outerHash, innerHash
+    let outerHash, innerHash, outerHashLatest, innerHashLatest
 
 
     let reference = effect(() => {
     let reference = effect(() => {
         let outer, inner
         let outer, inner

+ 3 - 5
packages/alpinejs/src/evaluator.js

@@ -41,11 +41,9 @@ export function normalEvaluator(el, expression) {
 
 
     let dataStack = [overriddenMagics, ...closestDataStack(el)]
     let dataStack = [overriddenMagics, ...closestDataStack(el)]
 
 
-    if (typeof expression === 'function') {
-        return generateEvaluatorFromFunction(dataStack, expression)
-    }
-
-    let evaluator = generateEvaluatorFromString(dataStack, expression, el)
+    let evaluator = (typeof expression === 'function')
+        ? generateEvaluatorFromFunction(dataStack, expression)
+        : generateEvaluatorFromString(dataStack, expression, el)
 
 
     return evaluator
     return evaluator
     return tryCatch.bind(null, el, expression, evaluator)
     return tryCatch.bind(null, el, expression, evaluator)

+ 4 - 1
packages/alpinejs/src/scheduler.js

@@ -2,6 +2,7 @@
 let flushPending = false
 let flushPending = false
 let flushing = false
 let flushing = false
 let queue = []
 let queue = []
+let lastFlushedIndex = -1
 
 
 export function scheduler (callback) { queueJob(callback) }
 export function scheduler (callback) { queueJob(callback) }
 
 
@@ -13,7 +14,7 @@ function queueJob(job) {
 export function dequeueJob(job) {
 export function dequeueJob(job) {
     let index = queue.indexOf(job)
     let index = queue.indexOf(job)
 
 
-    if (index !== -1) queue.splice(index, 1)
+    if (index !== -1 && index > lastFlushedIndex) queue.splice(index, 1)
 }
 }
 
 
 function queueFlush() {
 function queueFlush() {
@@ -30,9 +31,11 @@ export function flushJobs() {
 
 
     for (let i = 0; i < queue.length; i++) {
     for (let i = 0; i < queue.length; i++) {
         queue[i]()
         queue[i]()
+        lastFlushedIndex = i
     }
     }
 
 
     queue.length = 0
     queue.length = 0
+    lastFlushedIndex = -1
 
 
     flushing = false
     flushing = false
 }
 }

+ 1 - 1
packages/alpinejs/src/utils/on.js

@@ -104,7 +104,7 @@ function isKeyEvent(event) {
 
 
 function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) {
 function isListeningForASpecificKeyThatHasntBeenPressed(e, modifiers) {
     let keyModifiers = modifiers.filter(i => {
     let keyModifiers = modifiers.filter(i => {
-        return ! ['window', 'document', 'prevent', 'stop', 'once'].includes(i)
+        return ! ['window', 'document', 'prevent', 'stop', 'once', 'capture'].includes(i)
     })
     })
 
 
     if (keyModifiers.includes('debounce')) {
     if (keyModifiers.includes('debounce')) {

+ 1 - 1
packages/collapse/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/collapse",
     "name": "@alpinejs/collapse",
-    "version": "3.10.5",
+    "version": "3.12.0",
     "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.10.5-revision.1",
+    "version": "3.12.0-revision.1",
     "description": "The documentation for Alpine",
     "description": "The documentation for Alpine",
     "author": "Caleb Porzio",
     "author": "Caleb Porzio",
     "license": "MIT"
     "license": "MIT"

+ 2 - 2
packages/docs/src/en/advanced/csp.md

@@ -9,12 +9,12 @@ In order for Alpine to be able to execute plain strings from HTML attributes as
 
 
 > Under the hood, Alpine doesn't actually use eval() itself because it's slow and problematic. Instead it uses Function declarations, which are much better, but still violate "unsafe-eval".
 > Under the hood, Alpine doesn't actually use eval() itself because it's slow and problematic. Instead it uses Function declarations, which are much better, but still violate "unsafe-eval".
 
 
-In order to accommodate environments where this CSP is necessary, Alpine offers an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax.
+In order to accommodate environments where this CSP is necessary, Alpine will offer an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax.
 
 
 <a name="installation"></a>
 <a name="installation"></a>
 ## Installation
 ## Installation
 
 
-Like all Alpine extensions, you can include this either via `<script>` tag or module import:
+The CSP build hasn’t been officially released yet. In the meantime, you may [build it from source](https://github.com/alpinejs/alpine/tree/main/packages/csp). Once released, like all Alpine extensions, you will be able to include this either via `<script>` tag or module import:
 
 
 <a name="script-tag"></a>
 <a name="script-tag"></a>
 ### Script tag
 ### Script tag

+ 1 - 1
packages/docs/src/en/advanced/extending.md

@@ -228,7 +228,7 @@ Now if the directive is removed from this element or the element is removed itse
 ### Custom order
 ### Custom order
 
 
 By default, any new directive will run after the majority of the standard ones (with the exception of `x-teleport`). This is usually acceptable but some times you might need to run your custom directive before another specific one.
 By default, any new directive will run after the majority of the standard ones (with the exception of `x-teleport`). This is usually acceptable but some times you might need to run your custom directive before another specific one.
-This can be achieved by chaining the `.before() function to `Alpine.directive()` and specifing which directive needs to run after your custom one.
+This can be achieved by chaining the `.before() function to `Alpine.directive()` and specifying which directive needs to run after your custom one.
  
  
 ```js
 ```js
 Alpine.directive('foo', (el, { value, modifiers, expression }) => {
 Alpine.directive('foo', (el, { value, modifiers, expression }) => {

+ 5 - 5
packages/docs/src/en/directives/bind.md

@@ -11,7 +11,7 @@ For example, here's a component where we will use `x-bind` to set the placeholde
 
 
 ```alpine
 ```alpine
 <div x-data="{ placeholder: 'Type here...' }">
 <div x-data="{ placeholder: 'Type here...' }">
-  <input type="text" x-bind:placeholder="placeholder">
+    <input type="text" x-bind:placeholder="placeholder">
 </div>
 </div>
 ```
 ```
 
 
@@ -33,11 +33,11 @@ Here's a simple example of a simple dropdown toggle, but instead of using `x-sho
 
 
 ```alpine
 ```alpine
 <div x-data="{ open: false }">
 <div x-data="{ open: false }">
-  <button x-on:click="open = ! open">Toggle Dropdown</button>
+    <button x-on:click="open = ! open">Toggle Dropdown</button>
 
 
-  <div :class="open ? '' : 'hidden'">
-    Dropdown Contents...
-  </div>
+    <div :class="open ? '' : 'hidden'">
+        Dropdown Contents...
+    </div>
 </div>
 </div>
 ```
 ```
 
 

+ 3 - 3
packages/docs/src/en/directives/data.md

@@ -86,9 +86,9 @@ Let's refactor our component to use a getter called `isOpen` instead of accessin
 
 
 ```alpine
 ```alpine
 <div x-data="{
 <div x-data="{
-  open: false,
-  get isOpen() { return this.open },
-  toggle() { this.open = ! this.open },
+    open: false,
+    get isOpen() { return this.open },
+    toggle() { this.open = ! this.open },
 }">
 }">
     <button @click="toggle()">Toggle Content</button>
     <button @click="toggle()">Toggle Content</button>
 
 

+ 11 - 0
packages/docs/src/en/directives/model.md

@@ -337,6 +337,17 @@ The default throttle interval is 250 milliseconds, you can easily customize this
 <input type="text" x-model.throttle.500ms="search">
 <input type="text" x-model.throttle.500ms="search">
 ```
 ```
 
 
+<a name="fill"></a>
+### `.fill`
+
+By default, if an input has a value attribute, it is ignored by Alpine and instead, the value of the input is set to the value of the property bound using `x-model`.
+
+But if a bound property is empty, then you can use an input's value attribute to populate the property by adding the `.fill` modifier.
+
+<div x-data="{ message: null }">
+  <input x-model.fill="message" value="This is the default message.">
+</div>
+
 <a name="programmatic access"></a>
 <a name="programmatic access"></a>
 ## Programmatic access
 ## Programmatic access
 
 

+ 15 - 2
packages/docs/src/en/directives/on.md

@@ -100,7 +100,7 @@ Here's an example of a component that dispatches a custom DOM event and listens
 
 
 ```alpine
 ```alpine
 <div x-data @foo="alert('Button Was Clicked!')">
 <div x-data @foo="alert('Button Was Clicked!')">
-	<button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
+    <button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
 </div>
 </div>
 ```
 ```
 
 
@@ -112,7 +112,7 @@ Here's the same component re-written with the `$dispatch` magic property.
 
 
 ```alpine
 ```alpine
 <div x-data @foo="alert('Button Was Clicked!')">
 <div x-data @foo="alert('Button Was Clicked!')">
-  <button @click="$dispatch('foo')">...</button>
+    <button @click="$dispatch('foo')">...</button>
 </div>
 </div>
 ```
 ```
 
 
@@ -298,3 +298,16 @@ If you are listening for touch events, it's important to add `.passive` to your
 ```
 ```
 
 
 [→ Read more about passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
 [→ Read more about passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
+
+### .capture
+
+Add this modifier if you want to execute this listener in the event's capturing phase, e.g. before the event bubbles from the target element up the DOM.
+
+```
+<div @click.capture="console.log('I will log first')">
+    <button @click="console.log('I will log second')"></button>
+</div>
+```
+
+[→ Read more about the capturing and bubbling phase of events](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture)
+

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

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

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

@@ -87,7 +87,7 @@ document.addEventListener('alpine:init', () => {
 <a name="alpine-initialized"></a>
 <a name="alpine-initialized"></a>
 ### `alpine:initialized`
 ### `alpine:initialized`
 
 
-Alpine also offers a hook that you can use to execute code After it's done initializing called `alpine:initialized`:
+Alpine also offers a hook that you can use to execute code AFTER it's done initializing called `alpine:initialized`:
 
 
 ```js
 ```js
 document.addEventListener('alpine:initialized', () => {
 document.addEventListener('alpine:initialized', () => {

+ 14 - 1
packages/docs/src/en/plugins/mask.md

@@ -58,7 +58,7 @@ Alpine.plugin(mask)
 <button :aria-expanded="expanded" @click="expanded = ! expanded" class="text-cyan-600 font-medium underline">
 <button :aria-expanded="expanded" @click="expanded = ! expanded" class="text-cyan-600 font-medium underline">
     <span x-text="expanded ? 'Hide' : 'Show more'">Show</span> <span x-text="expanded ? '↑' : '↓'">↓</span>
     <span x-text="expanded ? 'Hide' : 'Show more'">Show</span> <span x-text="expanded ? '↑' : '↓'">↓</span>
 </button>
 </button>
- </div>
+</div>
 
 
 <a name="x-mask"></a>
 <a name="x-mask"></a>
 
 
@@ -171,3 +171,16 @@ You may also choose to override the thousands separator by supplying a third opt
     <input type="text" x-mask:dynamic="$money($input, '.', ' ')"  placeholder="3 000.00">
     <input type="text" x-mask:dynamic="$money($input, '.', ' ')"  placeholder="3 000.00">
 </div>
 </div>
 <!-- END_VERBATIM -->
 <!-- END_VERBATIM -->
+
+
+You can also override the default precision of 2 digits by using any desired number of digits as the fourth optional argument:
+
+```alpine
+<input x-mask:dynamic="$money($input, '.', ',', 4)">
+```
+
+<!-- START_VERBATIM -->
+<div class="demo" x-data>
+    <input type="text" x-mask:dynamic="$money($input, '.', ',', 4)"  placeholder="0.0001">
+</div>
+<!-- END_VERBATIM -->

+ 1 - 1
packages/focus/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/focus",
     "name": "@alpinejs/focus",
-    "version": "3.10.5",
+    "version": "3.12.0",
     "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": {

+ 1 - 1
packages/intersect/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/intersect",
     "name": "@alpinejs/intersect",
-    "version": "3.10.5",
+    "version": "3.12.0",
     "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.10.5",
+    "version": "3.12.0",
     "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": {

+ 48 - 38
packages/mask/src/index.js

@@ -4,39 +4,44 @@ export default function (Alpine) {
         let templateFn = () => expression
         let templateFn = () => expression
         let lastInputValue = ''
         let lastInputValue = ''
 
 
-        if (['function', 'dynamic'].includes(value)) {
-            // This is an x-mask:function directive.
-
-            let evaluator = evaluateLater(expression)
-
-            effect(() => {
-                templateFn = input => {
-                    let result
-
-                    // We need to prevent "auto-evaluation" of functions like
-                    // x-on expressions do so that we can use them as mask functions.
-                    Alpine.dontAutoEvaluateFunctions(() => {
-                        evaluator(value => {
-                            result = typeof value === 'function' ? value(input) : value
-                        }, { scope: {
-                            // These are "magics" we'll make available to the x-mask:function:
-                            '$input': input,
-                            '$money': formatMoney.bind({ el }),
-                        }})
-                    })
-
-                    return result
-                }
-
-                // Run on initialize which serves a dual purpose:
-                // - Initializing the mask on the input if it has an initial value.
-                // - Running the template function to set up reactivity, so that
-                //   when a dependency inside it changes, the input re-masks.
-                processInputValue(el)
-            })
-        } else {
-            processInputValue(el)
-        }
+        queueMicrotask(() => {
+            if (['function', 'dynamic'].includes(value)) {
+                // This is an x-mask:function directive.
+
+                let evaluator = evaluateLater(expression)
+
+                effect(() => {
+                    templateFn = input => {
+                        let result
+
+                        // We need to prevent "auto-evaluation" of functions like
+                        // x-on expressions do so that we can use them as mask functions.
+                        Alpine.dontAutoEvaluateFunctions(() => {
+                            evaluator(value => {
+                                result = typeof value === 'function' ? value(input) : value
+                            }, { scope: {
+                                // These are "magics" we'll make available to the x-mask:function:
+                                '$input': input,
+                                '$money': formatMoney.bind({ el }),
+                            }})
+                        })
+
+                        return result
+                    }
+
+                    // Run on initialize which serves a dual purpose:
+                    // - Initializing the mask on the input if it has an initial value.
+                    // - Running the template function to set up reactivity, so that
+                    //   when a dependency inside it changes, the input re-masks.
+                    processInputValue(el, false)
+                })
+            } else {
+                processInputValue(el, false)
+            }
+
+            // Override x-model's initial value...
+            if (el._x_model) el._x_model.set(el.value)
+        })
 
 
         el.addEventListener('input', () => processInputValue(el))
         el.addEventListener('input', () => processInputValue(el))
         // Don't "restoreCursorPosition" on "blur", because Safari
         // Don't "restoreCursorPosition" on "blur", because Safari
@@ -56,7 +61,9 @@ export default function (Alpine) {
                 return lastInputValue = el.value
                 return lastInputValue = el.value
             }
             }
 
 
-            let setInput = () => { lastInputValue = el.value = formatInput(input, template) }
+            let setInput = () => {
+                lastInputValue = el.value = formatInput(input, template)
+            }
 
 
             if (shouldRestoreCursor) {
             if (shouldRestoreCursor) {
                 // When an input element's value is set, it moves the cursor to the end
                 // When an input element's value is set, it moves the cursor to the end
@@ -79,7 +86,7 @@ export default function (Alpine) {
 
 
             return rebuiltInput
             return rebuiltInput
         }
         }
-    })
+    }).before('model')
 }
 }
 
 
 export function restoreCursorPosition(el, template, callback) {
 export function restoreCursorPosition(el, template, callback) {
@@ -163,7 +170,8 @@ export function buildUp(template, input) {
     return output
     return output
 }
 }
 
 
-export function formatMoney(input, delimiter = '.', thousands) {
+export function formatMoney(input, delimiter = '.', thousands, precision = 2) {
+    if (input === '-') return '-'
     if (/^\D+$/.test(input)) return '9'
     if (/^\D+$/.test(input)) return '9'
 
 
     thousands = thousands ?? (delimiter === "," ? "." : ",")
     thousands = thousands ?? (delimiter === "," ? "." : ",")
@@ -187,12 +195,14 @@ export function formatMoney(input, delimiter = '.', thousands) {
         return output
         return output
     }
     }
 
 
+    let minus = input.startsWith('-') ? '-' : ''
     let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimiter}]`, 'g'), '')
     let strippedInput = input.replaceAll(new RegExp(`[^0-9\\${delimiter}]`, 'g'), '')
     let template = Array.from({ length: strippedInput.split(delimiter)[0].length }).fill('9').join('')
     let template = Array.from({ length: strippedInput.split(delimiter)[0].length }).fill('9').join('')
 
 
-    template = addThousands(template, thousands)
+    template = `${minus}${addThousands(template, thousands)}`
 
 
-    if (input.includes(delimiter)) template += `${delimiter}99`
+    if (precision > 0 && input.includes(delimiter)) 
+        template += `${delimiter}` + '9'.repeat(precision)
 
 
     queueMicrotask(() => {
     queueMicrotask(() => {
         if (this.el.value.endsWith(delimiter)) return
         if (this.el.value.endsWith(delimiter)) return

+ 1 - 1
packages/morph/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/morph",
     "name": "@alpinejs/morph",
-    "version": "3.10.5",
+    "version": "3.12.0",
     "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": {

+ 1 - 1
packages/persist/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@alpinejs/persist",
     "name": "@alpinejs/persist",
-    "version": "3.10.5",
+    "version": "3.12.0",
     "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.10.5-beta.8",
+    "version": "3.12.0-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": {

+ 40 - 37
packages/ui/src/combobox.js

@@ -8,7 +8,7 @@ export default function (Alpine) {
         else if (directive.value === 'options')      handleOptions(el, Alpine)
         else if (directive.value === 'options')      handleOptions(el, Alpine)
         else if (directive.value === 'option')       handleOption(el, Alpine, directive, evaluate)
         else if (directive.value === 'option')       handleOption(el, Alpine, directive, evaluate)
         else                                         handleRoot(el, Alpine)
         else                                         handleRoot(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('combobox', el => {
     Alpine.magic('combobox', el => {
         let data = Alpine.$data(el)
         let data = Alpine.$data(el)
@@ -96,7 +96,7 @@ function handleRoot(el, Alpine) {
                     this.__nullable = Alpine.extractProp(el, 'nullable', false)
                     this.__nullable = Alpine.extractProp(el, 'nullable', false)
                     this.__compareBy = Alpine.extractProp(el, 'by')
                     this.__compareBy = Alpine.extractProp(el, 'by')
 
 
-                    this.__context = generateContext(this.__isMultiple, 'vertical', () => this.$data.__activateSelectedOrFirst())
+                    this.__context = generateContext(this.__isMultiple, 'vertical', () => this.__activateSelectedOrFirst())
 
 
                     let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
                     let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
 
 
@@ -120,18 +120,18 @@ function handleRoot(el, Alpine) {
                     this.__isTyping = false
                     this.__isTyping = false
                 },
                 },
                 __resetInput() {
                 __resetInput() {
-                    let input = this.$refs['__input']
+                    let input = this.$refs.__input
 
 
                     if (! input) return
                     if (! input) return
 
 
-                    let value = this.$data.__getCurrentValue()
+                    let value = this.__getCurrentValue()
 
 
                     input.value = value
                     input.value = value
                 },
                 },
                 __getCurrentValue() {
                 __getCurrentValue() {
-                    if (! this.$refs['__input']) return ''
+                    if (! this.$refs.__input) return ''
                     if (! this.__value) return ''
                     if (! this.__value) return ''
-                    if (this.$data.__displayValue) return this.$data.__displayValue(this.__value)
+                    if (this.__displayValue) return this.__displayValue(this.__value)
                     if (typeof this.__value === 'string') return this.__value
                     if (typeof this.__value === 'string') return this.__value
                     return ''
                     return ''
                 },
                 },
@@ -139,7 +139,7 @@ function handleRoot(el, Alpine) {
                     if (this.__isOpen) return
                     if (this.__isOpen) return
                     this.__isOpen = true
                     this.__isOpen = true
 
 
-                    let input = this.$refs['__input']
+                    let input = this.$refs.__input
 
 
                     // Make sure we always notify the parent component
                     // Make sure we always notify the parent component
                     // that the starting value is the empty string
                     // that the starting value is the empty string
@@ -161,7 +161,7 @@ function handleRoot(el, Alpine) {
                     // Probably because Alpine adds an extra tick when x-showing for @click.outside
                     // Probably because Alpine adds an extra tick when x-showing for @click.outside
                     let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
                     let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
 
 
-                    nextTick(() => this.$refs['__input'].focus({ preventScroll: true }))
+                    nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
                 },
                 },
                 __close() {
                 __close() {
                     this.__isOpen = false
                     this.__isOpen = false
@@ -171,31 +171,30 @@ function handleRoot(el, Alpine) {
                 __activateSelectedOrFirst(activateSelected = true) {
                 __activateSelectedOrFirst(activateSelected = true) {
                     if (! this.__isOpen) return
                     if (! this.__isOpen) return
 
 
-                    if (this.__context.activeKey) {
-                        this.__context.activateAndScrollToKey(this.__context.activeKey)
-                        return
-                    }
-
                     let firstSelectedValue
                     let firstSelectedValue
 
 
                     if (this.__isMultiple) {
                     if (this.__isMultiple) {
-                        firstSelectedValue = this.__value.find(i => {
-                            return !! this.__context.getItemByValue(i)
-                        })
+                        let activeElement = this.__context.getItemsByValues(this.__value)
+
+                        firstSelectedValue = activeElement.length ? activeElement[0].value : null
                     } else {
                     } else {
                         firstSelectedValue = this.__value
                         firstSelectedValue = this.__value
                     }
                     }
 
 
+                    let firstSelected = null
                     if (activateSelected && firstSelectedValue) {
                     if (activateSelected && firstSelectedValue) {
-                        let firstSelected = this.__context.getItemByValue(firstSelectedValue)
+                        firstSelected = this.__context.getItemByValue(firstSelectedValue)
+                    }
 
 
-                        firstSelected && this.__context.activateAndScrollToKey(firstSelected.key)
-                    } else {
-                        this.__context.activateAndScrollToKey(this.__context.firstKey())
+                    if (firstSelected) {
+                        this.__context.activateAndScrollToKey(firstSelected.key)
+                        return
                     }
                     }
+
+                    this.__context.activateAndScrollToKey(this.__context.firstKey())
                 },
                 },
                 __selectActive() {
                 __selectActive() {
-                    let active = this.$data.__context.getActiveItem()
+                    let active = this.__context.getActiveItem()
                     if (active) this.__toggleSelected(active.value)
                     if (active) this.__toggleSelected(active.value)
                 },
                 },
                 __selectOption(el) {
                 __selectOption(el) {
@@ -252,8 +251,8 @@ function handleRoot(el, Alpine) {
                 && ! this.$refs.__button.contains(e.target)
                 && ! this.$refs.__button.contains(e.target)
                 && ! this.$refs.__options.contains(e.target)
                 && ! this.$refs.__options.contains(e.target)
             ) {
             ) {
-                this.$data.__close()
-                this.$data.__resetInput()
+                this.__close()
+                this.__resetInput()
             }
             }
         }
         }
     })
     })
@@ -308,9 +307,8 @@ function handleInput(el, Alpine) {
 
 
             if (! this.$data.__isMultiple) {
             if (! this.$data.__isMultiple) {
                 this.$data.__close()
                 this.$data.__close()
+                this.$data.__resetInput()
             }
             }
-
-            this.$data.__resetInput()
         },
         },
         '@keydown.escape.prevent'(e) {
         '@keydown.escape.prevent'(e) {
             if (! this.$data.__static) e.stopPropagation()
             if (! this.$data.__static) e.stopPropagation()
@@ -434,11 +432,13 @@ function handleOption(el, Alpine) {
                     let value = Alpine.extractProp(this.$el, 'value')
                     let value = Alpine.extractProp(this.$el, 'value')
                     let disabled = Alpine.extractProp(this.$el, 'disabled', false, false)
                     let disabled = Alpine.extractProp(this.$el, 'disabled', false, false)
 
 
-                    this.$data.__context.registerItem(key, this.$el, value, disabled)
+                    // memoize the context as it's not going to change
+                    // and calling this.$data on mouse action is expensive
+                    this.__context.registerItem(key, this.$el, value, disabled)
                 },
                 },
                 destroy() {
                 destroy() {
-                    this.$data.__context.unregisterItem(this.$el.__optionKey)
-                },
+                    this.__context.unregisterItem(this.$el.__optionKey)
+                }
             }
             }
         },
         },
 
 
@@ -446,24 +446,27 @@ function handleOption(el, Alpine) {
         '@click'() {
         '@click'() {
             if (this.$comboboxOption.isDisabled) return;
             if (this.$comboboxOption.isDisabled) return;
 
 
-            this.$data.__selectOption(this.$el)
+            this.__selectOption(this.$el)
 
 
-            if (! this.$data.__isMultiple) {
-                this.$data.__close()
-                this.$data.__resetInput()
+            if (! this.__isMultiple) {
+                this.__close()
+                this.__resetInput()
             }
             }
 
 
-            this.$nextTick(() => this.$refs['__input'].focus({ preventScroll: true }))
+            this.$nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
+        },
+        '@mouseenter'(e) {
+            this.__context.activateEl(this.$el)
         },
         },
         '@mousemove'(e) {
         '@mousemove'(e) {
-            if (this.$data.__context.isActiveEl(this.$el)) return
+            if (this.__context.isActiveEl(this.$el)) return
 
 
-            this.$data.__context.activateEl(this.$el)
+            this.__context.activateEl(this.$el)
         },
         },
         '@mouseleave'(e) {
         '@mouseleave'(e) {
-            if (this.$data.__hold) return
+            if (this.__hold) return
 
 
-            this.$data.__context.deactivate()
+            this.__context.deactivate()
         },
         },
     })
     })
 }
 }

+ 1 - 1
packages/ui/src/disclosure.js

@@ -4,7 +4,7 @@ export default function (Alpine) {
         if      (! directive.value)            handleRoot(el, Alpine)
         if      (! directive.value)            handleRoot(el, Alpine)
         else if (directive.value === 'panel')  handlePanel(el, Alpine)
         else if (directive.value === 'panel')  handlePanel(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('disclosure', el => {
     Alpine.magic('disclosure', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 5 - 0
packages/ui/src/list-context.js

@@ -54,6 +54,11 @@ export function generateContext(multiple, orientation, activateSelectedOrFirst)
             return this.items.find(i => i.el === el)
             return this.items.find(i => i.el === el)
         },
         },
 
 
+        getItemsByValues(values) {
+            let rawValues = values.map(i => Alpine.raw(i));
+            return this.items.filter(i => rawValues.includes(Alpine.raw(i.value)))
+        },
+
         getActiveItem() {
         getActiveItem() {
             if (! this.hasActive()) return null
             if (! this.hasActive()) return null
 
 

+ 1 - 1
packages/ui/src/listbox.js

@@ -7,7 +7,7 @@ export default function (Alpine) {
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'options') handleOptions(el, Alpine)
         else if (directive.value === 'options') handleOptions(el, Alpine)
         else if (directive.value === 'option') handleOption(el, Alpine)
         else if (directive.value === 'option') handleOption(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('listbox', (el) => {
     Alpine.magic('listbox', (el) => {
         let data = Alpine.$data(el)
         let data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/menu.js

@@ -4,7 +4,7 @@ export default function (Alpine) {
         else if (directive.value === 'items') handleItems(el, Alpine)
         else if (directive.value === 'items') handleItems(el, Alpine)
         else if (directive.value === 'item') handleItem(el, Alpine)
         else if (directive.value === 'item') handleItem(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
         else if (directive.value === 'button') handleButton(el, Alpine)
-    });
+    }).before('bind')
 
 
     Alpine.magic('menuItem', el => {
     Alpine.magic('menuItem', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/radio.js

@@ -5,7 +5,7 @@ export default function (Alpine) {
         else if (directive.value === 'option')      handleOption(el, Alpine)
         else if (directive.value === 'option')      handleOption(el, Alpine)
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('radioOption', el => {
     Alpine.magic('radioOption', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/switch.js

@@ -5,7 +5,7 @@ export default function (Alpine) {
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'label')       handleLabel(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
         else if (directive.value === 'description') handleDescription(el, Alpine)
         else                                        handleRoot(el, Alpine)
         else                                        handleRoot(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('switch', el => {
     Alpine.magic('switch', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 1 - 1
packages/ui/src/tabs.js

@@ -6,7 +6,7 @@ export default function (Alpine) {
         else if (directive.value === 'tab')        handleTab(el, Alpine)
         else if (directive.value === 'tab')        handleTab(el, Alpine)
         else if (directive.value === 'panels')     handlePanels(el, Alpine)
         else if (directive.value === 'panels')     handlePanels(el, Alpine)
         else if (directive.value === 'panel')      handlePanel(el, Alpine)
         else if (directive.value === 'panel')      handlePanel(el, Alpine)
-    })
+    }).before('bind')
 
 
     Alpine.magic('tab', el => {
     Alpine.magic('tab', el => {
         let $data = Alpine.$data(el)
         let $data = Alpine.$data(el)

+ 6 - 17
scripts/build.js

@@ -1,6 +1,6 @@
+let { writeToPackageDotJson, getFromPackageDotJson } = require('./utils');
 let fs = require('fs');
 let fs = require('fs');
-let DotJson = require('dot-json');
-let brotliSize = require('brotli-size');
+let zlib = require('zlib');
 
 
 ([
 ([
     // Packages:
     // Packages:
@@ -37,7 +37,7 @@ function bundleFile(package, file) {
                 outfile: `packages/${package}/dist/${file}`,
                 outfile: `packages/${package}/dist/${file}`,
                 bundle: true,
                 bundle: true,
                 platform: 'browser',
                 platform: 'browser',
-                define: { CDN: true },
+                define: { CDN: 'true' },
             })
             })
 
 
             // Build a minified version.
             // Build a minified version.
@@ -47,7 +47,7 @@ function bundleFile(package, file) {
                 bundle: true,
                 bundle: true,
                 minify: true,
                 minify: true,
                 platform: 'browser',
                 platform: 'browser',
-                define: { CDN: true },
+                define: { CDN: 'true' },
             }).then(() => {
             }).then(() => {
                 outputSize(package, `packages/${package}/dist/${file.replace('.js', '.min.js')}`)
                 outputSize(package, `packages/${package}/dist/${file.replace('.js', '.min.js')}`)
             })
             })
@@ -86,26 +86,15 @@ function build(options) {
     options.define['process.env.NODE_ENV'] = process.argv.includes('--watch') ? `'production'` : `'development'`
     options.define['process.env.NODE_ENV'] = process.argv.includes('--watch') ? `'production'` : `'development'`
 
 
     return require('esbuild').build({
     return require('esbuild').build({
+        logLevel: process.argv.includes('--watch') ? 'info' : 'warning',
         watch: process.argv.includes('--watch'),
         watch: process.argv.includes('--watch'),
         // external: ['alpinejs'],
         // external: ['alpinejs'],
         ...options,
         ...options,
     }).catch(() => process.exit(1))
     }).catch(() => process.exit(1))
 }
 }
 
 
-function writeToPackageDotJson(package, key, value) {
-    let dotJson = new DotJson(`./packages/${package}/package.json`)
-
-    dotJson.set(key, value).save()
-}
-
-function getFromPackageDotJson(package, key) {
-    let dotJson = new DotJson(`./packages/${package}/package.json`)
-
-    return dotJson.get(key)
-}
-
 function outputSize(package, file) {
 function outputSize(package, file) {
-    let size = bytesToSize(brotliSize.sync(fs.readFileSync(file)))
+    let size = bytesToSize(zlib.brotliCompressSync(fs.readFileSync(file)).length)
 
 
     console.log("\x1b[32m", `${package}: ${size}`)
     console.log("\x1b[32m", `${package}: ${size}`)
 }
 }

+ 31 - 0
tests/cypress/integration/clone.spec.js

@@ -97,3 +97,34 @@ test('wont register listeners on clone',
         get('#copy span').should(haveText('1'))
         get('#copy span').should(haveText('1'))
     }
     }
 )
 )
+
+test('wont register extra listeners on x-model on clone',
+    html`
+        <script>
+            document.addEventListener('alpine:initialized', () => {
+                window.original = document.getElementById('original')
+                window.copy = document.getElementById('copy')
+            })
+        </script>
+
+        <button x-data @click="Alpine.clone(original, copy)">click</button>
+
+        <div x-data="{ checks: [] }" id="original">
+            <input type="checkbox" x-model="checks" value="1">
+            <span x-text="checks"></span>
+        </div>
+
+        <div x-data="{ checks: [] }" id="copy">
+            <input type="checkbox" x-model="checks" value="1">
+            <span x-text="checks"></span>
+        </div>
+    `,
+    ({ get }) => {
+        get('#original span').should(haveText(''))
+        get('#copy span').should(haveText(''))
+        get('button').click()
+        get('#copy span').should(haveText(''))
+        get('#copy input').click()
+        get('#copy span').should(haveText('1'))
+    }
+)

+ 37 - 0
tests/cypress/integration/directives/x-if.spec.js

@@ -73,3 +73,40 @@ test('x-if removed dom does not evaluate reactive expressions in dom tree',
         get('span').should(notExist())
         get('span').should(notExist())
     }
     }
 )
 )
+
+// Attempting to skip an already-flushed reactive effect would cause inconsistencies when updating other effects.
+// See https://github.com/alpinejs/alpine/issues/2803 for more details.
+test('x-if removed dom does not attempt skipping already-processed reactive effects in dom tree',
+    html`
+    <div x-data="{
+        isEditing: true,
+        foo: 'random text',
+        stopEditing() {
+          this.foo = '';
+          this.isEditing = false;
+        },
+    }">
+        <button @click="stopEditing">Stop editing</button>
+        <template x-if="isEditing">
+            <div id="div-editing">
+              <h2>Editing</h2>
+              <input id="foo" name="foo" type="text" x-model="foo" />
+            </div>
+        </template>
+
+        <template x-if="!isEditing">
+            <div id="div-not-editing"><h2>Not editing</h2></div>
+        </template>
+
+        <template x-if="!isEditing">
+            <div id="div-also-not-editing"><h2>Also not editing</h2></div>
+        </template>
+    </div>
+    `,
+    ({ get }) => {
+        get('button').click()
+        get('div#div-editing').should(notExist())
+        get('div#div-not-editing').should(exist())
+        get('div#div-also-not-editing').should(exist())
+    }
+)

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

@@ -129,3 +129,27 @@ test('x-model updates value when the form is reset',
         get('span').should(haveText(''))
         get('span').should(haveText(''))
     }
     }
 )
 )
+
+test('x-model with fill modifier takes input value on null or empty string',
+    html`
+    <div x-data="{ a: 123, b: 0, c: '', d: null }">
+      <input x-model.fill="a" value="123456" />
+      <span id="a" x-text="a"></span>
+      <input x-model.fill="b" value="123456" />
+      <span id="b" x-text="b"></span>
+      <input x-model.fill="c" value="123456" />
+      <span id="c" x-text="c"></span>
+      <input x-model.fill="d" value="123456" />
+      <span id="d" x-text="d"></span>
+    </div>
+    `,
+    ({ get }) => {
+        get('#a').should(haveText('123'))
+        get('#b').should(haveText('0'))
+        get('#c').should(haveText('123456'))
+        get('#d').should(haveText('123456'))
+    }
+)
+
+
+

+ 35 - 2
tests/cypress/integration/directives/x-on.spec.js

@@ -99,8 +99,8 @@ test('.stop modifier',
 
 
 test('.capture modifier',
 test('.capture modifier',
     html`
     html`
-        <div x-data="{ foo: 'bar' }">
-            <button @click.capture="foo = 'baz'">
+        <div x-data="{ foo: 'bar', count: 0 }">
+            <button @click.capture="count = count + 1; foo = 'baz'">
                 <h1>h1</h1>
                 <h1>h1</h1>
                 <h2 @click="foo = 'bob'">h2</h2>
                 <h2 @click="foo = 'bob'">h2</h2>
             </button>
             </button>
@@ -110,6 +110,39 @@ test('.capture modifier',
         get('div').should(haveData('foo', 'bar'))
         get('div').should(haveData('foo', 'bar'))
         get('h2').click()
         get('h2').click()
         get('div').should(haveData('foo', 'bob'))
         get('div').should(haveData('foo', 'bob'))
+        get('div').should(haveData('count', 1))
+    }
+)
+
+test('.capture modifier with @keyup',
+    html`
+        <div x-data="{ foo: 'bar', count: 0 }">
+            <span @keyup.capture="count = count + 1; foo = 'span'">
+                <input type="text" @keyup="foo = 'input'">
+            </span>
+        </div>
+    `,
+    ({ get }) => {
+        get('div').should(haveData('foo', 'bar'))
+        get('input').type('f')
+        get('div').should(haveData('foo', 'input'))
+        get('div').should(haveData('count', 1))
+    }
+)
+
+test('.capture modifier with @keyup and specified key',
+    html`
+        <div x-data="{ foo: 'bar', count: 0 }">
+            <span @keyup.enter.capture="count = count + 1; foo = 'span'">
+                <input type="text" @keyup.enter="foo = 'input'">
+            </span>
+        </div>
+    `,
+    ({ get }) => {
+        get('div').should(haveData('foo', 'bar'))
+        get('input').type('{enter}')
+        get('div').should(haveData('foo', 'input'))
+        get('div').should(haveData('count', 1))
     }
     }
 )
 )
 
 

+ 2 - 2
tests/cypress/integration/entangle.spec.js

@@ -1,6 +1,6 @@
 import { haveValue, html, test } from '../utils'
 import { haveValue, html, test } from '../utils'
 
 
-test('can entangle to getter/setter pairs',
+test.skip('can entangle to getter/setter pairs',
     [html`
     [html`
     <div x-data="{ outer: 'foo' }">
     <div x-data="{ outer: 'foo' }">
         <input x-model="outer" outer>
         <input x-model="outer" outer>
@@ -33,7 +33,7 @@ test('can entangle to getter/setter pairs',
     }
     }
 )
 )
 
 
-test('can release entanglement',
+test.skip('can release entanglement',
     [html`
     [html`
         <div x-data="{ outer: 'foo' }">
         <div x-data="{ outer: 'foo' }">
             <input x-model="outer" outer>
             <input x-model="outer" outer>

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

@@ -60,6 +60,19 @@ test('x-mask with x-model',
     },
     },
 )
 )
 
 
+test('x-mask with x-model with initial value',
+    [html`
+        <div x-data="{ value: '1234567890' }">
+            <input x-mask="(999) 999-9999" x-model="value" id="1">
+            <input id="2" x-model="value">
+        </div>
+    `],
+    ({ get }) => {
+        get('#1').should(haveValue('(123) 456-7890'))
+        get('#2').should(haveValue('(123) 456-7890'))
+    },
+)
+
 test('x-mask with a falsy input',
 test('x-mask with a falsy input',
     [html`<input x-data x-mask="">`],
     [html`<input x-data x-mask="">`],
     ({ get }) => {
     ({ get }) => {
@@ -178,3 +191,35 @@ test('$money mask should remove letters or non numeric characters',
         get('input').type('40').should(haveValue('40'))
         get('input').type('40').should(haveValue('40'))
     }
     }
 )
 )
+
+test('$money mask negative values',
+    [html`
+        <input id="1" x-data x-mask:dynamic="$money($input)" value="-1234.50" />
+        <input id="2" x-data x-mask:dynamic="$money($input)" />
+    `],
+    ({ get }) => {
+        get('#1').should(haveValue('-1,234.50'))
+        get('#2').type('-12.509').should(haveValue('-12.50'))
+        get('#2').type('{leftArrow}{leftArrow}{leftArrow}-').should(haveValue('-12.50'))
+        get('#2').type('{leftArrow}{leftArrow}{backspace}').should(haveValue('12.50'))
+        get('#2').type('{rightArrow}-').should(haveValue('12.50'))
+        get('#2').type('{rightArrow}-').should(haveValue('12.50'))
+        get('#2').type('{rightArrow}{rightArrow}{rightArrow}-').should(haveValue('12.50'))
+        get('#2').type('{leftArrow}{leftArrow}{leftArrow}{leftArrow}{leftArrow}-').should(haveValue('-12.50'))
+    }
+)
+
+test('$money with custom decimal precision',
+    [html`
+        <input id="0" x-data x-mask:dynamic="$money($input, '.', ',', 0)" />
+        <input id="1" x-data x-mask:dynamic="$money($input, '.', ',', 1)" />
+        <input id="2" x-data x-mask:dynamic="$money($input, '.', ',', 2)" />
+        <input id="3" x-data x-mask:dynamic="$money($input, '.', ',', 3)" />
+    `],
+    ({ get }) => {
+        get('#0').type('1234.5678').should(haveValue('12,345,678'))
+        get('#1').type('1234.5678').should(haveValue('1,234.5'))
+        get('#2').type('1234.5678').should(haveValue('1,234.56'))
+        get('#3').type('1234.5678').should(haveValue('1,234.567'))
+    }
+)

+ 1 - 1
tests/cypress/integration/plugins/navigate.spec.js

@@ -1,6 +1,6 @@
 import { beEqualTo, beVisible, haveAttribute, haveFocus, haveText, html, notBeVisible, test } from '../../utils'
 import { beEqualTo, beVisible, haveAttribute, haveFocus, haveText, html, notBeVisible, test } from '../../utils'
 
 
-// Test persistant peice of layout
+// Test persistent piece of layout
 // Handle non-origin links and such
 // Handle non-origin links and such
 // Handle 404
 // Handle 404
 // Middle/command click link in new tab works?
 // Middle/command click link in new tab works?

+ 244 - 5
tests/cypress/integration/plugins/ui/combobox.spec.js

@@ -1,4 +1,4 @@
-import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, contain, notContain, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test, haveValue} from '../../../utils'
+import { beVisible, beHidden, haveAttribute, haveClasses, notHaveClasses, haveText, contain, notContain, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test, haveValue, haveLength} from '../../../utils'
 
 
 test('it works with x-model',
 test('it works with x-model',
     [html`
     [html`
@@ -439,6 +439,11 @@ test('"multiple" prop',
         get('[option="4"]').click()
         get('[option="4"]').click()
         get('button').should(haveText('2'))
         get('button').should(haveText('2'))
         get('ul').should(beVisible())
         get('ul').should(beVisible())
+        get('input').type('Tom')
+        get('input').type('{enter}')
+        get('button').should(haveText('2,4'))
+        // input field doesn't reset when a new option is selected
+        get('input').should(haveValue('Tom'))
     },
     },
 );
 );
 
 
@@ -811,7 +816,7 @@ test('"static" prop',
     },
     },
 )
 )
 
 
-test('clicking outside the combobox closes it and resets the state',
+test('input reset',
     [html`
     [html`
         <div
         <div
             x-data="{
             x-data="{
@@ -876,17 +881,251 @@ test('clicking outside the combobox closes it and resets the state',
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <article x-text="selected?.name"></article>
+            <article>lorem ipsum</article>
         </div>
         </div>
     `],
     `],
     ({ get }) => {
     ({ get }) => {
+        // Test after closing with button
+        get('button').click()
+        get('input').type('w')
+        get('button').click()
+        get('input').should(haveValue(''))
+
+        // Test correct state after closing with ESC
+        get('button').click()
+        get('input').type('w')
+        get('input').type('{esc}')
+        get('input').should(haveValue(''))
+
+        // Test correct state after closing with TAB
         get('button').click()
         get('button').click()
+        get('input').type('w')
+        get('input').tab()
+        get('input').should(haveValue(''))
+
+        // Test correct state after closing with external click
+        get('button').click()
+        get('input').type('w')
+        get('article').click()
+        get('input').should(haveValue(''))
+
+        // Select something
+        get('button').click()
+        get('ul').should(beVisible())
         get('[option="2"]').click()
         get('[option="2"]').click()
         get('input').should(haveValue('Arlene Mccoy'))
         get('input').should(haveValue('Arlene Mccoy'))
+
+        // Test after closing with button
+        get('button').click()
+        get('input').type('w')
+        get('button').click()
+        get('input').should(haveValue('Arlene Mccoy'))
+
+        // Test correct state after closing with ESC and reopening
+        get('button').click()
+        get('input').type('w')
+        get('input').type('{esc}')
+        get('input').should(haveValue('Arlene Mccoy'))
+
+        // Test correct state after closing with TAB and reopening
+        get('button').click()
+        get('input').type('w')
+        get('input').tab()
+        get('input').should(haveValue('Arlene Mccoy'))
+
+        // Test correct state after closing with external click and reopening
         get('button').click()
         get('button').click()
-        get('input').type('W')
+        get('input').type('w')
         get('article').click()
         get('article').click()
-        get('ul').should(notBeVisible())
         get('input').should(haveValue('Arlene Mccoy'))
         get('input').should(haveValue('Arlene Mccoy'))
     },
     },
 )
 )
+
+test('combobox shows all options when opening',
+    [html`
+        <div
+            x-data="{
+                query: '',
+                selected: null,
+                people: [
+                    { id: 1, name: 'Wade Cooper' },
+                    { id: 2, name: 'Arlene Mccoy' },
+                    { id: 3, name: 'Devon Webb' },
+                    { id: 4, name: 'Tom Cook' },
+                    { id: 5, name: 'Tanya Fox', disabled: true },
+                    { id: 6, name: 'Hellen Schmidt' },
+                    { id: 7, name: 'Caroline Schultz' },
+                    { id: 8, name: 'Mason Heaney' },
+                    { id: 9, name: 'Claudie Smitham' },
+                    { id: 10, name: 'Emil Schaefer' },
+                ],
+                get filteredPeople() {
+                    return this.query === ''
+                        ? this.people
+                        : this.people.filter((person) => {
+                            return person.name.toLowerCase().includes(this.query.toLowerCase())
+                        })
+                },
+            }"
+        >
+            <div x-combobox x-model="selected">
+                <label x-combobox:label>Select person</label>
+
+                <div>
+                    <div>
+                        <input
+                            x-combobox:input
+                            :display-value="person => person.name"
+                            @change="query = $event.target.value"
+                            placeholder="Search..."
+                        />
+
+                        <button x-combobox:button>Toggle</button>
+                    </div>
+
+                    <div x-combobox:options>
+                        <ul>
+                            <template
+                                x-for="person in filteredPeople"
+                                :key="person.id"
+                                hidden
+                            >
+                                <li
+                                    x-combobox:option
+                                    :option="person.id"
+                                    :value="person"
+                                    :disabled="person.disabled"
+                                    x-text="person.name"
+                                >
+                                </li>
+                            </template>
+                        </ul>
+
+                        <p x-show="filteredPeople.length == 0">No people match your query.</p>
+                    </div>
+                </div>
+            </div>
+
+            <article>lorem ipsum</article>
+        </div>
+    `],
+    ({ get }) => {
+        get('button').click()
+        get('li').should(haveLength('10'))
+
+        // Test after closing with button and reopening
+        get('input').type('w').trigger('input')
+        get('li').should(haveLength('2'))
+        get('button').click()
+        get('button').click()
+        get('li').should(haveLength('10'))
+
+        // Test correct state after closing with ESC and reopening
+        get('input').type('w').trigger('input')
+        get('li').should(haveLength('2'))
+        get('input').type('{esc}')
+        get('button').click()
+        get('li').should(haveLength('10'))
+
+        // Test correct state after closing with TAB and reopening
+        get('input').type('w').trigger('input')
+        get('li').should(haveLength('2'))
+        get('input').tab()
+        get('button').click()
+        get('li').should(haveLength('10'))
+
+        // Test correct state after closing with external click and reopening
+        get('input').type('w').trigger('input')
+        get('li').should(haveLength('2'))
+        get('article').click()
+        get('button').click()
+        get('li').should(haveLength('10'))
+    },
+)
+
+test('active element logic when opening a combobox',
+    [html`
+        <div
+            x-data="{
+                query: '',
+                selected: null,
+                people: [
+                    { id: 1, name: 'Wade Cooper' },
+                    { id: 2, name: 'Arlene Mccoy' },
+                    { id: 3, name: 'Devon Webb' },
+                    { id: 4, name: 'Tom Cook' },
+                    { id: 5, name: 'Tanya Fox', disabled: true },
+                    { id: 6, name: 'Hellen Schmidt' },
+                    { id: 7, name: 'Caroline Schultz' },
+                    { id: 8, name: 'Mason Heaney' },
+                    { id: 9, name: 'Claudie Smitham' },
+                    { id: 10, name: 'Emil Schaefer' },
+                ],
+                get filteredPeople() {
+                    return this.query === ''
+                        ? this.people
+                        : this.people.filter((person) => {
+                            return person.name.toLowerCase().includes(this.query.toLowerCase())
+                        })
+                },
+            }"
+        >
+            <div x-combobox x-model="selected">
+                <label x-combobox:label>Select person</label>
+
+                <div>
+                    <div>
+                        <input
+                            x-combobox:input
+                            :display-value="person => person.name"
+                            @change="query = $event.target.value"
+                            placeholder="Search..."
+                        />
+
+                        <button x-combobox:button>Toggle</button>
+                    </div>
+
+                    <div x-combobox:options>
+                        <ul>
+                            <template
+                                x-for="person in filteredPeople"
+                                :key="person.id"
+                                hidden
+                            >
+                                <li
+                                    x-combobox:option
+                                    :option="person.id"
+                                    :value="person"
+                                    :disabled="person.disabled"
+                                    x-text="person.name"
+                                >
+                                </li>
+                            </template>
+                        </ul>
+
+                        <p x-show="filteredPeople.length == 0">No people match your query.</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+    `],
+    ({ get }) => {
+        get('button').click()
+        // First option is selected on opening if no preselection
+        get('ul').should(beVisible())
+        get('[option="1"]').should(haveAttribute('aria-selected', 'true'))
+        // First match is selected while typing
+        get('[option="4"]').should(haveAttribute('aria-selected', 'false'))
+        get('input').type('T')
+        get('input').trigger('change')
+        get('[option="4"]').should(haveAttribute('aria-selected', 'true'))
+        // Reset state and select option 3
+        get('button').click()
+        get('button').click()
+        get('[option="3"]').click()
+        // Previous selection is selected
+        get('button').click()
+        get('[option="4"]').should(haveAttribute('aria-selected', 'false'))
+        get('[option="3"]').should(haveAttribute('aria-selected', 'true'))
+    }
+)

+ 1 - 1
tests/cypress/integration/plugins/ui/popover.spec.js

@@ -156,7 +156,7 @@ test('focusing away doesnt close panel if focusing inside a group',
     },
     },
 )
 )
 
 
-test('focusing away still closes panel inside a group if the focus attribute is present',
+test.retry(5)('focusing away still closes panel inside a group if the focus attribute is present',
     [html`
     [html`
         <div x-data>
         <div x-data>
             <div x-popover:group>
             <div x-popover:group>