TargetEditor.vue (6356B)
1 <template> 2 <table class="target-editor"> 3 <thead> 4 <tr> 5 <th> 6 Edit 7 <input v-model="model.name" placeholder="Target set label" 8 aria-label="Target set label"/> 9 <button class="icon" :title="isCustomSet ? 'Delete target set' : 'Revert target set'" 10 @click="emit('revert')"> 11 <vue-feather :type="isCustomSet ? 'trash-2' : 'rotate-ccw'" aria-hidden="true"/> 12 </button> 13 </th> 14 15 <th> 16 <button class="icon" title="Close" @click="emit('close')"> 17 <vue-feather type="x" aria-hidden="true"/> 18 </button> 19 </th> 20 </tr> 21 </thead> 22 23 <tbody> 24 <tr v-for="(item, index) in model.targets" :key="index"> 25 <td> 26 <span v-if="setType === Calculators.Workout && customWorkoutNames"> 27 <input v-model="(item as WorkoutTarget).customName" aria-label="Custom target name" 28 :placeholder="workoutTargetToString(item as WorkoutTarget)"/>: 29 </span> 30 31 <span v-if="setType === Calculators.Workout"> 32 <decimal-input v-model="(item as WorkoutTarget).splitValue" 33 aria-label="Split distance value" :min="0" :digits="2"/> 34 <select v-model="(item as WorkoutTarget).splitUnit" aria-label="Split distance unit"> 35 <option v-for="(value, key) in DistanceUnitData" :key="key" :value="key"> 36 {{ value.name }} 37 </option> 38 </select> 39 </span> 40 41 <span v-if="setType === Calculators.Workout"> 42 @ 43 </span> 44 45 <span v-if="item.type === 'distance'"> 46 <decimal-input v-model="item.distanceValue" aria-label="Target distance value" 47 :min="0" :digits="2"/> 48 <select v-model="item.distanceUnit" aria-label="Target distance unit"> 49 <option v-for="(value, key) in DistanceUnitData" :key="key" :value="key"> 50 {{ value.name }} 51 </option> 52 </select> 53 </span> 54 55 <span v-else> 56 <time-input v-model="item.time" label="Target duration"/> 57 </span> 58 </td> 59 60 <td> 61 <button class="icon" title="Remove target" @click="removeTarget(index)"> 62 <vue-feather type="trash-2" aria-hidden="true"/> 63 </button> 64 </td> 65 </tr> 66 67 <tr v-if="model.targets.length === 0" class="empty-message"> 68 <td colspan="2"> 69 There aren't any targets in this set yet. 70 </td> 71 </tr> 72 </tbody> 73 74 <tfoot> 75 <tr> 76 <td colspan="2"> 77 <button title="Add distance target" @click="addDistanceTarget"> 78 Add distance target 79 </button> 80 <button title="Add time target" @click="addTimeTarget" 81 v-if="setType !== Calculators.Split"> 82 Add time target 83 </button> 84 </td> 85 </tr> 86 </tfoot> 87 </table> 88 </template> 89 90 <script setup lang="ts"> 91 import VueFeather from 'vue-feather'; 92 93 import { Calculators } from '@/core/calculators'; 94 import { TargetTypes, workoutTargetToString } from '@/core/targets'; 95 import type { StandardTargetSet, TargetSet, WorkoutTarget, WorkoutTargetSet } from '@/core/targets'; 96 import { DistanceUnitData, UnitSystems, getDefaultDistanceUnit } from '@/core/units'; 97 98 import DecimalInput from '@/components/DecimalInput.vue'; 99 import TimeInput from '@/components/TimeInput.vue'; 100 import useObjectModel from '@/composables/useObjectModel'; 101 102 interface Props { 103 /** 104 * Whether to allow custom names for workout targets (defaults to false) 105 */ 106 customWorkoutNames?: boolean, 107 /** 108 * The unit system to use when creating distance targets (defaults to metric) 109 */ 110 defaultUnitSystem?: UnitSystems, 111 112 /** 113 * Whether the target set is a custom or default set (defaults to false) 114 */ 115 isCustomSet?: boolean, 116 117 /** 118 * The component value 119 */ 120 modelValue: TargetSet, 121 122 /** 123 * The target set type (defaults to pace calculator target sets) 124 */ 125 setType?: Calculators, 126 } 127 128 const props = withDefaults(defineProps<Props>(), { 129 customWorkoutNames: false, 130 defaultUnitSystem: UnitSystems.Metric, 131 isCustomSet: false, 132 setType: Calculators.Pace, 133 }); 134 135 // Declare emitted events 136 const emit = defineEmits(['close', 'revert', 'update:modelValue']); 137 138 // Generate internal ref tied to modelValue prop 139 const model = useObjectModel<TargetSet>(() => props.modelValue, (x) => emit('update:modelValue', x)); 140 141 /** 142 * Add a new distance based target 143 */ 144 function addDistanceTarget() { 145 if (props.setType === Calculators.Workout) { 146 (model.value as WorkoutTargetSet).targets.push({ 147 type: TargetTypes.Distance, 148 distanceValue: 1, 149 distanceUnit: getDefaultDistanceUnit(props.defaultUnitSystem), 150 splitValue: 1, 151 splitUnit: getDefaultDistanceUnit(props.defaultUnitSystem), 152 }); 153 } else { 154 (model.value as StandardTargetSet).targets.push({ 155 type: TargetTypes.Distance, 156 distanceValue: 1, 157 distanceUnit: getDefaultDistanceUnit(props.defaultUnitSystem), 158 }); 159 } 160 } 161 162 /** 163 * Add a new time based target 164 */ 165 function addTimeTarget() { 166 if (props.setType === Calculators.Workout) { 167 (model.value as WorkoutTargetSet).targets.push({ 168 type: TargetTypes.Time, 169 time: 600, 170 splitValue: 1, 171 splitUnit: getDefaultDistanceUnit(props.defaultUnitSystem), 172 }); 173 } else { 174 (model.value as StandardTargetSet).targets.push({ 175 type: TargetTypes.Time, 176 time: 600, 177 }); 178 } 179 } 180 181 /** 182 * Remove a target 183 * @param {number} index The index of the target 184 */ 185 function removeTarget(index: number) { 186 model.value.targets.splice(index, 1); 187 } 188 </script> 189 190 <style scoped> 191 /* edit targets table */ 192 .target-editor th .icon { 193 margin-left: 0.3em; 194 } 195 .target-editor tbody td:first-child:not(.empty-message td) { 196 display: flex; 197 gap: 0.2em; 198 flex-wrap: wrap; 199 align-items: center; 200 } 201 .target-editor th:last-child, .target-editor td:last-child { 202 text-align: right; 203 } 204 .target-editor td select { 205 margin-left: 0.2em; 206 width: 8em; 207 } 208 .target-editor tfoot td { 209 text-align: center !important; 210 padding: 0.5em 0.2em; 211 } 212 .target-editor tfoot button { 213 margin: 0.5em; 214 } 215 @media only screen and (max-width: 800px) { 216 /* leave space for revert button on mobile devices */ 217 .target-editor th input { 218 width: 12em; 219 } 220 } 221 </style>