Dialog stack animations complete

This commit is contained in:
Stefan Zermatten
2019-01-24 14:40:38 +02:00
parent 00e8cbc1c8
commit 2e6ef52594
6 changed files with 113 additions and 63 deletions

View File

@@ -4,7 +4,8 @@
Test Dialog Test Dialog
</div> </div>
<div> <div>
<v-btn @click="openDialog(_uid + 'btn')" :id="_uid + 'btn'">Open Dialog</v-btn> <v-btn @click="openDialog(_uid + 'btn')" :id="_uid + 'btn'">Open Dialog</v-btn>
<v-btn fab @click="openDialog(_uid + 'fab')" :id="_uid + 'fab'" color="green">Open Dialog</v-btn>
</div> </div>
</dialog-base> </dialog-base>
</template> </template>

View File

@@ -1,20 +1,25 @@
<template> <template>
<v-card> <v-layout column style="height: 100%;">
<v-layout column style="height: 100%;"> <v-toolbar color="primary" dark class="base-dialog-toolbar" :flat="!offsetTop">
<v-toolbar color="primary" dark class="base-dialog-toolbar" :flat="!offsetTop"> <v-btn icon flat @click="close">
<slot name="toolbar"></slot> <v-icon>
</v-toolbar> arrow_back
<div id="base-dialog-body" v-scroll:#base-dialog-body="onScroll"> </v-icon>
<slot></slot> </v-btn>
</div> <slot name="toolbar"></slot>
<v-card-actions> </v-toolbar>
<slot name="actions"></slot> <v-card-text id="base-dialog-body" v-scroll:#base-dialog-body="onScroll">
</v-card-actions> <slot></slot>
</v-layout> </v-card-text>
</v-card> <v-card-actions>
<slot name="actions"></slot>
</v-card-actions>
</v-layout>
</template> </template>
<script> <script>
import store from "/imports/ui/vuexStore.js";
export default { export default {
data(){ return { data(){ return {
offsetTop: 0, offsetTop: 0,
@@ -23,6 +28,9 @@
onScroll(e){ onScroll(e){
this.offsetTop = e.target.scrollTop this.offsetTop = e.target.scrollTop
}, },
close(){
store.dispatch("popDialogStack");
},
}, },
} }
</script> </script>

View File

@@ -1,7 +1,9 @@
<template lang="html"> <template lang="html">
<v-card-text> <v-card-text>
<v-layout align-center justify-center> <v-layout align-center justify-center>
<v-btn @click="openDialog(_uid + 'btn')" :id="_uid + 'btn'"/> <v-btn @click="openDialog(_uid + 'btn')" :id="_uid + 'btn'">
Open Dialog
</v-btn>
</v-layout> </v-layout>
</v-card-text> </v-card-text>
</template> </template>

View File

@@ -1,10 +1,12 @@
<template> <template>
<v-layout class="dialog-stack" align-center justify-center> <v-layout class="dialog-stack" align-center justify-center>
<div <transition name="backdrop-fade">
class="backdrop" <div
@click="backdropClicked" class="backdrop"
:class="dialogs.length ? '' : 'hidden' " @click="backdropClicked"
></div> v-if="dialogs.length"
></div>
</transition>
<transition-group <transition-group
name="dialog-list" name="dialog-list"
class="dialog-sizer" class="dialog-sizer"
@@ -13,16 +15,17 @@
@leave="leave" @leave="leave"
> >
<div class="sibling" key="sibling"/> <div class="sibling" key="sibling"/>
<div <v-card
v-for="(dialog, index) in dialogs" v-for="(dialog, index) in dialogs"
:key="dialog._id" :key="dialog._id"
class="dialog" class="dialog"
:data-element-id="dialog.elementId" :data-element-id="dialog.elementId"
:data-index="index" :data-index="index"
:style="getDialogStyle(index)" :style="getDialogStyle(index)"
:elevation="6"
> >
<component :is="dialog.component" :data="dialog.data" @pop="popDialogStack($event)" class="dialog-component"></component> <component :is="dialog.component" :data="dialog.data" @pop="popDialogStack($event)" class="dialog-component"></component>
</div> </v-card>
</transition-group> </transition-group>
</v-layout> </v-layout>
</template> </template>
@@ -35,7 +38,7 @@
import Vue from "vue"; import Vue from "vue";
const OFFSET = 16; const OFFSET = 16;
const MOCK_DURATION = 8000; // Keep in sync with css transition of .dialog const MOCK_DURATION = 400; // Keep in sync with css transition of .dialog
export default { export default {
computed: { computed: {
@@ -64,7 +67,7 @@
// Get the original styles so we can repair them later // Get the original styles so we can repair them later
let originalStyle = { let originalStyle = {
transform: target.style.transform, transform: target.style.transform,
background: target.style.background, backgroundColor: target.style.backgroundColor,
borderRadius: target.style.borderRadius, borderRadius: target.style.borderRadius,
transition: target.style.transition, transition: target.style.transition,
boxShadow: target.style.boxShadow, boxShadow: target.style.boxShadow,
@@ -73,21 +76,21 @@
// hide the source // hide the source
source.style.transition = "none"; source.style.transition = "none";
source.style.visibility = "hidden"; source.style.opacity = "0";
// Instantly mock the source // Instantly mock the source
target.style.transition = 'none'; target.style.transition = 'none';
mockElement({source, target}); mockElement({source, target});
// After a full tick, repair the original styles // on the next animation frame, repair the styles
Vue.nextTick(() => { requestAnimationFrame(() => {
target.style.transform = originalStyle.transform; target.style.transform = originalStyle.transform;
target.style.background = originalStyle.background; target.style.backgroundColor = originalStyle.backgroundColor;
target.style.borderRadius = originalStyle.borderRadius; target.style.borderRadius = originalStyle.borderRadius;
target.style.transition = originalStyle.transition; target.style.transition = originalStyle.transition;
target.style.boxShadow = originalStyle.boxShadow; target.style.boxShadow = originalStyle.boxShadow;
source.style.transition = originalStyle.sourceTransition; source.style.transition = originalStyle.sourceTransition;
setTimeout(done, MOCK_DURATION); setTimeout(() => done, MOCK_DURATION);
}); });
}, },
leave(target, done){ leave(target, done){
@@ -101,8 +104,17 @@
mockElement({source, target}); mockElement({source, target});
} }
setTimeout(() => { setTimeout(() => {
source.style.visibility = null; let originalTransition = source.style.transition;
done(); source.style.opacity = null;
source.style.transition = 'none';
target.style.transition = `opacity ${MOCK_DURATION / 4}ms, pointer-events 0s`
requestAnimationFrame(() => {
source.style.transition = originalTransition;
target.style.opacity = "0";
target.style.pointerEvents = "none";
target.style.setProperty('box-shadow', "none", 'important');
setTimeout(done, MOCK_DURATION / 4);
});
}, MOCK_DURATION); }, MOCK_DURATION);
} }
}, },
@@ -110,6 +122,22 @@
</script> </script>
<style scoped> <style scoped>
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 4;
pointer-events: initial;
}
.backdrop-fade-enter-active, .backdrop-fade-leave-active {
transition: opacity 0.3s;
}
.backdrop-fade-enter, .backdrop-fade-leave-to {
opacity: 0;
}
.dialog-stack { .dialog-stack {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -121,35 +149,45 @@
} }
.dialog-sizer { .dialog-sizer {
position: relative; position: relative;
height: 600px; width: 80%;
width: 600px; width: calc(100% - 64px);
max-width: 1000px;
height: 80%;
height: calc(100% - 64px);
max-height: 800px;
z-index: 5; z-index: 5;
flex: initial; flex: initial;
} }
.backdrop { /* sm */
position: fixed; @media only screen and (max-width: 960px) and (min-width: 601px){
top: 0; .dialog-sizer {
left: 0; width: calc(100% - 32px);
right: 0; height: calc(100% - 32px);
bottom: 0; }
background-color: rgba(0, 0, 0, 0.4); }
z-index: 4; /* xs */
pointer-events: initial; @media only screen and (max-width: 600px) {
} .dialog-sizer {
.backdrop.hidden { width: 100%;
display: none height: 100%;
} }
}
.dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component { .dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component {
opacity: 0; opacity: 0;
} }
.dialog-list-enter-active .dialog-component { .dialog-list-enter-active .dialog-component {
transition: opacity 4s; transition: opacity 0.3s;
} }
.dialog-list-leave-active .dialog-component { .dialog-list-leave-active .dialog-component {
transition: opacity 4s 4s; transition: opacity 0.3s 0.1s;
}
.dialog-list-enter-active {
transition: all 0.4s, box-shadow 0.1s;
}
.dialog-list-leave-active {
transition: all 0.4s, box-shadow 0.1s 0.3s, opacity 0.1s, pointer-events 0s;
} }
.dialog { .dialog {
transition: all 8s;
transform-origin: top left; transform-origin: top left;
position: absolute; position: absolute;
height: 100%; height: 100%;
@@ -157,7 +195,6 @@
pointer-events: initial; pointer-events: initial;
z-index: 1; z-index: 1;
overflow: hidden; overflow: hidden;
background: white;
} }
.dialog > * { .dialog > * {
height: 100%; height: 100%;

View File

@@ -9,7 +9,6 @@ const dialogStackStore = {
mutations: { mutations: {
pushDialogStack(state, {component, data, elementId, returnElement, callback}){ pushDialogStack(state, {component, data, elementId, returnElement, callback}){
// Generate a new _id so that Vue knows how to shuffle the array // Generate a new _id so that Vue knows how to shuffle the array
console.log({elementId});
const _id = Random.id(); const _id = Random.id();
state.dialogs.push({ state.dialogs.push({
_id, _id,
@@ -22,7 +21,6 @@ const dialogStackStore = {
updateHistory(); updateHistory();
}, },
popDialogStackMutation (state, result){ popDialogStackMutation (state, result){
console.log({popped: result});
const dialog = state.dialogs.pop(); const dialog = state.dialogs.pop();
state.currentResult = null; state.currentResult = null;
updateHistory(); updateHistory();

View File

@@ -17,20 +17,20 @@ const transformedBoxShadow = (shadowString, deltaWidth, deltaHeight) => {
if (shadowString[0] === 'r'){ if (shadowString[0] === 'r'){
let strings = shadowString.match(/rgba\([^)]+\)[^,]+/g); let strings = shadowString.match(/rgba\([^)]+\)[^,]+/g);
strings = strings.map(string => { strings = strings.map(string => {
// TODO move color to end // Move color to end
strings.match(/(rgba\([^)]+\))([^,]+)/) let m = string.match(/(rgba\([^)]+\))([^,]+)/);
return `${m[2].trim()} ${m[1]}`;
}); });
shadowString = strings.join(', ');
} }
let scaleAverage = (deltaWidth + deltaHeight) / 2; let scaleAverage = (deltaWidth + deltaHeight) / 2;
let shadows = parse(shadowString); let shadows = parse(shadowString);
console.log({shadowString, shadows});
shadows.forEach(shadow => { shadows.forEach(shadow => {
shadow.offsetX /= deltaWidth; shadow.offsetX /= deltaWidth;
shadow.offsetY /= deltaHeight; shadow.offsetY /= deltaHeight;
shadow.blurRadius /= scaleAverage; shadow.blurRadius /= scaleAverage;
shadow.spreadRadius /= scaleAverage; shadow.spreadRadius /= scaleAverage;
}) })
console.log({newShadows: shadows});
return stringify(shadows); return stringify(shadows);
} }
@@ -48,11 +48,15 @@ export default function mockElement({source, target, offset = {x: 0, y: 0}}){
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` + target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
`scale(${deltaWidth}, ${deltaHeight})`; `scale(${deltaWidth}, ${deltaHeight})`;
target.style.background = getComputedStyle(source).background; target.style.backgroundColor = getComputedStyle(source).backgroundColor;
target.style.borderRadius = transformedRadius( // Edge might not combine all border radii into a single value,
getComputedStyle(source).borderRadius, deltaWidth, deltaHeight // So we just sample the top left one if we need to
); let oldRadius = getComputedStyle(source).borderRadius ||
//target.style.boxShadow = transformedBoxShadow( getComputedStyle(source).borderTopLeftRadius;
// getComputedStyle(source).boxShadow, deltaWidth, deltaHeight let borderRadius = transformedRadius(oldRadius, deltaWidth, deltaHeight);
//); target.style.borderRadius = borderRadius;
let boxShadow = transformedBoxShadow(
getComputedStyle(source).boxShadow, deltaWidth, deltaHeight
);
target.style.setProperty('box-shadow', boxShadow, 'important');
}; };