From e3fbce8ecefe9c0976c15b53268812cc3fa18864 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 29 Aug 2026 03:14:57 +0000 Subject: [PATCH] =?UTF-8?q?Fix=20slug=20input:=20free=20typing=20with=20li?= =?UTF-8?q?ve=20space=E2=86=92hyphen=20and=20lowercasing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-filtering through slugify ate hyphens and spaces (trailing hyphens are stripped) and jumped the cursor to the end on mid-text edits. Now only spaces→hyphens and lowercasing happen oninput (length-preserving, cursor stays put); the full slugify runs at commit before the server call, which keeps its own validation. --- frontend/src/StructureEditor.vue | 12 ++++++++---- frontend/src/StructureTree.vue | 25 +++++++++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/frontend/src/StructureEditor.vue b/frontend/src/StructureEditor.vue index ddc08c8..70fbc17 100644 --- a/frontend/src/StructureEditor.vue +++ b/frontend/src/StructureEditor.vue @@ -106,7 +106,8 @@ function discardPending() { async function commitPending() { const node = pending.value if (!node) return - // Empty slug: derive one from the title (transliterated to ASCII). + // The typed slug is slugified at commit; empty derives one from the + // title (transliterated to ASCII). const slug = slugify(node.slug.trim()) || slugify(node.title) if (!slug) { return @@ -216,10 +217,13 @@ function onTitleInput(node, ev) { }) } -// The slug inputs are filtered as you type (StructureTree onSlugInput, -// see slugify.js); the server re-validates and its reason is shown. +// Slug inputs are typed freely (spaces become hyphens live, see +// StructureTree onSlugInput); the value is slugified here at commit +// (blur/Enter) before talking to the server, which re-validates (e.g. +// reserved names) and its reason is shown. async function commitSlug(node, ev) { - const slug = ev.target.value.trim() + const slug = slugify(ev.target.value.trim()) + ev.target.value = slug if (slug === node.slug) return const parent = node.path.split('/').slice(0, -1).join('/') // Empty slug at top level = the front page (path ""). diff --git a/frontend/src/StructureTree.vue b/frontend/src/StructureTree.vue index b8cf74c..fe84c10 100644 --- a/frontend/src/StructureTree.vue +++ b/frontend/src/StructureTree.vue @@ -34,16 +34,17 @@ const props = defineProps({ const handlers = inject('structureHandlers') -// Live-filter the slug inputs as they are typed (oninput): invalid -// characters are simply not accepted, spaces become hyphens and unicode -// folds to ASCII (see slugify.js). Existing rows commit on change, the -// pending row is v-modeled. -function onSlugInput(ev) { - ev.target.value = slugify(ev.target.value) -} - -function onPendingSlugInput(element, ev) { - element.slug = slugify(ev.target.value) +// Slug inputs accept free typing; the only live rewrites are turning +// spaces into hyphens and lowercasing (both keep the length for ASCII, +// so the cursor stays put). Anything else (unicode folding, stripping, +// collapsing) is left for commit time, where the value is run through +// slugify before talking to the server (StructureEditor). `element` is +// the pending row (v-modeled), null for existing rows (plain :value +// binding, read back on commit). +function onSlugInput(element, ev) { + const v = ev.target.value.replace(/\s/g, '-').toLowerCase() + ev.target.value = v + if (element) element.slug = v } // Focus the title input of a fresh pending row. @@ -113,7 +114,7 @@ function onEnd() { class="edit slug-edit" :placeholder="slugify(element.title)" title="Slug (last path segment) — empty: derived from the title" - @input="onPendingSlugInput(element, $event)" + @input="onSlugInput(element, $event)" @keyup.enter="handlers.commitPending()" @keyup.esc="handlers.discardPending()" /> @@ -135,7 +136,7 @@ function onEnd() { :value="element.slug" placeholder="front page" title="Slug (last path segment) — renames move the whole subtree. Empty at top level = front page" - @input="onSlugInput" + @input="onSlugInput(null, $event)" @change="handlers.commitSlug(element, $event)" />