Demo
Nothing saved yet.
Owner
Click the name, or Tab to it and press Enter.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/inline_edit.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/inline_edit.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/inline_edit.qwe
component InlineEditDemoLive() { state log = "Nothing saved yet." <div.demo-stack> <InlineEdit name="ie-demo-title" label="Board title" value="Launch plan" size="lg" required={true} on_change={(v) => { log = `Saved “${v}”.` }}/> <p.demo-note>{log}</p> </div>}view InlineEditDemo() = <div.demo-stack.demo-narrow> <InlineEditDemoLive/> <InlineEdit name="ie-demo-owner" label="Owner" value="Grace Hopper" show_label={true} hint="Click the name, or Tab to it and press Enter."/> <InlineEdit name="ie-demo-note" label="Note" placeholder="Add a note" size="sm" maxlength={80} buttons={false}/> <InlineEdit name="ie-demo-locked" label="Plan" value="Team" disabled={true} size="sm"/> </div>Source
ui/inline_edit.qwe
// InlineEdit: text shown as it is, which turns into a text box to change it: a// title, a name, a price in a table. A click, Enter or Space starts; Enter or// leaving the box saves, Escape puts it back as it was. What is saved goes in a// hidden input named `name`, so a plain <form> sends it.//// <InlineEdit name="title" label="Project name" value="Quillo website"/>// <InlineEdit name="note" label="Note" placeholder="Add a note" size="sm"/>// <InlineEdit label="Board title" value={board.title} size="lg" required={true}// on_change={(v) => { save_title(v) }}/>//// label: what the text is, for screen readers ("Project name" is heard as "Project// name: Quillo website, edit"); show_label: also show it above. value: the text// to start with. placeholder: shown when it is empty ("Empty"). required: an empty// text is not saved. maxlength: at most so many characters. buttons: shows a// save and a cancel button beside the box (true by default).// size: "sm", "md" (default) or "lg" (for a heading). name, hint, disabled and field// (the box's id, "inline-edit-" + name by default) as for every field.// on_change(value): inside one of your components, given the text when it is// saved changed.import ui/theme.qwe use ui_clslet inline_edit_css = css`.ui-inline-edit { --ui-inline-edit-font: 14px; display: grid; gap: 4px; min-width: 0; font: var(--ui-inline-edit-font)/1.4 var(--ui-font); color: var(--ui-fg); }.ui-inline-edit-sm { --ui-inline-edit-font: 13px; }.ui-inline-edit-lg { --ui-inline-edit-font: 22px; font-weight: 600; }.ui-inline-edit-label { font-size: 13px; font-weight: 500; }.ui-inline-edit-show { display: flex; align-items: center; gap: 8px; width: fit-content; max-width: 100%; min-height: 2.1em; margin: 0 -8px; padding: 2px 8px; border: 1px solid transparent; border-radius: var(--ui-radius-sm); background: transparent; color: inherit; font: inherit; text-align: left; cursor: text; transition: background .15s, border-color .15s;}.ui-inline-edit-show:hover { background: var(--ui-subtle); }.ui-inline-edit-show:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-inline-edit-show:disabled { cursor: default; background: transparent; }.ui-inline-edit-text { min-width: 0; overflow-wrap: anywhere; }.ui-inline-edit-empty { color: var(--ui-muted); font-style: italic; font-weight: 400; }.ui-inline-edit-pen { flex: none; width: .9em; height: .9em; color: var(--ui-muted); opacity: 0; transition: opacity .15s; }.ui-inline-edit-show:hover .ui-inline-edit-pen, .ui-inline-edit-show:focus-visible .ui-inline-edit-pen { opacity: 1; }.ui-inline-edit-form { display: flex; align-items: center; gap: 6px; margin: 0 -8px; }.ui-inline-edit-form input { flex: 1; min-width: 0; height: 2.1em; padding: 2px 7px; border: 1px solid var(--ui-accent); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: inherit; box-shadow: var(--ui-focus);}.ui-inline-edit-form input:focus { outline: none; }.ui-inline-edit-form input[aria-invalid="true"] { border-color: var(--ui-danger); }.ui-inline-edit-btn { display: inline-grid; place-items: center; flex: none; width: 30px; height: 30px; padding: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-muted); cursor: pointer;}.ui-inline-edit-btn svg { width: 14px; height: 14px; }.ui-inline-edit-btn:hover { color: var(--ui-fg); border-color: var(--ui-strong); }.ui-inline-edit-ok { color: var(--ui-accent); }.ui-inline-edit-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-inline-edit-hint, .ui-inline-edit-error { margin: 0; font-size: 13px; font-weight: 400; color: var(--ui-muted); }.ui-inline-edit-error { color: var(--ui-danger); }.ui-inline-edit-disabled .ui-inline-edit-show { opacity: .6; }@media (prefers-reduced-motion: reduce) { .ui-inline-edit-show, .ui-inline-edit-pen { transition: none; } }`component InlineEdit(name: str? = null, label: str = "Text", value: str = "", placeholder: str = "Empty", show_label: bool = false, required: bool = false, maxlength: int? = null, buttons: bool = true, size: str = "md", hint: str? = null, disabled: bool = false, field: str? = null, on_change = null, class: str? = null) { style global inline_edit_css state saved = value state draft = value state editing = false state problem = "" state said = "" let id = field ?? `inline-edit-${name ?? re.replace(lower(label), "[^a-z0-9]+", "-")}` let described = ui_cls([hint != null && `${id}-hint`, problem != "" && `${id}-error`]) // Runs once the page shows the change: puts the focus on `sel` inside `root`, this one. // Callers find `root` before they change anything, as the element the event came from // can be gone from the page once they have. let then_focus = (root, sel: str) => { js("requestAnimationFrame", (t = null) => { let el = root.querySelector(sel) if el != null { el.focus() if sel == "input" { el.select() } } }) } let start = (e) => { let root = e.target.closest(".ui-inline-edit") draft = saved problem = "" said = "" editing = true then_focus(root, "input") } let finish = (e, keep: bool) => { let root = e.target.closest(".ui-inline-edit") let text = trim(draft) if keep && required && text == "" { problem = `${label} cannot be empty.` } else { editing = false problem = "" if keep && text != saved { saved = text said = `${label} saved` on_change && on_change(text) } else { said = keep ? "" : "Edit cancelled" } then_focus(root, ".ui-inline-edit-show") } } <div class={ui_cls(["ui-inline-edit", size != "md" && `ui-inline-edit-${size}`, disabled && "ui-inline-edit-disabled", class])}> {show_label && <span.ui-inline-edit-label id={`${id}-label`}>{label}</span>} {editing ? <div.ui-inline-edit-form @focusout={(e) => { let form = e.target.closest(".ui-inline-edit-form") if editing && form != null && !(e.relatedTarget != null && form.contains(e.relatedTarget)) { finish(e, true) } }}> <input type="text" id={id} value={draft} maxlength={maxlength} aria-label={label} required={required} aria-invalid={problem != "" ? "true" : null} aria-describedby={described != "" ? described : null} enterkeyhint="done" @input={(e) => { draft = e.value problem = "" }} @keydown={(e) => { if e.key == "Enter" { e.preventDefault() finish(e, true) } if e.key == "Escape" { e.preventDefault() finish(e, false) } }}/> {buttons && <button.ui-inline-edit-btn.ui-inline-edit-ok type="button" aria-label="Save" @mousedown={(e) => { e.preventDefault() }} @click={(e) => { finish(e, true) }}> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8.5l3.2 3.2L13 4.8"/></svg> </button>} {buttons && <button.ui-inline-edit-btn type="button" aria-label="Cancel" @mousedown={(e) => { e.preventDefault() }} @click={(e) => { finish(e, false) }}> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg> </button>} </div> : <button.ui-inline-edit-show type="button" disabled={disabled} aria-describedby={hint != null ? `${id}-hint` : null} @click={start}> <span.ui-sr>{label}: </span> <span class={ui_cls(["ui-inline-edit-text", saved == "" && "ui-inline-edit-empty"])}>{saved != "" ? saved : placeholder}</span> <span.ui-sr>, edit</span> <svg.ui-inline-edit-pen viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> <path d="M11 2.5l2.5 2.5L6 12.5 3 13l.5-3z"/> </svg> </button>} {name != null && <input type="hidden" name={name} value={saved}/>} <span.ui-sr aria-live="polite">{said}</span> {hint != null && <p.ui-inline-edit-hint id={`${id}-hint`}>{hint}</p>} {problem != "" && <p.ui-inline-edit-error id={`${id}-error`} role="alert">{problem}</p>} </div>}Related
CharacterCounterA text box or textarea with a live 120 / 280 count that warns near the limit, as a hard stop or a soft one that blocks sending.5 views · 6 downloadsMarkdownEditorA Markdown textarea with a toolbar that wraps the selection (bold, italic, link, list) and a Preview tab rendered as the page will show it.5 views · 8 downloadsMaskedInputA box that shapes what is typed to a pattern (### ###-####, AA-00-AA) as it is typed, dropping what does not fit.6 views · 8 downloadsRichTextEditorA formatted-text editor (bold, italic, lists, links, headings) on contenteditable, its HTML kept in a hidden input a form sends.7 views · 5 downloadsTextAreaA box for several lines, with a live count against a limit and an option to grow with what is typed.5 views · 8 downloadsTextFieldA labelled input with help text, an error message and short text before or after it.5 views · 8 downloads
Thread
Thread 0 posts
On InlineEdit: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.