274 lines
12 KiB
JavaScript
274 lines
12 KiB
JavaScript
import { createVNode as _createVNode, createTextVNode as _createTextVNode, createElementVNode as _createElementVNode, Fragment as _Fragment, normalizeClass as _normalizeClass } from "vue";
|
|
// Styles
|
|
import "./VTimePickerControls.css";
|
|
|
|
// Components
|
|
import { VTimePickerField } from "./VTimePickerField.js";
|
|
import { VBtn } from "../VBtn/index.js"; // Composables
|
|
import { useLocale } from "../../composables/locale.js";
|
|
import { useProxiedModel } from "../../composables/proxiedModel.js"; // Utilities
|
|
import { computed, nextTick, ref, watch } from 'vue';
|
|
import { makeTimeValidationProps, useTimeValidation } from "./useTimeValidation.js";
|
|
import { convert12to24, convert24to12, extractInteger, pad } from "./util.js";
|
|
import { clamp, genericComponent, propsFactory, useRender } from "../../util/index.js"; // Types
|
|
export const makeVTimePickerControlsProps = propsFactory({
|
|
ampm: Boolean,
|
|
color: String,
|
|
disabled: Boolean,
|
|
inputHints: Boolean,
|
|
hour: [Number, String],
|
|
minute: [Number, String],
|
|
second: [Number, String],
|
|
period: String,
|
|
readonly: Boolean,
|
|
useSeconds: Boolean,
|
|
value: Number,
|
|
viewMode: String,
|
|
...makeTimeValidationProps()
|
|
}, 'VTimePickerControls');
|
|
export const VTimePickerControls = genericComponent()({
|
|
name: 'VTimePickerControls',
|
|
props: makeVTimePickerControlsProps(),
|
|
emits: {
|
|
'update:period': data => true,
|
|
'update:viewMode': data => true,
|
|
'update:hour': v => true,
|
|
'update:minute': v => true,
|
|
'update:second': v => true
|
|
},
|
|
setup(props, {
|
|
emit
|
|
}) {
|
|
const {
|
|
t
|
|
} = useLocale();
|
|
const {
|
|
isAllowedHour,
|
|
isAllowedMinute,
|
|
isAllowedSecond,
|
|
findNextAllowed
|
|
} = useTimeValidation(props);
|
|
const currentHour = computed(() => props.hour !== null ? props.ampm ? convert12to24(Number(props.hour), props.period ?? 'am') : Number(props.hour) : null);
|
|
const currentMinute = computed(() => props.minute !== null ? Number(props.minute) : null);
|
|
const isHourValid = computed(() => {
|
|
if (props.hour === null) return true;
|
|
return isAllowedHour.value?.(Number(currentHour.value)) ?? true;
|
|
});
|
|
const isMinuteValid = computed(() => {
|
|
if (props.minute === null) return true;
|
|
return isAllowedMinute.value?.(currentHour.value, Number(props.minute)) ?? true;
|
|
});
|
|
const isSecondValid = computed(() => {
|
|
if (props.second === null) return true;
|
|
return isAllowedSecond.value?.(currentHour.value, currentMinute.value, Number(props.second)) ?? true;
|
|
});
|
|
const transformHours = {
|
|
in: v => {
|
|
if (v == null || isNaN(Number(v))) return null;
|
|
const val = Number(v);
|
|
return props.ampm ? pad(convert24to12(val)) : pad(val);
|
|
},
|
|
out: v => {
|
|
if (isNaN(Number(v)) || v == null || v === '') return null;
|
|
const val = typeof v === 'string' ? extractInteger(v) : Number(v);
|
|
if (val === null) return null;
|
|
return props.ampm ? convert12to24(val, props.period ?? 'am') : clamp(val, 0, 23);
|
|
}
|
|
};
|
|
const hour = useProxiedModel(props, 'hour', undefined, transformHours.in, transformHours.out);
|
|
const transformMinutesOrSeconds = {
|
|
in: v => v != null && !isNaN(Number(v)) ? pad(`${v}`) : null,
|
|
out: v => {
|
|
if (isNaN(Number(v)) || v == null || v === '') return null;
|
|
const val = typeof v === 'string' ? extractInteger(v) : Number(v);
|
|
return val !== null ? clamp(val, 0, 59) : null;
|
|
}
|
|
};
|
|
const minute = useProxiedModel(props, 'minute', undefined, transformMinutesOrSeconds.in, transformMinutesOrSeconds.out);
|
|
const second = useProxiedModel(props, 'second', undefined, transformMinutesOrSeconds.in, transformMinutesOrSeconds.out);
|
|
function onHourFieldKeydown(e) {
|
|
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const isAm = props.period === 'am';
|
|
const current = props.ampm ? convert12to24(Number(hour.value ?? 0), isAm ? 'am' : 'pm') : Number(hour.value ?? 0);
|
|
const next = findNextAllowed('hour', current, e.key === 'ArrowUp');
|
|
const togglePeriod = isAm && next >= 12 || !isAm && next < 12;
|
|
if (props.ampm && togglePeriod) {
|
|
emit('update:period', props.period === 'am' ? 'pm' : 'am');
|
|
nextTick(() => hour.value = pad(next));
|
|
} else {
|
|
hour.value = pad(next);
|
|
}
|
|
}
|
|
function onMinuteFieldKeydown(e) {
|
|
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const current = Number(minute.value ?? 0);
|
|
const next = findNextAllowed('minute', current, e.key === 'ArrowUp', currentHour.value);
|
|
minute.value = pad(next);
|
|
}
|
|
function onSecondFieldKeydown(e) {
|
|
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const current = Number(second.value ?? 0);
|
|
const next = findNextAllowed('second', current, e.key === 'ArrowUp', currentHour.value, currentMinute.value);
|
|
second.value = pad(next);
|
|
}
|
|
function createInputInterceptor(valueTransformOut, compare, apply) {
|
|
return e => {
|
|
if (!e.data) return;
|
|
const inputElement = e.target;
|
|
const {
|
|
value: existingTxt,
|
|
selectionStart,
|
|
selectionEnd
|
|
} = inputElement ?? {};
|
|
if (extractInteger(e.data) === null) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
const potentialNewInputVal = existingTxt ? existingTxt.slice(0, selectionStart) + e.data + existingTxt.slice(selectionEnd) : e.data;
|
|
if (potentialNewInputVal.length > 2) {
|
|
if (selectionStart === selectionEnd && selectionEnd === 0 && e.data.trim().startsWith('0')) {
|
|
e.preventDefault();
|
|
inputElement.value = potentialNewInputVal.trim().substring(0, 2);
|
|
apply(inputElement.value);
|
|
if (e.data.trim().length === 1) {
|
|
inputElement.setSelectionRange(1, 1);
|
|
}
|
|
return;
|
|
}
|
|
if (selectionStart === selectionEnd && selectionEnd === 1 && existingTxt.startsWith('0')) {
|
|
e.preventDefault();
|
|
inputElement.value = potentialNewInputVal.trim().substring(0, 2);
|
|
apply(inputElement.value);
|
|
return;
|
|
}
|
|
const maxValue = props.viewMode === 'hour' ? props.ampm ? 12 : 23 : 59;
|
|
const value = extractInteger(potentialNewInputVal);
|
|
if (value > maxValue) {
|
|
e.preventDefault();
|
|
inputElement.value = pad(String(extractInteger(e.data)).substring(0, 2));
|
|
apply(inputElement.value);
|
|
return;
|
|
}
|
|
}
|
|
const potentialNewNumber = valueTransformOut(potentialNewInputVal);
|
|
if (compare(potentialNewNumber)) {
|
|
// ignore input when results in the same number
|
|
// prevents typing more digits
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
}
|
|
function setPeriod(val) {
|
|
emit('update:period', val);
|
|
}
|
|
const hourInputRef = ref();
|
|
const minuteInputRef = ref();
|
|
const secondInputRef = ref();
|
|
watch(() => props.viewMode, (_, old) => {
|
|
switch (old) {
|
|
case 'hour':
|
|
hourInputRef.value.blur();
|
|
break;
|
|
case 'minute':
|
|
minuteInputRef.value.blur();
|
|
break;
|
|
case 'second':
|
|
secondInputRef.value.blur();
|
|
break;
|
|
}
|
|
});
|
|
const hourInputFilter = createInputInterceptor(transformHours.out, v => transformHours.in(v) === hour.value, v => hour.value = v);
|
|
const minuteInputFilter = createInputInterceptor(transformMinutesOrSeconds.out, v => transformMinutesOrSeconds.in(v) === minute.value, v => minute.value = v);
|
|
const secondInputFilter = createInputInterceptor(transformMinutesOrSeconds.out, v => transformMinutesOrSeconds.in(v) === second.value, v => second.value = v);
|
|
useRender(() => {
|
|
return _createElementVNode("div", {
|
|
"class": "v-time-picker-controls"
|
|
}, [_createElementVNode("div", {
|
|
"class": _normalizeClass({
|
|
'v-time-picker-controls__time': true,
|
|
'v-time-picker-controls__time--with-ampm': props.ampm,
|
|
'v-time-picker-controls__time--with-seconds': props.useSeconds
|
|
})
|
|
}, [_createVNode(VTimePickerField, {
|
|
"ref": hourInputRef,
|
|
"active": props.viewMode === 'hour',
|
|
"color": props.color,
|
|
"disabled": props.disabled,
|
|
"label": t('$vuetify.timePicker.hour'),
|
|
"showHint": props.inputHints,
|
|
"error": isHourValid.value ? undefined : t('$vuetify.timePicker.notAllowed'),
|
|
"modelValue": hour.value,
|
|
"onUpdate:modelValue": v => hour.value = v,
|
|
"onKeydown": onHourFieldKeydown,
|
|
"onBeforeinput": hourInputFilter,
|
|
"onFocus": () => emit('update:viewMode', 'hour')
|
|
}, null), _createElementVNode("span", {
|
|
"class": "v-time-picker-controls__time__separator"
|
|
}, [_createTextVNode(":")]), _createVNode(VTimePickerField, {
|
|
"ref": minuteInputRef,
|
|
"active": props.viewMode === 'minute',
|
|
"color": props.color,
|
|
"disabled": props.disabled,
|
|
"label": t('$vuetify.timePicker.minute'),
|
|
"showHint": props.inputHints,
|
|
"error": isMinuteValid.value ? undefined : t('$vuetify.timePicker.notAllowed'),
|
|
"modelValue": minute.value,
|
|
"onUpdate:modelValue": v => minute.value = v,
|
|
"onKeydown": onMinuteFieldKeydown,
|
|
"onBeforeinput": minuteInputFilter,
|
|
"onFocus": () => emit('update:viewMode', 'minute')
|
|
}, null), props.useSeconds && _createElementVNode("span", {
|
|
"key": "secondsDivider",
|
|
"class": "v-time-picker-controls__time__separator"
|
|
}, [_createTextVNode(":")]), props.useSeconds && _createElementVNode(_Fragment, null, [_createVNode(VTimePickerField, {
|
|
"key": "secondsVal",
|
|
"ref": secondInputRef,
|
|
"active": props.viewMode === 'second',
|
|
"color": props.color,
|
|
"disabled": props.disabled,
|
|
"label": t('$vuetify.timePicker.second'),
|
|
"showHint": props.inputHints,
|
|
"error": isSecondValid.value ? undefined : t('$vuetify.timePicker.notAllowed'),
|
|
"modelValue": second.value,
|
|
"onUpdate:modelValue": v => second.value = v,
|
|
"onKeydown": onSecondFieldKeydown,
|
|
"onBeforeinput": secondInputFilter,
|
|
"onFocus": () => emit('update:viewMode', 'second')
|
|
}, null)]), props.ampm && _createElementVNode("div", {
|
|
"class": "v-time-picker-controls__ampm"
|
|
}, [_createVNode(VBtn, {
|
|
"active": props.period === 'am',
|
|
"color": props.period === 'am' ? props.color : undefined,
|
|
"class": _normalizeClass({
|
|
'v-time-picker-controls__ampm__am': true,
|
|
'v-time-picker-controls__ampm__btn': true,
|
|
'v-time-picker-controls__ampm__btn__active': props.period === 'am'
|
|
}),
|
|
"disabled": props.disabled,
|
|
"text": t('$vuetify.timePicker.am'),
|
|
"variant": props.disabled && props.period === 'am' ? 'elevated' : 'tonal',
|
|
"onClick": () => props.period !== 'am' ? setPeriod('am') : null
|
|
}, null), _createVNode(VBtn, {
|
|
"active": props.period === 'pm',
|
|
"color": props.period === 'pm' ? props.color : undefined,
|
|
"class": _normalizeClass({
|
|
'v-time-picker-controls__ampm__pm': true,
|
|
'v-time-picker-controls__ampm__btn': true,
|
|
'v-time-picker-controls__ampm__btn__active': props.period === 'pm'
|
|
}),
|
|
"disabled": props.disabled,
|
|
"text": t('$vuetify.timePicker.pm'),
|
|
"variant": props.disabled && props.period === 'pm' ? 'elevated' : 'tonal',
|
|
"onClick": () => props.period !== 'pm' ? setPeriod('pm') : null
|
|
}, null)])])]);
|
|
});
|
|
return {};
|
|
}
|
|
});
|
|
//# sourceMappingURL=VTimePickerControls.js.map
|