Caleb Porzio 5 ani în urmă
părinte
comite
97463b80a8
6 a modificat fișierele cu 398 adăugiri și 59 ștergeri
  1. 270 24
      dist/minimal.js
  2. 1 1
      dist/mix-manifest.json
  3. 26 2
      index.html
  4. 22 0
      link-target/index.html
  5. 34 30
      src/component.js
  6. 45 2
      src/index.js

+ 270 - 24
dist/minimal.js

@@ -96,6 +96,201 @@ return /******/ (function(modules) { // webpackBootstrap
 /************************************************************************/
 /******/ ({
 
+/***/ "./node_modules/process/browser.js":
+/*!*****************************************!*\
+  !*** ./node_modules/process/browser.js ***!
+  \*****************************************/
+/*! no static exports found */
+/***/ (function(module, exports) {
+
+// shim for using process in browser
+var process = module.exports = {};
+
+// cached from whatever global is present so that test runners that stub it
+// don't break things.  But we need to wrap it in a try catch in case it is
+// wrapped in strict mode code which doesn't define any globals.  It's inside a
+// function because try/catches deoptimize in certain engines.
+
+var cachedSetTimeout;
+var cachedClearTimeout;
+
+function defaultSetTimout() {
+    throw new Error('setTimeout has not been defined');
+}
+function defaultClearTimeout () {
+    throw new Error('clearTimeout has not been defined');
+}
+(function () {
+    try {
+        if (typeof setTimeout === 'function') {
+            cachedSetTimeout = setTimeout;
+        } else {
+            cachedSetTimeout = defaultSetTimout;
+        }
+    } catch (e) {
+        cachedSetTimeout = defaultSetTimout;
+    }
+    try {
+        if (typeof clearTimeout === 'function') {
+            cachedClearTimeout = clearTimeout;
+        } else {
+            cachedClearTimeout = defaultClearTimeout;
+        }
+    } catch (e) {
+        cachedClearTimeout = defaultClearTimeout;
+    }
+} ())
+function runTimeout(fun) {
+    if (cachedSetTimeout === setTimeout) {
+        //normal enviroments in sane situations
+        return setTimeout(fun, 0);
+    }
+    // if setTimeout wasn't available but was latter defined
+    if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
+        cachedSetTimeout = setTimeout;
+        return setTimeout(fun, 0);
+    }
+    try {
+        // when when somebody has screwed with setTimeout but no I.E. maddness
+        return cachedSetTimeout(fun, 0);
+    } catch(e){
+        try {
+            // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
+            return cachedSetTimeout.call(null, fun, 0);
+        } catch(e){
+            // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
+            return cachedSetTimeout.call(this, fun, 0);
+        }
+    }
+
+
+}
+function runClearTimeout(marker) {
+    if (cachedClearTimeout === clearTimeout) {
+        //normal enviroments in sane situations
+        return clearTimeout(marker);
+    }
+    // if clearTimeout wasn't available but was latter defined
+    if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
+        cachedClearTimeout = clearTimeout;
+        return clearTimeout(marker);
+    }
+    try {
+        // when when somebody has screwed with setTimeout but no I.E. maddness
+        return cachedClearTimeout(marker);
+    } catch (e){
+        try {
+            // When we are in I.E. but the script has been evaled so I.E. doesn't  trust the global object when called normally
+            return cachedClearTimeout.call(null, marker);
+        } catch (e){
+            // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
+            // Some versions of I.E. have different rules for clearTimeout vs setTimeout
+            return cachedClearTimeout.call(this, marker);
+        }
+    }
+
+
+
+}
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+    if (!draining || !currentQueue) {
+        return;
+    }
+    draining = false;
+    if (currentQueue.length) {
+        queue = currentQueue.concat(queue);
+    } else {
+        queueIndex = -1;
+    }
+    if (queue.length) {
+        drainQueue();
+    }
+}
+
+function drainQueue() {
+    if (draining) {
+        return;
+    }
+    var timeout = runTimeout(cleanUpNextTick);
+    draining = true;
+
+    var len = queue.length;
+    while(len) {
+        currentQueue = queue;
+        queue = [];
+        while (++queueIndex < len) {
+            if (currentQueue) {
+                currentQueue[queueIndex].run();
+            }
+        }
+        queueIndex = -1;
+        len = queue.length;
+    }
+    currentQueue = null;
+    draining = false;
+    runClearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+    var args = new Array(arguments.length - 1);
+    if (arguments.length > 1) {
+        for (var i = 1; i < arguments.length; i++) {
+            args[i - 1] = arguments[i];
+        }
+    }
+    queue.push(new Item(fun, args));
+    if (queue.length === 1 && !draining) {
+        runTimeout(drainQueue);
+    }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+    this.fun = fun;
+    this.array = array;
+}
+Item.prototype.run = function () {
+    this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+process.prependListener = noop;
+process.prependOnceListener = noop;
+
+process.listeners = function (name) { return [] }
+
+process.binding = function (name) {
+    throw new Error('process.binding is not supported');
+};
+
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+    throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
+
+/***/ }),
+
 /***/ "./src/component.js":
 /*!**************************!*\
   !*** ./src/component.js ***!
@@ -159,29 +354,10 @@ function () {
               // If the element we are binding to is a select, a radio, or checkbox
               // we'll listen for the change event instead of the "input" event.
               var event = el.tagName.toLowerCase() === 'select' || ['checkbox', 'radio'].includes(el.type) || modifiers.includes('lazy') ? 'change' : 'input';
-              var rightSideOfExpression = '';
-
-              if (el.type === 'checkbox') {
-                // If the data we are binding to is an array, toggle it's value inside the array.
-                if (Array.isArray(_this.data[expression])) {
-                  rightSideOfExpression = "$event.target.checked ? ".concat(expression, ".concat([$event.target.value]) : [...").concat(expression, ".splice(0, ").concat(expression, ".indexOf($event.target.value)), ...").concat(expression, ".splice(").concat(expression, ".indexOf($event.target.value)+1)]");
-                } else {
-                  rightSideOfExpression = "$event.target.checked";
-                }
-              } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
-                rightSideOfExpression = modifiers.includes('number') ? 'Array.from($event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })' : 'Array.from($event.target.selectedOptions).map(option => { return option.value || option.text })';
-              } else {
-                rightSideOfExpression = modifiers.includes('number') ? 'parseFloat($event.target.value)' : modifiers.includes('trim') ? '$event.target.value.trim()' : '$event.target.value';
-              }
 
-              if (el.type === 'radio') {
-                // Radio buttons only work properly when they share a name attribute.
-                // People might assume we take care of that for them, because
-                // they already set a shared "x-model" attribute.
-                if (!el.hasAttribute('name')) el.setAttribute('name', expression);
-              }
+              var listenerExpression = _this.generateExpressionForXModelListener(el, modifiers, expression);
 
-              _this.registerListener(el, event, modifiers, "".concat(expression, " = ").concat(rightSideOfExpression));
+              _this.registerListener(el, event, modifiers, listenerExpression);
 
               var attrName = 'value';
 
@@ -262,6 +438,33 @@ function () {
         });
       });
     }
+  }, {
+    key: "generateExpressionForXModelListener",
+    value: function generateExpressionForXModelListener(el, modifiers, dataKey) {
+      var rightSideOfExpression = '';
+
+      if (el.type === 'checkbox') {
+        // If the data we are binding to is an array, toggle it's value inside the array.
+        if (Array.isArray(this.data[dataKey])) {
+          rightSideOfExpression = "$event.target.checked ? ".concat(dataKey, ".concat([$event.target.value]) : [...").concat(dataKey, ".splice(0, ").concat(dataKey, ".indexOf($event.target.value)), ...").concat(dataKey, ".splice(").concat(dataKey, ".indexOf($event.target.value)+1)]");
+        } else {
+          rightSideOfExpression = "$event.target.checked";
+        }
+      } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
+        rightSideOfExpression = modifiers.includes('number') ? 'Array.from($event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })' : 'Array.from($event.target.selectedOptions).map(option => { return option.value || option.text })';
+      } else {
+        rightSideOfExpression = modifiers.includes('number') ? 'parseFloat($event.target.value)' : modifiers.includes('trim') ? '$event.target.value.trim()' : '$event.target.value';
+      }
+
+      if (el.type === 'radio') {
+        // Radio buttons only work properly when they share a name attribute.
+        // People might assume we take care of that for them, because
+        // they already set a shared "x-model" attribute.
+        if (!el.hasAttribute('name')) el.setAttribute('name', dataKey);
+      }
+
+      return "".concat(dataKey, " = ").concat(rightSideOfExpression);
+    }
   }, {
     key: "registerListener",
     value: function registerListener(el, event, modifiers, expression) {
@@ -414,16 +617,58 @@ function () {
 
 "use strict";
 __webpack_require__.r(__webpack_exports__);
-/* harmony import */ var _component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./component */ "./src/component.js");
+/* WEBPACK VAR INJECTION */(function(process) {/* harmony import */ var _component__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./component */ "./src/component.js");
 /* @flow */
 
 var minimal = {
   start: function start() {
+    var _this = this;
+
+    this.discoverComponents(); // It's easier and more performant to just support Turbolinks than listen
+    // to MutationOberserver mutations at the document level.
+
+    document.addEventListener("turbolinks:load", function () {
+      _this.discoverComponents();
+    });
+    var targetNode = document.querySelector('body');
+    var observerOptions = {
+      childList: true,
+      attributes: true,
+      subtree: true //Omit or set to false to observe only changes to the parent node.
+
+    };
+    var observer = new MutationObserver(function (mutations) {
+      for (var i = 0; i < mutations.length; i++) {
+        if (mutations[i].addedNodes.length > 0) {
+          mutations[i].addedNodes.forEach(function (node) {
+            if (node.nodeType !== 1) return;
+
+            if (node.matches('[x-data]')) {
+              _this.initializeElement(node);
+            }
+          });
+        }
+      }
+    });
+    observer.observe(targetNode, observerOptions);
+  },
+  discoverComponents: function discoverComponents() {
+    var _this2 = this;
+
     var rootEls = document.querySelectorAll('[x-data]');
     rootEls.forEach(function (rootEl) {
-      // @todo - only set window.component in testing environments
-      window.component = new _component__WEBPACK_IMPORTED_MODULE_0__["default"](rootEl);
+      _this2.initializeElement(rootEl);
     });
+  },
+  initializeElement: function initializeElement(el) {
+    if (process.env.JEST_WORKER_ID) {
+      // This is so the component is accessible to Jest tests.
+      // It's ok to put this in a loop because Jest tests
+      // typically test only 1 component.
+      window.component = new _component__WEBPACK_IMPORTED_MODULE_0__["default"](el);
+    } else {
+      new _component__WEBPACK_IMPORTED_MODULE_0__["default"](el);
+    }
   }
 };
 
@@ -432,6 +677,7 @@ if (!window.minimal) {
 }
 
 /* harmony default export */ __webpack_exports__["default"] = (minimal);
+/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../node_modules/process/browser.js */ "./node_modules/process/browser.js")))
 
 /***/ }),
 

+ 1 - 1
dist/mix-manifest.json

@@ -1,6 +1,6 @@
 {
     "/rename-me.js": "/rename-me.js?id=bbf39d4a044a5ded45f2",
     "/rename-me.min.js": "/rename-me.min.js?id=727c8cae5cc9ff82a386",
-    "/minimal.js": "/minimal.js?id=106753738f90d6cd96e3",
+    "/minimal.js": "/minimal.js?id=8640ed0d65573caa94b9",
     "/minimal.min.js": "/minimal.min.js?id=6f15586b9a24e5201a68"
 }

+ 26 - 2
index.html

@@ -3,8 +3,23 @@
         <style>
             .hidden { display: none; }
         </style>
+
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js" integrity="sha256-iM4Yzi/zLj/IshPWMC1IluRxTtRjMqjPGd97TZ9yYpU=" crossorigin="anonymous"></script>
+        <script src="/dist/minimal.js" defer></script>
+        <script>
+            document.addEventListener('DOMContentLoaded', () => {
+                window.minimal.start()
+            })
+        </script>
     </head>
     <body>
+        <a href="/link-target">turbo-page</a>
+
+        <div>
+            <div id="goHere">
+                some nested thing
+            </div>
+        </div>
         <div x-data="{ foo: ['bar', 'baz'] }">
             <select x-model="foo" multiple>
                 <option disabled value="">Please select one</option>
@@ -45,9 +60,18 @@
             <span class="" x-bind:class="['hey']"></span>
         </div>
 
-        <script src="/dist/minimal.min.js"></script>
         <script>
-            window.minimal.start()
+            const thing = document.querySelector('#goHere')
+            const handler = (e) => {
+                thing.removeEventListener('click', handler)
+
+                const div = document.createElement('div')
+                div.setAttribute('x-data', '{hey: "there"}')
+                div.innerHTML = '<input type="text" x-model="hey"><h1 x-text="hey"></h1>'
+                e.target.appendChild(div)
+            }
+
+            var listener = thing.addEventListener('click', handler)
         </script>
     </body>
 </html>

+ 22 - 0
link-target/index.html

@@ -0,0 +1,22 @@
+<html>
+    <head>
+        <style>
+            .hidden { display: none; }
+        </style>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js" integrity="sha256-iM4Yzi/zLj/IshPWMC1IluRxTtRjMqjPGd97TZ9yYpU=" crossorigin="anonymous"></script>
+        <script src="/dist/minimal.js" defer></script>
+        <script>
+            document.addEventListener('DOMContentLoaded', () => {
+                window.minimal.start()
+            })
+        </script>
+    </head>
+    <body>
+        <div>hey</div>
+
+        <div x-data="{ lol: 'hey' }">
+            <input type="text" x-model="lol">
+            <span x-text="lol"></span>
+        </div>
+    </body>
+</html>

+ 34 - 30
src/component.js

@@ -23,39 +23,14 @@ export default class Component {
                     case 'model':
                         // If the element we are binding to is a select, a radio, or checkbox
                         // we'll listen for the change event instead of the "input" event.
-                        var event = (
-                            el.tagName.toLowerCase() === 'select')
-                            || (['checkbox', 'radio'].includes(el.type)
+                        var event = (el.tagName.toLowerCase() === 'select')
+                            || ['checkbox', 'radio'].includes(el.type)
                             || modifiers.includes('lazy')
-                            )
-                                ? 'change' : 'input'
-
-                        var rightSideOfExpression = ''
-                        if (el.type === 'checkbox') {
-                            // If the data we are binding to is an array, toggle it's value inside the array.
-                            if (Array.isArray(this.data[expression])) {
-                                rightSideOfExpression = `$event.target.checked ? ${expression}.concat([$event.target.value]) : [...${expression}.splice(0, ${expression}.indexOf($event.target.value)), ...${expression}.splice(${expression}.indexOf($event.target.value)+1)]`
-                            } else {
-                                rightSideOfExpression = `$event.target.checked`
-                            }
-                        } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
-                            rightSideOfExpression = modifiers.includes('number')
-                                ? 'Array.from($event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })'
-                                : 'Array.from($event.target.selectedOptions).map(option => { return option.value || option.text })'
-                        } else {
-                            rightSideOfExpression = modifiers.includes('number')
-                                ? 'parseFloat($event.target.value)'
-                                : (modifiers.includes('trim') ? '$event.target.value.trim()' : '$event.target.value')
-                        }
+                            ? 'change' : 'input'
 
-                        if (el.type === 'radio') {
-                            // Radio buttons only work properly when they share a name attribute.
-                            // People might assume we take care of that for them, because
-                            // they already set a shared "x-model" attribute.
-                            if (! el.hasAttribute('name')) el.setAttribute('name', expression)
-                        }
+                        const listenerExpression = this.generateExpressionForXModelListener(el, modifiers, expression)
 
-                        this.registerListener(el, event, modifiers, `${expression} = ${rightSideOfExpression}`)
+                        this.registerListener(el, event, modifiers, listenerExpression)
 
                         var attrName = 'value'
                         var { output } = this.evaluateReturnExpression(expression)
@@ -109,6 +84,35 @@ export default class Component {
         })
     }
 
+    generateExpressionForXModelListener(el, modifiers, dataKey) {
+        var rightSideOfExpression = ''
+        if (el.type === 'checkbox') {
+            // If the data we are binding to is an array, toggle it's value inside the array.
+            if (Array.isArray(this.data[dataKey])) {
+                rightSideOfExpression = `$event.target.checked ? ${dataKey}.concat([$event.target.value]) : [...${dataKey}.splice(0, ${dataKey}.indexOf($event.target.value)), ...${dataKey}.splice(${dataKey}.indexOf($event.target.value)+1)]`
+            } else {
+                rightSideOfExpression = `$event.target.checked`
+            }
+        } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
+            rightSideOfExpression = modifiers.includes('number')
+                ? 'Array.from($event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })'
+                : 'Array.from($event.target.selectedOptions).map(option => { return option.value || option.text })'
+        } else {
+            rightSideOfExpression = modifiers.includes('number')
+                ? 'parseFloat($event.target.value)'
+                : (modifiers.includes('trim') ? '$event.target.value.trim()' : '$event.target.value')
+        }
+
+        if (el.type === 'radio') {
+            // Radio buttons only work properly when they share a name attribute.
+            // People might assume we take care of that for them, because
+            // they already set a shared "x-model" attribute.
+            if (! el.hasAttribute('name')) el.setAttribute('name', dataKey)
+        }
+
+        return `${dataKey} = ${rightSideOfExpression}`
+    }
+
     registerListener(el, event, modifiers, expression) {
         if (modifiers.includes('away')) {
             // Listen for this event at the root level.

+ 45 - 2
src/index.js

@@ -3,12 +3,55 @@ import Component from './component'
 
 const minimal = {
     start: function () {
+        this.discoverComponents()
+
+        // It's easier and more performant to just support Turbolinks than listen
+        // to MutationOberserver mutations at the document level.
+        document.addEventListener("turbolinks:load", () => {
+            this.discoverComponents()
+        })
+
+        var targetNode = document.querySelector('body');
+        var observerOptions = {
+            childList: true,
+            attributes: true,
+            subtree: true //Omit or set to false to observe only changes to the parent node.
+        }
+
+        var observer = new MutationObserver((mutations) => {
+            for (var i=0; i < mutations.length; i++){
+                if (mutations[i].addedNodes.length > 0) {
+                    mutations[i].addedNodes.forEach(node => {
+                        if (node.nodeType !== 1) return
+
+                        if (node.matches('[x-data]')) {
+                            this.initializeElement(node)
+                        }
+                    })
+                }
+              }
+        });
+
+        observer.observe(targetNode, observerOptions);
+    },
+
+    discoverComponents: function () {
         const rootEls = document.querySelectorAll('[x-data]');
 
         rootEls.forEach(rootEl => {
-            // @todo - only set window.component in testing environments
-            window.component = new Component(rootEl)
+            this.initializeElement(rootEl)
         })
+    },
+
+    initializeElement: function (el) {
+        if (process.env.JEST_WORKER_ID) {
+            // This is so the component is accessible to Jest tests.
+            // It's ok to put this in a loop because Jest tests
+            // typically test only 1 component.
+            window.component = new Component(el)
+        } else {
+            new Component(el)
+        }
     }
 }