@@ -0,0 +1,252 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user