calendar/src/components/WeekdaySelector.vue
Leo Vasanko 018b9ecc55 vue (#1)
Port to Vue. Also implements plenty of new functionality.
2025-08-22 23:34:33 +01:00

253 lines
6.9 KiB
Vue

<template>
<div class="weekgrid" @pointerleave="dragging = false">
<button
v-for="(d, di) in displayLabels"
:key="d + di"
type="button"
class="day"
:class="{
on: anySelected && displayDisplayValues[di],
// Show fallback styling on the reordered fallback day when none selected
fallback: !anySelected && displayDefault[di],
pressing: isPressing(di),
preview: previewActive && inPreviewRange(di),
}"
@pointerdown="onPointerDown(di)"
@pointerenter="onDragOver(di)"
@pointerup="onPointerUp"
>
{{ d.slice(0, 3) }}
</button>
<button
v-for="g in barGroups"
:key="g.start"
type="button"
tabindex="-1"
class="workday-weekend"
:style="{ gridColumn: 'span ' + g.span }"
@click.stop="toggleWeekend(g.type)"
>
<div :class="{ workday: !g.type, weekend: g.type }"></div>
</button>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import {
getLocalizedWeekdayNames,
getLocaleFirstDay,
getLocaleWeekendDays,
reorderByFirstDay,
} from '@/utils/date'
const model = defineModel({
type: Array,
default: () => [false, false, false, false, false, false, false],
})
const props = defineProps({
weekend: { type: Array, default: undefined },
fallback: {
type: Array,
default: () => [false, false, false, false, false, false, false],
},
firstDay: { type: Number, default: null },
})
// If external model provided is entirely false, keep as-is (user will see fallback styling),
// only overwrite if null/undefined.
if (!model.value) model.value = [...props.fallback]
const labelsMondayFirst = getLocalizedWeekdayNames()
const labels = [labelsMondayFirst[6], ...labelsMondayFirst.slice(0, 6)]
const anySelected = computed(() => model.value.some(Boolean))
const localeFirst = getLocaleFirstDay()
const localeWeekend = getLocaleWeekendDays()
const firstDay = computed(() => (props.firstDay ?? localeFirst) % 7)
const weekendDays = computed(() => {
if (props.weekend && props.weekend.length === 7) return props.weekend
return localeWeekend
})
const displayLabels = computed(() => reorderByFirstDay(labels, firstDay.value))
const displayValuesCommitted = computed(() => reorderByFirstDay(model.value, firstDay.value))
const displayWorking = computed(() => reorderByFirstDay(weekendDays.value, firstDay.value))
const displayDefault = computed(() => reorderByFirstDay(props.fallback, firstDay.value))
// Mapping from display index to original model index
const orderIndices = computed(() => Array.from({ length: 7 }, (_, i) => (i + firstDay.value) % 7))
const barGroups = computed(() => {
const arr = displayWorking.value
const groups = []
let type = arr[0]
let start = 0
for (let i = 1; i <= arr.length; i++) {
if (i === arr.length || arr[i] !== type) {
groups.push({ type, start, span: i - start })
if (i < arr.length) {
type = arr[i]
start = i
}
}
}
return groups
})
const dragging = ref(false)
const previewActive = ref(false)
const dragVal = ref(false)
const dragStart = ref(null)
const previewEnd = ref(null)
let originalValues = null
// Preview (drag) values; when none selected, still return committed (not fallback) so 'on' class
// is suppressed and only fallback styling applies via displayDefault
const displayPreviewValues = computed(() => {
if (
!dragging.value ||
!previewActive.value ||
dragStart.value == null ||
previewEnd.value == null ||
!originalValues
) {
return displayValuesCommitted.value
}
const [s, e] =
dragStart.value < previewEnd.value
? [dragStart.value, previewEnd.value]
: [previewEnd.value, dragStart.value]
return displayValuesCommitted.value.map((v, di) => (di >= s && di <= e ? dragVal.value : v))
})
const displayDisplayValues = displayPreviewValues
function inPreviewRange(di) {
if (!previewActive.value || dragStart.value == null || previewEnd.value == null) return false
const [s, e] =
dragStart.value < previewEnd.value
? [dragStart.value, previewEnd.value]
: [previewEnd.value, dragStart.value]
return di >= s && di <= e
}
function isPressing(di) {
return dragging.value && !previewActive.value && dragStart.value === di
}
function onPointerDown(di) {
originalValues = [...model.value]
dragVal.value = !model.value[(di + firstDay.value) % 7]
dragStart.value = di
previewEnd.value = di
dragging.value = true
previewActive.value = false
window.addEventListener('pointerup', onPointerUp, { once: true })
}
function onDragOver(di) {
if (!dragging.value) return
if (previewEnd.value === di) return
if (!previewActive.value && di !== dragStart.value) previewActive.value = true
previewEnd.value = di
}
function onPointerUp() {
if (!dragging.value) return
if (!previewActive.value) {
// simple click: toggle single
const next = [...originalValues]
next[(dragStart.value + firstDay.value) % 7] = dragVal.value
model.value = next
cleanupDrag()
} else {
commitDrag()
}
}
function commitDrag() {
if (dragStart.value == null || previewEnd.value == null || !originalValues) return cancelDrag()
const [s, e] =
dragStart.value < previewEnd.value
? [dragStart.value, previewEnd.value]
: [previewEnd.value, dragStart.value]
const next = [...originalValues]
for (let di = s; di <= e; di++) next[orderIndices.value[di]] = dragVal.value
model.value = next
cleanupDrag()
}
function cancelDrag() {
cleanupDrag()
}
function cleanupDrag() {
dragging.value = false
previewActive.value = false
dragStart.value = null
previewEnd.value = null
originalValues = null
}
function toggleWeekend(work) {
const base = weekendDays.value
const target = work ? base : base.map((v) => !v)
const current = model.value
const allOn = current.every(Boolean)
const isTargetActive = current.every((v, i) => v === target[i])
if (allOn || isTargetActive) {
model.value = [false, false, false, false, false, false, false]
} else {
model.value = [...target]
}
}
</script>
<style scoped>
.weekgrid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: auto;
}
.workday-weekend {
height: 1em;
border: 0;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
}
.workday-weekend div {
height: 0.3em;
border-radius: 0.15em;
margin: 0.1em;
}
.workday {
background: var(--workday, #888);
}
.weekend {
background: var(--weekend, #f88);
}
.day {
flex: 1;
cursor: pointer;
background: var(--panel-alt);
color: var(--ink);
font-size: 0.65rem;
font-weight: 500;
padding: 0.55rem 0.35rem;
border: none;
margin: 0 1px;
border-radius: 0.4rem;
user-select: none;
}
.day.on {
background: var(--pill-active-bg);
color: var(--pill-active-ink);
font-weight: 600;
}
.day.pressing {
filter: brightness(1.15);
}
.day.preview {
filter: brightness(1.15);
}
.day.fallback {
background: var(--muted-alt);
opacity: 0.65;
}
</style>