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