vue #1

Merged
LeoVasanko merged 14 commits from vue into main 2025-08-22 23:34:34 +01:00
3 changed files with 121 additions and 43 deletions
Showing only changes of commit 86d38a5a29 - Show all commits

View File

@ -437,26 +437,25 @@ const recurrenceSummary = computed(() => {
</div> </div>
<div v-if="recurrenceEnabled" class="recurrence-form"> <div v-if="recurrenceEnabled" class="recurrence-form">
<div class="line compact"> <div class="line compact">
<span>Every</span>
<Numeric <Numeric
v-model="recurrenceInterval" v-model="recurrenceInterval"
:min="1" :prefix-values="[{ value: 1, display: 'Every' }]"
:max="999" :min="2"
:step="1" number-prefix="Every "
aria-label="Interval" aria-label="Interval"
/> />
<select v-model="recurrenceFrequency" class="freq-select"> <select v-model="recurrenceFrequency" class="freq-select">
<option value="weeks">weeks</option> <option value="weeks">{{ recurrenceInterval === 1 ? 'week' : 'weeks' }}</option>
<option value="months">months</option> <option value="months">
{{ recurrenceInterval === 1 ? 'month' : 'months' }}
</option>
</select> </select>
<Numeric <Numeric
class="occ-stepper" class="occ-stepper"
v-model="recurrenceOccurrences" v-model="recurrenceOccurrences"
:min="0" :min="2"
:max="999" :prefix-values="[{ value: 0, display: '∞' }]"
:step="1" number-postfix=" times"
:infinite-value="0"
infinite-display="∞"
aria-label="Occurrences (0 = no end)" aria-label="Occurrences (0 = no end)"
extra-class="occ" extra-class="occ"
/> />

View File

@ -63,18 +63,19 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
if (baseEvent.repeat === 'weeks') { if (baseEvent.repeat === 'weeks') {
const repeatWeekdays = baseEvent.repeatWeekdays const repeatWeekdays = baseEvent.repeatWeekdays
if (targetDate < baseStartDate) continue if (targetDate < baseStartDate) continue
const maxOccurrences = baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10) const maxOccurrences =
baseEvent.repeatCount === 'unlimited' ? Infinity : parseInt(baseEvent.repeatCount, 10)
if (maxOccurrences === 0) continue if (maxOccurrences === 0) continue
const interval = baseEvent.repeatInterval || 1 const interval = baseEvent.repeatInterval || 1
const msPerDay = 24*60*60*1000 const msPerDay = 24 * 60 * 60 * 1000
// Determine if targetDate lies within some occurrence span. We look backwards up to spanDays to find a start day. // Determine if targetDate lies within some occurrence span. We look backwards up to spanDays to find a start day.
let occStart = null let occStart = null
for (let back=0; back<=spanDays; back++) { for (let back = 0; back <= spanDays; back++) {
const cand = new Date(targetDate) const cand = new Date(targetDate)
cand.setDate(cand.getDate() - back) cand.setDate(cand.getDate() - back)
if (cand < baseStartDate) break if (cand < baseStartDate) break
const daysDiff = Math.floor((cand - baseStartDate)/msPerDay) const daysDiff = Math.floor((cand - baseStartDate) / msPerDay)
const weeksDiff = Math.floor(daysDiff / 7) const weeksDiff = Math.floor(daysDiff / 7)
if (weeksDiff % interval !== 0) continue if (weeksDiff % interval !== 0) continue
if (repeatWeekdays[cand.getDay()]) { if (repeatWeekdays[cand.getDay()]) {
@ -94,10 +95,10 @@ function generateRepeatOccurrencesForDate(targetDateStr) {
let occIdx = 0 let occIdx = 0
const cursor = new Date(baseStartDate) const cursor = new Date(baseStartDate)
while (cursor < occStart && occIdx < maxOccurrences) { while (cursor < occStart && occIdx < maxOccurrences) {
const cDaysDiff = Math.floor((cursor - baseStartDate)/msPerDay) const cDaysDiff = Math.floor((cursor - baseStartDate) / msPerDay)
const cWeeksDiff = Math.floor(cDaysDiff / 7) const cWeeksDiff = Math.floor(cDaysDiff / 7)
if (cWeeksDiff % interval === 0 && repeatWeekdays[cursor.getDay()]) occIdx++ if (cWeeksDiff % interval === 0 && repeatWeekdays[cursor.getDay()]) occIdx++
cursor.setDate(cursor.getDate()+1) cursor.setDate(cursor.getDate() + 1)
} }
if (occIdx >= maxOccurrences) continue if (occIdx >= maxOccurrences) continue
const occEnd = new Date(occStart) const occEnd = new Date(occStart)

View File

@ -7,7 +7,7 @@
role="spinbutton" role="spinbutton"
:aria-valuemin="minValue" :aria-valuemin="minValue"
:aria-valuemax="maxValue" :aria-valuemax="maxValue"
:aria-valuenow="current === infiniteValue ? undefined : current" :aria-valuenow="isPrefix(current) ? undefined : current"
:aria-valuetext="display" :aria-valuetext="display"
tabindex="0" tabindex="0"
@pointerdown="onPointerDown" @pointerdown="onPointerDown"
@ -26,8 +26,14 @@ const props = defineProps({
min: { type: Number, default: 0 }, min: { type: Number, default: 0 },
max: { type: Number, default: 999 }, max: { type: Number, default: 999 },
step: { type: Number, default: 1 }, step: { type: Number, default: 1 },
infiniteValue: { type: Number, default: 0 }, // model value representing infinity / special prefixValues: {
infiniteDisplay: { type: String, default: '∞' }, type: Array,
default: () => [],
validator: (arr) =>
arr.every((item) => typeof item === 'object' && 'value' in item && 'display' in item),
},
numberPrefix: { type: String, default: '' },
numberPostfix: { type: String, default: '' },
clamp: { type: Boolean, default: true }, clamp: { type: Boolean, default: true },
pixelsPerStep: { type: Number, default: 16 }, pixelsPerStep: { type: Number, default: 16 },
// Movement now restricted to horizontal (x). Prop retained for compatibility but ignored. // Movement now restricted to horizontal (x). Prop retained for compatibility but ignored.
@ -39,12 +45,39 @@ const props = defineProps({
const minValue = computed(() => props.min) const minValue = computed(() => props.min)
const maxValue = computed(() => props.max) const maxValue = computed(() => props.max)
// Helper to check if a value is in the prefix values
const isPrefix = (value) => {
return props.prefixValues.some((prefix) => prefix.value === value)
}
// Helper to get the display for a prefix value
const getPrefixDisplay = (value) => {
const prefix = props.prefixValues.find((p) => p.value === value)
return prefix ? prefix.display : null
}
// Get all valid values in order: prefixValues, then min to max
const allValidValues = computed(() => {
const prefixVals = props.prefixValues.map((p) => p.value)
const numericVals = []
for (let i = props.min; i <= props.max; i += props.step) {
numericVals.push(i)
}
return [...prefixVals, ...numericVals]
})
const current = computed({ const current = computed({
get() { get() {
return model.value return model.value
}, },
set(v) { set(v) {
if (props.clamp) { if (props.clamp) {
// If it's a prefix value, allow it
if (isPrefix(v)) {
model.value = v
return
}
// Otherwise clamp to numeric range
if (v < props.min) v = props.min if (v < props.min) v = props.min
if (v > props.max) v = props.max if (v > props.max) v = props.max
} }
@ -52,9 +85,16 @@ const current = computed({
}, },
}) })
const display = computed(() => const display = computed(() => {
current.value === props.infiniteValue ? props.infiniteDisplay : String(current.value), const prefixDisplay = getPrefixDisplay(current.value)
) if (prefixDisplay !== null) {
// For prefix values, show only the display text without number prefix/postfix
return prefixDisplay
}
// For numeric values, include prefix and postfix
const numericValue = String(current.value)
return `${props.numberPrefix}${numericValue}${props.numberPostfix}`
})
// Drag handling // Drag handling
const dragging = ref(false) const dragging = ref(false)
@ -82,12 +122,22 @@ function onPointerMove(e) {
if (e.pointerType === 'touch') e.preventDefault() if (e.pointerType === 'touch') e.preventDefault()
const primary = e.clientX - startX // horizontal only const primary = e.clientX - startX // horizontal only
const steps = Math.trunc(primary / props.pixelsPerStep) const steps = Math.trunc(primary / props.pixelsPerStep)
let next = startVal + steps * props.step
// Find current value index in all valid values
const currentIndex = allValidValues.value.indexOf(startVal)
if (currentIndex === -1) return // shouldn't happen
const newIndex = currentIndex + steps
if (props.clamp) { if (props.clamp) {
if (next < props.min) next = props.min const clampedIndex = Math.max(0, Math.min(newIndex, allValidValues.value.length - 1))
if (next > props.max) next = props.max const next = allValidValues.value[clampedIndex]
}
if (next !== current.value) current.value = next if (next !== current.value) current.value = next
} else {
if (newIndex >= 0 && newIndex < allValidValues.value.length) {
const next = allValidValues.value[newIndex]
if (next !== current.value) current.value = next
}
}
} }
function endDragListeners() { function endDragListeners() {
rootEl.value?.removeEventListener('pointermove', onPointerMove) rootEl.value?.removeEventListener('pointermove', onPointerMove)
@ -104,36 +154,64 @@ function onPointerCancel() {
function onKey(e) { function onKey(e) {
const key = e.key const key = e.key
let handled = false let handled = false
let delta = 0 let newValue = null
// Find current value index in all valid values
const currentIndex = allValidValues.value.indexOf(current.value)
switch (key) { switch (key) {
case 'ArrowRight': case 'ArrowRight':
case 'ArrowUp': case 'ArrowUp':
delta = props.step if (currentIndex !== -1 && currentIndex < allValidValues.value.length - 1) {
newValue = allValidValues.value[currentIndex + 1]
} else if (currentIndex === -1) {
// Current value not in list, try to increment normally
newValue = current.value + props.step
}
handled = true handled = true
break break
case 'ArrowLeft': case 'ArrowLeft':
case 'ArrowDown': case 'ArrowDown':
delta = -props.step if (currentIndex !== -1 && currentIndex > 0) {
newValue = allValidValues.value[currentIndex - 1]
} else if (currentIndex === -1) {
// Current value not in list, try to decrement normally
newValue = current.value - props.step
}
handled = true handled = true
break break
case 'PageUp': case 'PageUp':
delta = props.step * 10 if (currentIndex !== -1) {
const newIndex = Math.min(currentIndex + 10, allValidValues.value.length - 1)
newValue = allValidValues.value[newIndex]
} else {
newValue = current.value + props.step * 10
}
handled = true handled = true
break break
case 'PageDown': case 'PageDown':
delta = -props.step * 10 if (currentIndex !== -1) {
const newIndex = Math.max(currentIndex - 10, 0)
newValue = allValidValues.value[newIndex]
} else {
newValue = current.value - props.step * 10
}
handled = true handled = true
break break
case 'Home': case 'Home':
current.value = props.min newValue = allValidValues.value[0] || props.min
handled = true handled = true
break break
case 'End': case 'End':
current.value = props.max newValue = allValidValues.value[allValidValues.value.length - 1] || props.max
handled = true handled = true
break break
} }
if (delta !== 0) current.value = current.value + delta
if (newValue !== null) {
current.value = newValue
}
if (handled) { if (handled) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()