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

196 lines
5.1 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>(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 = Number.NaN
if (playheadEl.value) {
playheadEl.value.style.left = ''
}
emit('playhead', Number.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>
</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>