memories/src/components/SplitTimeline.vue

318 lines
7.4 KiB
Vue
Raw Normal View History

2023-02-08 21:35:42 +00:00
<template>
<div class="split-container" ref="container" :class="headerClass">
<div class="primary" ref="primary">
2023-02-08 21:35:42 +00:00
<component :is="primary" />
</div>
<div
class="separator"
ref="separator"
@pointerdown="sepDown"
@touchmove.passive="sepTouchMove"
@touchend.passive="pointerUp"
@touchcancel.passive="pointerUp"
></div>
2023-02-08 21:35:42 +00:00
<div class="timeline">
<div class="timeline-header" ref="timelineHeader">
<div class="swiper"></div>
<div class="title">
{{ t("memories", "{photoCount} photos", { photoCount }) }}
</div>
</div>
<div class="timeline-inner">
<Timeline @daysLoaded="daysLoaded" />
</div>
2023-02-08 21:35:42 +00:00
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Timeline from "./Timeline.vue";
2023-02-09 08:39:02 +00:00
const MapSplitMatter = () => import("./top-matter/MapSplitMatter.vue");
import { emit } from "@nextcloud/event-bus";
import Hammer from "hammerjs";
2023-02-08 21:35:42 +00:00
export default defineComponent({
name: "SplitTimeline",
components: {
Timeline,
},
data: () => ({
pointerDown: false,
primaryPos: 0,
containerSize: 0,
mobileOpen: 1,
hammer: null as HammerManager | null,
photoCount: 0,
}),
2023-02-08 21:35:42 +00:00
computed: {
primary() {
switch (this.$route.name) {
2023-02-08 22:13:13 +00:00
case "map":
return MapSplitMatter;
2023-02-08 21:35:42 +00:00
default:
return "None";
}
},
headerClass() {
switch (this.mobileOpen) {
case 0:
return "m-zero";
case 1:
return "m-one";
case 2:
return "m-two";
}
},
},
mounted() {
// Set up hammerjs hooks
this.hammer = new Hammer(this.$refs.timelineHeader as HTMLElement);
this.hammer.get("swipe").set({
direction: Hammer.DIRECTION_VERTICAL,
threshold: 3,
});
this.hammer.on("swipeup", this.mobileSwipeUp);
this.hammer.on("swipedown", this.mobileSwipeDown);
2023-02-08 21:35:42 +00:00
},
beforeDestroy() {
this.pointerUp();
this.hammer?.destroy();
},
methods: {
isVertical() {
return false; // for future
},
sepDown(event: PointerEvent) {
this.pointerDown = true;
// Get position of primary element
const primary = <HTMLDivElement>this.$refs.primary;
const rect = primary.getBoundingClientRect();
this.primaryPos = this.isVertical() ? rect.top : rect.left;
// Get size of container element
const container = <HTMLDivElement>this.$refs.container;
const cRect = container.getBoundingClientRect();
this.containerSize = this.isVertical() ? cRect.height : cRect.width;
// Let touch handle itself
if (event.pointerType === "touch") return;
// Otherwise, handle pointer events on document
document.addEventListener("pointermove", this.documentPointerMove);
document.addEventListener("pointerup", this.pointerUp);
// Prevent text selection
event.preventDefault();
event.stopPropagation();
},
sepTouchMove(event: TouchEvent) {
if (!this.pointerDown) return;
this.setFlexBasis(event.touches[0]);
},
documentPointerMove(event: PointerEvent) {
if (!this.pointerDown || !event.buttons) return this.pointerUp();
this.setFlexBasis(event);
},
pointerUp() {
// Get rid of listeners on document quickly
this.pointerDown = false;
document.removeEventListener("pointermove", this.documentPointerMove);
document.removeEventListener("pointerup", this.pointerUp);
emit("memories:window:resize", {});
},
setFlexBasis(pos: { clientX: number; clientY: number }) {
const ref = this.isVertical() ? pos.clientY : pos.clientX;
const newSize = Math.max(ref - this.primaryPos, 50);
const pctSize = (newSize / this.containerSize) * 100;
(<HTMLDivElement>this.$refs.primary).style.flexBasis = `${pctSize}%`;
},
daysLoaded({ count }: { count: number }) {
this.photoCount = count;
},
async mobileSwipeUp() {
this.mobileOpen = Math.min(this.mobileOpen + 1, 2);
// When swiping up, immediately emit a resize event
// so that we can prepare in advance for showing more photos
// on the timeline
await this.$nextTick();
emit("memories:window:resize", {});
},
async mobileSwipeDown() {
this.mobileOpen = Math.max(this.mobileOpen - 1, 0);
// When swiping down, wait for the animation to end, so that
// we don't hide the lower half of the timeline before the animation
// ends. Note that this is necesary: the height of the timeline inner
// div is also animated to the smaller size.
await new Promise((resolve) => setTimeout(resolve, 300));
emit("memories:window:resize", {});
},
},
2023-02-08 21:35:42 +00:00
});
</script>
<style lang="scss" scoped>
.split-container {
2023-02-08 21:35:42 +00:00
width: 100%;
height: 100%;
display: flex;
overflow: hidden;
> div {
height: 100%;
max-height: 100%;
}
> .primary {
flex-basis: 60%;
flex-shrink: 0;
}
> .timeline {
flex-basis: auto;
flex-grow: 1;
padding-left: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
> .timeline-header {
position: relative;
display: block;
height: 50px;
flex-shrink: 0;
flex-grow: 0;
border-bottom: 1px solid var(--color-border-dark);
.swiper {
display: none;
}
> .title {
width: 100%;
height: 100%;
text-align: center;
font-weight: 500;
padding-top: 12px;
}
}
> .timeline-inner {
flex-grow: 1;
overflow: hidden;
}
}
> .separator {
flex-grow: 0;
flex-shrink: 0;
width: 5px;
background-color: gray;
opacity: 0.1;
cursor: col-resize;
margin: 0 0 0 auto;
transition: opacity 0.4s ease-out, background-color 0.4s ease-out;
}
> .separator:hover {
opacity: 0.4;
background-color: var(--color-primary);
}
2023-02-08 21:35:42 +00:00
}
@media (max-width: 768px) {
/**
* On mobile the layout works completely differently
* Both components are full-height, and the separatator
* brings up the timeline to cover up the primary component
* fully when dragged up.
*/
.split-container {
display: block;
> div {
position: absolute;
width: 100%;
background-color: var(--color-main-background);
}
.primary {
height: 50%;
will-change: height;
}
> .separator {
display: none;
}
> .timeline {
height: 50%;
padding-left: 0;
// Note: you can't use transforms to animate the top
// because it causes the viewer to be rendered incorrectly
transition: top 0.2s ease, height 0.2s ease;
> .timeline-header {
height: 58px;
> .swiper {
display: block;
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%, 9px);
width: 22px;
height: 4px;
border-radius: 40px;
background-color: var(--color-text-light);
z-index: 1;
opacity: 0.75;
pointer-events: none;
}
> .title {
padding-top: 22px;
}
}
}
&.m-zero > .timeline {
top: calc(100% - 58px); // show attribution
}
&.m-zero > .primary {
height: calc(100% - 58px); // show full map
}
&.m-one > .timeline {
top: 50%; // show half map
}
&.m-two > .timeline {
top: 0%;
height: 100%;
}
2023-02-08 21:35:42 +00:00
}
}
</style>