m's request: typing "Mallorca 2026" into the new-item Title should
suggest "mallorca-2026" in the Slug field. Surface-only — server still
validates per itemwrite (^[a-z0-9][a-z0-9-]{0,62}$).
Inline ~25-line vanilla-JS handler on /new:
- normalize('NFD') + strip combining diacritics → ä→a, ñ→n, São→sao
- ß → ss (German sharp-s)
- non-alphanum run → single hyphen
- trim leading/trailing hyphens, collapse runs of hyphens
- slice(0, 63) to match the validator's length cap
Behavioural contract per m's brief:
- Slug syncs from Title on every Title input event UNTIL the user
edits the slug manually. After that the slug field is locked in
(`slug.dataset.userEdited === '1'`).
- A pre-filled slug counts as user-edited too — defensive against any
future flow that lands on /new with a slug already populated.
Scoped to /new only — the detail-page edit form intentionally keeps
manual slug control because auto-sync there would silently rename
existing items.
Template additions:
- Added `id="new-item-form"`, `id="new-title"`, `id="new-slug"` to the
form + inputs so the script can grab them by id rather than name
(name="slug" exists on the detail page too and we don't want to
cross-bind).
Test (web/new_form_test.go):
- TestNewFormHasSlugSuggestScript — asserts the inline script's
signature fragments (`normalize('NFD')`, `replace(/ß/g, 'ss')`,
`slice(0, 63)`, `dataset.userEdited`, the input ids) all render on
/new. Guards against a "harmless cleanup" pass silently stripping
the script.
Manual verification: typing "Mallorca 2026" updates slug to
"mallorca-2026"; typing in the slug field locks further sync.
Full web suite green.
70 lines
2.6 KiB
Cheetah
70 lines
2.6 KiB
Cheetah
{{define "content"}}
|
|
<h1>New item</h1>
|
|
<p class="meta">Suggested parent: <strong>{{if .Parent}}{{.Parent.PrimaryPath}}{{else}}(root){{end}}</strong></p>
|
|
|
|
<form method="post" action="/new" class="edit" id="new-item-form">
|
|
<input type="hidden" name="kind" value="project">
|
|
<label>Title <input id="new-title" name="title" required></label>
|
|
<label>Slug <input id="new-slug" name="slug" required pattern="[^.]+" placeholder="lowercase, no dots"></label>
|
|
<label>Parents <small class="muted">(hold Ctrl/Cmd to pick multiple — leave empty for a root item)</small>
|
|
<select name="parent_ids" multiple size="6">
|
|
{{range .ParentOptions}}
|
|
<option value="{{.ID}}" {{if and $.Parent (eq .ID $.Parent.ID)}}selected{{end}}>{{.Path}}</option>
|
|
{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Status
|
|
<select name="status">
|
|
{{range $opt := .StatusOptions}}<option value="{{$opt}}">{{$opt}}</option>{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Tags
|
|
<input name="tags" placeholder="comma-separated, e.g. work, dev">
|
|
</label>
|
|
<label>Management
|
|
<input name="management" placeholder="comma-separated: self, mai, external">
|
|
</label>
|
|
<label>Content
|
|
<textarea name="content_md" rows="10"></textarea>
|
|
</label>
|
|
<div class="actions">
|
|
<button type="submit">Create</button>
|
|
<a class="cancel" href="{{if .Parent}}/i/{{.Parent.PrimaryPath}}{{else}}/{{end}}">Cancel</a>
|
|
</div>
|
|
</form>
|
|
<script>
|
|
// Phase 5k: auto-suggest a kebab-case slug from Title as the user types.
|
|
// Strips diacritics (Müller → muller, São → sao), German ß → ss, collapses
|
|
// any non-alphanumeric run into a single hyphen, trims edge hyphens, caps
|
|
// at the 63-char limit the itemwrite validator enforces. Once the user
|
|
// edits the slug manually, the sync stops — typing in Title no longer
|
|
// clobbers their override. A pre-filled slug also counts as user-edited
|
|
// (rare for /new but defensive).
|
|
(function() {
|
|
var title = document.getElementById('new-title');
|
|
var slug = document.getElementById('new-slug');
|
|
if (!title || !slug) return;
|
|
function kebab(s) {
|
|
return s
|
|
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
.toLowerCase()
|
|
.replace(/ß/g, 'ss')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.replace(/-{2,}/g, '-')
|
|
.slice(0, 63);
|
|
}
|
|
if (slug.value && slug.value.length > 0) {
|
|
slug.dataset.userEdited = '1';
|
|
}
|
|
title.addEventListener('input', function() {
|
|
if (slug.dataset.userEdited === '1') return;
|
|
slug.value = kebab(title.value);
|
|
});
|
|
slug.addEventListener('input', function() {
|
|
slug.dataset.userEdited = '1';
|
|
});
|
|
})();
|
|
</script>
|
|
{{end}}
|