From d8f27e745bd31c89efa25cbe0f41bb0f70576fc5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 8 Sep 2026 13:25:16 -0400 Subject: [PATCH] refac --- .../components/common/RichTextInput.svelte | 26 ++- .../common/RichTextInput/Collaboration.ts | 11 + src/lib/components/notes/NoteEditor.svelte | 191 +++++++++--------- .../components/notes/Notes/NoteMenu.svelte | 22 ++ 4 files changed, 152 insertions(+), 98 deletions(-) diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index 7ea71d5921..37e4e9bdd3 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -239,6 +239,7 @@ }; export let richText = true; + export let autoFormat = true; export let dragHandle = false; export let link = false; export let image = false; @@ -334,6 +335,7 @@ let element: Element | null = null; let pendingUpdate = null; + let destroyed = false; const options = { throwOnError: false @@ -767,13 +769,15 @@ if (collaboration && editable && documentId && socket && user) { const { SocketIOCollaborationProvider } = await import('./RichTextInput/Collaboration'); + if (destroyed) return; provider = new SocketIOCollaborationProvider(documentId, socket, user, content); } + if (destroyed) return; editor = new Editor({ element: element, extensions: [ StarterKit.configure({ - link: link, + link: link ? { autolink: autoFormat, linkOnPaste: autoFormat } : false, code: false, // Disabled in favor of FixedCode (see workaround above) ...(messageInput ? { italic: false } : {}), // When rich text is on, ListKit + CodeBlockLowlight provide these. @@ -1003,8 +1007,8 @@ return false; }, handlePaste: (view, event) => { - // Force plain-text pasting when richText === false - if (!richText) { + // Paste literal text when automatic formatting is disabled. + if (!richText || !autoFormat) { // swallow HTML completely event.preventDefault(); const { state, dispatch } = view; @@ -1014,6 +1018,11 @@ '\n' ); + if (state.selection.$from.parent.type.spec.code) { + dispatch(state.tr.insertText(plainText).scrollIntoView()); + return true; + } + const lines = plainText.split('\n'); const nodes = []; @@ -1027,7 +1036,11 @@ }); const fragment = Fragment.fromArray(nodes); - dispatch(state.tr.replaceSelectionWith(fragment, false).scrollIntoView()); + dispatch( + state.tr + .replaceWith(state.selection.from, state.selection.to, fragment) + .scrollIntoView() + ); return true; // handled } @@ -1279,8 +1292,8 @@ floatingMenuElement.style.opacity = '0'; } }, - enableInputRules: richText, - enablePasteRules: richText + enableInputRules: richText && autoFormat, + enablePasteRules: richText && autoFormat }); provider?.setEditor(editor, () => ({ md: mdValue, html: htmlValue, json: jsonValue })); @@ -1291,6 +1304,7 @@ }); onDestroy(() => { + destroyed = true; if (pendingUpdate) { cancelAnimationFrame(pendingUpdate); } diff --git a/src/lib/components/common/RichTextInput/Collaboration.ts b/src/lib/components/common/RichTextInput/Collaboration.ts index 1ef1c063ae..c923ddface 100644 --- a/src/lib/components/common/RichTextInput/Collaboration.ts +++ b/src/lib/components/common/RichTextInput/Collaboration.ts @@ -1,6 +1,7 @@ import * as Y from 'yjs'; import { ySyncPlugin, + ySyncPluginKey, yCursorPlugin, yUndoPlugin, undo, @@ -11,6 +12,7 @@ import type { Socket } from 'socket.io-client'; import type { SessionUser } from '$lib/stores'; import { Editor, Extension } from '@tiptap/core'; import { keymap } from 'prosemirror-keymap'; +import { Plugin } from 'prosemirror-state'; import { tick } from 'svelte'; const USER_COLORS = [ @@ -62,6 +64,15 @@ export class SocketIOCollaborationProvider { if (!yXmlFragment) return []; const plugins = [ + new Plugin({ + filterTransaction: (tr) => { + // Preserve literal URLs received from another editor. + if (tr.getMeta(ySyncPluginKey)?.isChangeOrigin) { + tr.setMeta('preventAutolink', true); + } + return true; + } + }), ySyncPlugin(yXmlFragment), yUndoPlugin(), keymap({ diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte index 34093004e5..70ab909ef7 100644 --- a/src/lib/components/notes/NoteEditor.svelte +++ b/src/lib/components/notes/NoteEditor.svelte @@ -95,6 +95,7 @@ export let id: null | string = null; let editor = null; + let autoFormat = true; let note = null; const newNote = { @@ -174,6 +175,7 @@ (note?.data?.content?.md ? marked.parse(note.data.content.md) : ''); const init = async () => { + autoFormat = true; loading = true; const res = await getNoteById(localStorage.token, id).catch((error) => { toast.error(`${error}`); @@ -1208,6 +1210,8 @@ ${content} {/if} { downloadHandler(type); @@ -1356,105 +1360,108 @@ ${content} {/if} - { - const { from, to } = editor.state.selection; - const selectedText = editor.state.doc.textBetween(from, to, ' '); + {#key autoFormat} + { + const { from, to } = editor.state.selection; + const selectedText = editor.state.doc.textBetween(from, to, ' '); - if (selectedText.length === 0) { - selectedContent = null; - } else { - selectedContent = { - text: selectedText, - from: from, - to: to - }; - } - }} - onChange={(content) => { - lastLocalContentChangeAt = Date.now(); - note.data.content.html = content.html; - note.data.content.md = content.md; - - if (editor) { - wordCount = editor.storage.characterCount.words(); - charCount = editor.storage.characterCount.characters(); - } - }} - fileHandler={true} - onFileDrop={(currentEditor, files, pos) => { - files.forEach(async (file) => { - const fileItem = await inputFileHandler(file).catch((error) => { - return null; - }); - - if (fileItem?.type === 'image') { - // If the file is an image, insert it directly - currentEditor - .chain() - .insertContentAt(pos, { - type: 'image', - attrs: { - src: `data://${fileItem.id}` - } - }) - .focus() - .run(); + if (selectedText.length === 0) { + selectedContent = null; + } else { + selectedContent = { + text: selectedText, + from: from, + to: to + }; } - }); - }} - onFilePaste={() => {}} - on:paste={async (e) => { - e = e.detail.event || e; - const clipboardData = e.clipboardData || window.clipboardData; - console.log('Clipboard data:', clipboardData); + }} + onChange={(content) => { + lastLocalContentChangeAt = Date.now(); + note.data.content.html = content.html; + note.data.content.md = content.md; - if (clipboardData && clipboardData.items) { - console.log('Clipboard data items:', clipboardData.items); - for (const item of clipboardData.items) { - console.log('Clipboard item:', item); - if (item.type.indexOf('image') !== -1) { - const blob = item.getAsFile(); - const fileItem = await inputFileHandler(blob); + if (editor) { + wordCount = editor.storage.characterCount.words(); + charCount = editor.storage.characterCount.characters(); + } + }} + fileHandler={true} + onFileDrop={(currentEditor, files, pos) => { + files.forEach(async (file) => { + const fileItem = await inputFileHandler(file).catch((error) => { + return null; + }); - if (editor) { - editor - ?.chain() - .insertContentAt(editor.state.selection.$anchor.pos, { - type: 'image', - attrs: { - src: `data://${fileItem.id}` // Use data URI for the image - } - }) - .focus() - .run(); + if (fileItem?.type === 'image') { + // If the file is an image, insert it directly + currentEditor + .chain() + .insertContentAt(pos, { + type: 'image', + attrs: { + src: `data://${fileItem.id}` + } + }) + .focus() + .run(); + } + }); + }} + onFilePaste={() => {}} + on:paste={async (e) => { + e = e.detail.event || e; + const clipboardData = e.clipboardData || window.clipboardData; + console.log('Clipboard data:', clipboardData); + + if (clipboardData && clipboardData.items) { + console.log('Clipboard data items:', clipboardData.items); + for (const item of clipboardData.items) { + console.log('Clipboard item:', item); + if (item.type.indexOf('image') !== -1) { + const blob = item.getAsFile(); + const fileItem = await inputFileHandler(blob); + + if (editor) { + editor + ?.chain() + .insertContentAt(editor.state.selection.$anchor.pos, { + type: 'image', + attrs: { + src: `data://${fileItem.id}` // Use data URI for the image + } + }) + .focus() + .run(); + } + } else if (item?.kind === 'file') { + const file = item.getAsFile(); + await inputFileHandler(file); + e.preventDefault(); } - } else if (item?.kind === 'file') { - const file = item.getAsFile(); - await inputFileHandler(file); - e.preventDefault(); } } - } - }} - /> + }} + /> + {/key} {/if} diff --git a/src/lib/components/notes/Notes/NoteMenu.svelte b/src/lib/components/notes/Notes/NoteMenu.svelte index 1d3bab657f..fa36227e12 100644 --- a/src/lib/components/notes/Notes/NoteMenu.svelte +++ b/src/lib/components/notes/Notes/NoteMenu.svelte @@ -4,6 +4,8 @@ import Dropdown from '$lib/components/common/Dropdown.svelte'; import DropdownMenu from '$lib/components/common/DropdownMenu.svelte'; import DropdownSub from '$lib/components/common/DropdownSub.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; import Download from '$lib/components/icons/Download.svelte'; import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; @@ -12,6 +14,7 @@ import Pin from '$lib/components/icons/Pin.svelte'; import PinSlash from '$lib/components/icons/PinSlash.svelte'; import CloudArrowUp from '$lib/components/icons/CloudArrowUp.svelte'; + import Bold from '$lib/components/icons/Bold.svelte'; const i18n = getContext('i18n'); @@ -23,6 +26,8 @@ export let onPin = null; export let isPinned = false; export let onUploadFiles = null; + export let showAutoFormat = false; + export let autoFormat = true; export let onCopyLink = null; export let onCopyToClipboard = null; @@ -42,6 +47,23 @@
+ {#if showAutoFormat} + +
+ + {$i18n.t('Formatting')} + +
+
+
+ {/if}