running-tools

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

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             &nbsp;@&nbsp;
     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>