TimeInput.vue (3324B)
1 <template> 2 <div class="time-input"> 3 <integer-input class="hours" :aria-label="label + ' hours'" v-if="showHours" 4 :min="0" :max="99" :padding="1" v-model="hours" @keydown="onkeydown($event, 3600)"/> 5 <span v-if="showHours">:</span> 6 <integer-input class="minutes" :aria-label="label + ' minutes'" 7 :min="0" :max="59" :padding="2" v-model="minutes" 8 @keydown="onkeydown($event, 60)"/> 9 <span>:</span> 10 <decimal-input class="seconds" :aria-label="label + ' seconds'" 11 :min="0" :max="59.99" :padding="2" :digits="2" v-model="seconds" 12 @keydown="onkeydown($event, 1)"/> 13 </div> 14 </template> 15 16 <script setup lang="ts"> 17 import { computed, ref, watch } from 'vue'; 18 19 import IntegerInput from '@/components/IntegerInput.vue'; 20 import DecimalInput from '@/components/DecimalInput.vue'; 21 22 /** 23 * The component value 24 */ 25 const model = defineModel({ 26 type: Number, 27 default: 0, 28 validator(value: number) { 29 return value >= 0 && value <= 359999.99; 30 }, 31 }); 32 33 interface Props { 34 /** 35 * Whether to show the hour field (defaults to true) 36 */ 37 showHours?: boolean, 38 39 /** 40 * The prefix for each field's aria-label (defaults to 'Input') 41 */ 42 label?: string, 43 } 44 45 const props = withDefaults(defineProps<Props>(), { 46 showHours: true, 47 label: 'Input', 48 }); 49 50 /** 51 * The internal value 52 */ 53 const internalValue = ref(model.value); 54 55 /** 56 * The maximum value 57 */ 58 const max = computed(() => { 59 return props.showHours ? 359999.99 : 3599.99; 60 }); 61 62 /** 63 * The value of the hours field 64 */ 65 const hours = computed({ 66 get() { 67 return Math.floor(model.value / 3600); 68 }, 69 set(newValue) { 70 internalValue.value = (newValue * 3600) + (minutes.value * 60) + seconds.value; 71 }, 72 }); 73 74 /** 75 * The value of the minutes field 76 */ 77 const minutes = computed({ 78 get() { 79 return Math.floor((model.value % 3600) / 60); 80 }, 81 set(newValue) { 82 internalValue.value = (hours.value * 3600) + (newValue * 60) + seconds.value; 83 }, 84 }); 85 86 /** 87 * The value of the seconds field 88 */ 89 const seconds = computed({ 90 get() { 91 return model.value % 60; 92 }, 93 set(newValue) { 94 internalValue.value = (hours.value * 3600) + (minutes.value * 60) + newValue; 95 }, 96 }); 97 98 /** 99 * Update the internal value when the component value changes 100 */ 101 watch(model, (newValue: number) => { 102 if (newValue !== internalValue.value) { 103 internalValue.value = newValue; 104 } 105 }); 106 107 /** 108 * Update the component value when the internal value changes 109 */ 110 watch(internalValue, (newValue: number) => { 111 model.value = newValue; 112 }); 113 114 /** 115 * Process up and down arrow presses 116 * @param {Object} e The keydown event args 117 */ 118 function onkeydown(e: KeyboardEvent, step: number = 1) { 119 if (e.key === 'ArrowUp') { 120 if (Math.floor(internalValue.value) + step > max.value) { 121 internalValue.value = max.value; 122 } else { 123 internalValue.value = Math.floor(internalValue.value) + step; 124 } 125 e.preventDefault(); 126 } else if (e.key === 'ArrowDown') { 127 if (Math.ceil(internalValue.value) - step < 0) { 128 internalValue.value = 0; 129 } else { 130 internalValue.value = Math.ceil(internalValue.value) - step; 131 } 132 e.preventDefault(); 133 } 134 } 135 </script> 136 137 <style scoped> 138 div { 139 display: inline-block; 140 } 141 .hours, .minutes { 142 width: 2.5em; 143 } 144 .seconds { 145 width: 4em; 146 } 147 span { 148 font-weight: bold; 149 margin: 0px 0.2em; 150 } 151 </style>