Pārlūkot izejas kodu

Merge pull request #619 from HugoDF/feat-passive-listeners

Feature: `.passive` modifier for passive event listeners
Caleb Porzio 5 gadi atpakaļ
vecāks
revīzija
4a71a77b9c
6 mainītis faili ar 81 papildinājumiem un 16 dzēšanām
  1. 5 0
      README.md
  2. 7 7
      package-lock.json
  3. 1 1
      package.json
  4. 9 6
      src/directives/on.js
  5. 2 1
      src/polyfills.js
  6. 57 1
      test/on.spec.js

+ 5 - 0
README.md

@@ -355,6 +355,11 @@ Adding `.window` to an event listener will install the listener on the global wi
 
 
 Adding the `.once` modifier to an event listener will ensure that the listener will only be handled once. This is useful for things you only want to do once, like fetching HTML partials and such.
 Adding the `.once` modifier to an event listener will ensure that the listener will only be handled once. This is useful for things you only want to do once, like fetching HTML partials and such.
 
 
+**`.passive` modifier**
+**Example:** `<button x-on:mousedown.passive="interactive = true"></button>`
+
+Adding the `.passive` modifier to an event listener will make the listener a passive one, which means `preventDefault()` will not work on any events being processed, this can help, for example with scroll performance on touch devices.
+
 **`.debounce` modifier**
 **`.debounce` modifier**
 **Example:** `<input x-on:input.debounce="fetchSomething()">`
 **Example:** `<input x-on:input.debounce="fetchSomething()">`
 
 

+ 7 - 7
package-lock.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "alpinejs",
     "name": "alpinejs",
-    "version": "2.3.5",
+    "version": "2.4.1",
     "lockfileVersion": 1,
     "lockfileVersion": 1,
     "requires": true,
     "requires": true,
     "dependencies": {
     "dependencies": {
@@ -4580,12 +4580,6 @@
                 }
                 }
             }
             }
         },
         },
-        "custom-event-polyfill": {
-            "version": "1.0.7",
-            "resolved": "https://registry.npmjs.org/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz",
-            "integrity": "sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==",
-            "dev": true
-        },
         "dashdash": {
         "dashdash": {
             "version": "1.14.1",
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -4838,6 +4832,12 @@
             "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
             "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
             "dev": true
             "dev": true
         },
         },
+        "events-polyfill": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/events-polyfill/-/events-polyfill-2.1.2.tgz",
+            "integrity": "sha512-vx4kpGzymyD3CEjmg2wTQA6k5e0RhGTkX3ZwfC9m/Ol7+me2tbVuJ0GjSd8eIJxFioubicA0nUL0SIOAyfrgZA==",
+            "dev": true
+        },
         "exec-sh": {
         "exec-sh": {
             "version": "0.3.4",
             "version": "0.3.4",
             "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
             "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",

+ 1 - 1
package.json

@@ -27,9 +27,9 @@
         "classlist-polyfill": "^1.2.0",
         "classlist-polyfill": "^1.2.0",
         "concurrently": "^5.2.0",
         "concurrently": "^5.2.0",
         "core-js": "^3.6.5",
         "core-js": "^3.6.5",
-        "custom-event-polyfill": "^1.0.7",
         "element-closest": "^3.0.2",
         "element-closest": "^3.0.2",
         "element-remove": "^1.0.4",
         "element-remove": "^1.0.4",
+        "events-polyfill": "^2.1.2",
         "jest": "^25.5.4",
         "jest": "^25.5.4",
         "jsdom-simulant": "^1.1.2",
         "jsdom-simulant": "^1.1.2",
         "observable-membrane": "^0.26.1",
         "observable-membrane": "^0.26.1",

+ 9 - 6
src/directives/on.js

@@ -1,9 +1,12 @@
 import { kebabCase, debounce, isNumeric } from '../utils'
 import { kebabCase, debounce, isNumeric } from '../utils'
 
 
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
+    const options = {
+        passive: modifiers.includes('passive'),
+    };
     if (modifiers.includes('away')) {
     if (modifiers.includes('away')) {
         let handler = e => {
         let handler = e => {
-            // Don't do anything if the click came form the element or within it.
+            // Don't do anything if the click came from the element or within it.
             if (el.contains(e.target)) return
             if (el.contains(e.target)) return
 
 
             // Don't do anything if this element isn't currently visible.
             // Don't do anything if this element isn't currently visible.
@@ -14,12 +17,12 @@ export function registerListener(component, el, event, modifiers, expression, ex
             runListenerHandler(component, expression, e, extraVars)
             runListenerHandler(component, expression, e, extraVars)
 
 
             if (modifiers.includes('once')) {
             if (modifiers.includes('once')) {
-                document.removeEventListener(event, handler)
+                document.removeEventListener(event, handler, options)
             }
             }
         }
         }
 
 
         // Listen for this event at the root level.
         // Listen for this event at the root level.
-        document.addEventListener(event, handler)
+        document.addEventListener(event, handler, options)
     } else {
     } else {
         let listenerTarget = modifiers.includes('window')
         let listenerTarget = modifiers.includes('window')
             ? window : (modifiers.includes('document') ? document : el)
             ? window : (modifiers.includes('document') ? document : el)
@@ -29,7 +32,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
             // has been removed. It's now stale.
             // has been removed. It's now stale.
             if (listenerTarget === window || listenerTarget === document) {
             if (listenerTarget === window || listenerTarget === document) {
                 if (! document.body.contains(el)) {
                 if (! document.body.contains(el)) {
-                    listenerTarget.removeEventListener(event, handler)
+                    listenerTarget.removeEventListener(event, handler, options)
                     return
                     return
                 }
                 }
             }
             }
@@ -53,7 +56,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
                     e.preventDefault()
                     e.preventDefault()
                 } else {
                 } else {
                     if (modifiers.includes('once')) {
                     if (modifiers.includes('once')) {
-                        listenerTarget.removeEventListener(event, handler)
+                        listenerTarget.removeEventListener(event, handler, options)
                     }
                     }
                 }
                 }
             }
             }
@@ -65,7 +68,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
             handler = debounce(handler, wait, this)
             handler = debounce(handler, wait, this)
         }
         }
 
 
-        listenerTarget.addEventListener(event, handler)
+        listenerTarget.addEventListener(event, handler, options)
     }
     }
 }
 }
 
 

+ 2 - 1
src/polyfills.js

@@ -5,6 +5,7 @@ import "element-closest/browser.js"
 import "element-remove"
 import "element-remove"
 import "classlist-polyfill"
 import "classlist-polyfill"
 import "@webcomponents/template"
 import "@webcomponents/template"
-import "custom-event-polyfill"
+import "events-polyfill/src/constructors/CustomEvent"
+import "events-polyfill/src/ListenerOptions"
 
 
 SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains
 SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains

+ 57 - 1
test/on.spec.js

@@ -42,6 +42,30 @@ test('nested data modified in event listener updates affected attribute bindings
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 })
 
 
+test('.passive modifier should disable e.preventDefault()', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ defaultPrevented: null }">
+            <button
+                x-on:mousedown.passive="
+                    $event.preventDefault();
+                    defaultPrevented = $event.defaultPrevented;
+                "
+            >
+                <span></span>
+            </button>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(null)
+
+    fireEvent.mouseDown(document.querySelector('button'))
+
+    await wait(() => {
+        expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(false)
+    })
+})
 
 
 test('.stop modifier', async () => {
 test('.stop modifier', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
@@ -336,6 +360,39 @@ test('click away', async () => {
     await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
     await wait(() => { expect(document.querySelector('ul').classList.contains('hidden')).toEqual(false) })
 })
 })
 
 
+test('.passive + .away modifier still disables e.preventDefault()', async () => {
+    // Pretend like all the elements are visible
+    Object.defineProperties(window.HTMLElement.prototype, {
+        offsetHeight: {
+            get: () => 1
+        },
+        offsetWidth: {
+            get: () => 1
+        }
+    });
+    document.body.innerHTML = `
+        <div x-data="{ defaultPrevented: null }">
+            <button
+                x-on:mousedown.away.passive="
+                    $event.preventDefault();
+                    defaultPrevented = $event.defaultPrevented;
+                "
+            ></button>
+            <span></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(null)
+
+    fireEvent.mouseDown(document.querySelector('span'))
+
+    await wait(() => {
+        expect(document.querySelector('div').__x.$data.defaultPrevented).toEqual(false)
+    })
+})
+
 test('supports short syntax', async () => {
 test('supports short syntax', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">
@@ -353,7 +410,6 @@ test('supports short syntax', async () => {
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 })
 
 
-
 test('event with colon', async () => {
 test('event with colon', async () => {
     document.body.innerHTML = `
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
         <div x-data="{ foo: 'bar' }">