Improved the dialog stack animations
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()});
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user