Merge branch 'feat/release-v2' of https://github.com/classroomio/classroomio into feat/release-v2

This commit is contained in:
Rotimi Best
2025-12-25 13:23:30 +00:00
9 changed files with 299 additions and 44 deletions
@@ -26,6 +26,10 @@ import Table from '@lucide/svelte/icons/table';
import Underline from '@lucide/svelte/icons/underline';
import Undo from '@lucide/svelte/icons/undo-2';
import Video from '@lucide/svelte/icons/video';
import Radical from '@lucide/svelte/icons/radical';
import SquareRadical from '@lucide/svelte/icons/square-radical';
import { isTextSelection } from '@tiptap/core';
import Pilcrow from '@lucide/svelte/icons/pilcrow';
import { isMac } from '../utils';
const commands: Record<string, EdraToolBarCommands[]> = {
@@ -64,6 +68,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 1 }).run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 1 }).run();
},
clickable: (editor) => {
return editor.can().toggleHeading({ level: 1 });
},
@@ -79,6 +86,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 2 }).run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 2 }).run();
},
clickable: (editor) => {
return editor.can().toggleHeading({ level: 2 });
},
@@ -94,6 +104,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 3 }).run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 3 }).run();
},
clickable: (editor) => {
return editor.can().toggleHeading({ level: 3 });
},
@@ -109,6 +122,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleHeading({ level: 4 }).run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setHeading({ level: 4 }).run();
},
clickable: (editor) => {
return editor.can().toggleHeading({ level: 4 });
},
@@ -136,6 +152,24 @@ const commands: Record<string, EdraToolBarCommands[]> = {
return editor.isActive('link');
}
},
{
icon: Pilcrow,
name: 'paragraph',
tooltip: 'Paragraph',
shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}0`,
onClick: (editor) => {
editor.chain().focus().setParagraph().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setParagraph().run();
},
clickable: (editor) => {
return editor.can().setParagraph();
},
isActive: (editor) => {
return editor.isActive('paragraph');
}
},
{
icon: Bold,
name: 'bold',
@@ -144,6 +178,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleBold().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('bold').run();
},
clickable: (editor) => {
return editor.can().toggleBold();
},
@@ -159,6 +196,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleItalic().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('italic').run();
},
clickable: (editor) => {
return editor.can().toggleItalic();
},
@@ -174,6 +214,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleUnderline().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('underline').run();
},
clickable: (editor) => {
return editor.can().toggleUnderline();
},
@@ -189,6 +232,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleStrike().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).setMark('strike').run();
},
clickable: (editor) => {
return editor.can().toggleStrike();
},
@@ -204,6 +250,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleBlockquote().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleBlockquote().run();
},
clickable: (editor) => {
return editor.can().toggleBlockquote();
},
@@ -219,6 +268,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleCode().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleCodeBlock().run();
},
clickable: (editor) => {
return editor.can().toggleCode();
},
@@ -320,6 +372,9 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleBulletList().run();
},
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleBulletList().run();
},
isActive: (editor) => editor.isActive('bulletList')
},
{
@@ -330,7 +385,15 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleOrderedList().run();
},
isActive: (editor) => editor.isActive('orderedList')
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleOrderedList().run();
},
clickable: (editor) => {
return editor.can().toggleOrderedList();
},
isActive: (editor) => {
return editor.isActive('orderedList');
}
},
{
icon: ListChecks,
@@ -340,7 +403,15 @@ const commands: Record<string, EdraToolBarCommands[]> = {
onClick: (editor) => {
editor.chain().focus().toggleTaskList().run();
},
isActive: (editor) => editor.isActive('taskList')
turnInto: (editor, pos) => {
editor.chain().setNodeSelection(pos).toggleTaskList().run();
},
clickable: (editor) => {
return editor.can().toggleTaskList();
},
isActive: (editor) => {
return editor.isActive('taskList');
}
}
],
media: [
@@ -398,6 +469,34 @@ const commands: Record<string, EdraToolBarCommands[]> = {
},
isActive: (editor) => editor.isActive('table')
}
],
math: [
{
icon: Radical,
name: 'mathematics',
tooltip: 'Inline Expression',
onClick: (editor) => {
let latex = 'a^2 + b^2 = c^2';
const chain = editor.chain().focus();
if (isTextSelection(editor.view.state.selection)) {
const { from, to } = editor.view.state.selection;
latex = editor.view.state.doc.textBetween(from, to);
chain.deleteRange({ from, to });
}
chain.insertInlineMath({ latex }).run();
},
isActive: (editor) => editor.isActive('inlineMath')
},
{
icon: SquareRadical,
name: 'mathematics',
tooltip: 'Block Expression',
onClick: (editor) => {
const latex = 'a^2 + b^2 = c^2';
editor.chain().focus().insertBlockMath({ latex }).run();
},
isActive: (editor) => editor.isActive('blockMath')
}
]
};
@@ -7,6 +7,7 @@ export interface EdraToolBarCommands {
tooltip?: string;
shortCut?: string;
onClick?: (editor: Editor) => void;
turnInto?: (editor: Editor, pos: number) => void;
isActive?: (editor: Editor) => boolean;
clickable?: (editor: Editor) => boolean;
}
+45 -6
View File
@@ -5,11 +5,12 @@
}
/* For Placeholder */
.tiptap .is-empty::before {
.tiptap .is-empty:not(blockquote.is-empty)::before {
pointer-events: none;
float: left;
height: 0;
color: var(--border-color-hover);
color: var(--color-muted-foreground);
content: attr(data-placeholder);
}
@@ -66,12 +67,12 @@
}
.tiptap blockquote::before {
content: '';
content: ' ';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0.25rem;
width: 0.4rem;
background-color: var(--blockquote-border);
border-radius: 0.5rem;
}
@@ -208,8 +209,8 @@ input[type='checkbox'] {
align-items: center;
cursor: grab;
opacity: 100;
transition-property: opacity;
transition-duration: 200ms;
transition-property: all;
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
color: var(--border-color-hover);
}
@@ -452,3 +453,41 @@ input[type='checkbox'] {
.tiptap iframe {
aspect-ratio: 16 / 9;
}
/* Mathematics extension styles */
.tiptap .tiptap-mathematics-render {
padding: 0 0.25rem;
border-radius: 0.25rem;
}
/* Editable math block */
.tiptap .tiptap-mathematics-render--editable {
cursor: pointer;
transition: background 0.2s;
}
.tiptap .tiptap-mathematics-render--editable:hover {
background: var(--code-bg);
}
/* Inline math */
.tiptap .tiptap-mathematics-render[data-type='inline-math'] {
display: inline-block;
}
/* Block math */
.tiptap .tiptap-mathematics-render[data-type='block-math'] {
display: block;
margin: 1rem 0;
padding: 0.5rem;
text-align: center;
font-size: 1.25rem;
}
/* Error styles */
.tiptap .tiptap-mathematics-render.inline-math-error,
.tiptap .tiptap-mathematics-render.block-math-error {
background: var(--code-bg);
color: var(--color-destructive);
border: 1px solid darkred;
}
+34 -13
View File
@@ -12,10 +12,12 @@ import SearchAndReplace from './extensions/FindAndReplace';
import { TaskItem, TaskList } from '@tiptap/extension-list';
import { Table, TableCell, TableRow, TableHeader } from './extensions/table';
import { Placeholder } from '@tiptap/extensions';
import { Markdown } from 'tiptap-markdown';
import MathExtension from '@aarkue/tiptap-math-extension';
import { Markdown } from '@tiptap/markdown';
import MathMatics from '@tiptap/extension-mathematics';
import AutoJoiner from 'tiptap-extension-auto-joiner';
import 'katex/dist/katex.min.css';
import { InlineMathReplacer } from './extensions/InlineMathReplacer';
export default (
element?: HTMLElement,
@@ -82,22 +84,41 @@ export default (
nested: true
}),
SearchAndReplace,
MathExtension.configure({ evaluation: true }),
InlineMathReplacer,
MathMatics.configure({
blockOptions: {
onClick: (node, pos) => {
const newCalculation = prompt('Enter new calculation:', node.attrs.latex);
if (newCalculation) {
editor
.chain()
.setNodeSelection(pos)
.updateBlockMath({ latex: newCalculation })
.focus()
.run();
}
}
},
inlineOptions: {
onClick: (node, pos) => {
const newCalculation = prompt('Enter new calculation:', node.attrs.latex);
if (newCalculation) {
editor
.chain()
.setNodeSelection(pos)
.updateInlineMath({ latex: newCalculation })
.focus()
.run();
}
}
}
}),
AutoJoiner,
Table,
TableHeader,
TableRow,
TableCell,
Markdown.configure({
html: true,
tightLists: true,
tightListClass: 'tight',
bulletListMarker: '-',
linkify: true,
breaks: true,
transformPastedText: true,
transformCopiedText: false
}),
Markdown,
...(extensions ?? [])
],
@@ -0,0 +1,21 @@
import { textInputRule } from '@tiptap/core';
import { InlineMath } from '@tiptap/extension-mathematics';
export const InlineMathReplacer = InlineMath.extend({
name: 'inlineMathReplacer',
addInputRules() {
return [
textInputRule({
find: /\$\$([^$]+)\$\$/,
replace: ({ match, commands }) => {
const latex = match[1];
// Insert the inline math node with the LaTeX content
commands.insertInlineMath({
latex
});
return '';
}
})
];
}
});
@@ -42,6 +42,7 @@ export const GROUPS: Group[] = [
actions: [
...commands.media,
...commands.table,
...commands.math,
{
icon: Minus,
name: 'horizontalRule',
@@ -10,7 +10,10 @@
import Duplicate from '@lucide/svelte/icons/copy';
import Clipboard from '@lucide/svelte/icons/clipboard';
import Delete from '@lucide/svelte/icons/trash-2';
import Repeat2 from '@lucide/svelte/icons/repeat-2';
import Plus from '@lucide/svelte/icons/plus';
import { NodeSelection } from '@tiptap/pm/state';
import commands from '../commands/toolbar-commands';
interface Props {
editor: Editor;
@@ -21,6 +24,9 @@
let currentNode: Node | null = $state(null);
let currentNodePos: number = $state(-1);
let open = $state(false);
const turnIntoCommand = Object.values(commands)
.flat()
.filter((c) => c.turnInto !== undefined);
const pluginKey = 'globalDragHandle';
@@ -71,10 +77,39 @@
const handleDelete = () => {
editor.chain().setMeta('hideDragHandle', true).setNodeSelection(currentNodePos).deleteSelection().run();
};
const handleAddNodeNext = () => {
if (currentNodePos === -1) return;
const currentNodeSize = currentNode?.nodeSize || 0;
const insertPos = currentNodePos + currentNodeSize;
const currentNodeIsEmptyParagraph =
currentNode?.type.name === 'paragraph' && currentNode?.content?.size === 0;
const focusPos = currentNodeIsEmptyParagraph ? currentNodePos + 2 : insertPos + 2;
editor
.chain()
.command(({ dispatch, tr, state }) => {
if (dispatch) {
if (currentNodeIsEmptyParagraph) {
tr.insertText('/', currentNodePos, currentNodePos + 1);
} else {
tr.insert(
insertPos,
state.schema.nodes.paragraph.create(null, [state.schema.text('/')])
);
}
return dispatch(tr);
}
return true;
})
.focus(focusPos)
.run();
};
</script>
<div class="drag-handle">
<Button variant="ghost" class="ui:rounded-sm ui:p-0 !size-6" onclick={() => (open = true)}>
<Button variant="ghost" class="!ui:size-6 ui:rounded-sm ui:p-0" onclick={() => (open = true)}>
<GripVertical />
</Button>
<DropdownMenu.Root bind:open>
@@ -82,10 +117,43 @@
<span>Drag Handle</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.GroupHeading class="ui:text-muted-foreground ui:capitalize">
{currentNode?.type.name}
</DropdownMenu.GroupHeading>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger openDelay={300}>
<Repeat2 />
Turn Into
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent class="ui:max-h-96 ui:overflow-auto ui:duration-300">
{#each turnIntoCommand as command (command)}
{@const Icon = command.icon}
<DropdownMenu.Item
onclick={() => {
if (currentNode && currentNodePos) command.turnInto?.(editor, currentNodePos);
}}
>
<Icon />
<span>{command.tooltip}</span>
<DropdownMenu.Shortcut class="ui:bg-background ui:rounded ui:border ui:p-0.5"
>{command.shortCut}</DropdownMenu.Shortcut
>
</DropdownMenu.Item>
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleAddNodeNext}>
<Plus />
Add Node
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleRemoveFormatting}>
<RemoveFormatting />
Remove Formatting
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDuplicate}>
<Duplicate />
Duplicate Node
@@ -94,6 +162,7 @@
<Clipboard />
Copy to clipboard
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}>
<Delete class="ui:text-destructive" />
Delete Node
@@ -106,7 +106,11 @@
{:else}
{@const commandGroup = commands[cmd]}
{#each commandGroup as command (command)}
<ToolBarIcon {editor} {command} />
{#if command.name === 'paragraph'}
<span></span>
{:else}
<ToolBarIcon {editor} {command} />
{/if}
{/each}
{/if}
{/each}