Browse Source

chore(update with master)

Ryan Chandler 5 years ago
parent
commit
aa047588ba
13 changed files with 691 additions and 163 deletions
  1. 7 2
      README.md
  2. 59 11
      README.ru.md
  3. 479 116
      dist/alpine-ie11.js
  4. 28 11
      dist/alpine.js
  5. 7 7
      package-lock.json
  6. 1 1
      package.json
  7. 15 4
      src/component.js
  8. 10 6
      src/directives/on.js
  9. 7 1
      src/index.js
  10. 2 1
      src/polyfills.js
  11. 2 2
      test/bind.spec.js
  12. 17 0
      test/custom-magic-properties.spec.js
  13. 57 1
      test/on.spec.js

+ 7 - 2
README.md

@@ -97,7 +97,7 @@ You can even use it for non-trivial things:
 
 ## Learn
 
-There are 13 directives available to you:
+There are 14 directives available to you:
 
 | Directive | Description |
 | --- | --- |
@@ -360,6 +360,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.
 
+**`.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**
 **Example:** `<input x-on:input.debounce="fetchSomething()">`
 
@@ -539,7 +544,7 @@ These behave exactly like VueJs's transition directives, except they have differ
 ### `x-spread`
 **Example:**
 ```html
-<div x-data="dropdown">
+<div x-data="dropdown()">
     <button x-spread="trigger">Open Dropdown</button>
 
     <span x-spread="dialogue">Dropdown Contents</span>

+ 59 - 11
README.ru.md

@@ -93,7 +93,7 @@ Alpine.js можно использовать и для более серьез
 
 ## Изучение
 
-Всего в Alpine 13 директив:
+Всего в Alpine 14 директив:
 
 | Директива | Описание |
 | --- | --- |
@@ -109,12 +109,12 @@ Alpine.js можно использовать и для более серьез
 | [`x-if`](#x-if) | При невыполнении переданного условия полностью удаляет элемент из DOM. Должна использоваться в теге `<template>`. |
 | [`x-for`](#x-for) | Создает новые DOM узлы для каждого элемента в массиве. Должна использоваться в теге `<template>`. |
 | [`x-transition`](#x-transition) | Директивы для добавления классов различным стадиям перехода (transition) элемента |
-| [`x-spread`](#x-spread) | Позволяет привязать объект директивы Alpine к элементу для более удобного повторного использования |
+| [`x-spread`](#x-spread) | Позволяет вам привязывать объект с директивами Alpine к элементам, улучшая переиспользуемость. |
 | [`x-cloak`](#x-cloak) | Удаляется при инициализации Alpine. Полезна для скрытия DOM до инициализации. |
 
-И 6 волшебных свойств (magic properties):
+И 6 магических свойств (magic properties):
 
-| Волшебное свойство | Описание |
+| Магическое свойство | Описание |
 | --- | --- |
 | [`$el`](#el) |  Получить DOM-узел корневого компонента. |
 | [`$refs`](#refs) | Получить DOM-элементы компонента, отмеченные `x-ref`. |
@@ -301,6 +301,12 @@ Alpine.js можно использовать и для более серьез
 
 Если в этом выражении меняются какие-либо данные, другие элементы, "привязанные" к этим данным, будут обновлены.
 
+> Замечание: Вы также можете задать имя JS-функции.
+
+**Пример:** `<button x-on:click="myFunction"></button>`
+
+Это равноценно: `<button x-on:click="myFunction($event)"></button>`
+
 **Модификатор `keydown`**
 
 **Пример:** `<input type="text" x-on:keydown.escape="open = false">`
@@ -525,12 +531,12 @@ Alpine предлагает 6 разных transition-директив для д
 ---
 
 ### `x-spread`
-**Example:**
+**Пример:**
 ```html
 <div x-data="dropdown">
-    <button x-spread="trigger">Open Dropdown</button>
+    <button x-spread="trigger">Открыть дропдаун</button>
 
-    <span x-spread="dialogue">Dropdown Contents</span>
+    <span x-spread="dialogue">Содержимое дропдауна</span>
 </div>
 
 <script>
@@ -555,11 +561,11 @@ Alpine предлагает 6 разных transition-директив для д
 </script>
 ```
 
-`x-spread` позволяет извлекать элементы привязок в объекты многоразового использования.
+`x-spread` позволяем вам вынести привязки Alpine из элементов в переиспользуемый объект.
 
-Ключами объекта являются директивы (любые, включая модификаторы), а значения — колбэками, которые будет оценивать Alpine.
+Ключи объекта – это директивы (любые, в том числе и с модификаторами), а значения – колбэки, с которыми будет работать Alpine.
 
-> Примечание: Единственная аномалия с x-spread — при использование с `x-for`. В это случае вы должны вернуть строку нормального выражения из колбэка. Например: `['x-for']() { return 'item in items' }`.
+> Замечание: Единственная особенность при работе с x-spread – это то, как обрабатывается `x-for`. Когда директива, используемая в x-spread – это `x-for`, в колбэке необходимо возвращать выражение в виде строки. К примеру: `['x-for']() { return 'item in items' }`.
 
 ---
 
@@ -574,7 +580,9 @@ Alpine предлагает 6 разных transition-директив для д
 </style>
 ```
 
-### Magic Properties
+### Магические свойства
+
+> Не считая `$el`, магические свойства **не доступны внутри `x-data`**, так как компонент еще не инициализирован.
 
 ---
 
@@ -588,6 +596,12 @@ Alpine предлагает 6 разных transition-директив для д
 
 `$el` – магическое свойство, которое используется для получения корневого компонента DOM-узла.
 
+> Замечание: Свойство $event доступно только в DOM-выражениях.
+
+Если вам нужен доступ к $event внутри JS-функции, вы можете передать его напрямую:
+
+`<button x-on:click="myFunction($event)"></button>`
+
 ### `$refs`
 **Пример:**
 ```html
@@ -619,6 +633,34 @@ Alpine предлагает 6 разных transition-директив для д
 </div>
 ```
 
+**Примечание по распространению событий (event propagation)**
+
+Когда вам нужно перехватить событие, вызванное из узла на том же уровне вложенности, вам нужно использовать модификатор [`.window`](https://github.com/alpinejs/alpine/blob/master/README.ru.md#x-on):
+
+**Неправильный пример:**
+
+```html
+<div x-data>
+    <span @custom-event="console.log($event.detail.foo)"></span>
+    <button @click="$dispatch('custom-event', { foo: 'bar' })">
+<div>
+```
+
+> Это не будет работать, потому что, когда вызывается `custom-event`, он сразу всплывает ([event bubbling](https://en.wikipedia.org/wiki/Event_bubbling)) к родителю `div`.
+
+**Диспатч для компонентов**
+
+Вы также можете использовать предыдущую технику для общения компонентов друг с другом:
+
+**Пример:**
+
+```html
+<div x-data @custom-event.window="console.log($event.detail)"></div>
+
+<button x-data @click="$dispatch('custom-event', 'Hello World!')">
+<!-- При нажатии в консоль выведется "Hello World!". -->
+```
+
 `$dispatch` – это сокращение для создания `CustomEvent` и его вызова (диспатча) с помощью `.dispatchEvent()`. Существует множество сценариев использования передачи данных между компонентами с помощью пользовательских событий. [Пройдите по этой ссылке](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events), чтобы узнать больше о системе, лежащей в основе `CustomEvent` в браузерах.
 
 Любые данные, переданные как второй параметр в `$dispatch('some-event', { some: 'data' })`, становятся доступны через свойство "detail" события: `$event.detail.some`. Добавление событию пользовательских данных через свойство `.detail` – стандартная практика для `CustomEvent` в браузерах. [Подробнее здесь](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail).
@@ -634,6 +676,12 @@ Alpine предлагает 6 разных transition-директив для д
 </div>
 ```
 
+> Замечание: Свойство $dispatch доступно только в DOM-выражениях.
+
+Если вам нужен доступ к $dispatch внутри JS-функции, вы можете передать его напрямую:
+
+`<button x-on:click="myFunction($dispatch)"></button>`
+
 ---
 
 ### `$nextTick`

+ 479 - 116
dist/alpine-ie11.js

@@ -884,60 +884,396 @@
 
   })();
 
-  // Polyfill for creating CustomEvents on IE9/10/11
-
-  // code pulled from:
-  // https://github.com/d4tocchini/customevent-polyfill
-  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill
-
-  (function() {
-    if (typeof window === 'undefined') {
-      return;
-    }
-
-    try {
-      var ce = new window.CustomEvent('test', { cancelable: true });
-      ce.preventDefault();
-      if (ce.defaultPrevented !== true) {
-        // IE has problems with .preventDefault() on custom events
-        // http://stackoverflow.com/questions/23349191
-        throw new Error('Could not prevent default');
-      }
-    } catch (e) {
-      var CustomEvent = function(event, params) {
-        var evt, origPrevent;
-        params = params || {};
-        params.bubbles = !!params.bubbles;
-        params.cancelable = !!params.cancelable;
-
-        evt = document.createEvent('CustomEvent');
-        evt.initCustomEvent(
-          event,
-          params.bubbles,
-          params.cancelable,
-          params.detail
-        );
-        origPrevent = evt.preventDefault;
-        evt.preventDefault = function() {
-          origPrevent.call(this);
-          try {
-            Object.defineProperty(this, 'defaultPrevented', {
-              get: function() {
-                return true;
-              }
-            });
-          } catch (e) {
-            this.defaultPrevented = true;
-          }
-        };
-        return evt;
-      };
+  var ApplyThisPrototype = (function() {
+    return function ApplyThisPrototype(event, target) {
+      if ((typeof target === 'object') && (target !== null)) {
+        var proto = Object.getPrototypeOf(target);
+        var property;
+
+        for (property in proto) {
+          if (!(property in event)) {
+            var descriptor = Object.getOwnPropertyDescriptor(proto, property);
+            if (descriptor) {
+              Object.defineProperty(event, property, descriptor);
+            }
+          }
+        }
+
+        for (property in target) {
+          if (!(property in event)) {
+            event[property] = target[property];
+          }
+        }
+      }
+    }
+  })();
 
-      CustomEvent.prototype = window.Event.prototype;
-      window.CustomEvent = CustomEvent; // expose definition to window
-    }
+  (function(ApplyThisPrototype) {
+    /**
+     * Polyfill CustomEvent
+     */
+    try {
+      var event = new window.CustomEvent('event', { bubbles: true, cancelable: true });
+    } catch (error) {
+      var CustomEventOriginal = window.CustomEvent || window.Event;
+      var CustomEvent = function(eventName, params) {
+        params = params || {};
+        var event = document.createEvent('CustomEvent');
+        event.initCustomEvent(
+          eventName,
+          (params.bubbles === void 0) ? false : params.bubbles,
+          (params.cancelable === void 0) ? false : params.cancelable,
+          (params.detail === void 0) ? {} : params.detail
+        );
+        ApplyThisPrototype(event, this);
+        return event;
+      };
+      CustomEvent.prototype = CustomEventOriginal.prototype;
+      window.CustomEvent = CustomEvent;
+    }
+  })(ApplyThisPrototype);
+
+  var EventListenerInterceptor = (function() {
+
+    if(typeof EventTarget === 'undefined') {
+      window.EventTarget = Node;
+    }
+
+    /**
+     * Event listener interceptor
+     */
+
+    var EventListenerInterceptor = {
+      interceptors: [] // { target: EventTarget, interceptors: [{ add: Function, remove: Function }, ...] }
+    };
+
+
+    /**
+     * Returns if exists a previously registered listener from a target and the normalized arguments
+     * @param target
+     * @param normalizedArguments
+     * @return {*}
+     */
+    EventListenerInterceptor.getRegisteredEventListener = function(target, normalizedArguments) {
+      var key = normalizedArguments.type + '-' + (normalizedArguments.options.capture ? '1' : '0');
+      if(
+        (target.__eventListeners !== void 0) &&
+        (target.__eventListeners[key] !== void 0)
+      ) {
+        var map = target.__eventListeners[key];
+        for(var i = 0; i < map.length; i++) {
+          if(map[i].listener === normalizedArguments.listener) {
+            return map[i];
+          }
+        }
+      }
+      return null;
+    };
+
+    /**
+     * Registers a listener on a target with some options
+     * @param target
+     * @param normalizedArguments
+     */
+    EventListenerInterceptor.registerEventListener = function(target, normalizedArguments) {
+      var key = normalizedArguments.type + '-' + (normalizedArguments.options.capture ? '1' : '0');
+
+      if(target.__eventListeners === void 0) {
+        target.__eventListeners = {};
+      }
+
+      if(target.__eventListeners[key] === void 0) {
+        target.__eventListeners[key] = [];
+      }
+
+      target.__eventListeners[key].push(normalizedArguments);
+    };
+
+    /**
+     * Unregisters a listener on a target with some options
+     * @param target
+     * @param normalizedArguments
+     */
+    EventListenerInterceptor.unregisterEventListener = function(target, normalizedArguments) {
+      var key = normalizedArguments.type + '-' + (normalizedArguments.options.capture ? '1' : '0');
+      if(
+        (target.__eventListeners !==  void 0) &&
+        (target.__eventListeners[key] !== void 0)
+      ) {
+        var map = target.__eventListeners[key];
+        for(var i = 0; i < map.length; i++) {
+          if(map[i].listener === normalizedArguments.listener) {
+            map.splice(i, 1);
+          }
+        }
+
+        if(map.length === 0) {
+          delete target.__eventListeners[key];
+        }
+      }
+    };
+
+
+
+    EventListenerInterceptor.normalizeListenerCallback = function(listener) {
+      if((typeof listener === 'function') || (listener === null) || (listener === void 0)) {
+        return listener;
+      } else if((typeof listener === 'object') && (typeof listener.handleEvent === 'function')) {
+        return listener.handleEvent;
+      } else {
+        // to support Symbol
+        return function(event) {
+          listener(event);
+        };
+      }
+    };
+
+    EventListenerInterceptor.normalizeListenerOptions = function(options) {
+      switch(typeof options) {
+        case 'boolean':
+          options = { capture: options };
+          break;
+        case 'undefined':
+          options = { capture: false };
+          break;
+        case 'object':
+          if (options === null) {
+            options = { capture: false };
+          }
+          break;
+        default:
+          throw new Error('Unsupported options type for addEventListener');
+      }
+
+      options.once      = Boolean(options.once);
+      options.passive   = Boolean(options.passive);
+      options.capture   = Boolean(options.capture);
+
+      return options;
+    };
+
+    EventListenerInterceptor.normalizeListenerArguments = function(type, listener, options) {
+      return {
+        type: type,
+        listener: this.normalizeListenerCallback(listener),
+        options: this.normalizeListenerOptions(options)
+      };
+    };
+
+
+
+    EventListenerInterceptor.intercept = function(target, interceptors) {
+      // get an interceptor with this target or null
+      var interceptor = null;
+      for (var i = 0; i < this.interceptors.length; i++) {
+        if(this.interceptors[i].target === target) {
+          interceptor = this.interceptors[i];
+        }
+      }
+
+      // if no interceptor already set
+      if (interceptor === null) {
+        interceptor = { target: target, interceptors: [interceptors] };
+        this.interceptors.push(interceptor);
+
+        this.interceptAddEventListener(target, interceptor);
+        this.interceptRemoveEventListener(target, interceptor);
+      } else { // if an interceptor already set, simply add interceptors to the list
+        interceptor.interceptors.push(interceptors);
+      }
+
+      // var release = function() {
+      //   target.prototype.addEventListener = addEventListener;
+      //   target.prototype.removeEventListener = removeEventListener;
+      // };
+      // this.interceptors.push(release);
+      // return release;
+    };
+
+    EventListenerInterceptor.interceptAddEventListener = function(target, interceptor) {
+      var _this = this;
+
+      var addEventListener = target.prototype.addEventListener;
+      target.prototype.addEventListener = function(type, listener, options) {
+        var normalizedArguments = _this.normalizeListenerArguments(type, listener, options);
+        var registeredEventListener = _this.getRegisteredEventListener(this, normalizedArguments);
+
+        if (!registeredEventListener) {
+
+          normalizedArguments.polyfilled = {
+            type: normalizedArguments.type,
+            listener: normalizedArguments.listener,
+            options: {
+              capture: normalizedArguments.options.capture,
+              once: normalizedArguments.options.once,
+              passive: normalizedArguments.options.passive
+            }
+          };
+
+          for (var i = 0; i < interceptor.interceptors.length; i++) {
+            var interceptors = interceptor.interceptors[i];
+            if (typeof interceptors.add === 'function') {
+              interceptors.add(normalizedArguments);
+            }
+          }
+
+          // console.log('normalizedArguments', normalizedArguments.polyfilled);
+
+          _this.registerEventListener(this, normalizedArguments);
+
+          addEventListener.call(
+            this,
+            normalizedArguments.polyfilled.type,
+            normalizedArguments.polyfilled.listener,
+            normalizedArguments.polyfilled.options
+          );
+        }
+      };
+
+      return function() {
+        target.prototype.addEventListener = addEventListener;
+      };
+    };
+
+    EventListenerInterceptor.interceptRemoveEventListener = function(target, interceptor) {
+      var _this = this;
+
+      var removeEventListener = target.prototype.removeEventListener;
+      target.prototype.removeEventListener = function(type, listener, options) {
+        var normalizedArguments = _this.normalizeListenerArguments(type, listener, options);
+        var registeredEventListener = _this.getRegisteredEventListener(this, normalizedArguments);
+
+        if (registeredEventListener) {
+          _this.unregisterEventListener(this, normalizedArguments);
+          removeEventListener.call(
+            this,
+            registeredEventListener.polyfilled.type,
+            registeredEventListener.polyfilled.listener,
+            registeredEventListener.polyfilled.options
+          );
+        } else {
+          removeEventListener.call(this, type, listener, options);
+        }
+      };
+
+      return function() {
+        target.prototype.removeEventListener = removeEventListener;
+      };
+    };
+
+    EventListenerInterceptor.interceptAll = function(interceptors) {
+      this.intercept(EventTarget, interceptors);
+      if(!(window instanceof EventTarget)) {
+        this.intercept(Window, interceptors);
+      }
+    };
+
+    EventListenerInterceptor.releaseAll = function() {
+      for(var i = 0, l = this.interceptors.length; i < l; i++) {
+        this.interceptors();
+      }
+    };
+
+
+    EventListenerInterceptor.error = function(error) {
+      // throw error;
+      console.error(error);
+    };
+
+    return EventListenerInterceptor;
   })();
 
+  (function(EventListenerInterceptor) {
+    /**
+     * Event listener options support
+     */
+
+    EventListenerInterceptor.detectSupportedOptions = function() {
+      var _this = this;
+
+      this.supportedOptions = {
+        once: false,
+        passive: false,
+        capture: false,
+
+        all: false,
+        some: false
+      };
+
+      document.createDocumentFragment().addEventListener('test', function() {}, {
+        get once() {
+          _this.supportedOptions.once = true;
+          return false;
+        },
+        get passive() {
+          _this.supportedOptions.passive = true;
+          return false;
+        },
+        get capture() {
+          _this.supportedOptions.capture = true;
+          return false;
+        }
+      });
+
+      // useful shortcuts to detect if options are all/some supported
+      this.supportedOptions.all  = this.supportedOptions.once && this.supportedOptions.passive && this.supportedOptions.capture;
+      this.supportedOptions.some = this.supportedOptions.once || this.supportedOptions.passive || this.supportedOptions.capture;
+    };
+
+    EventListenerInterceptor.polyfillListenerOptions = function() {
+      this.detectSupportedOptions();
+      if (!this.supportedOptions.all) {
+        var _this = this;
+
+        this.interceptAll({
+          add: function(normalizedArguments) {
+            // console.log('intercepted', normalizedArguments);
+
+            var once = normalizedArguments.options.once && !_this.supportedOptions.once;
+            var passive = normalizedArguments.options.passive && !_this.supportedOptions.passive;
+
+            if (once || passive) {
+              var listener = normalizedArguments.polyfilled.listener;
+
+              normalizedArguments.polyfilled.listener = function(event) {
+                if(once) {
+                  this.removeEventListener(normalizedArguments.type, normalizedArguments.listener, normalizedArguments.options);
+                }
+
+                if(passive) {
+                  event.preventDefault = function() {
+                    throw new Error('Unable to preventDefault inside passive event listener invocation.');
+                  };
+                }
+
+                return listener.call(this, event);
+              };
+            }
+
+            if (!_this.supportedOptions.some) {
+              normalizedArguments.polyfilled.options = normalizedArguments.options.capture;
+            }
+          }
+        });
+      }
+    };
+
+
+    EventListenerInterceptor.polyfillListenerOptions();
+
+
+    // var onclick = function() {
+    //   console.log('click');
+    // };
+
+    // document.body.addEventListener('click', onclick, false);
+    // document.body.addEventListener('click', onclick, { once: true });
+    // document.body.addEventListener('click', onclick, { once: true });
+    // document.body.addEventListener('click', onclick, false);
+    // document.body.addEventListener('click', onclick, false);
+
+  })(EventListenerInterceptor);
+
   // For the IE11 build.
   SVGElement.prototype.contains = SVGElement.prototype.contains || HTMLElement.prototype.contains;
 
@@ -3338,6 +3674,46 @@
     }
   });
 
+  var propertyIsEnumerable = objectPropertyIsEnumerable.f;
+
+  // `Object.{ entries, values }` methods implementation
+  var createMethod$4 = function (TO_ENTRIES) {
+    return function (it) {
+      var O = toIndexedObject(it);
+      var keys = objectKeys(O);
+      var length = keys.length;
+      var i = 0;
+      var result = [];
+      var key;
+      while (length > i) {
+        key = keys[i++];
+        if (!descriptors || propertyIsEnumerable.call(O, key)) {
+          result.push(TO_ENTRIES ? [key, O[key]] : O[key]);
+        }
+      }
+      return result;
+    };
+  };
+
+  var objectToArray = {
+    // `Object.entries` method
+    // https://tc39.github.io/ecma262/#sec-object.entries
+    entries: createMethod$4(true),
+    // `Object.values` method
+    // https://tc39.github.io/ecma262/#sec-object.values
+    values: createMethod$4(false)
+  };
+
+  var $entries = objectToArray.entries;
+
+  // `Object.entries` method
+  // https://tc39.github.io/ecma262/#sec-object.entries
+  _export({ target: 'Object', stat: true }, {
+    entries: function entries(O) {
+      return $entries(O);
+    }
+  });
+
   // `SameValue` abstract operation
   // https://tc39.github.io/ecma262/#sec-samevalue
   var sameValue = Object.is || function is(x, y) {
@@ -4671,7 +5047,7 @@
   var rtrim = RegExp(whitespace + whitespace + '*$');
 
   // `String.prototype.{ trim, trimStart, trimEnd, trimLeft, trimRight }` methods implementation
-  var createMethod$4 = function (TYPE) {
+  var createMethod$5 = function (TYPE) {
     return function ($this) {
       var string = String(requireObjectCoercible($this));
       if (TYPE & 1) string = string.replace(ltrim, '');
@@ -4683,13 +5059,13 @@
   var stringTrim = {
     // `String.prototype.{ trimLeft, trimStart }` methods
     // https://tc39.github.io/ecma262/#sec-string.prototype.trimstart
-    start: createMethod$4(1),
+    start: createMethod$5(1),
     // `String.prototype.{ trimRight, trimEnd }` methods
     // https://tc39.github.io/ecma262/#sec-string.prototype.trimend
-    end: createMethod$4(2),
+    end: createMethod$5(2),
     // `String.prototype.trim` method
     // https://tc39.github.io/ecma262/#sec-string.prototype.trim
-    trim: createMethod$4(3)
+    trim: createMethod$5(3)
   };
 
   var getOwnPropertyNames = objectGetOwnPropertyNames.f;
@@ -4760,46 +5136,6 @@
     redefine(global_1, NUMBER, NumberWrapper);
   }
 
-  var propertyIsEnumerable = objectPropertyIsEnumerable.f;
-
-  // `Object.{ entries, values }` methods implementation
-  var createMethod$5 = function (TO_ENTRIES) {
-    return function (it) {
-      var O = toIndexedObject(it);
-      var keys = objectKeys(O);
-      var length = keys.length;
-      var i = 0;
-      var result = [];
-      var key;
-      while (length > i) {
-        key = keys[i++];
-        if (!descriptors || propertyIsEnumerable.call(O, key)) {
-          result.push(TO_ENTRIES ? [key, O[key]] : O[key]);
-        }
-      }
-      return result;
-    };
-  };
-
-  var objectToArray = {
-    // `Object.entries` method
-    // https://tc39.github.io/ecma262/#sec-object.entries
-    entries: createMethod$5(true),
-    // `Object.values` method
-    // https://tc39.github.io/ecma262/#sec-object.values
-    values: createMethod$5(false)
-  };
-
-  var $entries = objectToArray.entries;
-
-  // `Object.entries` method
-  // https://tc39.github.io/ecma262/#sec-object.entries
-  _export({ target: 'Object', stat: true }, {
-    entries: function entries(O) {
-      return $entries(O);
-    }
-  });
-
   var $values = objectToArray.values;
 
   // `Object.values` method
@@ -6322,6 +6658,9 @@
     var _this = this;
 
     var extraVars = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : {};
+    var options = {
+      passive: modifiers.includes('passive')
+    };
 
     if (modifiers.includes('camel')) {
       event = camelCase(event);
@@ -6331,7 +6670,7 @@
       var _handler = function handler(e) {
         _newArrowCheck(this, _this);
 
-        // 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; // Don't do anything if this element isn't currently visible.
 
         if (el.offsetWidth < 1 && el.offsetHeight < 1) return; // Now that we are sure the element is visible, AND the click
@@ -6340,12 +6679,12 @@
         runListenerHandler(component, expression, e, extraVars);
 
         if (modifiers.includes('once')) {
-          document.removeEventListener(event, _handler);
+          document.removeEventListener(event, _handler, options);
         }
       }.bind(this); // Listen for this event at the root level.
 
 
-      document.addEventListener(event, _handler);
+      document.addEventListener(event, _handler, options);
     } else {
       var listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
 
@@ -6356,7 +6695,7 @@
         // has been removed. It's now stale.
         if (listenerTarget === window || listenerTarget === document) {
           if (!document.body.contains(el)) {
-            listenerTarget.removeEventListener(event, _handler2);
+            listenerTarget.removeEventListener(event, _handler2, options);
             return;
           }
         }
@@ -6379,7 +6718,7 @@
             e.preventDefault();
           } else {
             if (modifiers.includes('once')) {
-              listenerTarget.removeEventListener(event, _handler2);
+              listenerTarget.removeEventListener(event, _handler2, options);
             }
           }
         }
@@ -6391,7 +6730,7 @@
         _handler2 = debounce(_handler2, wait);
       }
 
-      listenerTarget.addEventListener(event, _handler2);
+      listenerTarget.addEventListener(event, _handler2, options);
     }
   }
 
@@ -6647,7 +6986,7 @@
     function Component(el) {
       var _this = this;
 
-      var seedDataForCloning = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+      var componentForClone = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
 
       _classCallCheck(this, Component);
 
@@ -6655,7 +6994,7 @@
       var dataAttr = this.$el.getAttribute('x-data');
       var dataExpression = dataAttr === '' ? '{}' : dataAttr;
       var initExpression = this.$el.getAttribute('x-init');
-      this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {
+      this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, {
         $el: this.$el
       });
       /* IE11-ONLY:START */
@@ -6666,6 +7005,11 @@
       this.unobservedData.$refs = null;
       this.unobservedData.$nextTick = null;
       this.unobservedData.$watch = null;
+      Object.keys(Alpine.magicProperties).forEach(function (name) {
+        _newArrowCheck(this, _this);
+
+        this.unobservedData["$".concat(name)] = null;
+      }.bind(this));
       /* IE11-ONLY:END */
       // Construct a Proxy-based observable. This will be used to handle reactivity.
 
@@ -6696,11 +7040,26 @@
         this.watchers[property].push(callback);
       }.bind(this);
 
+      var canonicalComponentElementReference = componentForClone ? componentForClone.$el : this.$el; // Register custom magic properties.
+
+      Object.entries(Alpine.magicProperties).forEach(function (_ref) {
+        _newArrowCheck(this, _this);
+
+        var _ref2 = _slicedToArray(_ref, 2),
+            name = _ref2[0],
+            callback = _ref2[1];
+
+        Object.defineProperty(this.unobservedData, "$".concat(name), {
+          get: function get() {
+            return callback(canonicalComponentElementReference);
+          }
+        });
+      }.bind(this));
       this.showDirectiveStack = [];
       this.showDirectiveLastElement;
       var initReturnedCallback; // If x-init is present AND we aren't cloning (skip x-init on clone)
 
-      if (initExpression && !seedDataForCloning) {
+      if (initExpression && !componentForClone) {
         // We want to allow data manipulation, but not trigger DOM updates just yet.
         // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
         this.pauseReactivity = true;
@@ -6947,13 +7306,13 @@
       value: function registerListeners(el, extraVars) {
         var _this15 = this;
 
-        getXAttrs(el, this).forEach(function (_ref) {
+        getXAttrs(el, this).forEach(function (_ref3) {
           _newArrowCheck(this, _this15);
 
-          var type = _ref.type,
-              value = _ref.value,
-              modifiers = _ref.modifiers,
-              expression = _ref.expression;
+          var type = _ref3.type,
+              value = _ref3.value,
+              modifiers = _ref3.modifiers,
+              expression = _ref3.expression;
 
           switch (type) {
             case 'on':
@@ -6989,15 +7348,15 @@
           }
         }
 
-        attrs.forEach(function (_ref2) {
+        attrs.forEach(function (_ref4) {
           var _this17 = this;
 
           _newArrowCheck(this, _this16);
 
-          var type = _ref2.type,
-              value = _ref2.value,
-              modifiers = _ref2.modifiers,
-              expression = _ref2.expression;
+          var type = _ref4.type,
+              value = _ref4.value,
+              modifiers = _ref4.modifiers,
+              expression = _ref4.expression;
 
           switch (type) {
             case 'model':
@@ -7106,7 +7465,7 @@
               (function () {
                 var _this22 = this;
 
-                var rawData = saferEval(mutations[i].target.getAttribute('x-data'), {
+                var rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', {
                   $el: _this21.$el
                 });
                 Object.keys(rawData).forEach(function (key) {
@@ -7191,6 +7550,7 @@
   var Alpine = {
     version: "2.4.1",
     pauseMutationObserver: false,
+    magicProperties: {},
     start: function () {
       var _start = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
         var _this = this;
@@ -7329,8 +7689,11 @@
     },
     clone: function clone(component, newEl) {
       if (!newEl.__x) {
-        newEl.__x = new Component(newEl, component.getUnobservedData());
+        newEl.__x = new Component(newEl, component);
       }
+    },
+    addMagicProperty: function addMagicProperty(name, callback) {
+      this.magicProperties[name] = callback;
     }
   };
 

+ 28 - 11
dist/alpine.js

@@ -733,13 +733,17 @@
   }
 
   function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
+    const options = {
+      passive: modifiers.includes('passive')
+    };
+
     if (modifiers.includes('camel')) {
       event = camelCase(event);
     }
 
     if (modifiers.includes('away')) {
       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; // Don't do anything if this element isn't currently visible.
 
         if (el.offsetWidth < 1 && el.offsetHeight < 1) return; // Now that we are sure the element is visible, AND the click
@@ -748,12 +752,12 @@
         runListenerHandler(component, expression, e, extraVars);
 
         if (modifiers.includes('once')) {
-          document.removeEventListener(event, handler);
+          document.removeEventListener(event, handler, options);
         }
       }; // Listen for this event at the root level.
 
 
-      document.addEventListener(event, handler);
+      document.addEventListener(event, handler, options);
     } else {
       let listenerTarget = modifiers.includes('window') ? window : modifiers.includes('document') ? document : el;
 
@@ -762,7 +766,7 @@
         // has been removed. It's now stale.
         if (listenerTarget === window || listenerTarget === document) {
           if (!document.body.contains(el)) {
-            listenerTarget.removeEventListener(event, handler);
+            listenerTarget.removeEventListener(event, handler, options);
             return;
           }
         }
@@ -785,7 +789,7 @@
             e.preventDefault();
           } else {
             if (modifiers.includes('once')) {
-              listenerTarget.removeEventListener(event, handler);
+              listenerTarget.removeEventListener(event, handler, options);
             }
           }
         }
@@ -797,7 +801,7 @@
         handler = debounce(handler, wait);
       }
 
-      listenerTarget.addEventListener(event, handler);
+      listenerTarget.addEventListener(event, handler, options);
     }
   }
 
@@ -1314,12 +1318,12 @@
   }
 
   class Component {
-    constructor(el, seedDataForCloning = null) {
+    constructor(el, componentForClone = null) {
       this.$el = el;
       const dataAttr = this.$el.getAttribute('x-data');
       const dataExpression = dataAttr === '' ? '{}' : dataAttr;
       const initExpression = this.$el.getAttribute('x-init');
-      this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {
+      this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, {
         $el: this.$el
       });
       // Construct a Proxy-based observable. This will be used to handle reactivity.
@@ -1347,11 +1351,20 @@
         this.watchers[property].push(callback);
       };
 
+      let canonicalComponentElementReference = componentForClone ? componentForClone.$el : this.$el; // Register custom magic properties.
+
+      Object.entries(Alpine.magicProperties).forEach(([name, callback]) => {
+        Object.defineProperty(this.unobservedData, `$${name}`, {
+          get: function get() {
+            return callback(canonicalComponentElementReference);
+          }
+        });
+      });
       this.showDirectiveStack = [];
       this.showDirectiveLastElement;
       var initReturnedCallback; // If x-init is present AND we aren't cloning (skip x-init on clone)
 
-      if (initExpression && !seedDataForCloning) {
+      if (initExpression && !componentForClone) {
         // We want to allow data manipulation, but not trigger DOM updates just yet.
         // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
         this.pauseReactivity = true;
@@ -1620,7 +1633,7 @@
           if (!(closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue;
 
           if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
-            const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {
+            const rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', {
               $el: this.$el
             });
             Object.keys(rawData).forEach(key => {
@@ -1677,6 +1690,7 @@
   const Alpine = {
     version: "2.4.1",
     pauseMutationObserver: false,
+    magicProperties: {},
     start: async function start() {
       if (!isTesting()) {
         await domReady();
@@ -1750,8 +1764,11 @@
     },
     clone: function clone(component, newEl) {
       if (!newEl.__x) {
-        newEl.__x = new Component(newEl, component.getUnobservedData());
+        newEl.__x = new Component(newEl, component);
       }
+    },
+    addMagicProperty: function addMagicProperty(name, callback) {
+      this.magicProperties[name] = callback;
     }
   };
 

+ 7 - 7
package-lock.json

@@ -1,6 +1,6 @@
 {
     "name": "alpinejs",
-    "version": "2.3.5",
+    "version": "2.4.1",
     "lockfileVersion": 1,
     "requires": true,
     "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": {
             "version": "1.14.1",
             "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -4838,6 +4832,12 @@
             "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
             "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": {
             "version": "0.3.4",
             "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",
         "concurrently": "^5.2.0",
         "core-js": "^3.6.5",
-        "custom-event-polyfill": "^1.0.7",
         "element-closest": "^3.0.2",
         "element-remove": "^1.0.4",
+        "events-polyfill": "^2.1.2",
         "jest": "^25.5.4",
         "jsdom-simulant": "^1.1.2",
         "observable-membrane": "^0.26.1",

+ 15 - 4
src/component.js

@@ -8,16 +8,17 @@ import { handleIfDirective } from './directives/if'
 import { registerModelListener } from './directives/model'
 import { registerListener } from './directives/on'
 import { unwrap, wrap } from './observable'
+import Alpine from './index'
 
 export default class Component {
-    constructor(el, seedDataForCloning = null) {
+    constructor(el, componentForClone = null) {
         this.$el = el
 
         const dataAttr = this.$el.getAttribute('x-data')
         const dataExpression = dataAttr === '' ? '{}' : dataAttr
         const initExpression = this.$el.getAttribute('x-init')
 
-        this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, { $el: this.$el })
+        this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(dataExpression, { $el: this.$el })
 
         /* IE11-ONLY:START */
             // For IE11, add our magic properties to the original data for access.
@@ -26,6 +27,9 @@ export default class Component {
             this.unobservedData.$refs = null
             this.unobservedData.$nextTick = null
             this.unobservedData.$watch = null
+            Object.keys(Alpine.magicProperties).forEach(name => {
+                this.unobservedData[`$${name}`] = null
+            })
         /* IE11-ONLY:END */
 
         // Construct a Proxy-based observable. This will be used to handle reactivity.
@@ -50,12 +54,19 @@ export default class Component {
             this.watchers[property].push(callback)
         }
 
+        let canonicalComponentElementReference = componentForClone ? componentForClone.$el : this.$el
+
+        // Register custom magic properties.
+        Object.entries(Alpine.magicProperties).forEach(([name, callback]) => {
+            Object.defineProperty(this.unobservedData, `$${name}`, { get: function () { return callback(canonicalComponentElementReference) } });
+        })
+
         this.showDirectiveStack = []
         this.showDirectiveLastElement
 
         var initReturnedCallback
         // If x-init is present AND we aren't cloning (skip x-init on clone)
-        if (initExpression && ! seedDataForCloning) {
+        if (initExpression && ! componentForClone) {
             // We want to allow data manipulation, but not trigger DOM updates just yet.
             // We haven't even initialized the elements with their Alpine bindings. I mean c'mon.
             this.pauseReactivity = true
@@ -343,7 +354,7 @@ export default class Component {
                 if (! (closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue
 
                 if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
-                    const rawData = saferEval(mutations[i].target.getAttribute('x-data'), { $el: this.$el })
+                    const rawData = saferEval(mutations[i].target.getAttribute('x-data') || '{}', { $el: this.$el })
 
                     Object.keys(rawData).forEach(key => {
                         if (this.$data[key] !== rawData[key]) {

+ 10 - 6
src/directives/on.js

@@ -1,13 +1,17 @@
 import { kebabCase, camelCase, debounce, isNumeric } from '../utils'
 
 export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
+    const options = {
+        passive: modifiers.includes('passive'),
+    };
+
     if (modifiers.includes('camel')) {
         event = camelCase(event);
     }
 
     if (modifiers.includes('away')) {
         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
 
             // Don't do anything if this element isn't currently visible.
@@ -18,12 +22,12 @@ export function registerListener(component, el, event, modifiers, expression, ex
             runListenerHandler(component, expression, e, extraVars)
 
             if (modifiers.includes('once')) {
-                document.removeEventListener(event, handler)
+                document.removeEventListener(event, handler, options)
             }
         }
 
         // Listen for this event at the root level.
-        document.addEventListener(event, handler)
+        document.addEventListener(event, handler, options)
     } else {
         let listenerTarget = modifiers.includes('window')
             ? window : (modifiers.includes('document') ? document : el)
@@ -33,7 +37,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
             // has been removed. It's now stale.
             if (listenerTarget === window || listenerTarget === document) {
                 if (! document.body.contains(el)) {
-                    listenerTarget.removeEventListener(event, handler)
+                    listenerTarget.removeEventListener(event, handler, options)
                     return
                 }
             }
@@ -57,7 +61,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
                     e.preventDefault()
                 } else {
                     if (modifiers.includes('once')) {
-                        listenerTarget.removeEventListener(event, handler)
+                        listenerTarget.removeEventListener(event, handler, options)
                     }
                 }
             }
@@ -69,7 +73,7 @@ export function registerListener(component, el, event, modifiers, expression, ex
             handler = debounce(handler, wait, this)
         }
 
-        listenerTarget.addEventListener(event, handler)
+        listenerTarget.addEventListener(event, handler, options)
     }
 }
 

+ 7 - 1
src/index.js

@@ -6,6 +6,8 @@ const Alpine = {
 
     pauseMutationObserver: false,
 
+    magicProperties: {},
+
     start: async function () {
         if (! isTesting()) {
             await domReady()
@@ -95,8 +97,12 @@ const Alpine = {
 
     clone: function (component, newEl) {
         if (! newEl.__x) {
-            newEl.__x = new Component(newEl, component.getUnobservedData())
+            newEl.__x = new Component(newEl, component)
         }
+    },
+
+    addMagicProperty: function (name, callback) {
+        this.magicProperties[name] = callback
     }
 }
 

+ 2 - 1
src/polyfills.js

@@ -5,6 +5,7 @@ import "element-closest/browser.js"
 import "element-remove"
 import "classlist-polyfill"
 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

+ 2 - 2
test/bind.spec.js

@@ -112,7 +112,7 @@ test('class attribute bindings are added by object syntax', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
 })
 
-test('multiple classes are added by object syntax', async () => {
+test('multiple classes are removed by object syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{ isOn: false }">
             <span class="foo bar" x-bind:class="{ 'foo bar': isOn }"></span>
@@ -125,7 +125,7 @@ test('multiple classes are added by object syntax', async () => {
     expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
 })
 
-test('multiple classes are removed by object syntax', async () => {
+test('multiple classes are added by object syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{ isOn: true }">
             <span x-bind:class="{ 'foo bar': isOn }"></span>

+ 17 - 0
test/custom-magic-properties.spec.js

@@ -0,0 +1,17 @@
+import Alpine from 'alpinejs'
+
+test('can register custom magic properties', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-text="$foo.bar"></span>
+        </div>
+    `
+
+    Alpine.addMagicProperty('foo', () => {
+        return { bar: 'baz' }
+    })
+
+    Alpine.start()
+
+    expect(document.querySelector('span').innerText).toEqual('baz')
+})

+ 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') })
 })
 
+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 () => {
     document.body.innerHTML = `
@@ -336,6 +360,39 @@ test('click away', async () => {
     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 () => {
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">
@@ -353,7 +410,6 @@ test('supports short syntax', async () => {
     await wait(() => { expect(document.querySelector('span').getAttribute('foo')).toEqual('baz') })
 })
 
-
 test('event with colon', async () => {
     document.body.innerHTML = `
         <div x-data="{ foo: 'bar' }">