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>