1
0
Fork 0
muzika-gromche/Frontend/src/components/library/Card.vue

189 lines
5.0 KiB
Vue

<script setup lang="ts">
import { shallowRef, useTemplateRef } from 'vue';
// Any card with a subtle outline and hover effect
const {
hoverEnabled = true,
playheadEnabled = true,
selected = false,
} = defineProps<{
hoverEnabled?: boolean,
playheadEnabled?: boolean,
selected?: boolean,
}>();
const emit = defineEmits<{
(e: 'select'): void;
(e: 'activate'): void;
(e: 'playhead', pos: number): void;
}>();
// Timeline / playhead position on hover in range 0..1,
// or NaN when not hovered or hover is not enabled.
const playheadPosition01 = shallowRef<number>(NaN);
const playheadEl = useTemplateRef('playheadEl');
const card = useTemplateRef('card');
// Simply tracks pointer enter/leave
const isHovered = shallowRef(false);
// flag to detect a recent touch tap so the following click doesn't also emit 'select'
const lastTapWasTouch = shallowRef(false);
// show playhead on touch while pressing
const isTouchActive = shallowRef(false);
// Once dragging starts, pointer should no longer cause click on release/up
const isTouchDragging = shallowRef(false);
// Playhead is active on mouse hover or touch down, but not after touch up which leaves dangling :hover on mobile.
defineExpose({
playheadPosition01,
});
// Returns false if playhead wasn't updated
function updatePlayhead(event: PointerEvent): boolean {
const target = event.currentTarget as HTMLElement | null;
if (!hoverEnabled || !playheadEnabled || !target) {
return false;
}
const rect = target.getBoundingClientRect();
const x = event.clientX - rect.left;
const pos = rect.width > 0 ? Math.max(0, Math.min(1, x / rect.width)) : 0;
playheadPosition01.value = pos;
if (playheadEl.value) {
// position the 1px playhead using percentage so it adapts to responsive widths
playheadEl.value.style.left = `${pos * 100}%`;
}
// emit normalized position for parent components to react (e.g. preview color)
emit('playhead', pos);
return true;
}
function onPointerEnter(_event: PointerEvent) {
if (hoverEnabled) {
isHovered.value = true;
}
}
function onPointerLeave(_event: PointerEvent) {
if (hoverEnabled) {
isHovered.value = false;
}
isTouchActive.value = false;
isTouchDragging.value = false;
playheadPosition01.value = NaN;
if (playheadEl.value) {
playheadEl.value.style.left = "";
}
emit('playhead', NaN);
}
function onPointerDown(event: PointerEvent) {
if (event.pointerType !== 'touch') return;
if (updatePlayhead(event)) {
isTouchActive.value = true;
}
if (card.value) {
card.value.setPointerCapture(event.pointerId);
}
}
function onPointerUp(event: PointerEvent) {
// Treat a single touch tap as activation
if (event.pointerType === 'touch') {
if (!isTouchDragging.value) {
emit('activate');
}
lastTapWasTouch.value = true;
// keep the flag true briefly so the subsequent click handler can suppress 'select'
setTimeout(() => { lastTapWasTouch.value = false; }, 50);
// clear touch-active playhead state
isTouchActive.value = false;
isTouchDragging.value = false;
onPointerLeave(event);
}
}
function onPointerMove(event: PointerEvent) {
updatePlayhead(event);
if (event.pointerType === 'touch') {
isTouchDragging.value = true;
}
}
function onClick(event: MouseEvent) {
// If this click follows a touch tap, suppress the 'select' event
if (lastTapWasTouch.value) {
event.preventDefault();
lastTapWasTouch.value = false;
return;
}
emit('select');
}
function onDblClick(_event: MouseEvent) {
emit('activate')
}
</script>
<template>
<div ref="card" class="card card-border tw:min-w-10 tw:min-h-10 tw:grid" :class="{
'hover-enabled': hoverEnabled,
'playhead-enabled': hoverEnabled && playheadEnabled,
'playhead-active': isHovered || isTouchActive,
selected,
}" @pointerenter="onPointerEnter" @pointerleave="onPointerLeave" @pointerdown="onPointerDown"
@pointerup="onPointerUp" @pointermove="onPointerMove" @click="onClick" @dblclick="onDblClick"
@focusin="emit('select')">
<!-- content container -->
<div class="tw:row-span-full tw:col-span-full tw:w-full tw:h-full">
<slot />
</div>
<!-- playhead container -->
<div class="playhead-container tw:pointer-events-none tw:row-span-full tw:col-span-full">
<div ref="playheadEl" class="playhead"></div>
</div>
</div>
</template>
<style scoped>
.card {
color: var(--inactive-text-color);
background-color: var(--card-background-color);
overflow: hidden;
touch-action: pan-y;
}
.card.hover-enabled:hover,
.card.selected {
color: var(--active-text-color);
}
.card.hover-enabled:hover {
outline: 2px solid var(--card-outline-color);
}
.card.selected,
.card.selected:hover {
outline: 2px solid var(--card-outline-selected-color);
}
.playhead-container {
display: none;
position: relative;
}
.card.playhead-enabled.playhead-active .playhead-container {
display: unset;
}
.playhead {
width: 1px;
height: 100%;
/* center the 1px line on the pointer */
transform: translateX(-50%);
background-color: var(--timeline-playhead-color);
position: absolute;
top: 0;
}
</style>