Rebuilt dialog animations with cross-fade effect
This commit is contained in:
@@ -6,23 +6,23 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
visibility: visible;
|
||||
transition: visibility 0s linear;
|
||||
transition: visibility 0s;
|
||||
}
|
||||
|
||||
.dialog-stack.hide {
|
||||
visibility: hidden;
|
||||
transition: visibility 0s linear 0.4s;
|
||||
transition: visibility 0s linear 400ms;
|
||||
}
|
||||
|
||||
.dialog-stack .backdrop {
|
||||
background: rgba(0,0,0,0.4);
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s linear;
|
||||
transition: opacity 400ms linear;
|
||||
}
|
||||
|
||||
.dialog-stack.hide .backdrop {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s linear;
|
||||
transition: opacity 200ms linear;
|
||||
}
|
||||
|
||||
.dialog-stack .dialog-sizer {
|
||||
@@ -39,9 +39,9 @@
|
||||
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
|
||||
0 3px 14px 2px rgba(0, 0, 0, 0.12),
|
||||
0 5px 5px -3px rgba(0, 0, 0, 0.4);
|
||||
transition: all 0.4s ease;
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.dialog-stack .dialog .testButton {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
dialogs = new ReactiveArray();
|
||||
const offset = 16;
|
||||
const duration = 400;
|
||||
|
||||
pushDialogStack = function({template, data, element, callback}){
|
||||
// Generate a new _id so that Blaze knows how to shuffle the array
|
||||
@@ -19,6 +20,11 @@ popDialogStack = function(result){
|
||||
dialog.callback && dialog.callback(result);
|
||||
};
|
||||
|
||||
let cloneHolder;
|
||||
Template.dialogStack.onRendered(function(){
|
||||
cloneHolder = this.find(".clone-holder");
|
||||
});
|
||||
|
||||
Template.dialogStack.helpers({
|
||||
dialogStackClass(){
|
||||
if (!dialogs.get().length) return "hide";
|
||||
@@ -27,7 +33,9 @@ Template.dialogStack.helpers({
|
||||
return dialogs.get();
|
||||
},
|
||||
dialogStyle(index){
|
||||
const num = dialogs.get().length - 1;
|
||||
const length = dialogs.get().length;
|
||||
if (!length) return;
|
||||
const num = length - 1;
|
||||
const left = (num - index) * -offset;
|
||||
const top = (num - index) * -offset;
|
||||
return `left:${left}px; top:${top}px;`;
|
||||
@@ -45,69 +53,111 @@ Template.dialogStack.events({
|
||||
},
|
||||
});
|
||||
|
||||
const heroAnimate = ({from, to, duration, useClone, callback}) => {
|
||||
if (!from) throw "From element must be defined";
|
||||
if (!to) throw "To element must be defined";
|
||||
duration = duration || 400;
|
||||
// Get the bounding rectangles of both elements
|
||||
const toRect = to.getBoundingClientRect();
|
||||
const fromRect = from.getBoundingClientRect();
|
||||
let originalNode;
|
||||
let originalVis;
|
||||
if (useClone){
|
||||
originalNode = to;
|
||||
to = originalNode.cloneNode(true);
|
||||
originalNode.parentNode.insertBefore(to, originalNode);
|
||||
to.style.position = "fixed";
|
||||
to.style.zIndex = "9999";
|
||||
originalVis = originalNode.style.visibility;
|
||||
originalNode.style.visibility = "hidden";
|
||||
}
|
||||
// Get how they have changed
|
||||
const deltaLeft = fromRect.left - toRect.left;
|
||||
const deltaTop = fromRect.top - toRect.top;
|
||||
const deltaWidth = fromRect.width / toRect.width;
|
||||
const deltaHeight = fromRect.height / toRect.height;
|
||||
// Make the "to" element imitate the "from" element
|
||||
to.style.transition = "none";
|
||||
to.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
|
||||
`scale(${deltaWidth}, ${deltaHeight})`;
|
||||
to.style.background = $(from).css("background");
|
||||
to.style.boxShadow = $(from).css("box-shadow");
|
||||
// Imitate the border radius after transform
|
||||
// Only supports border radius defined like "20px" or "100%"
|
||||
let radius = $(from).css("border-radius");
|
||||
if (/^\d+\.?\d*px$/.test(radius)){
|
||||
// Only supports border radius defined like "20px" or "100%"
|
||||
const transformedRadius = (radiusString, deltaWidth, deltaHeight) => {
|
||||
if (/^\d+\.?\d*px$/.test(radiusString)){
|
||||
//The radius is defined in pixel units, so get the radius as a number
|
||||
const rad = +radius.match(/\d+\.?\d*/)[0];
|
||||
const rad = +radiusString.match(/\d+\.?\d*/)[0];
|
||||
// Set the x and y radius of the "to" element, compensating for scale
|
||||
to.style.borderRadius = `${rad / deltaWidth}px / ${rad / deltaHeight}px`;
|
||||
} else if (/^\d+\.?\d*%$/.test(radius)) {
|
||||
return `${rad / deltaWidth}px / ${rad / deltaHeight}px`;
|
||||
} else if (/^\d+\.?\d*%$/.test(radiusString)) {
|
||||
//The radius is defined as a percentage, so just use it as is
|
||||
to.style.borderRadius = radius;
|
||||
return radiusString;
|
||||
}
|
||||
// Don't animate to the imitation position
|
||||
to.style.transition = "none";
|
||||
// We calculate everything from the top left, so use that as origin
|
||||
to.style.transformOrigin = "top left";
|
||||
};
|
||||
|
||||
const imitate = (
|
||||
element, source, deltaLeft, deltaTop, deltaWidth, deltaHeight
|
||||
) => {
|
||||
element.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
|
||||
`scale(${deltaWidth}, ${deltaHeight})`;
|
||||
element.style.background = $(source).css("background");
|
||||
// Imitate the border radius after transform
|
||||
element.style.borderRadius = transformedRadius($(source).css("border-radius"));
|
||||
}
|
||||
|
||||
const dialogOpenAnimation = ({element, dialog}) => {
|
||||
const dialogRect = dialog.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
element.style.visibility = "hidden";
|
||||
// Get how must the element change to become the dialog
|
||||
const deltaLeft = elementRect.left - dialogRect.left;
|
||||
const deltaTop = elementRect.top - dialogRect.top;
|
||||
const deltaWidth = elementRect.width / dialogRect.width;
|
||||
const deltaHeight = elementRect.height / dialogRect.height;
|
||||
|
||||
// Make the dialog imitate the element, immediately
|
||||
dialog.style.transition = "none";
|
||||
imitate(dialog, element, deltaLeft, deltaTop, deltaWidth, deltaHeight);
|
||||
|
||||
// Next frame, undo the imitation, let "to" animate into its place
|
||||
_.defer(() => {
|
||||
to.style.transition = `all ${duration / 1000}s ease, ` +
|
||||
`box-shadow ${duration / 1000}s linear 0.1s`;
|
||||
to.style.transform = "";
|
||||
to.style.borderRadius = "";
|
||||
to.style.background = "";
|
||||
to.style.boxShadow = "";
|
||||
// Next frame, undo the imitation, let dialog animate back into place
|
||||
dialog.style.transition = `all ${duration}ms ease`;
|
||||
dialog.style.transform = "";
|
||||
dialog.style.borderRadius = "";
|
||||
dialog.style.background = "";
|
||||
});
|
||||
// Clean up after the animation is done and call our callback
|
||||
_.delay(() => {
|
||||
to.style.transition = "";
|
||||
to.style.transformOrigin = "";
|
||||
if (useClone){
|
||||
originalNode.style.visibility = originalVis;
|
||||
to.remove();
|
||||
}
|
||||
dialog.style.transition = "";
|
||||
}, duration);
|
||||
}
|
||||
|
||||
const dialogCloseAnimation = ({element, dialog, callback}) => {
|
||||
// Reset the dialog if it is mid-transition
|
||||
dialog.style.transition = "none";
|
||||
dialog.style.transform = "none";
|
||||
dialog.style.borderRadius = "";
|
||||
dialog.style.background = "";
|
||||
dialog.style.opacity = "1";
|
||||
// Get the original bounding rectangles of both elements
|
||||
const dialogRect = dialog.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
// Set up a clone of the original element
|
||||
// This lets us have a fixed position element which isn't clipped
|
||||
clone = element.cloneNode(true);
|
||||
clone.style.position = "fixed";
|
||||
clone.style.top = 0;
|
||||
clone.style.left = 0;
|
||||
clone.style.width = elementRect.width + "px";
|
||||
clone.style.height = elementRect.height + "px";
|
||||
clone.style.visibility = "";
|
||||
clone.style.zIndex = 9999;
|
||||
// Insert clone before its progenitor so it can inherit css correctly
|
||||
element.parentNode.insertBefore(clone, element);
|
||||
// Polymer messes up fixed positioning, measure and compensate
|
||||
startingRect = clone.getBoundingClientRect();
|
||||
clone.style.top = elementRect.top - startingRect.top + "px";
|
||||
clone.style.left = elementRect.left - startingRect.left + "px";
|
||||
|
||||
// How must the original dialog change to become the element
|
||||
const deltaLeft = dialogRect.left - elementRect.left;
|
||||
const deltaTop = dialogRect.top - elementRect.top;
|
||||
const deltaWidth = dialogRect.width / elementRect.width;
|
||||
const deltaHeight = dialogRect.height / elementRect.height;
|
||||
|
||||
// Make the clone imitate the dialog
|
||||
clone.style.transition = "none";
|
||||
clone.style.transformOrigin = "top left"
|
||||
imitate(clone, dialog, deltaLeft, deltaTop, deltaWidth, deltaHeight);
|
||||
|
||||
_.defer(() => {
|
||||
// Next frame, undo the imitation, let clone animate into its place
|
||||
clone.style.transition = `all ${duration}ms ease`;
|
||||
clone.style.transform = "";
|
||||
clone.style.borderRadius = "";
|
||||
clone.style.background = "";
|
||||
// Make the dialog follow the clone in and fade away
|
||||
dialog.style.transition = `all ${duration}ms ease, ` +
|
||||
`opacity ${duration * 0.75}ms ease-in`;
|
||||
dialog.style.opacity = 0;
|
||||
imitate(dialog, element, -deltaLeft,
|
||||
-deltaTop, 1 / deltaWidth, 1 / deltaHeight);
|
||||
});
|
||||
// Clean up after the animation is done and call our callback
|
||||
_.delay(() => {
|
||||
element.style.visibility = "";
|
||||
clone.remove();
|
||||
if (callback) callback();
|
||||
}, duration);
|
||||
};
|
||||
@@ -119,19 +169,25 @@ Template.dialogStack.uihooks({
|
||||
$(node).insertBefore(next);
|
||||
const data = Blaze.getData(node);
|
||||
if (data.element){
|
||||
data.element.style.visibility = "hidden";
|
||||
// Store the reference to the element on the DOM node itself,
|
||||
// since Blaze won't keep the data around for the remove hook
|
||||
node["data-element"] = data.element;
|
||||
heroAnimate({from: data.element, to: node});
|
||||
dialogOpenAnimation({
|
||||
element: data.element,
|
||||
dialog: node,
|
||||
});
|
||||
}
|
||||
},
|
||||
remove: function(node, tpl) {
|
||||
const element = node["data-element"];
|
||||
if (element){
|
||||
element.style.visibility = "";
|
||||
heroAnimate({from: node, to: element, useClone: true});
|
||||
node.remove();
|
||||
dialogCloseAnimation({
|
||||
element,
|
||||
dialog: node,
|
||||
callback(){
|
||||
node.remove();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user