ChatInput
A chat message box that grows as you type, sends on Enter and breaks the line on Shift+Enter; a plain form in a page.
Demo
Nothing sent yet.
Enter sends, Shift+Enter starts a new line. Below, one that posts a form:
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/chat_input.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/chat_input.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/chat_input.qwe
component ChatInputDemoLive() { state sent = [] state typed = 0 <div.demo-stack> <ChatInput id="chat-input-demo-live" on_send={(t) => { sent = sent | push(t) }} on_typing={() => { typed += 1 }} before={<button type="button" class="ui-chat-composer-send" aria-label="Attach" style="background: var(--ui-subtle); color: var(--ui-muted)"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.4 11.1l-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></svg> </button>}/> <p.demo-note>{len(sent) == 0 ? "Nothing sent yet." : `Sent: ${join(sent | map((s) => `"${s}"`), ", ")}.`}{typed > 0 ? ` Typing told ${typed} ${typed == 1 ? "time" : "times"}.` : ""}</p> </div>}view ChatInputDemo() = <div.demo-stack> <ChatInputDemoLive/> <p.demo-note>Enter sends, Shift+Enter starts a new line. Below, one that posts a form:</p> <ChatInput id="chat-input-demo-form" method="get" action="#chat_input" placeholder="Message support…" send_label="Send to support"> <input type="hidden" name="ticket" value="77"/> </ChatInput> </div>Source
ui/chat_input.qwe
// ChatInput: the box at the bottom of a chat. It grows with what is typed (up to a// few lines, then scrolls), Enter sends, Shift+Enter starts a new line, and the Send// button is off while it is empty. It is a real <textarea> named `name`, so in a page// a plain <form> sends it; inside one of your components, on_send(text) gets the text// and the box is emptied.//// <ChatInput action="/chat/42"><input type="hidden" name="thread" value="42"/></ChatInput>// <ChatInput on_send={(text) => { send(text) }} on_typing={() => { tell_them() }}/>//// name ("message"), placeholder ("Write a message…"), send_label ("Send"), rows: the// most lines it grows to before scrolling (5). disabled. id: the text box's id// ("ui-chat-composer"). method ("post") and action for the form in a page, with children// inside it (hidden inputs). autofocus: the box takes the focus on the page.// children can also be buttons at the left of the box (attach, emoji): put them in// `before`, a slot of HTML.// on_send(text): inside one of your components, called with the text trimmed.// on_typing(): called on each key while something is written, for a typing indicator.import ui/theme.qwe use ui_clslet chat_input_css = css`.ui-chat-composer { display: flex; align-items: flex-end; gap: 8px; min-width: 0; margin: 0; padding: 8px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.45 var(--ui-font); color: var(--ui-fg); }.ui-chat-composer-before { display: flex; align-items: center; gap: 2px; align-self: flex-end; height: 38px; }.ui-chat-composer-box { flex: 1; min-width: 0; min-height: 38px; max-height: calc(var(--ui-chat-composer-rows, 5) * 1.45em + 18px); margin: 0; padding: 8px 12px; resize: none; field-sizing: content; border: 1px solid transparent; border-radius: 19px; background: var(--ui-subtle); color: var(--ui-fg); font: 14px/1.45 var(--ui-font);}.ui-chat-composer-box::placeholder { color: var(--ui-muted); opacity: 1; }.ui-chat-composer-box:focus { outline: none; border-color: var(--ui-accent); background: var(--ui-surface); box-shadow: var(--ui-focus); }.ui-chat-composer-box:disabled { opacity: .55; cursor: not-allowed; }.ui-chat-composer-send { display: grid; place-items: center; flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%; background: var(--ui-accent); color: var(--ui-accent-fg); cursor: pointer; transition: background .15s, opacity .15s;}.ui-chat-composer-send svg { width: 18px; height: 18px; }.ui-chat-composer-send:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-chat-composer-send:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-chat-composer-send:disabled { opacity: .45; cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-chat-composer-send { transition: none; } }`component ChatInput(name: str = "message", placeholder: str = "Write a message…", send_label: str = "Send", rows: int = 5, action: str? = null, method: str = "post", disabled: bool = false, autofocus: bool = false, id: str = "ui-chat-composer", before = null, on_send = null, on_typing = null, class: str? = null, children) { style global chat_input_css state draft = "" state box = null let ready = trim(draft) != "" let send = () => { if ready && on_send != null { on_send(trim(draft)) draft = "" box.value = "" box.focus() } } <form class={ui_cls(["ui-chat-composer", class])} method={method} action={action} style={`--ui-chat-composer-rows: ${rows}`} @submit={on_send != null ? send : null}> {children} {before != null && <div.ui-chat-composer-before>{before}</div>} <label.ui-sr for={id}>{placeholder}</label> <textarea.ui-chat-composer-box id={id} name={name} rows="1" placeholder={placeholder} required={true} disabled={disabled} autofocus={autofocus} enterkeyhint="send" autocomplete="off" @mount={(el) => { box = el }} @input={(e) => { draft = e.value if trim(e.value) != "" && on_typing != null { on_typing() } }} @keydown={(e) => { if e.key == "Enter" && !e.shiftKey && !e.isComposing { e.preventDefault() if trim(e.target.value) != "" { if on_send != null { send() } else { e.target.form.requestSubmit() } } } }}></textarea> <button.ui-chat-composer-send type="submit" aria-label={send_label} title={send_label} disabled={disabled || !ready}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg> </button> </form>}Related
PostComposerWrite a post with a character ring and count, attach files, and post with a button or Ctrl+Enter; a plain form in a page.6 views · 6 downloadsReplyBoxA reply box that is one line until focused, then grows and shows Post and Cancel; it posts a form or calls back.6 views · 5 downloadsMentionInputA textarea that offers people when @ is typed, puts @handle in, and sends the handles mentioned beside the text.5 views · 6 downloadsMessageBubbleOne chat message, yours on the right or theirs on the left, with the time and sent, delivered, read or failed ticks.6 views · 8 downloadsReactionsEmoji reaction chips with counts, yours marked, and an add button that opens a row of emoji to pick from.6 views · 5 downloadsTypingIndicator"Ana is typing…" with pulsing dots, naming one, two or more people, and nothing at all when nobody is.6 views · 6 downloads
Thread
Thread 0 posts
On ChatInput: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.