mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-17 23:52:29 +00:00
refac
This commit is contained in:
parent
56de6dd792
commit
d8f27e745b
4 changed files with 152 additions and 98 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
<NoteMenu
|
||||
bind:autoFormat
|
||||
showAutoFormat={note?.write_access ?? false}
|
||||
onUploadFiles={note?.write_access ? uploadNoteFilesHandler : null}
|
||||
onDownload={(type) => {
|
||||
downloadHandler(type);
|
||||
|
|
@ -1356,105 +1360,108 @@ ${content}
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<RichTextInput
|
||||
bind:this={inputElement}
|
||||
bind:editor
|
||||
id={`note-${note.id}`}
|
||||
className="input-prose-sm px-0.5 flex-1 min-h-[12rem]"
|
||||
json={true}
|
||||
bind:value={note.data.content.json}
|
||||
html={editorHtml}
|
||||
documentId={`note:${note.id}`}
|
||||
collaboration={true}
|
||||
socket={$socket}
|
||||
user={$user}
|
||||
dragHandle={true}
|
||||
link={true}
|
||||
image={true}
|
||||
{files}
|
||||
placeholder={$i18n.t('Write something...')}
|
||||
editable={versionIdx === null && note?.write_access}
|
||||
onSelectionUpdate={({ editor }) => {
|
||||
const { from, to } = editor.state.selection;
|
||||
const selectedText = editor.state.doc.textBetween(from, to, ' ');
|
||||
{#key autoFormat}
|
||||
<RichTextInput
|
||||
{autoFormat}
|
||||
bind:this={inputElement}
|
||||
bind:editor
|
||||
id={`note-${note.id}`}
|
||||
className="input-prose-sm px-0.5 flex-1 min-h-[12rem]"
|
||||
json={true}
|
||||
bind:value={note.data.content.json}
|
||||
html={editorHtml}
|
||||
documentId={`note:${note.id}`}
|
||||
collaboration={true}
|
||||
socket={$socket}
|
||||
user={$user}
|
||||
dragHandle={true}
|
||||
link={true}
|
||||
image={true}
|
||||
{files}
|
||||
placeholder={$i18n.t('Write something...')}
|
||||
editable={versionIdx === null && note?.write_access}
|
||||
onSelectionUpdate={({ editor }) => {
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="min-w-[11.25rem]">
|
||||
{#if showAutoFormat}
|
||||
<Tooltip
|
||||
className="w-full"
|
||||
content={$i18n.t(
|
||||
'Format Markdown as you type and paste. Turn off to keep Markdown characters and paste plain text. Existing formatting is preserved.'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="select-none flex h-[1.6875rem] items-center gap-2 rounded-xl px-2 text-[0.8125rem] hover:bg-gray-50/60 dark:hover:bg-gray-800/60"
|
||||
>
|
||||
<Bold className="size-3.5 shrink-0" strokeWidth="2" />
|
||||
<span class="flex-1">{$i18n.t('Formatting')}</span>
|
||||
<Switch bind:state={autoFormat} ariaLabel={$i18n.t('Formatting')} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<hr />
|
||||
{/if}
|
||||
<DropdownSub contentClass="select-none z-50">
|
||||
<button
|
||||
slot="trigger"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue