diff --git a/package.json b/package.json index 5a06a546..91a770e1 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,17 @@ "@codemirror/view": "^6.36.4", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/forms": "^0.5.10", + "@tiptap/core": "^2.14.0", + "@tiptap/extension-bubble-menu": "^2.14.0", + "@tiptap/extension-dropcursor": "^2.14.0", + "@tiptap/extension-image": "^2.14.0", + "@tiptap/extension-link": "^2.14.0", + "@tiptap/extension-placeholder": "^2.14.0", + "@tiptap/extension-task-item": "^2.14.0", + "@tiptap/extension-task-list": "^2.14.0", + "@tiptap/extension-typography": "^2.14.0", + "@tiptap/starter-kit": "^2.14.0", + "@tiptap/suggestion": "^2.14.0", "@tsconfig/svelte": "^5.0.4", "@types/chrome": "^0.0.308", "@types/color": "^4.2.0", @@ -92,6 +103,7 @@ "mathjs": "^14.4.0", "million": "^3.1.11", "motion": "^12.4.12", + "motion-start": "^0.1.15", "postcss": "^8.5.3", "react": "17", "react-best-gradient-color-picker": "3.0.11", @@ -99,6 +111,7 @@ "rss-parser": "^3.13.0", "sortablejs": "^1.15.6", "svelte": "^5.22.6", + "svelte-hero-icons": "^5.2.0", "typescript": "^5.8.2", "uuid": "^11.1.0", "vite": "^6.2.1", diff --git a/src/pageState.js b/src/pageState.js index 8ee66117..5ca7cfd6 100644 --- a/src/pageState.js +++ b/src/pageState.js @@ -222,5 +222,23 @@ window.addEventListener("message", (event) => { }); document.dispatchEvent(keyboardEvent); + } else if (event.data.type === "ckeditorSetData") { + // Handle CKEditor data setting + const { editorId, content } = event.data; + + if (window.CKEDITOR && window.CKEDITOR.instances && window.CKEDITOR.instances[editorId]) { + window.CKEDITOR.instances[editorId].setData(content); + } else { + console.warn(`[pageState] CKEditor instance '${editorId}' not found`); + } + } else if (event.data.type === "ckeditorGetData") { + const { editorId } = event.data; + if (window.CKEDITOR && window.CKEDITOR.instances && window.CKEDITOR.instances[editorId]) { + const data = window.CKEDITOR.instances[editorId].getData(); + window.postMessage({ + type: "ckeditorGetDataResponse", + data, + }, "*"); + } } }); diff --git a/src/plugins/built-in/customMessageEditor/BetterEditor.svelte b/src/plugins/built-in/customMessageEditor/BetterEditor.svelte new file mode 100644 index 00000000..c3e45fb7 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/BetterEditor.svelte @@ -0,0 +1,65 @@ + + +
+ +
\ No newline at end of file diff --git a/src/plugins/built-in/customMessageEditor/Editor/Editor.svelte b/src/plugins/built-in/customMessageEditor/Editor/Editor.svelte new file mode 100644 index 00000000..a0c513cb --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/Editor.svelte @@ -0,0 +1,154 @@ + + +
+
+
+ +
+ + \ No newline at end of file diff --git a/src/plugins/built-in/customMessageEditor/Editor/EditorOverrideStyles.css b/src/plugins/built-in/customMessageEditor/Editor/EditorOverrideStyles.css new file mode 100644 index 00000000..3b6bcb00 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/EditorOverrideStyles.css @@ -0,0 +1,398 @@ +.editor-prose { + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + 'Noto Sans', + sans-serif !important; + line-height: 1.6 !important; + color: #374151 !important; + font-size: 14px !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; + box-sizing: border-box !important; + + .dark & * { + color: #d1d5db !important; + } + + * { + color: #374151 !important; + } + + h1, + h2, + h3, + h4, + h5, + h6, + p, + ul, + ol { + width: 100% !important; + min-width: 2px !important; + box-sizing: border-box !important; + } + + h1 { + font-size: 1.5rem !important; + font-weight: 700 !important; + margin: 0.75rem 0 0.5rem 0 !important; + line-height: 1.3 !important; + color: #111827 !important; + padding: 0 !important; + border: none !important; + background: none !important; + text-shadow: none !important; + + .dark & { + color: #f9fafb !important; + } + } + + h2 { + font-size: 1.25rem !important; + font-weight: 600 !important; + margin: 0.6rem 0 0.4rem 0 !important; + line-height: 1.4 !important; + color: #1f2937 !important; + padding: 0 !important; + border: none !important; + background: none !important; + text-shadow: none !important; + + .dark & { + color: #e5e7eb !important; + } + } + + h3 { + font-size: 1.125rem !important; + font-weight: 600 !important; + margin: 0.5rem 0 0.3rem 0 !important; + line-height: 1.4 !important; + color: #374151 !important; + padding: 0 !important; + border: none !important; + background: none !important; + text-shadow: none !important; + + .dark & { + color: #d1d5db !important; + } + } + + p { + margin: 0.4rem 0 !important; + line-height: 1.6 !important; + font-size: 0.875rem !important; + padding: 0 !important; + border: none !important; + background: none !important; + color: inherit !important; + text-shadow: none !important; + } + + ul { + margin: 0.5rem 0 !important; + padding-left: 1.25rem !important; + list-style-type: disc !important; + border: none !important; + background: none !important; + + ul { + list-style-type: circle !important; + + ul { + list-style-type: square !important; + } + } + + &[data-type='taskList'] { + list-style: none !important; + padding: 0 !important; + margin: 0.5rem 0 !important; + border: none !important; + background: none !important; + + p { + margin: 0 !important; + font-size: 0.875rem !important; + line-height: 1.5 !important; + padding: 0 !important; + border: none !important; + background: none !important; + color: inherit !important; + text-shadow: none !important; + } + + li { + display: flex !important; + align-items: flex-start !important; + margin: 0.25rem 0 !important; + padding: 0 !important; + border: none !important; + background: none !important; + color: inherit !important; + text-shadow: none !important; + list-style: none !important; + + > label { + flex: 0 0 auto !important; + margin-right: 0.5rem !important; + margin-top: 0.125rem !important; + user-select: none !important; + padding: 0 !important; + border: none !important; + background: none !important; + + input[type='checkbox'] { + width: 1rem !important; + height: 1rem !important; + border-radius: 0.25rem !important; + border: 2px solid #d1d5db !important; + background-color: #fff !important; + cursor: pointer !important; + appearance: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; + position: relative !important; + margin: 0 !important; + padding: 0 !important; + box-shadow: none !important; + + &:hover { + border-color: #3b82f6 !important; + } + + &:checked { + background-color: #3b82f6 !important; + border-color: #3b82f6 !important; + + &::after { + content: '' !important; + position: absolute !important; + left: 0.125rem !important; + top: 0.0625rem !important; + width: 0.375rem !important; + height: 0.625rem !important; + border: 2px solid white !important; + border-top: 0 !important; + border-left: 0 !important; + transform: rotate(45deg) !important; + } + } + + .dark & { + border-color: #4b5563 !important; + background-color: #374151 !important; + + &:hover { + border-color: #60a5fa !important; + } + + &:checked { + background-color: #60a5fa !important; + border-color: #60a5fa !important; + } + } + } + } + + > div { + flex: 1 1 auto !important; + margin: 0 !important; + padding: 0 !important; + border: none !important; + background: none !important; + color: inherit !important; + } + } + } + + li { + margin: 0.25rem 0 !important; + line-height: 1.5 !important; + font-size: 0.875rem !important; + display: list-item !important; + list-style-type: disc !important; + padding: 0 !important; + border: none !important; + background: none !important; + color: inherit !important; + text-shadow: none !important; + } + } + + ol { + margin: 0.5rem 0 !important; + padding-left: 1.25rem !important; + list-style-type: decimal !important; + border: none !important; + background: none !important; + + li { + margin: 0.25rem 0 !important; + line-height: 1.5 !important; + font-size: 0.875rem !important; + display: list-item !important; + list-style-type: decimal !important; + padding: 0 !important; + border: none !important; + background: none !important; + color: inherit !important; + text-shadow: none !important; + } + } + + strong { + font-weight: 600 !important; + color: #111827 !important; + text-shadow: none !important; + + .dark & { + color: #f9fafb !important; + } + } + + em { + font-style: italic !important; + text-shadow: none !important; + } + + a { + color: #3b82f6 !important; + text-decoration: underline !important; + text-decoration-color: rgba(59, 130, 246, 0.3) !important; + text-shadow: none !important; + background: none !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; + + &:hover { + text-decoration-color: #3b82f6 !important; + background: none !important; + } + + .dark & { + color: #60a5fa !important; + + &:hover { + text-decoration-color: #60a5fa !important; + } + } + } + + blockquote { + padding: 0.2rem 1rem !important; + margin: 1rem 0 !important; + font-style: italic !important; + color: #6b7280 !important; + text-align: left !important; + border-right: none !important; + border-top: none !important; + border-bottom: none !important; + box-shadow: none !important; + text-shadow: none !important; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background-color: #d1d5db; + z-index: 1; + border-radius: 0.5rem; + } + + .dark &::before { + background-color: #4b5563; + } + + .dark & { + color: #9ca3af !important; + } + } + + pre { + background-color: #f3f4f6 !important; + color: #1f2937 !important; + padding: 1rem !important; + border-radius: 0.5rem !important; + margin: 1rem 0 !important; + overflow-x: auto !important; + font-family: + ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace !important; + font-size: 0.875rem !important; + line-height: 1.5 !important; + text-align: left !important; + white-space: pre !important; + border: none !important; + box-shadow: none !important; + text-shadow: none !important; + + .dark & { + background-color: rgba(35, 36, 41, 0.5) !important; + border: 1px solid rgba(35, 36, 41, 0.5) !important; + color: #e5e7eb !important; + } + + code { + background-color: transparent !important; + color: inherit !important; + padding: 0 !important; + border-radius: 0 !important; + font-size: inherit !important; + font-family: inherit !important; + border: none !important; + margin: 0 !important; + + .dark & { + background-color: transparent !important; + color: inherit !important; + } + } + } + + hr { + border: none !important; + border-top: 1px solid #e5e7eb !important; + margin: 1rem 0 !important; + width: 100% !important; + background: none !important; + height: 0 !important; + padding: 0 !important; + + .dark & { + border-top-color: #3f4854 !important; + } + } + + code { + background-color: #f3f4f6 !important; + color: #d97706 !important; + padding: 0.125rem 0.25rem !important; + border-radius: 0.25rem !important; + font-size: 0.8125rem !important; + font-family: + ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace !important; + border: none !important; + margin: 0 !important; + text-shadow: none !important; + + .dark & { + background-color: #374151 !important; + color: #fbbf24 !important; + } + } +} diff --git a/src/plugins/built-in/customMessageEditor/Editor/EditorStyles.css b/src/plugins/built-in/customMessageEditor/Editor/EditorStyles.css new file mode 100644 index 00000000..33f86331 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/EditorStyles.css @@ -0,0 +1,256 @@ +/* Editor-specific styles (animations, transitions, editor-only features) - !these are not applied to sent messages! */ + +/* Nested content styling with animated borders */ +.editor-prose li > *:not(:first-child) { + position: relative; + margin-left: -0.5rem; +} + +.editor-prose li:not(:has(> label)) > *:not(:first-child)::before { + content: ''; + position: absolute; + left: -0.75rem; + top: 0; + bottom: 0; + width: 1.5px; + background-color: #e5e7eb7e; + transform-origin: top; + animation: expandDown 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.dark .editor-prose li > *:not(:first-child)::before { + background-color: #4b55637b; +} + +/* Special handling for nested lists to extend the line properly */ +.editor-prose li > ul, +.editor-prose li > ol { + margin-left: -0.5rem; +} + +.editor-prose li > ul::before, +.editor-prose li > ol::before { + bottom: -0.25rem; /* Extend slightly below for better visual connection */ +} + +@keyframes expandDown { + 0% { + transform: scaleY(0); + opacity: 0; + } + 100% { + transform: scaleY(1); + opacity: 1; + } +} + +/* Placeholders for editor-only */ +.editor-prose p::before, +.editor-prose h1::before, +.editor-prose h2::before, +.editor-prose h3::before, +.editor-prose h4::before, +.editor-prose h5::before, +.editor-prose h6::before { + content: attr(data-placeholder); + color: #9ca3af; + float: left; + height: 0; +} + +.dark .editor-prose p::before, +.dark .editor-prose h1::before, +.dark .editor-prose h2::before, +.dark .editor-prose h3::before, +.dark .editor-prose h4::before, +.dark .editor-prose h5::before, +.dark .editor-prose h6::before { + color: #6b7280; +} + +.bnEditor { + outline: none; + padding-inline: 50px; + border-radius: 8px; + + /* Define a set of colors to be used throughout the app for consistency + see https://atlassian.design/foundations/color for more info */ + --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */ + --N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */ +} + +/* +bnRoot should be applied to all top-level elements + +This includes the Prosemirror editor, but also
element such as +Tippy popups that are appended to document.body directly +*/ +.bnRoot { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.bnRoot *, +.bnRoot *::before, +.bnRoot *::after { + -webkit-box-sizing: inherit; + -moz-box-sizing: inherit; + box-sizing: inherit; +} + +/* reset styles, they will be set on blockContent */ +.defaultStyles p, +.defaultStyles h1, +.defaultStyles h2, +.defaultStyles h3, +.defaultStyles li { + all: unset !important; + margin: 0; + padding: 0; + font-size: inherit; + /* min width to make sure cursor is always visible */ + min-width: 2px !important; +} + +.defaultStyles { + font-size: 16px; + font-weight: normal; + font-family: + 'Inter', + 'SF Pro Display', + -apple-system, + BlinkMacSystemFont, + 'Open Sans', + 'Segoe UI', + 'Roboto', + 'Oxygen', + 'Ubuntu', + 'Cantarell', + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.dragPreview { + position: absolute; + top: -1000px; +} + +@keyframes fadeInScale { + 0% { + opacity: 0; + transform: scale(0.95); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +/* Animate headers only */ +.editor-prose h1, +.editor-prose h2, +.editor-prose h3, +.editor-prose h4, +.editor-prose h5, +.editor-prose h6 { + animation: fadeInScale 0.2s cubic-bezier(0.4, 0, 0.2, 1); + transform-origin: left center; +} + +/* Smooth transitions for all interactive elements */ +.editor-prose { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Bold and italic transitions */ +.editor-prose strong, +.editor-prose em { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Selected node styling (Notion-like) */ +.ProseMirror-selectednode { + box-shadow: 0 0 0 4px #3b82f6; + border-radius: 4px; + background-color: rgba(59, 130, 246, 0.05); + transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.dark .ProseMirror-selectednode { + box-shadow: 0 0 0 2px #3a3e44; + background-color: rgba(96, 165, 250, 0.08); +} + +/* Ensure selected nodes have proper spacing */ +.ProseMirror-selectednode { + margin: 2px; +} + +/* Drag and drop containment */ +.editor-prose { + position: relative; + overflow: hidden; + contain: layout style; +} + +/* Image drag styling */ +.editor-prose img.tiptap-image { + cursor: grab; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 4px; + max-width: 100%; + height: auto; +} + +.editor-prose img.tiptap-image:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: scale(1.02); +} + +.editor-prose img.tiptap-image:active { + cursor: grabbing; + transform: scale(0.98); +} + +/* Dropcursor styling */ +.tiptap-dropcursor { + pointer-events: none; + border-radius: 2px; + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +/* Prevent drag operations outside editor */ +.editor-prose * { + -webkit-user-drag: auto; + -moz-user-drag: auto; + user-drag: auto; +} + +/* Ensure only images within editor are draggable */ +.editor-prose img { + -webkit-user-drag: element; + -moz-user-drag: element; + user-drag: element; +} + +/* Prevent text selection during drag */ +.editor-prose.ProseMirror-dragover * { + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; +} diff --git a/src/plugins/built-in/customMessageEditor/Editor/Plugins/BubbleMenu.svelte b/src/plugins/built-in/customMessageEditor/Editor/Plugins/BubbleMenu.svelte new file mode 100644 index 00000000..c2b31314 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/Plugins/BubbleMenu.svelte @@ -0,0 +1,196 @@ + + + + + + + {#if editor} + + +
+ showTurnInto = !showTurnInto} + class="flex gap-1 items-center px-3 py-2 text-sm rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800" + title="Turn into" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + {getCurrentBlockType()} + + + + + + {#if showTurnInto} + + {#each turnIntoOptions as option} + {#if option.id === 'separator'} +
+ {:else} + + {/if} + {/each} +
+ {/if} +
+ +
+ + editor.chain().focus().toggleBold().run()} + class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('bold') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }" + title="Bold" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + > + + + + editor.chain().focus().toggleItalic().run()} + class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('italic') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }" + title="Italic" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + > + + + + editor.chain().focus().toggleStrike().run()} + class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('strike') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }" + title="Strikethrough" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + > + + + + editor.chain().focus().toggleCode().run()} + class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('code') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }" + title="Code" + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + > + + +
+ {/if} +
\ No newline at end of file diff --git a/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/CommandList.svelte b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/CommandList.svelte new file mode 100644 index 00000000..d59818c5 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/CommandList.svelte @@ -0,0 +1,172 @@ + + + + +{#if $slashVisible} +
{}} + onclick={closeSlashMenu} + role="menu" + tabindex="-1"> +
+ +
+
Basic Blocks
+ {#each $slashItems as { title, subtitle, command }, i} +
handleItemClick({ command })} + onkeydown={() => {}} + role="menuitem" + tabindex="-1" + bind:this={elements[i]}> +
+ {@html getCommandIcon(title)} +
+
+
+ {title} +
+

+ {subtitle ? subtitle : ''} +

+
+
+ {/each} +
+{/if} diff --git a/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/command.ts b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/command.ts new file mode 100644 index 00000000..4ae5ab34 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/command.ts @@ -0,0 +1,26 @@ +import { Extension } from '@tiptap/core'; +import Suggestion from '@tiptap/suggestion'; + +export default Extension.create({ + name: 'slash', + + addOptions() { + return { + suggestion: { + char: '/', + command: ({ editor, range, props }: any) => { + props.command({ editor, range }); + }, + }, + }; + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + ]; + }, +}); diff --git a/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/stores.ts b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/stores.ts new file mode 100644 index 00000000..2138ba65 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/stores.ts @@ -0,0 +1,46 @@ +import { writable } from 'svelte/store'; +import type { Writable } from 'svelte/store'; + +type SlashItems = SlashItem[]; + +type SlashItem = { + title: string; + subtitle: string; + command: ({ editor, range }: EditorProps) => void; +}; + +type Component = { + name: string; + description: string; + code: string; +}; + +type Components = Component[]; + +type EditorProps = { + editor: any; + range: number | null; +}; + +type Location = { + x: number; + y: number; + height: number; +}; + +// For now we'll keep using stores until we can fully convert to runes in all components +export const slashItems: Writable = writable([]); +export const slashVisible: Writable = writable(false); +export const slashLocation: Writable = writable({ + x: 0, + y: 0, + height: 0, +}); +export const slashProps: Writable = writable({ + editor: null, + range: null, +}); +export const desktopMenu: Writable = writable(true); +export const components: Writable = writable([]); +export const editorWidth: Writable = writable(0); +export const selectedIndex: Writable = writable(0); diff --git a/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/suggestion.ts b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/suggestion.ts new file mode 100644 index 00000000..226abde5 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/Plugins/Commands/suggestion.ts @@ -0,0 +1,159 @@ +import { slashVisible, slashItems, slashLocation, slashProps, selectedIndex } from './stores'; + +export default { + items: ({ query }: any) => { + return [ + { + title: 'To Dos', + subtitle: 'Create a to do list with checkboxes', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).toggleTaskList().run(); + }, + }, + { + title: 'Heading 1', + subtitle: 'BIG heading', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run(); + }, + }, + { + title: 'Heading 2', + subtitle: 'Less Big heading', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run(); + }, + }, + { + title: 'Heading 3', + subtitle: 'Medium big heading', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run(); + }, + }, + { + title: 'Bullet List', + subtitle: 'Pew pew pew', + command: ({ editor, range }: any) => { + editor.commands.deleteRange(range); + editor.commands.toggleBulletList(); + }, + }, + { + title: 'Numbered List', + subtitle: '1, 2, 3, 4...', + command: ({ editor, range }: any) => { + editor.commands.deleteRange(range); + editor.commands.toggleOrderedList(); + }, + }, + { + title: 'Text', + subtitle: 'Just plain text paragraph', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).setNode('paragraph').run(); + }, + }, + { + title: 'Quote', + subtitle: 'Capture important quotes', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).toggleBlockquote().run(); + }, + }, + { + title: 'Code Block', + subtitle: 'Formatted code snippet', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); + }, + }, + { + title: 'Divider', + subtitle: 'Add a horizontal line', + command: ({ editor, range }: any) => { + editor.chain().focus().deleteRange(range).setHorizontalRule().run(); + }, + }, + { + title: 'Bold Text', + subtitle: 'Make text bold', + command: ({ editor, range }: any) => { + editor.commands.deleteRange(range); + editor.commands.toggleBold(); + }, + }, + { + title: 'Italic Text', + subtitle: 'Make text italic', + command: ({ editor, range }: any) => { + editor.commands.deleteRange(range); + editor.commands.toggleItalic(); + }, + }, + { + title: 'Link', + subtitle: 'Add a web link', + command: ({ editor, range }: any) => { + const url = prompt('Enter the URL:'); + if (url) { + editor + .chain() + .focus() + .deleteRange(range) + .setLink({ href: url }) + .insertContent('Link text') + .run(); + } + }, + }, + { + title: 'Inline Code', + subtitle: 'Inline code snippet', + command: ({ editor, range }: any) => { + editor.commands.deleteRange(range); + editor.commands.toggleCode(); + }, + }, + ] + .filter((item) => item.title.toLowerCase().startsWith(query.toLowerCase())) + .slice(0, 10); + }, + + render: () => { + return { + onStart: (props: any) => { + let editor = props.editor; + let range = props.range; + let location = props.clientRect(); + const editorRect = editor.view.dom.getBoundingClientRect(); + slashProps.set({ editor, range }); + slashVisible.set(true); + slashLocation.set({ + x: location.x - editorRect.left, + y: location.y - editorRect.top + location.height / 2 + 4, + height: location.height, + }); + slashItems.set(props.items); + selectedIndex.set(0); + }, + + onUpdate(props: any) { + slashItems.set(props.items); + selectedIndex.set(0); + }, + + onKeyDown(props: any) { + if (props.event.key === 'Escape') { + slashVisible.set(false); + return true; + } + }, + + onExit() { + slashVisible.set(false); + selectedIndex.set(0); + }, + }; + }, +}; diff --git a/src/plugins/built-in/customMessageEditor/Editor/TiptapStyles.css b/src/plugins/built-in/customMessageEditor/Editor/TiptapStyles.css new file mode 100644 index 00000000..bbd90655 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/TiptapStyles.css @@ -0,0 +1,75 @@ +.ProseMirror { + position: relative; +} + +.ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ +} + +.ProseMirror [contenteditable="false"] { + white-space: normal; +} + +.ProseMirror [contenteditable="false"] [contenteditable="true"] { + white-space: pre-wrap; +} + +.ProseMirror pre { + white-space: pre-wrap; +} + +img.ProseMirror-separator { + display: inline !important; + border: none !important; + margin: 0 !important; + width: 0 !important; + height: 0 !important; +} + +.ProseMirror-gapcursor { + display: none; + pointer-events: none; + position: absolute; + margin: 0; +} + +.ProseMirror-gapcursor:after { + content: ""; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid black; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; +} + +@keyframes ProseMirror-cursor-blink { + to { + visibility: hidden; + } +} + +.ProseMirror-hideselection *::selection { + background: transparent; +} + +.ProseMirror-hideselection *::-moz-selection { + background: transparent; +} + +.ProseMirror-hideselection * { + caret-color: transparent; +} + +.ProseMirror-focused .ProseMirror-gapcursor { + display: block; +} + +.tippy-box[data-animation=fade][data-state=hidden] { + opacity: 0 +} \ No newline at end of file diff --git a/src/plugins/built-in/customMessageEditor/Editor/userHTML.css b/src/plugins/built-in/customMessageEditor/Editor/userHTML.css new file mode 100644 index 00000000..fd7326c0 --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/Editor/userHTML.css @@ -0,0 +1,238 @@ +/* SEQTA Applied styles on DMs (applied to ensure consistency) */ + +.editor-prose { + font-family: 'Roboto', sans-serif; + border: 0; + padding: 0 8px; + margin: 0; + line-height: 1.2; + /* Removed font size because drag and drop text content within the editor insert html span with font sizing */ + + font-size: 10pt; + + /* Macro: Image */ + + /* Macro: Image gallery (display) */ + + /* Fake macro element from plugin "seqta-macro" */ + img[data-macro], + a[data-macro] { + border: 2px dashed #ccc; + padding: 8px; + border-radius: 4px; + position: relative; + box-sizing: border-box; + } + + img[data-macro].selected, + a[data-macro].selected { + border: 2px solid #204a87; + box-shadow: inset 0 0 4px #204a87; + } + + img[data-macro='Resource'] { + background-image: repeating-linear-gradient( + -45deg, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0) 12px, + rgba(0, 0, 0, 0.05) 12px, + rgba(0, 0, 0, 0.05) 24px + ); + } + + img[data-macro='Embed'] { + background-image: repeating-linear-gradient( + 45deg, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0) 12px, + rgba(0, 0, 0, 0.05) 12px, + rgba(0, 0, 0, 0.05) 24px + ); + } + + img[data-macro='Embed'][data-full] { + width: 100%; + } + + img[data-macro='Gallery'] { + display: block; + margin: 0 auto; + max-width: 100%; + padding: 0; + } + + /* Direqt message-specific styling */ + blockquote.forward { + margin: 0; + background: rgba(0, 0, 0, 0.05); + border: 1px solid rgba(0, 0, 0, 0.1); + } + + blockquote.forward > .preamble { + background: rgba(255, 255, 255, 0.1); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 8px; + } + + blockquote.forward > .preamble > .date > .label, + blockquote.forward > .preamble > .sender > .label { + color: rgba(0, 0, 0, 0.7); + } + + blockquote.forward > .preamble > .date > .value, + blockquote.forward > .preamble > .sender > .value { + color: rgba(0, 0, 0, 0.9); + } + + blockquote.forward > .body { + padding: 8px; + } + + /** Assessment display **/ + + .assessmentWrapper { + position: relative; + display: inline-block; + bottom: -15px; + } + + .macro-assessment { + padding: 8px 16px; + margin: 0 8px; + border: 4px solid rgba(0, 0, 0, 0.25); + display: inline-block; + width: 256px; + background-color: #fff; + overflow: hidden; + text-shadow: none; + position: relative; + word-wrap: break-word; + min-height: 30px; + } + + .macro-assessment > .title { + font-size: 150%; + max-width: 230px; + } + + .macro-assessment > .due > span.weight { + padding-left: 24px; + } + + .macro-assessment > .due > span.marked { + padding-left: 24px; + font-weight: bold; + } + + .macro-assessment > .hidden, + .macro-assessment > .deleted { + font-style: italic; + max-width: 230px; + } + + /** Syllabus display **/ + + .macro-syllabus { + padding: 8px; + margin: 0 8px; + border: 4px solid #eee; + display: inline-block; + max-width: 200px; + background-color: #fff; + overflow: hidden; + color: #444; + text-shadow: none; + position: relative; + bottom: -15px; + } + + .macro-syllabus > .label { + font-weight: bold; + } + + .macro-syllabus > .extra { + font-style: italic; + } + + .macro-syllabus > .meta { + text-transform: uppercase; + font-size: var(--small-text); + color: #999; + } + + /* Drop-down menu for plugins like "seqta-macro" */ + .cke_panel_block > h1 { + display: none; + } + + .cke_panel_block > .cke_panel_list { + list-style: none; + padding: 0; + margin: 0; + } + + .cke_panel_block > .cke_panel_list > li { + color: #888; + margin: 0; + cursor: pointer; + } + + .cke_panel_block > .cke_panel_list > li:hover { + color: white; + background: #1b315e; + } + + .cke_panel_block > .cke_panel_list > li > a { + display: block; + color: inherit; + text-decoration: inherit; + text-transform: uppercase; + font-size: 90%; + padding: 8px; /* Padding on the , not the
  • , so our click target is full size. */ + text-shadow: none; + } + + .cke_panel_block > .cke_panel_list > li > a > p, + .cke_panel_block > .cke_panel_list > li > a > h1, + .cke_panel_block > .cke_panel_list > li > a > h2, + .cke_panel_block > .cke_panel_list > li > a > h3, + .cke_panel_block > .cke_panel_list > li > a > pre { + margin: 0; + color: inherit; + padding: 0; + } + + .moodleFrame > .userHTML { + width: 100%; + height: 600px; + margin: 16px 0 16px 0; + } + + .application.restricted { + display: block; + max-width: 320px; + margin: 32px auto; + border: 1px dashed #ccc; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + padding: 24px; + background: #f8f8f8; + background-image: -webkit-linear-gradient(315deg, #fff, #f8f8f8); + background-image: linear-gradient(135deg, #fff, #f8f8f8); + border-radius: 8px; + box-sizing: border-box; + } + + .application.restricted > .title { + margin: 0; + padding: 0; + font-size: 100%; + font-weight: bold; + color: #666; + } + + .application.restricted > .message { + margin: 0; + padding: 0; + font-size: var(--small-text); + } +} diff --git a/src/plugins/built-in/customMessageEditor/index.ts b/src/plugins/built-in/customMessageEditor/index.ts new file mode 100644 index 00000000..d496636d --- /dev/null +++ b/src/plugins/built-in/customMessageEditor/index.ts @@ -0,0 +1,236 @@ +import type { Plugin } from "@/plugins/core/types"; +import { BasePlugin } from "@/plugins/core/settings"; +import { defineSettings } from "@/plugins/core/settingsHelpers"; +import { waitForElm } from "@/seqta/utils/waitForElm"; +import renderSvelte from "@/interface/main"; +import BetterEditor from "./BetterEditor.svelte"; +import { unmount } from "svelte"; + +const settings = defineSettings({}); + +class CustomMessageEditorPlugin extends BasePlugin {} + +const settingsInstance = new CustomMessageEditorPlugin(); + +const customMessageEditorPlugin: Plugin = { + id: "custom-message-editor", + name: "Custom Message Editor", + description: "Enhanced message editor with better editing capabilities", + version: "1.0.0", + settings: settingsInstance.settings, + defaultEnabled: true, + + run: async (api) => { + let currentShadowContainer: HTMLElement | null = null; + let currentSvelteApp: any = null; + let currentEditorId: string | null = null; + let lastCKEditorContent: string = ""; + + const cleanup = (resetEditorId = true) => { + if (currentSvelteApp) { + unmount(currentSvelteApp); + currentSvelteApp = null; + } + if (currentShadowContainer) { + currentShadowContainer.remove(); + currentShadowContainer = null; + } + if (resetEditorId) { + currentEditorId = null; + } + }; + + const handleEditorChange = (value: string) => { + if (currentEditorId) { + window.postMessage( + { + type: "ckeditorSetData", + editorId: currentEditorId, + content: value, + }, + "*", + ); + } + }; + + const getCKEditorContent = () => { + if (currentEditorId) { + window.postMessage( + { + type: "ckeditorGetData", + editorId: currentEditorId, + }, + "*", + ); + } + }; + + const messageListener = (event: MessageEvent) => { + if (event.data.type === "ckeditorGetDataResponse") { + lastCKEditorContent = event.data.data; + console.log("Retrieved CKEditor content:", lastCKEditorContent); + } + }; + + window.addEventListener("message", messageListener); + + const injectBetterEditorButton = async (composer: Element) => { + try { + const pillbox = await waitForElm( + ".coneqtMessage.composer .footer .pillbox", + true, + 100, + 50, + ); + + if (!pillbox) { + console.error("Could not find pillbox element"); + return; + } + + if (pillbox.querySelector(".better-editor-btn")) { + return; + } + + const betterEditorBtn = document.createElement("button"); + betterEditorBtn.type = "button"; + betterEditorBtn.className = "notLast editorMode better-editor-btn"; + betterEditorBtn.textContent = "Better Editor"; + betterEditorBtn.setAttribute("data-key", "better"); + + const htmlEditorBtn = pillbox.querySelector( + 'button[data-key="html"]', + ) as HTMLButtonElement; + if (!htmlEditorBtn) { + console.error("Could not find HTML editor button"); + return; + } + + pillbox.insertBefore(betterEditorBtn, htmlEditorBtn); + + betterEditorBtn.addEventListener("click", async () => { + const simpleEditorBtn = pillbox.querySelector( + 'button[data-key="content"]', + ) as HTMLButtonElement; + if (simpleEditorBtn) { + simpleEditorBtn.click(); + } + + pillbox.querySelectorAll(".editorMode").forEach((btn) => { + btn.classList.remove("depressed"); + }); + if (simpleEditorBtn) { + simpleEditorBtn.classList.add("depressed"); + } + + const wrapper = composer.querySelector( + ".prime .body .formattedText .wrapper", + ); + const ckeElement = wrapper?.querySelector(".cke"); + + if (!wrapper || !ckeElement) { + console.error("Could not find wrapper or CKE elements"); + return; + } + + if (ckeElement.id) { + const ckeMatch = ckeElement.id.match(/^cke_(.+)$/); + if (ckeMatch) { + currentEditorId = ckeMatch[1]; + console.log("Found CKEditor ID:", currentEditorId); + } + } + + let initialContent = ""; + + if (currentEditorId) { + window.postMessage( + { + type: "ckeditorGetData", + editorId: currentEditorId, + }, + "*", + ); + + initialContent = await new Promise((resolve) => { + const timeout = setTimeout(() => resolve(""), 1000); + + const responseListener = (event: MessageEvent) => { + if (event.data.type === "ckeditorGetDataResponse") { + clearTimeout(timeout); + window.removeEventListener("message", responseListener); + resolve(event.data.data || ""); + } + }; + + window.addEventListener("message", responseListener); + }); + } + + (ckeElement as HTMLElement).style.display = "none"; + + cleanup(false); + + const shadowContainer = document.createElement("div"); + shadowContainer.className = "better-editor-container"; + shadowContainer.style.cssText = + "width: 100%; height: 100%; min-height: 200px; overflow-y: scroll; background: var(--background-primary); border-radius: 16px; padding: 4px;"; + + const shadowRoot = shadowContainer.attachShadow({ mode: "open" }); + + currentSvelteApp = renderSvelte(BetterEditor, shadowRoot, { + initialContent, + onchange: handleEditorChange, + }); + + wrapper.appendChild(shadowContainer); + currentShadowContainer = shadowContainer; + + pillbox.querySelectorAll(".editorMode").forEach((btn) => { + btn.classList.remove("depressed"); + }); + betterEditorBtn.classList.add("depressed"); + }); + + pillbox + .querySelectorAll(".editorMode:not(.better-editor-btn)") + .forEach((btn) => { + btn.addEventListener("click", () => { + getCKEditorContent(); + + cleanup(false); + + const wrapper = composer.querySelector( + ".prime .body .formattedText .wrapper", + ); + const ckeElement = wrapper?.querySelector(".cke"); + if (ckeElement) { + (ckeElement as HTMLElement).style.display = ""; + } + }); + }); + } catch (error) { + console.error("Error injecting Better Editor button:", error); + } + }; + + const { unregister } = api.seqta.onMount(".uiSlidePane", (slidePane) => { + console.log("Found slide pane, checking for message composer"); + const messageComposer = slidePane.querySelector( + ".coneqtMessage.composer", + ); + if (messageComposer) { + console.log("Found message composer, injecting Better Editor button"); + injectBetterEditorButton(messageComposer); + } + }); + + return () => { + cleanup(); + unregister(); + window.removeEventListener("message", messageListener); + }; + }, +}; + +export default customMessageEditorPlugin; diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 1c06d785..9e7cbc87 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -9,6 +9,7 @@ import assessmentsAveragePlugin from "./built-in/assessmentsAverage"; import globalSearchPlugin from "./built-in/globalSearch/src/core"; import profilePicturePlugin from "./built-in/profilePicture"; import assessmentsOverviewPlugin from "./built-in/assessmentsOverview"; +import customMessageEditorPlugin from "./built-in/customMessageEditor"; //import testPlugin from './built-in/test'; // Initialize plugin manager @@ -23,6 +24,7 @@ pluginManager.registerPlugin(timetablePlugin); pluginManager.registerPlugin(globalSearchPlugin); pluginManager.registerPlugin(profilePicturePlugin); pluginManager.registerPlugin(assessmentsOverviewPlugin); +pluginManager.registerPlugin(customMessageEditorPlugin); //pluginManager.registerPlugin(testPlugin); export { init as Monofile } from "./monofile";