Kaynağa Gözat

Fixed transition overlaping

Muzaffer Dede 5 yıl önce
ebeveyn
işleme
d1b87404db
6 değiştirilmiş dosya ile 205 ekleme ve 37 silme
  1. 59 30
      dist/alpine-ie11.js
  2. 25 4
      dist/alpine.js
  3. 8 0
      examples/index.html
  4. 12 2
      src/directives/show.js
  5. 13 1
      src/utils.js
  6. 88 0
      test/transition.spec.js

+ 59 - 30
dist/alpine-ie11.js

@@ -5490,7 +5490,7 @@
     var additionalHelperVariables = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
 
     if (typeof expression === 'function') {
-      expression.call(dataContext);
+      return expression.call(dataContext);
     } // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     // Where "foo" is a function. Also, we'll pass the function the event instance when we call it.
 
@@ -5510,7 +5510,7 @@
     var name = replaceAtAndColonWithStandardSyntax(attr.name);
     return xAttrRE.test(name);
   }
-  function getXAttrs(el, type, component) {
+  function getXAttrs(el, component, type) {
     var _this2 = this;
 
     return Array.from(el.attributes).filter(isXAttr).map(parseHtmlAttribute).flatMap(function (i) {
@@ -5586,8 +5586,8 @@
     var forceSkip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
     // We don't want to transition on the initial page load.
     if (forceSkip) return show();
-    var attrs = getXAttrs(el, 'transition', component);
-    var showAttr = getXAttrs(el, 'show', component)[0]; // If this is triggered by a x-show.transition.
+    var attrs = getXAttrs(el, component, 'transition');
+    var showAttr = getXAttrs(el, component, 'show')[0]; // If this is triggered by a x-show.transition.
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
       var modifiers = showAttr.modifiers; // If x-show.transition.out, we'll skip the "in" transition.
@@ -5606,7 +5606,7 @@
 
       return ['enter', 'enter-start', 'enter-end'].includes(attr.value);
     }.bind(this)).length > 0) {
-      transitionClassesIn(el, attrs, show);
+      transitionClassesIn(el, component, attrs, show);
     } else {
       // If neither, just show that damn thing.
       show();
@@ -5617,8 +5617,8 @@
 
     var forceSkip = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
     if (forceSkip) return hide();
-    var attrs = getXAttrs(el, 'transition', component);
-    var showAttr = getXAttrs(el, 'show', component)[0];
+    var attrs = getXAttrs(el, component, 'transition');
+    var showAttr = getXAttrs(el, component, 'show')[0];
 
     if (showAttr && showAttr.modifiers.includes('transition')) {
       var modifiers = showAttr.modifiers;
@@ -5635,7 +5635,7 @@
 
       return ['leave', 'leave-start', 'leave-end'].includes(attr.value);
     }.bind(this)).length > 0) {
-      transitionClassesOut(el, attrs, hide);
+      transitionClassesOut(el, component, attrs, hide);
     } else {
       hide();
     }
@@ -5758,38 +5758,44 @@
     };
     transition(el, stages);
   }
-  function transitionClassesIn(el, directives, showCallback) {
+  function transitionClassesIn(el, component, directives, showCallback) {
     var _this9 = this;
 
-    var enter = (directives.find(function (i) {
+    var ensureStringExpression = function ensureStringExpression(expression) {
+      _newArrowCheck(this, _this9);
+
+      return typeof expression === 'function' ? component.evaluateReturnExpression(el, expression) : expression;
+    }.bind(this);
+
+    var enter = ensureStringExpression((directives.find(function (i) {
       _newArrowCheck(this, _this9);
 
       return i.value === 'enter';
     }.bind(this)) || {
       expression: ''
-    }).expression.split(' ').filter(function (i) {
+    }).expression).split(' ').filter(function (i) {
       _newArrowCheck(this, _this9);
 
       return i !== '';
     }.bind(this));
-    var enterStart = (directives.find(function (i) {
+    var enterStart = ensureStringExpression((directives.find(function (i) {
       _newArrowCheck(this, _this9);
 
       return i.value === 'enter-start';
     }.bind(this)) || {
       expression: ''
-    }).expression.split(' ').filter(function (i) {
+    }).expression).split(' ').filter(function (i) {
       _newArrowCheck(this, _this9);
 
       return i !== '';
     }.bind(this));
-    var enterEnd = (directives.find(function (i) {
+    var enterEnd = ensureStringExpression((directives.find(function (i) {
       _newArrowCheck(this, _this9);
 
       return i.value === 'enter-end';
     }.bind(this)) || {
       expression: ''
-    }).expression.split(' ').filter(function (i) {
+    }).expression).split(' ').filter(function (i) {
       _newArrowCheck(this, _this9);
 
       return i !== '';
@@ -5798,7 +5804,7 @@
       _newArrowCheck(this, _this9);
     }.bind(this));
   }
-  function transitionClassesOut(el, directives, hideCallback) {
+  function transitionClassesOut(el, component, directives, hideCallback) {
     var _this10 = this;
 
     var leave = (directives.find(function (i) {
@@ -5915,8 +5921,9 @@
 
         _newArrowCheck(this, _this14);
 
-        stages.end();
-        setTimeout(function () {
+        stages.end(); // Asign current transition to el in case we need to force it
+
+        el.__x_remaning_transitions = function () {
           _newArrowCheck(this, _this15);
 
           stages.hide(); // Adding an "isConnected" check, in case the callback
@@ -5924,6 +5931,18 @@
 
           if (el.isConnected) {
             stages.cleanup();
+          } // Safe to remove transition from el since it is completed
+
+
+          delete el.__x_remaning_transitions;
+        }.bind(this);
+
+        setTimeout(function () {
+          _newArrowCheck(this, _this15);
+
+          // We only want to run remaning transitions in the end if they exists
+          if (el.__x_remaning_transitions) {
+            el.__x_remaning_transitions();
           }
         }.bind(this), duration);
       }.bind(this));
@@ -5937,7 +5956,7 @@
     var _this = this;
 
     warnIfNotTemplateTag(templateEl);
-    var iteratorNames = parseForExpression(expression);
+    var iteratorNames = typeof expression === 'function' ? parseForExpression(component.evaluateReturnExpression(templateEl, expression)) : parseForExpression(expression);
     var items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars); // As we walk the array, we'll also walk the DOM (updating/creating as we go).
 
     var currentEl = templateEl;
@@ -5976,7 +5995,7 @@
       currentEl = nextEl;
       currentEl.__x_for_key = currentKey;
     }.bind(this));
-    removeAnyLeftOverElementsFromPreviousUpdate(currentEl);
+    removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component);
   } // This was taken from VueJS 2.* core. Thanks Vue!
 
   function parseForExpression(expression) {
@@ -6016,7 +6035,7 @@
   function generateKeyForIteration(component, el, index, iterationScopeVariables) {
     var _this3 = this;
 
-    var bindKeyAttribute = getXAttrs(el, 'bind', component).filter(function (attr) {
+    var bindKeyAttribute = getXAttrs(el, component, 'bind').filter(function (attr) {
       _newArrowCheck(this, _this3);
 
       return attr.value === 'key';
@@ -6035,7 +6054,7 @@
   }
 
   function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
-    var ifAttribute = getXAttrs(el, 'if', component)[0];
+    var ifAttribute = getXAttrs(el, component, 'if')[0];
 
     if (ifAttribute && !component.evaluateReturnExpression(el, ifAttribute.expression)) {
       return [];
@@ -6068,7 +6087,7 @@
     }
   }
 
-  function removeAnyLeftOverElementsFromPreviousUpdate(currentEl) {
+  function removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component) {
     var nextElementFromOldLoop = currentEl.nextElementSibling && currentEl.nextElementSibling.__x_for_key !== undefined ? currentEl.nextElementSibling : false;
 
     var _loop = function _loop() {
@@ -6230,6 +6249,11 @@
 
     var initialUpdate = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
 
+    // Resolve any previous pending transitions before starting a new one
+    if (el.__x_remaning_transitions) {
+      el.__x_remaning_transitions();
+    }
+
     var hide = function hide() {
       _newArrowCheck(this, _this);
 
@@ -6268,11 +6292,16 @@
 
             _newArrowCheck(this, _this2);
 
-            resolve(function () {
-              _newArrowCheck(this, _this3);
-
+            // If previous transitions still there, don't use resolve
+            if (el.__x_remaning_transitions) {
               hide();
-            }.bind(this));
+            } else {
+              resolve(function () {
+                _newArrowCheck(this, _this3);
+
+                hide();
+              }.bind(this));
+            }
           }.bind(this), component);
         } else {
           resolve(function () {
@@ -6858,7 +6887,7 @@
       value: function initializeElement(el, extraVars) {
         // To support class attribute merging, we have to know what the element's
         // original class attribute looked like for reference.
-        if (el.hasAttribute('class') && getXAttrs(el, undefined, this).length > 0) {
+        if (el.hasAttribute('class') && getXAttrs(el, this).length > 0) {
           el.__x_original_classes = el.getAttribute('class').split(' ');
         }
 
@@ -6955,7 +6984,7 @@
       value: function registerListeners(el, extraVars) {
         var _this14 = this;
 
-        getXAttrs(el, undefined, this).forEach(function (_ref) {
+        getXAttrs(el, this).forEach(function (_ref) {
           var type = _ref.type,
               value = _ref.value,
               modifiers = _ref.modifiers,
@@ -6981,7 +7010,7 @@
 
         var initialUpdate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
         var extraVars = arguments.length > 2 ? arguments[2] : undefined;
-        var attrs = getXAttrs(el, undefined, this);
+        var attrs = getXAttrs(el, this);
 
         if (el.type !== undefined && el.type === 'radio') {
           // If there's an x-model on a radio input, move it to end of attribute list

+ 25 - 4
dist/alpine.js

@@ -404,13 +404,24 @@
 
       stages.show();
       requestAnimationFrame(() => {
-        stages.end();
-        setTimeout(() => {
+        stages.end(); // Asign current transition to el in case we need to force it
+
+        el.__x_remaning_transitions = () => {
           stages.hide(); // Adding an "isConnected" check, in case the callback
           // removed the element from the DOM.
 
           if (el.isConnected) {
             stages.cleanup();
+          } // Safe to remove transition from el since it is completed
+
+
+          delete el.__x_remaning_transitions;
+        };
+
+        setTimeout(() => {
+          // We only want to run remaning transitions in the end if they exists
+          if (el.__x_remaning_transitions) {
+            el.__x_remaning_transitions();
           }
         }, duration);
       });
@@ -635,6 +646,11 @@
   }
 
   function handleShowDirective(component, el, value, modifiers, initialUpdate = false) {
+    // Resolve any previous pending transitions before starting a new one
+    if (el.__x_remaning_transitions) {
+      el.__x_remaning_transitions();
+    }
+
     const hide = () => {
       el.style.display = 'none';
     };
@@ -661,9 +677,14 @@
       if (!value) {
         if (el.style.display !== 'none') {
           transitionOut(el, () => {
-            resolve(() => {
+            // If previous transitions still there, don't use resolve
+            if (el.__x_remaning_transitions) {
               hide();
-            });
+            } else {
+              resolve(() => {
+                hide();
+              });
+            }
           }, component);
         } else {
           resolve(() => {});

+ 8 - 0
examples/index.html

@@ -38,6 +38,14 @@
         <script src="/dist/alpine.js" defer></script>
     </head>
     <body>
+
+    <div x-data="{open: false}">
+        <button x-on:click="open = !open">Toggle</button>
+        <div>
+            Open : <span x-text="open"></span>
+        </div>
+        <div x-show.transition.duration.1000="open">Content</div>
+    </div>
         <table>
             <thead>
                 <tr>

+ 12 - 2
src/directives/show.js

@@ -1,6 +1,11 @@
 import { transitionIn, transitionOut } from '../utils'
 
 export function handleShowDirective(component, el, value, modifiers, initialUpdate = false) {
+    // Resolve any previous pending transitions before starting a new one
+    if (el.__x_remaning_transitions) {
+        el.__x_remaning_transitions()
+    }
+
     const hide = () => {
         el.style.display = 'none'
     }
@@ -26,9 +31,14 @@ export function handleShowDirective(component, el, value, modifiers, initialUpda
         if (! value) {
             if ( el.style.display !== 'none' ) {
                 transitionOut(el, () => {
-                    resolve(() => {
+                    // If previous transitions still there, don't use resolve
+                    if (el.__x_remaning_transitions) {
                         hide()
-                    })
+                    } else {
+                        resolve(() => {
+                            hide()
+                        })
+                    }
                 }, component)
             } else {
                 resolve(() => {})

+ 13 - 1
src/utils.js

@@ -397,7 +397,9 @@ export function transition(el, stages) {
         requestAnimationFrame(() => {
             stages.end()
 
-            setTimeout(() => {
+            // Asign current transition to el in case we need to force it
+            el.__x_remaning_transitions = () => {
+
                 stages.hide()
 
                 // Adding an "isConnected" check, in case the callback
@@ -405,6 +407,16 @@ export function transition(el, stages) {
                 if (el.isConnected) {
                     stages.cleanup()
                 }
+
+                // Safe to remove transition from el since it is completed
+                delete el.__x_remaning_transitions
+            }
+
+            setTimeout(() => {
+                // We only want to run remaning transitions in the end if they exists
+                if (el.__x_remaning_transitions) {
+                    el.__x_remaning_transitions()
+                }
             }, duration);
         })
     });

+ 88 - 0
test/transition.spec.js

@@ -620,3 +620,91 @@ test('x-transition supports css animation', async () => {
     )
     expect(document.querySelector('span').classList.contains('animation-leave')).toEqual(false)
 })
+
+test('remaning transitions forced to complete if they exists', async () => {
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
+        setTimeout(callback, 0)
+    });
+
+    // (hardcoding 10ms animation time for later assertions)
+    jest.spyOn(window, 'getComputedStyle').mockImplementation(el => {
+        return {
+            transitionDuration: '0s',
+            animationDuration: '.1s'
+        }
+    });
+
+    document.body.innerHTML = `
+        <div x-data="{ show: false }">
+            <button x-on:click="show = ! show"></button>
+
+            <span
+                x-show="show"
+                x-transition:enter="animation-enter"
+                x-transition:leave="animation-leave"
+            ></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
+
+    // animation in
+    document.querySelector('button').click()
+    // animation out
+    document.querySelector('button').click()
+    // animation in
+    document.querySelector('button').click()
+
+    // Wait for the first requestAnimationFrame
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 0)
+    )
+    expect(document.querySelector('span').classList.contains('animation-enter')).toEqual(true)
+
+    // The class should still be there since the animationDuration property is 100ms
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 99)
+    )
+    expect(document.querySelector('span').classList.contains('animation-enter')).toEqual(true)
+
+    // The class shouldn't be there anymore
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 10)
+    )
+    expect(document.querySelector('span').classList.contains('animation-enter')).toEqual(false)
+
+    // animation out
+    document.querySelector('button').click()
+
+    // Wait for the first requestAnimationFrame
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 0)
+    )
+    expect(document.querySelector('span').classList.contains('animation-leave')).toEqual(true)
+
+    // The class should still be there since the animationDuration property is 100ms
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 99)
+    )
+    expect(document.querySelector('span').classList.contains('animation-leave')).toEqual(true)
+
+    // The class shouldn't be there anymore
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 10)
+    )
+    expect(document.querySelector('span').classList.contains('animation-leave')).toEqual(false)
+  })