running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

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>