From 431f14b35de235b96348c929c7e6801c6dd83b2d Mon Sep 17 00:00:00 2001 From: Shibo Lyu <github@of.sb> Date: Tue, 3 Sep 2024 15:28:39 +0800 Subject: [PATCH] fix: enter to send --- src/lib/components/InputFrame.svelte | 2 +- src/lib/components/RichTextInput.svelte | 14 ++++- .../RichTextInput/ClientInput.svelte | 61 ++++++++++++------- .../RichTextInput/keyboardSubmitModule.ts | 15 +++++ src/lib/richText.ts | 30 +++++++-- .../(app)/chats/[chatId]/ChatHeader.svelte | 2 +- .../(app)/chats/[chatId]/ChatInput.svelte | 19 +++--- 7 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 src/lib/components/RichTextInput/keyboardSubmitModule.ts diff --git a/src/lib/components/InputFrame.svelte b/src/lib/components/InputFrame.svelte index 6fdea09..9238959 100644 --- a/src/lib/components/InputFrame.svelte +++ b/src/lib/components/InputFrame.svelte @@ -7,7 +7,7 @@ <div class={tw( - 'flex items-center gap-1 rounded-md px-2 py-1.5 shadow-[inset_0_1px_2px_0_rgb(0_0_0/0.05)] ring-1 ring-ss-secondary', + 'flex items-center gap-1 rounded-md px-2 py-1.5 caret-accent-500 shadow-[inset_0_1px_2px_0_rgb(0_0_0/0.05)] ring-1 ring-ss-secondary', className )} > diff --git a/src/lib/components/RichTextInput.svelte b/src/lib/components/RichTextInput.svelte index 90ee199..eed5f57 100644 --- a/src/lib/components/RichTextInput.svelte +++ b/src/lib/components/RichTextInput.svelte @@ -1,12 +1,14 @@ <script lang="ts"> import { browser } from '$app/environment'; - import type { Delta } from 'typewriter-editor'; + import type { Delta, Editor } from 'typewriter-editor'; import InputFrame from '$lib/components/InputFrame.svelte'; import { tw } from '$lib/tw'; export let delta: Delta | null = null; export let plainText: string | undefined = undefined; + export let keyboardSubmitMethod: 'enter' | 'shiftEnter' | undefined = undefined; export let placeholder: string = ''; + export let editor: Editor | undefined; let className = ''; export { className as class }; @@ -24,7 +26,15 @@ <p>{placeholder}</p> </div> {:then Input} - <svelte:component this={Input} bind:delta bind:plainText {placeholder} on:keydown> + <svelte:component + this={Input} + bind:delta + bind:plainText + {placeholder} + bind:editor + {keyboardSubmitMethod} + on:keyboardSubmit + > <slot /> </svelte:component> {/await} diff --git a/src/lib/components/RichTextInput/ClientInput.svelte b/src/lib/components/RichTextInput/ClientInput.svelte index 55d25b4..5819380 100644 --- a/src/lib/components/RichTextInput/ClientInput.svelte +++ b/src/lib/components/RichTextInput/ClientInput.svelte @@ -1,33 +1,49 @@ <script lang="ts"> + import { createEventDispatcher } from 'svelte'; import { Delta, Editor, asRoot, h } from 'typewriter-editor'; + import { keyboardSubmit } from './keyboardSubmitModule'; export let delta: Delta = new Delta(); export let plainText: string | undefined = undefined; export let placeholder: string = ''; + export let keyboardSubmitMethod: 'enter' | 'shiftEnter' | undefined = undefined; - const editor = new Editor(); - editor.typeset.formats.add({ - name: 'underline', - selector: 'span[data-weblah-brt=underline]', - styleSelector: '[style*="text-decoration:underline"], [style*="text-decoration: underline"]', - commands: (editor) => () => editor.toggleTextFormat({ underline: true }), - shortcuts: 'Mod+U', - render: (attributes, children) => h('span', { 'data-weblah-brt': 'underline' }, children) - }); - editor.typeset.formats.add({ - name: 'strikethrough', - selector: 's', - styleSelector: - '[style*="text-decoration:line-through"], [style*="text-decoration: line-through"]', - commands: (editor) => () => editor.toggleTextFormat({ strikethrough: true }), - shortcuts: 'Mod+Shift+X', - render: (attributes, children) => h('s', null, children) - }); + const dispatch = createEventDispatcher<{ + keyboardSubmit: void; + }>(); - editor.on('change', () => { - delta = editor.getDelta(); - if (typeof plainText === 'string') plainText = editor.getText(); - }); + let editor: Editor; + + function initEditor() { + const modules = keyboardSubmitMethod + ? { keyboardSubmit: keyboardSubmit(() => dispatch('keyboardSubmit'), keyboardSubmitMethod) } + : undefined; + editor = new Editor({ modules }); + editor.typeset.formats.add({ + name: 'underline', + selector: 'span[data-weblah-brt=underline]', + styleSelector: '[style*="text-decoration:underline"], [style*="text-decoration: underline"]', + commands: (editor) => () => editor.toggleTextFormat({ underline: true }), + shortcuts: 'Mod+U', + render: (attributes, children) => h('span', { 'data-weblah-brt': 'underline' }, children) + }); + editor.typeset.formats.add({ + name: 'strikethrough', + selector: 's', + styleSelector: + '[style*="text-decoration:line-through"], [style*="text-decoration: line-through"]', + commands: (editor) => () => editor.toggleTextFormat({ strikethrough: true }), + shortcuts: 'Mod+Shift+X', + render: (attributes, children) => h('s', null, children) + }); + + editor.on('change', () => { + delta = editor.getDelta(); + if (typeof plainText === 'string') plainText = editor.getText(); + }); + } + + $: if (keyboardSubmitMethod || typeof keyboardSubmitMethod === 'undefined') initEditor(); $: editor.setDelta(delta ?? new Delta()); $: if (typeof plainText === 'string' && plainText !== editor.getText()) editor.setText(plainText); @@ -40,7 +56,6 @@ ? 'true' : undefined} data-weblah-placeholder={placeholder} - on:keydown role="textbox" tabindex="0" > diff --git a/src/lib/components/RichTextInput/keyboardSubmitModule.ts b/src/lib/components/RichTextInput/keyboardSubmitModule.ts new file mode 100644 index 0000000..59c4251 --- /dev/null +++ b/src/lib/components/RichTextInput/keyboardSubmitModule.ts @@ -0,0 +1,15 @@ +import type { ModuleInitializer } from 'typewriter-editor'; + +export const keyboardSubmit = function keyboardSubmit( + onSubmit: () => void, + method: 'enter' | 'shiftEnter' +): ModuleInitializer { + return () => ({ + commands: { + keyboardSubmit: onSubmit + }, + shortcuts: { + [method === 'enter' ? 'Enter' : 'Shift+Enter']: 'keyboardSubmit' + } + }); +}; diff --git a/src/lib/richText.ts b/src/lib/richText.ts index 69e9a4a..f7f278d 100644 --- a/src/lib/richText.ts +++ b/src/lib/richText.ts @@ -45,17 +45,22 @@ function deltaAttributesToBlahRichTextSpanAttributes( return isObjectEmpty(blahRichTextSpanAttributes) ? null : blahRichTextSpanAttributes; } -export function deltaToBlahRichText(delta: Delta): BlahRichText { +export function deltaToBlahRichText(delta: Delta, trim?: boolean = true): BlahRichText { const spans: BlahRichText = []; let lastText = ''; let lastAttributes: BlahRichTextSpanAttributes | null = null; let canonicalizedLastAttributes: string = 'null'; - function commitSpan() { - spans.push(lastAttributes === null ? lastText : [lastText, lastAttributes]); + function commitSpan(trim?: 'start' | 'end'): boolean { + const trimmedLastText = + trim === 'start' ? lastText.trimStart() : trim === 'end' ? lastText.trimEnd() : lastText; + if (trimmedLastText === '') return false; + spans.push(lastAttributes === null ? trimmedLastText : [trimmedLastText, lastAttributes]); + return true; } + let isFirstSpan = true; for (const op of delta.ops) { // Not sure in what cases op.insert would not be a string, but let's be safe if (typeof op.insert !== 'string') continue; @@ -68,12 +73,27 @@ export function deltaToBlahRichText(delta: Delta): BlahRichText { continue; } - commitSpan(); + const commited = commitSpan(trim && isFirstSpan ? 'start' : undefined); + if (commited) isFirstSpan = false; + lastText = op.insert; lastAttributes = attributes; canonicalizedLastAttributes = canonicalizedAttributes; } - commitSpan(); + const lastCommited = commitSpan(trim ? 'end' : undefined); + if (trim && !lastCommited) { + // The last segment is empty, so we need to trim the one before it + let lastSpan = spans.pop(); + if (!lastSpan) return spans; + + if (typeof lastSpan === 'string') { + lastSpan = lastSpan.trimEnd(); + if (lastSpan !== '') spans.push(lastSpan); + } else { + lastSpan[0] = lastSpan[0].trimEnd(); + if (lastSpan[0] !== '') spans.push(lastSpan); + } + } return spans; } diff --git a/src/routes/(app)/chats/[chatId]/ChatHeader.svelte b/src/routes/(app)/chats/[chatId]/ChatHeader.svelte index 1481987..0cc31e0 100644 --- a/src/routes/(app)/chats/[chatId]/ChatHeader.svelte +++ b/src/routes/(app)/chats/[chatId]/ChatHeader.svelte @@ -9,7 +9,7 @@ </script> <div - class="relative box-border flex min-h-[calc(3rem+1px)] w-full items-center gap-2 border-b border-ss-secondary bg-sb-primary p-2 shadow-sm" + class="relative z-10 box-border flex min-h-[calc(3rem+1px)] w-full items-center gap-2 border-b border-ss-secondary bg-sb-primary p-2 shadow-sm" > <Button href="/" class="rounded-full sm:hidden"> <svg diff --git a/src/routes/(app)/chats/[chatId]/ChatInput.svelte b/src/routes/(app)/chats/[chatId]/ChatInput.svelte index c34a735..53fe00b 100644 --- a/src/routes/(app)/chats/[chatId]/ChatInput.svelte +++ b/src/routes/(app)/chats/[chatId]/ChatInput.svelte @@ -4,27 +4,24 @@ import Button from '$lib/components/Button.svelte'; import RichTextInput from '$lib/components/RichTextInput.svelte'; import { deltaToBlahRichText } from '$lib/richText'; - import type { Delta } from 'typewriter-editor'; + import type { Delta, Editor } from 'typewriter-editor'; export let roomId: string; export let server: BlahChatServerConnection | undefined; + let editor: Editor | undefined; let delta: Delta; let plainText: string = ''; let form: HTMLFormElement | null = null; let sendDisabled = false; - function onInputKeydown(event: KeyboardEvent) { - console.log(event.key, event.shiftKey, event.isComposing, plainText); - if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { - event.preventDefault(); - form?.requestSubmit(); - } + function onKeyboardSubmit() { + editor?.select(null); + form?.requestSubmit(); } async function submit() { if (!server || plainText.trim() === '') return; - console.log('submit'); const brt = deltaToBlahRichText(delta); sendDisabled = true; @@ -44,7 +41,7 @@ plainText = ''; } - $: sendDisabled = !!server; + $: sendDisabled = !server; </script> <form @@ -70,11 +67,13 @@ <span class="sr-only">Attach</span> </Button> <RichTextInput + bind:editor bind:delta bind:plainText placeholder="Message" class="max-h-40 flex-1" - on:keydown={onInputKeydown} + keyboardSubmitMethod="enter" + on:keyboardSubmit={onKeyboardSubmit} /> <Button class="p-1.5" variant="primary" type="submit" disabled={sendDisabled}> <svg