running-tools

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

TargetSetSelector.vue (4311B)


      1 <template>
      2   <span class="target-set-selector">
      3     <select v-model="internalValue" aria-label="Selected target set">
      4       <option v-for="(item, index) in targetSets" :key="index" :value="index">
      5         {{ item.name }}
      6       </option>
      7       <option value="_new">[ Create New Target Set ]</option>
      8     </select>
      9 
     10     <button class="icon" title="Edit target set" @click="editTargetSet()">
     11       <vue-feather type="edit" aria-hidden="true"/>
     12     </button>
     13 
     14     <dialog ref="dialogElement" class="target-set-editor-dialog" aria-label="Edit target set">
     15       <target-editor @close="sortTargetSet(); dialogElement.close()"
     16         @revert="revertTargetSet" :customWorkoutNames="customWorkoutNames"
     17         :default-unit-system="defaultUnitSystem" :setType="setType"
     18         v-model="targetSets[internalValue]" :isCustomSet="!internalValue.startsWith('_')"/>
     19     </dialog>
     20   </span>
     21 </template>
     22 
     23 <script setup lang="ts">
     24 import { computed, nextTick, ref } from 'vue';
     25 
     26 import VueFeather from 'vue-feather';
     27 
     28 import { Calculators } from '@/core/calculators';
     29 import { sort, defaultTargetSets } from '@/core/targets';
     30 import type { TargetSet, TargetSets } from '@/core/targets';
     31 import { deepCopy } from '@/core/utils';
     32 import { UnitSystems } from '@/core/units';
     33 
     34 import TargetEditor from '@/components/TargetEditor.vue';
     35 import useObjectModel from '@/composables/useObjectModel';
     36 
     37 /**
     38  * The selected target set
     39  */
     40 const model = defineModel('selectedTargetSet', {
     41   type: String,
     42   required: true,
     43 });
     44 
     45 interface Props {
     46   /**
     47    * Whether to allow custom names for workout targets (defaults to false)
     48    */
     49   customWorkoutNames?: boolean,
     50 
     51   /**
     52    * The unit system to use when creating distance targets (defaults to metric)
     53    */
     54   defaultUnitSystem?: UnitSystems,
     55 
     56   /**
     57    * The target set type (defaults to pace calculator target sets)
     58    */
     59   setType?: Calculators,
     60 
     61   /**
     62    * The target sets
     63    */
     64   targetSets: TargetSets,
     65 };
     66 
     67 
     68 const props = withDefaults(defineProps<Props>(), {
     69   customWorkoutNames: false,
     70   defaultUnitSystem: UnitSystems.Metric,
     71   setType: Calculators.Pace,
     72 });
     73 
     74 // Generate internal ref tied to modelValue prop
     75 const emit = defineEmits(['update:targetSets']);
     76 const targetSets = useObjectModel<TargetSets>(() => props.targetSets, (x) => emit('update:targetSets', x));
     77 
     78 /**
     79  * The dialog element
     80  */
     81 const dialogElement = ref();
     82 
     83 /**
     84  * The internal value
     85  */
     86 const internalValue = computed({
     87   get: () => {
     88     if (model.value == '_new') {
     89       newTargetSet();
     90     }
     91     return model.value;
     92   },
     93   set: async (newValue: string) => {
     94     if (newValue == '_new') {
     95       await nextTick(); // <select> won't update if value changed immediately
     96       newTargetSet();
     97     } else {
     98       model.value = newValue;
     99     }
    100   },
    101 });
    102 
    103 /**
    104  * Open TargetEditor for the current target set
    105  */
    106 function editTargetSet() {
    107   if (dialogElement.value && dialogElement.value.showModal) {
    108     // Missing in test environments, but is difficult to mock because it may be referenced on mount
    109     dialogElement.value.showModal();
    110   }
    111 }
    112 
    113 /**
    114  * Create and select a new target
    115  */
    116 function newTargetSet() {
    117   const key = Date.now().toString();
    118   targetSets.value = {
    119     ...targetSets.value,
    120     [key]: {
    121       name: 'New target set',
    122       targets: [],
    123     },
    124   };
    125   model.value = key;
    126   editTargetSet();
    127 }
    128 
    129 /**
    130  * Revert or remove the current target set
    131  */
    132 function revertTargetSet() {
    133   if (internalValue.value.startsWith('_')) {
    134     // Revert default set
    135     targetSets.value[internalValue.value] =
    136       deepCopy<TargetSet>(defaultTargetSets[internalValue.value]);
    137     sortTargetSet();
    138   } else {
    139     // Remove custom set
    140     delete targetSets.value[internalValue.value];
    141     internalValue.value = [...Object.keys(targetSets.value), '_new'][0];
    142     if (dialogElement.value.close) dialogElement.value.close();
    143   }
    144 }
    145 
    146 /**
    147  * Sort the current target set
    148  */
    149 function sortTargetSet() {
    150   targetSets.value[internalValue.value].targets =
    151     sort(targetSets.value[internalValue.value].targets);
    152 }
    153 </script>
    154 
    155 <style scoped>
    156 .target-set-selector .icon {
    157   margin-left: 0.3em;
    158 }
    159 
    160 .target-set-editor-dialog {
    161   width: min(100% - 2em, 450px);
    162   max-height: min(100% - 2em, 815px);
    163   margin-top: 100px;
    164 }
    165 @media only screen and (max-height: 800px) {
    166   .target-set-editor-dialog {
    167     margin-top: 1em;
    168   }
    169 }
    170 </style>