119 lines
2.4 KiB
Vue
119 lines
2.4 KiB
Vue
<script setup lang="ts">
|
|
import type { Node } from "~/types"
|
|
|
|
const props = defineProps<{
|
|
node: Node
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
rename: [title: string]
|
|
}>()
|
|
|
|
const editing = ref(false)
|
|
const val = ref(props.node.event.title)
|
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
|
|
watch(
|
|
() => [props.node.id, props.node.event.title],
|
|
() => {
|
|
val.value = props.node.event.title
|
|
}
|
|
)
|
|
|
|
const commit = () => {
|
|
// Enter submits the form AND triggers blur as the input unmounts — guard the
|
|
// re-entry, and only emit when the title actually changed (no no-op rename).
|
|
if (!editing.value) return
|
|
editing.value = false
|
|
const v = val.value.trim()
|
|
if (v && v !== props.node.event.title) emit("rename", v)
|
|
else val.value = props.node.event.title
|
|
}
|
|
|
|
const cancel = () => {
|
|
val.value = props.node.event.title
|
|
editing.value = false
|
|
}
|
|
|
|
const startEditing = async () => {
|
|
editing.value = true
|
|
await nextTick()
|
|
inputRef.value?.focus()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<form v-if="editing" class="title-form" @submit.prevent="commit">
|
|
<input
|
|
ref="inputRef"
|
|
v-model="val"
|
|
class="title-input"
|
|
@blur="commit"
|
|
@keydown.esc="cancel"
|
|
>
|
|
</form>
|
|
<button
|
|
v-else
|
|
type="button"
|
|
class="title-edit"
|
|
:title="$t('voyage.node.rename')"
|
|
@click="startEditing"
|
|
>
|
|
<span class="title-text">{{ node.event.title }}</span>
|
|
<Icon name="carbon:edit" size="1rem" class="title-pen" />
|
|
</button>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.title-form {
|
|
display: contents;
|
|
}
|
|
|
|
.title-input,
|
|
.title-edit {
|
|
font-family: var(--font-display);
|
|
font-weight: var(--weight-semibold);
|
|
font-size: 1.85rem;
|
|
line-height: 1.05;
|
|
color: var(--primary-color);
|
|
text-align: center;
|
|
}
|
|
|
|
.title-input {
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
background: var(--surface-input);
|
|
border: 2px solid var(--color-6);
|
|
border-radius: var(--radius-md);
|
|
padding: 2px 10px;
|
|
outline: none;
|
|
}
|
|
|
|
.title-edit {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
max-width: 100%;
|
|
border: 2px solid transparent;
|
|
border-radius: var(--radius-md);
|
|
background: transparent;
|
|
cursor: text;
|
|
padding: 2px 10px;
|
|
}
|
|
|
|
.title-text {
|
|
text-shadow: 0 0 8px var(--neutral-0);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.title-pen {
|
|
color: var(--neutral-40);
|
|
flex-shrink: 0;
|
|
opacity: 0;
|
|
transition: opacity 140ms ease;
|
|
}
|
|
|
|
.title-edit:hover .title-pen {
|
|
opacity: 1;
|
|
}
|
|
</style>
|