|
| 1 | +<script setup> |
| 2 | +import LoadingSpinner from '@/components/icons/LoadingSpinner.vue' |
| 3 | +import ValidationError from '@/components/ValidationError.vue' |
| 4 | +import { required } from '@vuelidate/validators' |
| 5 | +import useVuelidate from '@vuelidate/core' |
| 6 | +import { CheckIcon, PencilIcon, XIcon } from '@heroicons/vue/outline' |
| 7 | +import { computed, defineEmits, defineProps, reactive, ref, watchEffect } from 'vue' |
| 8 | +
|
| 9 | +const emit = defineEmits(['change']) |
| 10 | +
|
| 11 | +const props = defineProps({ |
| 12 | + busy: Boolean, |
| 13 | + disabled: Boolean, |
| 14 | + placeholder: String, |
| 15 | + validation: [Function, Object], |
| 16 | + value: String |
| 17 | +}) |
| 18 | +
|
| 19 | +const formState = reactive({ |
| 20 | + newValue: '' |
| 21 | +}) |
| 22 | +
|
| 23 | +const v$ = useVuelidate({ newValue: props.validation || { required } }, formState) |
| 24 | +
|
| 25 | +const canSave = computed(() => !props.disabled && !v$.value.$invalid) |
| 26 | +const editing = ref(false) |
| 27 | +const inputRef = ref(null) |
| 28 | +
|
| 29 | +function cancelEdit() { |
| 30 | + editing.value = false |
| 31 | + reset() |
| 32 | +} |
| 33 | +
|
| 34 | +function reset() { |
| 35 | + v$.value.newValue.$model = props.value || '' |
| 36 | + v$.value.$reset() |
| 37 | +} |
| 38 | +
|
| 39 | +function startEdit() { |
| 40 | + reset() |
| 41 | + editing.value = true |
| 42 | +} |
| 43 | +
|
| 44 | +async function stopEdit() { |
| 45 | + if (!await v$.value.$validate()) { |
| 46 | + console.log(v$.value.$errors) |
| 47 | + return |
| 48 | + } |
| 49 | +
|
| 50 | + emit('change', v$.value.newValue.$model) |
| 51 | +
|
| 52 | + editing.value = false |
| 53 | +} |
| 54 | +
|
| 55 | +watchEffect(() => { |
| 56 | + if (inputRef.value) { |
| 57 | + inputRef.value.focus() |
| 58 | + } |
| 59 | +}) |
| 60 | +</script> |
| 61 | + |
| 62 | +<template> |
| 63 | + <div class="editable-title__container flex"> |
| 64 | + <div v-if="editing"> |
| 65 | + <input |
| 66 | + v-model="v$.newValue.$model" |
| 67 | + @keypress.enter="stopEdit" |
| 68 | + class="editable-title__value" |
| 69 | + :placeholder="placeholder" |
| 70 | + ref="inputRef" |
| 71 | + type="text" |
| 72 | + /> |
| 73 | + <ValidationError :errors="v$.newValue.$errors" /> |
| 74 | + </div> |
| 75 | + <h1 v-else class="w-max mb-0">{{ busy ? v$.newValue.$model : value }}</h1> |
| 76 | + |
| 77 | + <div class="mt-3"> |
| 78 | + <button v-if="busy" class="ml-2" disabled> |
| 79 | + <LoadingSpinner /> |
| 80 | + </button> |
| 81 | + <button v-else-if="editing" class="ml-2" @click="stopEdit" :disabled="!canSave"> |
| 82 | + <CheckIcon class="button__icon text-green hover:text-green-600" /> |
| 83 | + </button> |
| 84 | + <button v-else class="ml-2" @click="startEdit" :disabled="disabled"> |
| 85 | + <PencilIcon class="button__icon text-gray-400 hover:text-green" /> |
| 86 | + </button> |
| 87 | + |
| 88 | + <button v-if="!busy && editing" @click="cancelEdit" class="ml-2"> |
| 89 | + <XIcon class="button__icon text-red hover:text-red-600" /> |
| 90 | + </button> |
| 91 | + </div> |
| 92 | + </div> |
| 93 | +</template> |
| 94 | + |
| 95 | +<style> |
| 96 | +.editable-title__value { |
| 97 | + @apply bg-transparent text-3xl text-gray-600 border-b border-gray-400 w-full; |
| 98 | + @apply focus:outline-none focus:border-green focus:text-green; |
| 99 | +} |
| 100 | +
|
| 101 | +.editable-title__container .button__icon { |
| 102 | + @apply w-5 ml-1; |
| 103 | +} |
| 104 | +
|
| 105 | +.editable-title__container button:disabled, |
| 106 | +.editable-title__container button:disabled .button__icon { |
| 107 | + @apply text-gray-400 hover:no-underline; |
| 108 | +} |
| 109 | +</style> |
0 commit comments