Improved the dialog stack animations

This commit is contained in:
Stefan Zermatten
2017-01-13 13:21:05 +02:00
parent dbbb3739d0
commit c416adc85b
3 changed files with 116 additions and 40 deletions

View File

@@ -1,6 +1,28 @@
.dialog-stack { .dialog-stack {
background: rgba(0,0,0,0.4);
z-index: 1; z-index: 1;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
visibility: visible;
transition: visibility 0s linear;
}
.dialog-stack.hide {
visibility: hidden;
transition: visibility 0s linear 0.4s;
}
.dialog-stack .backdrop {
background: rgba(0,0,0,0.4);
opacity: 1;
transition: opacity 0.4s linear;
}
.dialog-stack.hide .backdrop {
opacity: 0;
transition: opacity 0.2s linear;
} }
.dialog-stack .dialog-sizer { .dialog-stack .dialog-sizer {
@@ -11,12 +33,20 @@
.dialog-stack .dialog { .dialog-stack .dialog {
position: absolute; position: absolute;
transform-origin: top left;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: white; background: white;
box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 3px 14px 2px rgba(0, 0, 0, 0.12),
0 5px 5px -3px rgba(0, 0, 0, 0.4); 0 5px 5px -3px rgba(0, 0, 0, 0.4);
transition: all ease 0.5s; transition: all 0.4s ease;
overflow: hidden;
border-radius: 2px;
}
.dialog-stack .dialog .testButton {
height: 200px;
width: 100px;
background: red;
border-radius: 30px;
} }

View File

@@ -3,7 +3,8 @@
It creates a stack of dialogs that can be closed individually It creates a stack of dialogs that can be closed individually
--> -->
<template name="dialogStack"> <template name="dialogStack">
<div class="fit dialog-stack layout vertical center center-justified" style={{dialogStackStyle}}> <div class="dialog-stack layout vertical center center-justified {{dialogStackClass}}">
<div class="fit backdrop"></div>
<div class="dialog-sizer"> <div class="dialog-sizer">
{{#each dialogs}} {{#each dialogs}}
<div class="dialog" style={{dialogStyle @index}}> <div class="dialog" style={{dialogStyle @index}}>
@@ -19,8 +20,13 @@
Test dialog {{data}} Test dialog {{data}}
</paper-toolbar> </paper-toolbar>
<div> <div>
<div class="testButton" <div class="testButton">
style="height: 200px; width: 100px; background: red; border-radius: 30px;">
</div> </div>
<div class="testButton">
</div>
<div class="testButton">
</div>
<div class="testButton">
</div>
</div> </div>
</template> </template>

View File

@@ -20,8 +20,8 @@ popDialogStack = function(result){
}; };
Template.dialogStack.helpers({ Template.dialogStack.helpers({
dialogStackStyle(){ dialogStackClass(){
if (!dialogs.get().length) return "display: none;"; if (!dialogs.get().length) return "hide";
}, },
dialogs(){ dialogs(){
return dialogs.get(); return dialogs.get();
@@ -45,51 +45,91 @@ 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, 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)){
//The radius is defined in pixel units, so get the radius as a number
const rad = +radius.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)) {
//The radius is defined as a percentage, so just use it as is
to.style.borderRadius = radius;
}
// 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";
// 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 = "";
});
// 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();
}
if (callback) callback();
}, duration);
};
Template.dialogStack.uihooks({ Template.dialogStack.uihooks({
".dialog": { ".dialog": {
container: ".dialog-sizer", container: ".dialog-sizer",
insert: function(node, next, tpl) { insert: function(node, next, tpl) {
$(node).insertBefore(next); $(node).insertBefore(next);
const data = Blaze.getData(node); const data = Blaze.getData(node);
if (data.element){ if (data.element){
data.element.style.visibility = "hidden"; data.element.style.visibility = "hidden";
const toRect = node.getBoundingClientRect(); // Store the reference to the element on the DOM node itself,
const fromRect = data.element.getBoundingClientRect(); // since Blaze won't keep the data around for the remove hook
const deltaLeft = fromRect.left - toRect.left;
const deltaTop = fromRect.top - toRect.top;
const deltaWidth = fromRect.width / toRect.width;
const deltaHeight = fromRect.height / toRect.height;
node.style.transition = "none";
node.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
`scale(${deltaWidth}, ${deltaHeight})`;
node.style.borderRadius = $(data.element).css("border-radius");
node["data-element"] = data.element; node["data-element"] = data.element;
_.defer(() => { heroAnimate({from: data.element, to: node});
node.style.transition = "";
node.style.transform = "";
node.style.borderRadius = "";
});
} }
}, },
remove: function(node, tpl) { remove: function(node, tpl) {
//TODO maybe make the element transform to the dialog size
// and then return to its place?
const element = node["data-element"]; const element = node["data-element"];
if (element){ if (element){
const toRect = node.getBoundingClientRect(); element.style.visibility = "";
const fromRect = element.getBoundingClientRect(); heroAnimate({from: node, to: element, useClone: true});
let deltaLeft = fromRect.left - toRect.left; node.remove();
let deltaTop = fromRect.top - toRect.top;
const deltaWidth = fromRect.width / toRect.width;
const deltaHeight = fromRect.height / toRect.height;
node.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
`scale(${deltaWidth}, ${deltaHeight})`;
node.style.borderRadius = $(element).css("border-radius");
_.delay(() => {
element.style.visibility = "";
node.remove();
}, 500);
} else { } else {
node.remove(); node.remove();
} }
@@ -100,5 +140,5 @@ Template.dialogStack.uihooks({
Template.testDialog.events({ Template.testDialog.events({
"click .testButton": function(event, template){ "click .testButton": function(event, template){
pushDialogStack({template: "testDialog", element: event.currentTarget, data: Random.id()}); pushDialogStack({template: "testDialog", element: event.currentTarget, data: Random.id()});
} },
}) })