mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2638157d25 |
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}, "*");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import Editor from './Editor/Editor.svelte';
|
||||
import EditorStyles from './Editor/EditorStyles.css?raw';
|
||||
import EditorOverrideStyles from './Editor/EditorOverrideStyles.css?raw';
|
||||
import TiptapStyles from './Editor/TiptapStyles.css?raw';
|
||||
import { onMount } from 'svelte';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
interface Props {
|
||||
onchange: (value: string) => void;
|
||||
initialContent?: string;
|
||||
scale?: number; // Scale factor for the editor (1.0 = normal, 1.2 = 120%, etc.)
|
||||
}
|
||||
|
||||
let { onchange, initialContent = '', scale = 1.3 }: Props = $props();
|
||||
|
||||
let content = $state('');
|
||||
let betterEditor = $state<HTMLElement | null>(null);
|
||||
|
||||
// Watch for content changes and call the callback
|
||||
$effect(() => {
|
||||
if (onchange) {
|
||||
onchange(content);
|
||||
}
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (betterEditor) {
|
||||
const styles = EditorStyles + EditorOverrideStyles + TiptapStyles;
|
||||
|
||||
const scalingCSS = `
|
||||
.better-editor {
|
||||
--scale-factor: ${scale};
|
||||
}
|
||||
|
||||
.better-editor .editor-prose {
|
||||
transform-origin: top left;
|
||||
zoom: ${scale};
|
||||
-moz-transform: scale(${scale});
|
||||
-moz-transform-origin: 0 0;
|
||||
}
|
||||
|
||||
/* For Firefox which doesn't support zoom */
|
||||
@-moz-document url-prefix() {
|
||||
.better-editor .editor-prose {
|
||||
transform: scale(${scale});
|
||||
width: ${100 / scale}%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = styles + scalingCSS;
|
||||
betterEditor.appendChild(styleElement);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-full better-editor {settingsState.DarkMode ? 'dark' : ''}"
|
||||
bind:this={betterEditor}
|
||||
style="font-size: {scale * 16}px; --editor-scale: {scale};"
|
||||
>
|
||||
<Editor bind:content {initialContent} />
|
||||
</div>
|
||||
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import Commands from './Plugins/Commands/command';
|
||||
import { Dropcursor } from '@tiptap/extension-dropcursor';
|
||||
import Image from '@tiptap/extension-image'
|
||||
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import TaskList from '@tiptap/extension-task-list';
|
||||
import TaskItem from '@tiptap/extension-task-item';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Link from '@tiptap/extension-link';
|
||||
|
||||
import { Editor } from '@tiptap/core';
|
||||
|
||||
import CommandList from './Plugins/Commands/CommandList.svelte';
|
||||
import suggestion from './Plugins/Commands/suggestion';
|
||||
import { slashVisible } from './Plugins/Commands/stores';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import BubbleMenuComponent from './Plugins/BubbleMenu.svelte';
|
||||
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import EditorStyles from './EditorOverrideStyles.css?raw';
|
||||
|
||||
// Make htmlContent bindable from parent components
|
||||
let { content = $bindable(''), initialContent = '' } = $props<{ content: string; initialContent?: string }>();
|
||||
|
||||
let commandListInstance = $state<any>(null);
|
||||
|
||||
let element = $state<HTMLElement | null>(null);
|
||||
let editor = $state<Editor | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element: element!,
|
||||
content: initialContent || '',
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'focus:outline-none px-3 md:px-0',
|
||||
},
|
||||
handleKeyDown: (_, event) => {
|
||||
// Handle keyboard events when slash menu is visible
|
||||
if (get(slashVisible) && commandListInstance) {
|
||||
if (event.key === 'Enter' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
const handled = commandListInstance.handleKeydown(event, editor);
|
||||
if (handled) {
|
||||
return true; // Prevent TipTap from handling this event
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // Let TipTap handle other events
|
||||
},
|
||||
},
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }: { node: any }) => {
|
||||
if (node.type.name === 'heading') {
|
||||
return 'Heading';
|
||||
} else if (node.type.name === 'paragraph') {
|
||||
return "Type '/' for commands";
|
||||
}
|
||||
|
||||
return 'Type something...';
|
||||
},
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem,
|
||||
Link,
|
||||
Typography,
|
||||
Commands.configure({
|
||||
suggestion,
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
element: document.querySelector('.menu') as HTMLElement,
|
||||
}),
|
||||
Dropcursor.configure({ width: 5, color: '#ddeeff' }),
|
||||
Image.configure({
|
||||
allowBase64: true,
|
||||
}),
|
||||
],
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor;
|
||||
},
|
||||
onUpdate: ({ editor }: { editor: Editor }) => {
|
||||
// Update the htmlContent with the editor's HTML plus CSS
|
||||
const editorHTML = editor.getHTML();
|
||||
content = `<div class="editor-prose">${editorHTML}<${''}style>${EditorStyles}</${''}style></div>`;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (initialContent) {
|
||||
content = initialContent;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
function handleKeydownCapture(event: KeyboardEvent) {
|
||||
if (commandListInstance && editor && get(slashVisible)) {
|
||||
if (event.key === 'Escape') {
|
||||
if (commandListInstance.handleKeydown(event, editor)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
if (!editor) return;
|
||||
|
||||
// Check if the click happened in empty space below content
|
||||
const editorElement = element;
|
||||
if (!editorElement) return;
|
||||
|
||||
const clickY = event.clientY;
|
||||
|
||||
// Get the last node in the editor
|
||||
const lastNode = editorElement.lastElementChild;
|
||||
if (lastNode) {
|
||||
const lastNodeRect = lastNode.getBoundingClientRect();
|
||||
|
||||
// If click is below the last content node, move cursor to end
|
||||
if (clickY > lastNodeRect.bottom) {
|
||||
const docSize = editor.state.doc.content.size;
|
||||
editor.commands.setTextSelection(docSize);
|
||||
editor.commands.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative h-full">
|
||||
<div
|
||||
class="w-full min-h-full editor-prose"
|
||||
bind:this={element}
|
||||
onkeydown={handleKeydownCapture}
|
||||
onclick={handleClick}
|
||||
role="textbox"
|
||||
tabindex="-1">
|
||||
</div>
|
||||
<CommandList bind:this={commandListInstance} />
|
||||
</div>
|
||||
|
||||
<BubbleMenuComponent bind:editor />
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <div> 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;
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { Icon, Bold, Italic, Strikethrough, CodeBracket, ChevronDown } from 'svelte-hero-icons';
|
||||
import { M } from 'motion-start';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
|
||||
let { editor = $bindable() } = $props<{ editor: Editor | null }>();
|
||||
|
||||
// Turn into dropdown state
|
||||
let showTurnInto = $state(false);
|
||||
|
||||
// Turn into options
|
||||
const turnIntoOptions = [
|
||||
{ id: 'paragraph', label: 'Text', icon: 'T', iconClass: 'font-mono' },
|
||||
{ id: 'heading1', label: 'Heading 1', icon: 'H1', iconClass: 'font-bold' },
|
||||
{ id: 'heading2', label: 'Heading 2', icon: 'H2', iconClass: 'font-bold' },
|
||||
{ id: 'heading3', label: 'Heading 3', icon: 'H3', iconClass: 'font-bold' },
|
||||
{ id: 'separator' },
|
||||
{ id: 'bulletList', label: 'Bulleted list', icon: '•' },
|
||||
{ id: 'orderedList', label: 'Numbered list', icon: '1.' },
|
||||
{ id: 'taskList', label: 'To-do list', icon: '☐' },
|
||||
{ id: 'separator' },
|
||||
{ id: 'codeBlock', label: 'Code', icon: '</>' },
|
||||
{ id: 'blockquote', label: 'Quote', icon: '"' }
|
||||
];
|
||||
|
||||
function getCurrentBlockType(): string {
|
||||
if (!editor) return 'Text';
|
||||
|
||||
if (editor.isActive('heading', { level: 1 })) return 'Heading 1';
|
||||
if (editor.isActive('heading', { level: 2 })) return 'Heading 2';
|
||||
if (editor.isActive('heading', { level: 3 })) return 'Heading 3';
|
||||
if (editor.isActive('bulletList')) return 'Bulleted list';
|
||||
if (editor.isActive('orderedList')) return 'Numbered list';
|
||||
if (editor.isActive('taskList')) return 'To-do list';
|
||||
if (editor.isActive('codeBlock')) return 'Code';
|
||||
if (editor.isActive('blockquote')) return 'Quote';
|
||||
|
||||
return 'Text';
|
||||
}
|
||||
|
||||
function turnInto(type: string) {
|
||||
if (!editor) return;
|
||||
|
||||
switch (type) {
|
||||
case 'paragraph':
|
||||
editor.chain().focus().setParagraph().run();
|
||||
break;
|
||||
case 'heading1':
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run();
|
||||
break;
|
||||
case 'heading2':
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run();
|
||||
break;
|
||||
case 'heading3':
|
||||
editor.chain().focus().toggleHeading({ level: 3 }).run();
|
||||
break;
|
||||
case 'bulletList':
|
||||
editor.chain().focus().toggleBulletList().run();
|
||||
break;
|
||||
case 'orderedList':
|
||||
editor.chain().focus().toggleOrderedList().run();
|
||||
break;
|
||||
case 'taskList':
|
||||
editor.chain().focus().toggleTaskList().run();
|
||||
break;
|
||||
case 'codeBlock':
|
||||
editor.chain().focus().toggleCodeBlock().run();
|
||||
break;
|
||||
case 'blockquote':
|
||||
editor.chain().focus().toggleBlockquote().run();
|
||||
break;
|
||||
}
|
||||
showTurnInto = false;
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Close modals/dropdowns on Escape
|
||||
if (event.key === 'Escape') {
|
||||
if (showTurnInto) {
|
||||
showTurnInto = false;
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
// Close turn into dropdown if clicking outside
|
||||
if (showTurnInto && !(event.target as Element).closest('.turn-into-dropdown')) {
|
||||
showTurnInto = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} onclick={handleClick} />
|
||||
|
||||
<!-- Main Bubble Menu -->
|
||||
<M.div
|
||||
class="flex gap-1 items-center p-1 rounded-lg border shadow-xl backdrop-blur-lg menu dark:bg-zinc-900/90 bg-white/90 dark:border-zinc-700/30 border-zinc-200/50"
|
||||
layout
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
>
|
||||
{#if editor}
|
||||
<M.div
|
||||
class="flex gap-1 items-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<!-- Turn Into Dropdown -->
|
||||
<div class="relative turn-into-dropdown">
|
||||
<M.button
|
||||
onclick={() => 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()}
|
||||
<M.div
|
||||
animate={{ rotate: showTurnInto ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Icon src={ChevronDown} size="14" />
|
||||
</M.div>
|
||||
</M.button>
|
||||
|
||||
{#if showTurnInto}
|
||||
<M.div
|
||||
class="absolute left-0 top-full z-50 mt-1 w-48 bg-white rounded-lg border shadow-xl dark:bg-zinc-800 border-zinc-200/40 dark:border-zinc-700/40"
|
||||
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
{#each turnIntoOptions as option}
|
||||
{#if option.id === 'separator'}
|
||||
<div class="my-1 h-px bg-zinc-200/60 dark:bg-zinc-600/60"></div>
|
||||
{:else}
|
||||
<button
|
||||
onclick={() => turnInto(option.id)}
|
||||
class="flex gap-2 items-center px-3 py-2 w-full text-sm text-left transition-colors hover:bg-zinc-100/60 dark:hover:bg-zinc-700/40"
|
||||
>
|
||||
<span class="{option.iconClass || ''}">{option.icon}</span>
|
||||
{option.label}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</M.div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mx-1 w-px h-6 bg-zinc-300 dark:bg-zinc-600"></div>
|
||||
|
||||
<M.button
|
||||
onclick={() => 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 }}
|
||||
>
|
||||
<Icon src={Bold} size="16" />
|
||||
</M.button>
|
||||
|
||||
<M.button
|
||||
onclick={() => 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 }}
|
||||
>
|
||||
<Icon src={Italic} size="16" />
|
||||
</M.button>
|
||||
|
||||
<M.button
|
||||
onclick={() => 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 }}
|
||||
>
|
||||
<Icon src={Strikethrough} size="16" />
|
||||
</M.button>
|
||||
|
||||
<M.button
|
||||
onclick={() => 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 }}
|
||||
>
|
||||
<Icon src={CodeBracket} size="16" />
|
||||
</M.button>
|
||||
</M.div>
|
||||
{/if}
|
||||
</M.div>
|
||||
@@ -0,0 +1,172 @@
|
||||
<script lang="ts">
|
||||
import { slashVisible, slashItems, slashLocation, slashProps, selectedIndex } from './stores';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
let height = $state(0);
|
||||
let elements = $state<any[]>([]);
|
||||
|
||||
export function handleKeydown(event: any, editor: any) {
|
||||
if (!get(slashVisible)) return;
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
selectItem(editor);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function upHandler() {
|
||||
const currentIndex = get(selectedIndex);
|
||||
const itemsLength = get(slashItems).length;
|
||||
const newIndex = currentIndex === 0 ? itemsLength - 1 : currentIndex - 1;
|
||||
selectedIndex.set(newIndex);
|
||||
}
|
||||
|
||||
function downHandler() {
|
||||
const currentIndex = get(selectedIndex);
|
||||
const itemsLength = get(slashItems).length;
|
||||
const newIndex = currentIndex === itemsLength - 1 ? 0 : currentIndex + 1;
|
||||
selectedIndex.set(newIndex);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const element = elements[$selectedIndex];
|
||||
if (!element) return;
|
||||
|
||||
const container = element.closest('.overflow-auto');
|
||||
if (!container) return;
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
const elementTop = elementRect.top - containerRect.top + container.scrollTop;
|
||||
const elementBottom = elementTop + elementRect.height;
|
||||
const containerHeight = containerRect.height;
|
||||
|
||||
// Check if element is outside visible area
|
||||
if (elementTop < container.scrollTop) {
|
||||
// Element is above visible area
|
||||
container.scrollTop = elementTop - 8;
|
||||
} else if (elementBottom > container.scrollTop + containerHeight) {
|
||||
// Element is below visible area
|
||||
container.scrollTop = elementBottom - containerHeight + 8;
|
||||
}
|
||||
});
|
||||
|
||||
function selectItem(editor: any) {
|
||||
const item = get(slashItems)[get(selectedIndex)];
|
||||
|
||||
if (item) {
|
||||
let range = get(slashProps).range;
|
||||
item.command({ editor, range });
|
||||
slashVisible.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
function closeSlashMenu() {
|
||||
slashVisible.set(false);
|
||||
selectedIndex.set(0);
|
||||
}
|
||||
|
||||
function handleItemClick(item: any) {
|
||||
const editor = get(slashProps).editor;
|
||||
const range = get(slashProps).range;
|
||||
slashVisible.set(false);
|
||||
selectedIndex.set(0);
|
||||
item.command({ editor, range });
|
||||
}
|
||||
|
||||
function getCommandIcon(title: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
'To Dos':
|
||||
'<svg class="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"></path></svg>',
|
||||
'Heading 1':
|
||||
'<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path><text x="2" y="18" font-size="12" font-weight="bold" fill="currentColor">H1</text></svg>',
|
||||
'Heading 2':
|
||||
'<svg class="w-5 h-5 text-purple-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path><text x="2" y="18" font-size="12" font-weight="bold" fill="currentColor">H2</text></svg>',
|
||||
'Heading 3':
|
||||
'<svg class="w-5 h-5 text-purple-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path><text x="2" y="18" font-size="12" font-weight="bold" fill="currentColor">H3</text></svg>',
|
||||
'Bullet List':
|
||||
'<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>',
|
||||
'Numbered List':
|
||||
'<svg class="w-5 h-5 text-orange-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>',
|
||||
Text: '<svg class="w-5 h-5 text-zinc-300" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6z" clip-rule="evenodd"></path><path d="M8 6h4M8 8h4M8 10h2"></path></svg>',
|
||||
Quote:
|
||||
'<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0-3a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0-3a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0-3a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm8-3a3 3 0 11-6 0 3 3 0 016 0z" clip-rule="evenodd"></path></svg>',
|
||||
'Code Block':
|
||||
'<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>',
|
||||
Divider:
|
||||
'<svg class="w-5 h-5 text-zinc-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>',
|
||||
'Bold Text':
|
||||
'<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20"><path d="M6 4v12h3.5c2.5 0 4.5-2 4.5-4.5S12 7 9.5 7H9V4H6zm3 5.5h.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H9V9.5z"></path></svg>',
|
||||
'Italic Text':
|
||||
'<svg class="w-5 h-5 text-pink-400" fill="currentColor" viewBox="0 0 20 20"><path d="M8 4h4l-2 12H6l2-12z"></path></svg>',
|
||||
Link: '<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"></path></svg>',
|
||||
'Inline Code':
|
||||
'<svg class="w-5 h-5 text-cyan-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm4.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L14.586 7l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>',
|
||||
};
|
||||
|
||||
return (
|
||||
icons[title] ||
|
||||
'<svg class="w-5 h-5 text-zinc-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={height} />
|
||||
|
||||
{#if $slashVisible}
|
||||
<div
|
||||
class="fixed top-0 w-full h-screen"
|
||||
onkeydown={() => {}}
|
||||
onclick={closeSlashMenu}
|
||||
role="menu"
|
||||
tabindex="-1">
|
||||
</div>
|
||||
|
||||
<div
|
||||
transition:fly={{ y: 10, duration: 300 }}
|
||||
class="overflow-auto absolute pb-2 w-80 max-w-full max-h-80 rounded-xl border shadow-xl backdrop-blur-lg origin-top-left scale-125 dark:bg-zinc-900/70 bg-zinc-100/70 dark:border-zinc-700/20 border-zinc-200"
|
||||
style="left: {$slashLocation.x}px; top: {$slashLocation.y + $slashLocation.height + 320 > height
|
||||
? $slashLocation.y - $slashLocation.height - 320
|
||||
: $slashLocation.y + $slashLocation.height}px;">
|
||||
<div class="p-2 text-sm text-zinc-500">Basic Blocks</div>
|
||||
{#each $slashItems as { title, subtitle, command }, i}
|
||||
<div
|
||||
class="p-2 flex gap-3 cursor-pointer {i == $selectedIndex &&
|
||||
'dark:bg-zinc-950/50 bg-zinc-300/50'} dark:hover:bg-zinc-950/30 hover:bg-zinc-300/20 rounded-lg mx-2"
|
||||
onclick={() => handleItemClick({ command })}
|
||||
onkeydown={() => {}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
bind:this={elements[i]}>
|
||||
<div class="flex justify-center items-center w-8 h-8 rounded-lg bg-zinc-800">
|
||||
{@html getCommandIcon(title)}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium truncate dark:text-white">
|
||||
{title}
|
||||
</div>
|
||||
<p class="text-xs truncate text-zinc-400">
|
||||
{subtitle ? subtitle : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -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,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -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<SlashItems> = writable([]);
|
||||
export const slashVisible: Writable<boolean> = writable(false);
|
||||
export const slashLocation: Writable<Location> = writable({
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 0,
|
||||
});
|
||||
export const slashProps: Writable<EditorProps> = writable({
|
||||
editor: null,
|
||||
range: null,
|
||||
});
|
||||
export const desktopMenu: Writable<boolean> = writable(true);
|
||||
export const components: Writable<Components> = writable([]);
|
||||
export const editorWidth: Writable<number> = writable(0);
|
||||
export const selectedIndex: Writable<number> = writable(0);
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 <a>, not the <li>, 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);
|
||||
}
|
||||
}
|
||||
@@ -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<typeof settings> {}
|
||||
|
||||
const settingsInstance = new CustomMessageEditorPlugin();
|
||||
|
||||
const customMessageEditorPlugin: Plugin<typeof settings> = {
|
||||
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<string>((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;
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user