فهرست منبع

refac/fix: rich text input in ff

Timothy Jaeryang Baek 3 ماه پیش
والد
کامیت
e84c521177

+ 29 - 16
src/lib/components/channel/MessageInput.svelte

@@ -31,6 +31,7 @@
 	let content = '';
 	let files = [];
 
+	let chatInputElement;
 	let filesInputElement;
 	let inputFiles;
 
@@ -287,8 +288,9 @@
 
 		await tick();
 
-		const chatInputElement = document.getElementById(`chat-input-${id}`);
-		chatInputElement?.focus();
+		if (chatInputElement) {
+			chatInputElement.focus();
+		}
 	};
 
 	$: if (content) {
@@ -297,9 +299,10 @@
 
 	onMount(async () => {
 		window.setTimeout(() => {
-			const chatInput = document.getElementById(`chat-input-${id}`);
-			chatInput?.focus();
-		}, 0);
+			if (chatInputElement) {
+				chatInputElement.focus();
+			}
+		}, 100);
 
 		window.addEventListener('keydown', handleKeyDown);
 		await tick();
@@ -402,7 +405,10 @@
 						recording = false;
 
 						await tick();
-						document.getElementById(`chat-input-${id}`)?.focus();
+
+						if (chatInputElement) {
+							chatInputElement.focus();
+						}
 					}}
 					onConfirm={async (data) => {
 						const { text, filename } = data;
@@ -410,7 +416,10 @@
 						recording = false;
 
 						await tick();
-						document.getElementById(`chat-input-${id}`)?.focus();
+
+						if (chatInputElement) {
+							chatInputElement.focus();
+						}
 					}}
 				/>
 			{:else}
@@ -485,17 +494,21 @@
 								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
 							>
 								<RichTextInput
-									bind:value={content}
-									id={`chat-input-${id}`}
+									bind:this={chatInputElement}
+									json={true}
 									messageInput={true}
-									shiftEnter={!$mobile ||
-										!(
-											'ontouchstart' in window ||
-											navigator.maxTouchPoints > 0 ||
-											navigator.msMaxTouchPoints > 0
-										)}
-									{placeholder}
+									shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
+										(!$mobile ||
+											!(
+												'ontouchstart' in window ||
+												navigator.maxTouchPoints > 0 ||
+												navigator.msMaxTouchPoints > 0
+											))}
 									largeTextAsFile={$settings?.largeTextAsFile ?? false}
+									onChange={(e) => {
+										const { md } = e;
+										content = md;
+									}}
 									on:keydown={async (e) => {
 										e = e.detail.event;
 										const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac

+ 0 - 1
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -385,7 +385,6 @@
 	};
 
 	onMount(async () => {
-		console.log('codeblock', lang, code);
 		if (token) {
 			onUpdate(token);
 		}

+ 62 - 67
src/lib/components/common/RichTextInput.svelte

@@ -176,35 +176,34 @@
 		if (!editor) return;
 		text = text.replaceAll('\n\n', '\n');
 		const { state, view } = editor;
+		const { schema, tr } = state;
 
 		if (text.includes('\n')) {
 			// Multiple lines: make paragraphs
-			const { schema, tr } = state;
 			const lines = text.split('\n');
-
 			// Map each line to a paragraph node (empty lines -> empty paragraph)
 			const nodes = lines.map((line) =>
 				schema.nodes.paragraph.create({}, line ? schema.text(line) : undefined)
 			);
-
 			// Create a document fragment containing all parsed paragraphs
 			const fragment = Fragment.fromArray(nodes);
-
 			// Replace current selection with these paragraphs
 			tr.replaceSelectionWith(fragment, false /* don't select new */);
-
-			// You probably want to move the cursor after the inserted content
-			// tr.setSelection(Selection.near(tr.doc.resolve(tr.selection.to)));
-
 			view.dispatch(tr);
 		} else if (text === '') {
-			// Empty: delete selection or paragraph
-			editor.commands.clearContent();
+			// Empty: replace with empty paragraph using tr
+			const emptyParagraph = schema.nodes.paragraph.create();
+			tr.replaceSelectionWith(emptyParagraph, false);
+			view.dispatch(tr);
 		} else {
-			editor.commands.setContent(editor.state.schema.text(text));
+			// Single line: create paragraph with text
+			const paragraph = schema.nodes.paragraph.create({}, schema.text(text));
+			tr.replaceSelectionWith(paragraph, false);
+			view.dispatch(tr);
 		}
 
 		selectNextTemplate(editor.view.state, editor.view.dispatch);
+		focus();
 	};
 
 	export const replaceVariables = (variables) => {
@@ -253,7 +252,7 @@
 	export const focus = () => {
 		if (editor) {
 			editor.view.focus();
-			// Scroll to the top of the editor
+			// Scroll to the current selection
 			editor.view.dispatch(editor.view.state.tr.scrollIntoView());
 		}
 	};
@@ -326,11 +325,7 @@
 			setTimeout(() => {
 				const templateFound = selectNextTemplate(editor.view.state, editor.view.dispatch);
 				if (!templateFound) {
-					// If no template found, set cursor at the end
-					const endPos = editor.view.state.doc.content.size;
-					editor.view.dispatch(
-						editor.view.state.tr.setSelection(TextSelection.create(editor.view.state.doc, endPos))
-					);
+					editor.commands.focus('end');
 				}
 			}, 0);
 		}
@@ -339,7 +334,11 @@
 	onMount(async () => {
 		let content = value;
 
-		if (!json) {
+		if (json) {
+			if (!content) {
+				content = html ? html : null;
+			}
+		} else {
 			if (preserveBreaks) {
 				turndownService.addRule('preserveBreaks', {
 					filter: 'br', // Target <br> elements
@@ -370,10 +369,6 @@
 				// Usage example
 				content = await tryParse(value);
 			}
-		} else {
-			if (html && !content) {
-				content = html;
-			}
 		}
 
 		console.log('content', content);
@@ -417,40 +412,34 @@
 				// force re-render so `editor.isActive` works as expected
 				editor = editor;
 
-				html = editor.getHTML();
+				const htmlValue = editor.getHTML();
+				const jsonValue = editor.getJSON();
+				let mdValue = turndownService
+					.turndown(
+						htmlValue
+							.replace(/<p><\/p>/g, '<br/>')
+							.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
+					)
+					.replace(/\u00a0/g, ' ');
 
 				onChange({
-					html: editor.getHTML(),
-					json: editor.getJSON(),
-					md: turndownService
-						.turndown(
-							editor
-								.getHTML()
-								.replace(/<p><\/p>/g, '<br/>')
-								.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
-						)
-						.replace(/\u00a0/g, ' ')
+					html: htmlValue,
+					json: jsonValue,
+					md: mdValue
 				});
 
 				if (json) {
-					value = editor.getJSON();
+					value = jsonValue;
 				} else {
-					if (!raw) {
-						let newValue = turndownService
-							.turndown(
-								editor
-									.getHTML()
-									.replace(/<p><\/p>/g, '<br/>')
-									.replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
-							)
-							.replace(/\u00a0/g, ' ');
-
+					if (raw) {
+						value = htmlValue;
+					} else {
 						if (!preserveBreaks) {
-							newValue = newValue.replace(/<br\/>/g, '');
+							mdValue = mdValue.replace(/<br\/>/g, '');
 						}
 
-						if (value !== newValue) {
-							value = newValue;
+						if (value !== mdValue) {
+							value = mdValue;
 
 							// check if the node is paragraph as well
 							if (editor.isActive('paragraph')) {
@@ -459,8 +448,6 @@
 								}
 							}
 						}
-					} else {
-						value = editor.getHTML();
 					}
 				}
 			},
@@ -609,36 +596,44 @@
 	const onValueChange = () => {
 		if (!editor) return;
 
+		const jsonValue = editor.getJSON();
+		const htmlValue = editor.getHTML();
+		let mdValue = turndownService
+			.turndown(
+				(preserveBreaks ? htmlValue.replace(/<p><\/p>/g, '<br/>') : htmlValue).replace(
+					/ {2,}/g,
+					(m) => m.replace(/ /g, '\u00a0')
+				)
+			)
+			.replace(/\u00a0/g, ' ');
+
+		if (value === '') {
+			editor.commands.clearContent(); // Clear content if value is empty
+			selectTemplate();
+
+			return;
+		}
+
 		if (json) {
-			if (JSON.stringify(value) !== JSON.stringify(editor.getJSON())) {
+			if (JSON.stringify(value) !== JSON.stringify(jsonValue)) {
 				editor.commands.setContent(value);
 				selectTemplate();
 			}
 		} else {
 			if (raw) {
-				if (value !== editor.getHTML()) {
+				if (value !== htmlValue) {
 					editor.commands.setContent(value);
 					selectTemplate();
 				}
 			} else {
-				if (
-					value !==
-					turndownService
-						.turndown(
-							(preserveBreaks
-								? editor.getHTML().replace(/<p><\/p>/g, '<br/>')
-								: editor.getHTML()
-							).replace(/ {2,}/g, (m) => m.replace(/ /g, '\u00a0'))
-						)
-						.replace(/\u00a0/g, ' ')
-				) {
-					preserveBreaks
-						? editor.commands.setContent(value)
-						: editor.commands.setContent(
-								marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), {
+				if (value !== mdValue) {
+					editor.commands.setContent(
+						preserveBreaks
+							? value
+							: marked.parse(value.replaceAll(`\n<br/>`, `<br/>`), {
 									breaks: false
 								})
-							); // Update editor content
+					);
 
 					selectTemplate();
 				}