|
@@ -80,6 +80,9 @@
|
|
|
function kebabCase(subject) {
|
|
|
return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase();
|
|
|
}
|
|
|
+ function camelCase(subject) {
|
|
|
+ return subject.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (match, char) => char.toUpperCase());
|
|
|
+ }
|
|
|
function walk(el, callback) {
|
|
|
if (callback(el) === false) return;
|
|
|
let node = el.firstElementChild;
|
|
@@ -147,11 +150,8 @@
|
|
|
})));
|
|
|
}
|
|
|
|
|
|
- return directives.filter(i => {
|
|
|
- // If no type is passed in for filtering, bypass filter
|
|
|
- if (!type) return true;
|
|
|
- return i.type === type;
|
|
|
- });
|
|
|
+ if (type) return directives.filter(i => i.type === type);
|
|
|
+ return directives;
|
|
|
}
|
|
|
|
|
|
function parseHtmlAttribute({
|
|
@@ -188,8 +188,18 @@
|
|
|
function convertClassStringToArray(classList, filterFn = Boolean) {
|
|
|
return classList.split(' ').filter(filterFn);
|
|
|
}
|
|
|
+ const TRANSITION_TYPE_IN = 'in';
|
|
|
+ const TRANSITION_TYPE_OUT = 'out';
|
|
|
function transitionIn(el, show, component, forceSkip = false) {
|
|
|
+ // We don't want to transition on the initial page load.
|
|
|
if (forceSkip) return show();
|
|
|
+
|
|
|
+ if (el.__x_transition && el.__x_transition.type === TRANSITION_TYPE_IN) {
|
|
|
+ // there is already a similar transition going on, this was probably triggered by
|
|
|
+ // a change in a different property, let's just leave the previous one doing its job
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const attrs = getXAttrs(el, component, 'transition');
|
|
|
const showAttr = getXAttrs(el, component, 'show')[0]; // If this is triggered by a x-show.transition.
|
|
|
|
|
@@ -201,7 +211,7 @@
|
|
|
|
|
|
modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers;
|
|
|
transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
|
|
|
- } else if (attrs.filter(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value)).length > 0) {
|
|
|
+ } else if (attrs.some(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value))) {
|
|
|
transitionClassesIn(el, component, attrs, show);
|
|
|
} else {
|
|
|
// If neither, just show that damn thing.
|
|
@@ -211,6 +221,13 @@
|
|
|
function transitionOut(el, hide, component, forceSkip = false) {
|
|
|
// We don't want to transition on the initial page load.
|
|
|
if (forceSkip) return hide();
|
|
|
+
|
|
|
+ if (el.__x_transition && el.__x_transition.type === TRANSITION_TYPE_OUT) {
|
|
|
+ // there is already a similar transition going on, this was probably triggered by
|
|
|
+ // a change in a different property, let's just leave the previous one doing its job
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
const attrs = getXAttrs(el, component, 'transition');
|
|
|
const showAttr = getXAttrs(el, component, 'show')[0];
|
|
|
|
|
@@ -220,7 +237,7 @@
|
|
|
const settingBothSidesOfTransition = modifiers.includes('in') && modifiers.includes('out');
|
|
|
modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers;
|
|
|
transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
|
|
|
- } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
|
|
|
+ } else if (attrs.some(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value))) {
|
|
|
transitionClassesOut(el, component, attrs, hide);
|
|
|
} else {
|
|
|
hide();
|
|
@@ -240,7 +257,7 @@
|
|
|
scale: 100
|
|
|
}
|
|
|
};
|
|
|
- transitionHelper(el, modifiers, showCallback, () => {}, styleValues);
|
|
|
+ transitionHelper(el, modifiers, showCallback, () => {}, styleValues, TRANSITION_TYPE_IN);
|
|
|
}
|
|
|
function transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hideCallback) {
|
|
|
// Make the "out" transition .5x slower than the "in". (Visually better)
|
|
@@ -259,7 +276,7 @@
|
|
|
scale: modifierValue(modifiers, 'scale', 95)
|
|
|
}
|
|
|
};
|
|
|
- transitionHelper(el, modifiers, () => {}, hideCallback, styleValues);
|
|
|
+ transitionHelper(el, modifiers, () => {}, hideCallback, styleValues, TRANSITION_TYPE_OUT);
|
|
|
}
|
|
|
|
|
|
function modifierValue(modifiers, key, fallback) {
|
|
@@ -292,8 +309,14 @@
|
|
|
return rawValue;
|
|
|
}
|
|
|
|
|
|
- function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
|
|
|
- // If the user set these style values, we'll put them back when we're done with them.
|
|
|
+ function transitionHelper(el, modifiers, hook1, hook2, styleValues, type) {
|
|
|
+ // clear the previous transition if exists to avoid caching the wrong styles
|
|
|
+ if (el.__x_transition) {
|
|
|
+ cancelAnimationFrame(el.__x_transition.nextFrame);
|
|
|
+ el.__x_transition.callback && el.__x_transition.callback();
|
|
|
+ } // If the user set these style values, we'll put them back when we're done with them.
|
|
|
+
|
|
|
+
|
|
|
const opacityCache = el.style.opacity;
|
|
|
const transformCache = el.style.transform;
|
|
|
const transformOriginCache = el.style.transformOrigin; // If no modifiers are present: x-show.transition, we'll default to both opacity and scale.
|
|
@@ -340,7 +363,7 @@
|
|
|
}
|
|
|
|
|
|
};
|
|
|
- transition(el, stages);
|
|
|
+ transition(el, stages, type);
|
|
|
}
|
|
|
function transitionClassesIn(el, component, directives, showCallback) {
|
|
|
let ensureStringExpression = expression => {
|
|
@@ -356,7 +379,7 @@
|
|
|
const enterEnd = convertClassStringToArray(ensureStringExpression((directives.find(i => i.value === 'enter-end') || {
|
|
|
expression: ''
|
|
|
}).expression));
|
|
|
- transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {});
|
|
|
+ transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {}, TRANSITION_TYPE_IN);
|
|
|
}
|
|
|
function transitionClassesOut(el, component, directives, hideCallback) {
|
|
|
const leave = convertClassStringToArray((directives.find(i => i.value === 'leave') || {
|
|
@@ -368,9 +391,15 @@
|
|
|
const leaveEnd = convertClassStringToArray((directives.find(i => i.value === 'leave-end') || {
|
|
|
expression: ''
|
|
|
}).expression);
|
|
|
- transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback);
|
|
|
+ transitionClasses(el, leave, leaveStart, leaveEnd, () => {}, hideCallback, TRANSITION_TYPE_OUT);
|
|
|
}
|
|
|
- function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2) {
|
|
|
+ function transitionClasses(el, classesDuring, classesStart, classesEnd, hook1, hook2, type) {
|
|
|
+ // clear the previous transition if exists to avoid caching the wrong classes
|
|
|
+ if (el.__x_transition) {
|
|
|
+ cancelAnimationFrame(el.__x_transition.nextFrame);
|
|
|
+ el.__x_transition.callback && el.__x_transition.callback();
|
|
|
+ }
|
|
|
+
|
|
|
const originalClasses = el.__x_original_classes || [];
|
|
|
const stages = {
|
|
|
start() {
|
|
@@ -401,12 +430,31 @@
|
|
|
}
|
|
|
|
|
|
};
|
|
|
- transition(el, stages);
|
|
|
- }
|
|
|
- function transition(el, stages) {
|
|
|
+ transition(el, stages, type);
|
|
|
+ }
|
|
|
+ function transition(el, stages, type) {
|
|
|
+ el.__x_transition = {
|
|
|
+ // Set transition type so we can avoid clearing transition if the direction is the same
|
|
|
+ type: type,
|
|
|
+ // create a callback for the last stages of the transition so we can call it
|
|
|
+ // from different point and early terminate it. Once will ensure that function
|
|
|
+ // is only called one time.
|
|
|
+ callback: once(() => {
|
|
|
+ stages.hide(); // Adding an "isConnected" check, in case the callback
|
|
|
+ // removed the element from the DOM.
|
|
|
+
|
|
|
+ if (el.isConnected) {
|
|
|
+ stages.cleanup();
|
|
|
+ }
|
|
|
+
|
|
|
+ delete el.__x_transition;
|
|
|
+ }),
|
|
|
+ // This store the next animation frame so we can cancel it
|
|
|
+ nextFrame: null
|
|
|
+ };
|
|
|
stages.start();
|
|
|
stages.during();
|
|
|
- requestAnimationFrame(() => {
|
|
|
+ el.__x_transition.nextFrame = requestAnimationFrame(() => {
|
|
|
// Note: Safari's transitionDuration property will list out comma separated transition durations
|
|
|
// for every single transition property. Let's grab the first one and call it a day.
|
|
|
let duration = Number(getComputedStyle(el).transitionDuration.replace(/,.*/, '').replace('s', '')) * 1000;
|
|
@@ -416,22 +464,25 @@
|
|
|
}
|
|
|
|
|
|
stages.show();
|
|
|
- requestAnimationFrame(() => {
|
|
|
- stages.end(); // Assign current transition to el in case we need to force it.
|
|
|
-
|
|
|
- setTimeout(() => {
|
|
|
- stages.hide(); // Adding an "isConnected" check, in case the callback
|
|
|
- // removed the element from the DOM.
|
|
|
-
|
|
|
- if (el.isConnected) {
|
|
|
- stages.cleanup();
|
|
|
- }
|
|
|
- }, duration);
|
|
|
+ el.__x_transition.nextFrame = requestAnimationFrame(() => {
|
|
|
+ stages.end();
|
|
|
+ setTimeout(el.__x_transition.callback, duration);
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
function isNumeric(subject) {
|
|
|
return !isNaN(subject);
|
|
|
+ } // Thanks @vuejs
|
|
|
+ // https://github.com/vuejs/vue/blob/4de4649d9637262a9b007720b59f80ac72a5620c/src/shared/util.js
|
|
|
+
|
|
|
+ function once(callback) {
|
|
|
+ let called = false;
|
|
|
+ return function () {
|
|
|
+ if (!called) {
|
|
|
+ called = true;
|
|
|
+ callback.apply(this, arguments);
|
|
|
+ }
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
|
|
@@ -551,7 +602,7 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType) {
|
|
|
+ function handleAttributeBindingDirective(component, el, attrName, expression, extraVars, attrType, modifiers) {
|
|
|
var value = component.evaluateReturnExpression(el, expression, extraVars);
|
|
|
|
|
|
if (attrName === 'value') {
|
|
@@ -612,7 +663,8 @@
|
|
|
el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '));
|
|
|
}
|
|
|
} else {
|
|
|
- // If an attribute's bound value is null, undefined or false, remove the attribute
|
|
|
+ attrName = modifiers.includes('camel') ? camelCase(attrName) : attrName; // If an attribute's bound value is null, undefined or false, remove the attribute
|
|
|
+
|
|
|
if ([null, undefined, false].includes(value)) {
|
|
|
el.removeAttribute(attrName);
|
|
|
} else {
|
|
@@ -674,9 +726,12 @@
|
|
|
|
|
|
const handle = resolve => {
|
|
|
if (value) {
|
|
|
- transitionIn(el, () => {
|
|
|
- show();
|
|
|
- }, component);
|
|
|
+ if (el.style.display === 'none' || el.__x_transition) {
|
|
|
+ transitionIn(el, () => {
|
|
|
+ show();
|
|
|
+ }, component);
|
|
|
+ }
|
|
|
+
|
|
|
resolve(() => {});
|
|
|
} else {
|
|
|
if (el.style.display !== 'none') {
|
|
@@ -715,7 +770,7 @@
|
|
|
warnIfMalformedTemplate(el, 'x-if');
|
|
|
const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true;
|
|
|
|
|
|
- if (expressionResult && !elementHasAlreadyBeenAdded) {
|
|
|
+ if (expressionResult && (!elementHasAlreadyBeenAdded || el.__x_transition)) {
|
|
|
const clone = document.importNode(el.content, true);
|
|
|
el.parentElement.insertBefore(clone, el.nextElementSibling);
|
|
|
transitionIn(el.nextElementSibling, () => {}, component, initialUpdate);
|
|
@@ -733,6 +788,10 @@
|
|
|
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 from the element or within it.
|
|
@@ -1310,12 +1369,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.
|
|
@@ -1343,11 +1402,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;
|
|
@@ -1366,6 +1434,10 @@
|
|
|
// Alpine's got it's grubby little paws all over everything.
|
|
|
initReturnedCallback.call(this.$data);
|
|
|
}
|
|
|
+
|
|
|
+ componentForClone || setTimeout(() => {
|
|
|
+ Alpine.onComponentInitializeds.forEach(callback => callback(this));
|
|
|
+ }, 0);
|
|
|
}
|
|
|
|
|
|
getUnobservedData() {
|
|
@@ -1539,13 +1611,13 @@
|
|
|
}) => {
|
|
|
switch (type) {
|
|
|
case 'model':
|
|
|
- handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type);
|
|
|
+ handleAttributeBindingDirective(this, el, 'value', expression, extraVars, type, modifiers);
|
|
|
break;
|
|
|
|
|
|
case 'bind':
|
|
|
// The :key binding on an x-for is special, ignore it.
|
|
|
if (el.tagName.toLowerCase() === 'template' && value === 'key') return;
|
|
|
- handleAttributeBindingDirective(this, el, value, expression, extraVars, type);
|
|
|
+ handleAttributeBindingDirective(this, el, value, expression, extraVars, type, modifiers);
|
|
|
break;
|
|
|
|
|
|
case 'text':
|
|
@@ -1565,7 +1637,7 @@
|
|
|
case 'if':
|
|
|
// If this element also has x-for on it, don't process x-if.
|
|
|
// We will let the "x-for" directive handle the "if"ing.
|
|
|
- if (attrs.filter(i => i.type === 'for').length > 0) return;
|
|
|
+ if (attrs.some(i => i.type === 'for')) return;
|
|
|
var output = this.evaluateReturnExpression(el, expression, extraVars);
|
|
|
handleIfDirective(this, el, output, initialUpdate, extraVars);
|
|
|
break;
|
|
@@ -1673,6 +1745,8 @@
|
|
|
const Alpine = {
|
|
|
version: "2.4.1",
|
|
|
pauseMutationObserver: false,
|
|
|
+ magicProperties: {},
|
|
|
+ onComponentInitializeds: [],
|
|
|
start: async function start() {
|
|
|
if (!isTesting()) {
|
|
|
await domReady();
|
|
@@ -1746,8 +1820,14 @@
|
|
|
},
|
|
|
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;
|
|
|
+ },
|
|
|
+ onComponentInitialized: function onComponentInitialized(callback) {
|
|
|
+ this.onComponentInitializeds.push(callback);
|
|
|
}
|
|
|
};
|
|
|
|