[WEB-480] fix: code block paste from VSCode and indentation (#4198)

* fix: stroing the transactions in page

* fix: page details changes

* chore: page response change

* chore: removed duplicated endpoints

* chore: optimised the urls

* chore: removed archived and favorite pages

* chore: revamping pages store and components

* mentions loading state part done

* fixed mentions not showing in modals

* removed comments and cleaned up types

* removed unused types

* reset: head

* chore: pages store and component updates

* style: pages list item UI

* fix: improved colors and drag handle width

* fix: slash commands are no more shown in the code blocks

* fix: cleanup/hide drag handles post drop

* fix: hide/cleanup drag handles post drag start

* fix: aligning the drag handles better with the node post css changes of the length

* fix: juggling back and forth of drag handles in ordered and unordered lists

* chore: fix imports, ts errors and other things

* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected

* chore: filtering, ordering and searching implemented

* chore: updated pages store and updated UI

* chore: new core editor just for document editor created

* chore: removed setIsSubmitting prop in doc editor

* fix: fixed submitting state for image uploads

* refactor: setShouldShowAlert removed

* refactor: rerenderOnPropsChange prop removed

* chore: type inference magic in ref to expose an api for controlling editor menu items from outside

* fix: naming imports

* chore: change names of the exposed functions and removing old types

* refactor: remove debouncedUpdatesEnabled prop;

* refactor: editor heading markings now parsed using html

* chore: removed unrelated components from the document editor

* refactor: page details granular components

* fix: remove onActionCompleteHandler

* refactor: removed rerenderOnProps change prop

* feat: added getMarkDown function

* chore: update dropdown option actions

* fix: sidebar markings update logic

* chore: add image and to-do list actions to the toolbar

* fix: handling refs and populating them via callbacks

* feat: scroll to node api exposed

* cleaning up editor refs when the editor is destroyed

* feat: scrolling added to read only instance of the editor

* fix: markings logic

* fix: build errors with types

* fix: build erros

* fix: subscribing to transactions of editor via ref

* chore: remove debug statements

* fix: type errors

* fix: temporary different slash commands for document editor

* chore: inline code extension style

* chore: remove border from readOnly editor

* fix: editor bottom padding

* chore: pages improvements

* chore: handle Enter key on the page title

* feat: added loading indicator logic in mentions

* fix: mentions and slash commands now work well with multiple editors in one place

* refactor: page store structure, filtering logic

* feat: added better seperation in inline code blocks

* feat: list autojoining added

* fix: pages folder structure

* fix: image refocus from external parts

* working lists somewhat

* chore: implement page reactions

* fix: build errors

* fix: build errors

* fixed drag handles stuff

* task list item fixed

* working

* fix: working on multiple nested lists

* chore: remove debug statements

* fix: Tab key on first list item handled to not go out of editor focus

* feat: threshold auto scroll support added and multi nested list selection fixed

* fix: caret color bug with improved inline code blocks

* fix: node range error when bulk deleting with list

* fix: removed slash commands from working in code blocks

* chore: update typography margins

* chore: new field added in page model

* fix: better type inference in slash commands

* chore: code block UI

* feat: image insertion at correct position using ref added

* feat: added improved mentions support for space

* fix: type errors in mentions for comments in web app

* sync: core with document-core

* fix: build errors

* fix: fallback for appendTo not being able to find active container instantly

* fix: page store

* fix: page description

* fix: css quality issues

* chore: code cleanup

* chore: removed placeholder text in codeblocks

* chore: archived pages response change

* chore: archived pages response change

* fix: initial pages list fetch

* fix: pages list filters and ordering

* chore: add access change option in the quick actions dropdown

* fix: inline code block caret fixed

* regression: removing extra text

* chore: caret color removed

* feat: copy code button added in code blocks

* fix: initial load of page details

* fix: initial load of page details

* fix: image resizing weird behavior on click/expanding it too much fixed now

* chore: copy page response

* fix: todo list spacing

* chore: description html in the copy page

* chore: handle latest description on refetch

* fix: saner scroll behaviours

* fix: block menu positioning

* fix: updated empty string description

* feat: tab change sync support added

* fix: infinite rerendering with markings

* fix: block menu finally

* fix: intial load on reload bug fixed

* fix: nested lists alignment

* fix: editor padding

* fix: first level list items copyable

* chore: list spacing

* fix: title change

* fix: pages list block items interaction

* fix: saving chip position

* fix: delete action from block menu to focus properly

* fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node

* style: table, chore: lite text editor toolbar

* fix: page description tab sync

* fix: lists spacing and alignment

* refactor: document editor props

* feat: rich text editor wrapper created and migrated core

* feat: created wrapper around lite text editor and merged core

* chore: add lite text editor toolbar

* fix: build errors

* fix: type errors and addead live updation of toolbar

* chore: pages migration

* fix: inbox issue

* refactor: remove redundant package

* refactor: unused files

* fix: add dompurify to space app

* fix: inline code margin

* fix: editor className props

* fix: build errors

* fix: traversing up the tree before assuming the parent is not a list item

* fix: drag handle positions for list items fixed

* fix: removed focus at end logic after deleting block

* fix: image wrapper overflow scroll fix with block menu's position

* fix: selection and deletion logic for nested lists fixed!!

* fix: hiding the block menu while scrolling in the document/app

* fix: merge conflicts resolved from develop

* fix: inbox issue description

* chore: move page title to the web app

* fix: handling edge cases for table selection

* chore: lint issues

* refactor: list item functions moved to same file

* refactor: use mention hook

* fix: added try catch blocks for mention suggestions

* chore: remove unused code

* fix: remove console logs

* fix: remove console logs

* fix: code block paste handler

* fix: tracking image uploading status to prevent sync rerenders

* chore: remove unnecessary props

* fix: code block pasting logic from vs code handled properly

* feat: additional checks for doc bounds

* fix: type of cancel button changed to button instead of default submit

* feat: editor focus on saved position while syncing via swr

* fix: type errors

* fix: changing names of plugins and removing packages

* fix: readonly editor synced

* removed console logs

* fix: stringifying instead of dom injection

* fix: use-editor try catch error handling

* fix: editor container click in try catch

* fix: some more error handling

* chore: removed commented out code

* fix: backspace error handling

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
M. Palanikannan 2024-04-16 18:50:45 +05:30 committed by GitHub
parent 59772be014
commit 27db8699c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 965 additions and 319 deletions

View File

@ -31,8 +31,6 @@
"@plane/ui": "*", "@plane/ui": "*",
"@tiptap/core": "^2.1.13", "@tiptap/core": "^2.1.13",
"@tiptap/extension-blockquote": "^2.1.13", "@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-code-block-lowlight": "^2.1.13",
"@tiptap/extension-color": "^2.1.13",
"@tiptap/extension-image": "^2.1.13", "@tiptap/extension-image": "^2.1.13",
"@tiptap/extension-list-item": "^2.1.13", "@tiptap/extension-list-item": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13", "@tiptap/extension-mention": "^2.1.13",

View File

@ -7,11 +7,22 @@ export const insertContentAtSavedSelection = (
content: string, content: string,
savedSelection: Selection savedSelection: Selection
) => { ) => {
if (editorRef.current && savedSelection) { if (!editorRef.current || editorRef.current.isDestroyed) {
editorRef.current console.error("Editor reference is not available or has been destroyed.");
.chain() return;
.focus() }
.insertContentAt(savedSelection?.anchor, content)
.run(); if (!savedSelection) {
console.error("Saved selection is invalid.");
return;
}
const docSize = editorRef.current.state.doc.content.size;
const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize));
try {
editorRef.current.chain().focus().insertContentAt(safePosition, content).run();
} catch (error) {
console.error("An error occurred while inserting content at saved selection:", error);
} }
}; };

View File

@ -108,15 +108,19 @@ export const useEditor = ({
// supported and value is undefined when the data from swr is not populated // supported and value is undefined when the data from swr is not populated
if (value === null || value === undefined) return; if (value === null || value === undefined) return;
if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) { if (editor && !editor.isDestroyed && !editor.storage.image.uploadInProgress) {
editor.commands.setContent(value); try {
const currentSavedSelection = savedSelectionRef.current; editor.commands.setContent(value);
if (currentSavedSelection) { const currentSavedSelection = savedSelectionRef.current;
editor.view.focus(); if (currentSavedSelection) {
const docLength = editor.state.doc.content.size; editor.view.focus();
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1); const docLength = editor.state.doc.content.size;
editor.commands.setTextSelection(relativePosition); const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
} else { editor.commands.setTextSelection(relativePosition);
editor.commands.focus("end"); } else {
editor.commands.focus("end");
}
} catch (error) {
console.error("Error syncing editor content with external value:", error);
} }
} }
}, [editor, value, id]); }, [editor, value, id]);
@ -179,12 +183,21 @@ export const useEditor = ({
scrollSummary(editorRef.current, marking); scrollSummary(editorRef.current, marking);
}, },
setFocusAtPosition: (position: number) => { setFocusAtPosition: (position: number) => {
if (!editorRef.current) return; if (!editorRef.current || editorRef.current.isDestroyed) {
editorRef.current console.error("Editor reference is not available or has been destroyed.");
.chain() return;
.insertContentAt(position, [{ type: "paragraph" }]) }
.focus() try {
.run(); const docSize = editorRef.current.state.doc.content.size;
const safePosition = Math.max(0, Math.min(position, docSize));
editorRef.current
.chain()
.insertContentAt(safePosition, [{ type: "paragraph" }])
.focus()
.run();
} catch (error) {
console.error("An error occurred while setting focus at position:", error);
}
}, },
}), }),
[editorRef, savedSelection, uploadFile] [editorRef, savedSelection, uploadFile]

View File

@ -34,32 +34,82 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
else editor.chain().focus().toggleUnderline().run(); else editor.chain().focus().toggleUnderline().run();
}; };
export const toggleCodeBlock = (editor: Editor, range?: Range) => { const replaceCodeBlockWithContent = (editor: Editor) => {
// Check if code block is active then toggle code block try {
if (editor.isActive("codeBlock")) { const { schema } = editor.state;
if (range) { const { paragraph } = schema.nodes;
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(); let replaced = false;
return;
const replaceCodeBlock = (from: number, to: number, textContent: string) => {
const docSize = editor.state.doc.content.size;
if (from < 0 || to > docSize || from > to) {
console.error("Invalid range for replacement: ", from, to, "in a document of size", docSize);
return;
}
// split the textContent by new lines to handle each line as a separate paragraph
const lines = textContent.split(/\r?\n/);
const tr = editor.state.tr;
// Calculate the position for inserting the first paragraph
let insertPos = from;
// Remove the code block first
tr.delete(from, to);
// For each line, create a paragraph node and insert it
lines.forEach((line) => {
const paragraphNode = paragraph.create({}, schema.text(line));
tr.insert(insertPos, paragraphNode);
// Update insertPos for the next insertion
insertPos += paragraphNode.nodeSize;
});
// Dispatch the transaction
editor.view.dispatch(tr);
replaced = true;
};
editor.state.doc.nodesBetween(editor.state.selection.from, editor.state.selection.to, (node, pos) => {
if (node.type === schema.nodes.codeBlock) {
const startPos = pos;
const endPos = pos + node.nodeSize;
const textContent = node.textContent;
replaceCodeBlock(startPos, endPos, textContent);
return false;
}
});
if (!replaced) {
console.log("No code block to replace.");
} }
editor.chain().focus().toggleCodeBlock().run(); } catch (error) {
return; console.error("An error occurred while replacing code block content:", error);
} }
};
// Check if user hasn't selected any text export const toggleCodeBlock = (editor: Editor, range?: Range) => {
const isSelectionEmpty = editor.state.selection.empty; try {
if (editor.isActive("codeBlock")) {
if (isSelectionEmpty) { replaceCodeBlockWithContent(editor);
if (range) {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
return; return;
} }
editor.chain().focus().toggleCodeBlock().run();
} else { const { from, to } = range || editor.state.selection;
if (range) { const text = editor.state.doc.textBetween(from, to, "\n");
editor.chain().focus().deleteRange(range).toggleCode().run(); const isMultiline = text.includes("\n");
return;
if (editor.state.selection.empty) {
editor.chain().focus().toggleCodeBlock().run();
} else if (isMultiline) {
editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, `\`\`\`\n${text}\n\`\`\``).run();
} else {
editor.chain().focus().toggleCode().run();
} }
editor.chain().focus().toggleCode().run(); } catch (error) {
console.error("An error occurred while toggling code block:", error);
} }
}; };

View File

@ -15,36 +15,40 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
const handleContainerClick = () => { const handleContainerClick = () => {
if (!editor) return; if (!editor) return;
if (!editor.isEditable) return; if (!editor.isEditable) return;
if (editor.isFocused) return; // If editor is already focused, do nothing try {
if (editor.isFocused) return; // If editor is already focused, do nothing
const { selection } = editor.state; const { selection } = editor.state;
const currentNode = selection.$from.node(); const currentNode = selection.$from.node();
editor?.chain().focus("end", { scrollIntoView: false }).run(); // Focus the editor at the end editor?.chain().focus("end", { scrollIntoView: false }).run(); // Focus the editor at the end
if ( if (
currentNode.content.size === 0 && // Check if the current node is empty currentNode.content.size === 0 && // Check if the current node is empty
!( !(
editor.isActive("orderedList") || editor.isActive("orderedList") ||
editor.isActive("bulletList") || editor.isActive("bulletList") ||
editor.isActive("taskItem") || editor.isActive("taskItem") ||
editor.isActive("table") || editor.isActive("table") ||
editor.isActive("blockquote") || editor.isActive("blockquote") ||
editor.isActive("codeBlock") editor.isActive("codeBlock")
) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block ) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block
) { ) {
return; return;
}
// Insert a new paragraph at the end of the document
const endPosition = editor?.state.doc.content.size;
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
// Focus the newly added paragraph for immediate editing
editor
.chain()
.setTextSelection(endPosition + 1)
.run();
} catch (error) {
console.error("An error occurred while handling container click to insert new empty node at bottom:", error);
} }
// Insert a new paragraph at the end of the document
const endPosition = editor?.state.doc.content.size;
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
// Focus the newly added paragraph for immediate editing
editor
.chain()
.setTextSelection(endPosition + 1)
.run();
}; };
return ( return (

View File

@ -0,0 +1,30 @@
// import CodeBlock, { CodeBlockOptions } from "@tiptap/extension-code-block";
import { CodeBlockOptions, CodeBlock } from "./code-block";
import { LowlightPlugin } from "./lowlight-plugin";
export interface CodeBlockLowlightOptions extends CodeBlockOptions {
lowlight: any;
defaultLanguage: string | null | undefined;
}
export const CodeBlockLowlight = CodeBlock.extend<CodeBlockLowlightOptions>({
addOptions() {
return {
...this.parent?.(),
lowlight: {},
defaultLanguage: null,
};
},
addProseMirrorPlugins() {
return [
...(this.parent?.() || []),
LowlightPlugin({
name: this.name,
lowlight: this.options.lowlight,
defaultLanguage: this.options.defaultLanguage,
}),
];
},
});

View File

@ -49,7 +49,7 @@ export const CodeBlockComponent: React.FC<CodeBlockComponentProps> = ({ node })
</button> </button>
<pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-8 pl-9 pr-4"> <pre className="bg-custom-background-90 text-custom-text-100 rounded-lg p-8 pl-9 pr-4">
<NodeViewContent as="code" /> <NodeViewContent as="code" className="whitespace-[pre-wrap]" />
</pre> </pre>
</NodeViewWrapper> </NodeViewWrapper>
); );

View File

@ -0,0 +1,346 @@
import { mergeAttributes, Node, textblockTypeInputRule } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
export interface CodeBlockOptions {
/**
* Adds a prefix to language classes that are applied to code tags.
* Defaults to `'language-'`.
*/
languageClassPrefix: string;
/**
* Define whether the node should be exited on triple enter.
* Defaults to `true`.
*/
exitOnTripleEnter: boolean;
/**
* Define whether the node should be exited on arrow down if there is no node after it.
* Defaults to `true`.
*/
exitOnArrowDown: boolean;
/**
* Custom HTML attributes that should be added to the rendered HTML tag.
*/
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
codeBlock: {
/**
* Set a code block
*/
setCodeBlock: (attributes?: { language: string }) => ReturnType;
/**
* Toggle a code block
*/
toggleCodeBlock: (attributes?: { language: string }) => ReturnType;
};
}
}
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
export const CodeBlock = Node.create<CodeBlockOptions>({
name: "codeBlock",
addOptions() {
return {
languageClassPrefix: "language-",
exitOnTripleEnter: true,
exitOnArrowDown: true,
HTMLAttributes: {},
};
},
content: "text*",
marks: "",
group: "block",
code: true,
defining: true,
addAttributes() {
return {
language: {
default: null,
parseHTML: (element) => {
const { languageClassPrefix } = this.options;
// @ts-expect-error element is a DOM element
const classNames = [...(element.firstElementChild?.classList || [])];
const languages = classNames
.filter((className) => className.startsWith(languageClassPrefix))
.map((className) => className.replace(languageClassPrefix, ""));
const language = languages[0];
if (!language) {
return null;
}
return language;
},
rendered: false,
},
};
},
parseHTML() {
return [
{
tag: "pre",
preserveWhitespace: "full",
},
];
},
renderHTML({ node, HTMLAttributes }) {
return [
"pre",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
[
"code",
{
class: node.attrs.language ? this.options.languageClassPrefix + node.attrs.language : null,
},
0,
],
];
},
addCommands() {
return {
setCodeBlock:
(attributes) =>
({ commands }) =>
commands.setNode(this.name, attributes),
toggleCodeBlock:
(attributes) =>
({ commands }) =>
commands.toggleNode(this.name, "paragraph", attributes),
};
},
addKeyboardShortcuts() {
return {
"Mod-Alt-c": () => this.editor.commands.toggleCodeBlock(),
// remove code block when at start of document or code block is empty
Backspace: () => {
try {
const { empty, $anchor } = this.editor.state.selection;
const isAtStart = $anchor.pos === 1;
if (!empty || $anchor.parent.type.name !== this.name) {
return false;
}
if (isAtStart || !$anchor.parent.textContent.length) {
return this.editor.commands.clearNodes();
}
return false;
} catch (error) {
console.error("Error handling Backspace in code block:", error);
return false;
}
},
// exit node on triple enter
Enter: ({ editor }) => {
try {
if (!this.options.exitOnTripleEnter) {
return false;
}
const { state } = editor;
const { selection } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
const endsWithDoubleNewline = $from.parent.textContent.endsWith("\n\n");
if (!isAtEnd || !endsWithDoubleNewline) {
return false;
}
return editor
.chain()
.command(({ tr }) => {
tr.delete($from.pos - 2, $from.pos);
return true;
})
.exitCode()
.run();
} catch (error) {
console.error("Error handling Enter in code block:", error);
return false;
}
},
// exit node on arrow down
ArrowDown: ({ editor }) => {
try {
if (!this.options.exitOnArrowDown) {
return false;
}
const { state } = editor;
const { selection, doc } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
if (!isAtEnd) {
return false;
}
const after = $from.after();
if (after === undefined) {
return false;
}
const nodeAfter = doc.nodeAt(after);
if (nodeAfter) {
return false;
}
return editor.commands.exitCode();
} catch (error) {
console.error("Error handling ArrowDown in code block:", error);
return false;
}
},
};
},
addInputRules() {
return [
textblockTypeInputRule({
find: backtickInputRegex,
type: this.type,
getAttributes: (match) => ({
language: match[1],
}),
}),
textblockTypeInputRule({
find: tildeInputRegex,
type: this.type,
getAttributes: (match) => ({
language: match[1],
}),
}),
];
},
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("codeBlockVSCodeHandlerCustom"),
props: {
handlePaste: (view, event) => {
try {
if (!event.clipboardData) {
return false;
}
if (this.editor.isActive(this.type.name)) {
return false;
}
if (this.editor.isActive("code")) {
// Check if it's an inline code block
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
if (!text) {
console.error("Pasted text is empty.");
return false;
}
const { tr } = view.state;
const { $from, $to } = tr.selection;
if ($from.pos > $to.pos) {
console.error("Invalid selection range.");
return false;
}
const docSize = tr.doc.content.size;
if ($from.pos < 0 || $to.pos > docSize) {
console.error("Selection range is out of document bounds.");
return false;
}
// Extend the current selection to replace it with the pasted text
// wrapped in an inline code mark
const codeMark = view.state.schema.marks.code.create();
tr.replaceWith($from.pos, $to.pos, view.state.schema.text(text, [codeMark]));
view.dispatch(tr);
return true;
}
event.preventDefault();
const text = event.clipboardData.getData("text/plain");
const vscode = event.clipboardData.getData("vscode-editor-data");
const vscodeData = vscode ? JSON.parse(vscode) : undefined;
const language = vscodeData?.mode;
if (vscodeData && language) {
const { tr } = view.state;
const { $from } = tr.selection;
// Check if the current line is empty
const isCurrentLineEmpty = !$from.parent.textContent.trim();
let insertPos;
if (isCurrentLineEmpty) {
// If the current line is empty, use the current position
insertPos = $from.pos - 1;
} else {
// If the current line is not empty, insert below the current block node
insertPos = $from.end($from.depth) + 1;
}
// Ensure insertPos is within document bounds
if (insertPos < 0 || insertPos > tr.doc.content.size) {
console.error("Invalid insert position.");
return false;
}
// Create a new code block node with the pasted content
const textNode = view.state.schema.text(text.replace(/\r\n?/g, "\n"));
const codeBlock = this.type.create({ language }, textNode);
if (insertPos <= tr.doc.content.size) {
tr.insert(insertPos, codeBlock);
view.dispatch(tr);
return true;
}
return false;
} else {
// TODO: complicated paste logic, to be handled later
return false;
}
} catch (error) {
console.error("Error handling paste in CodeBlock extension:", error);
return false;
}
},
},
}),
];
},
});

View File

@ -1,5 +1,3 @@
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { common, createLowlight } from "lowlight"; import { common, createLowlight } from "lowlight";
import ts from "highlight.js/lib/languages/typescript"; import ts from "highlight.js/lib/languages/typescript";
@ -9,6 +7,7 @@ lowlight.register("ts", ts);
import { Selection } from "@tiptap/pm/state"; import { Selection } from "@tiptap/pm/state";
import { ReactNodeViewRenderer } from "@tiptap/react"; import { ReactNodeViewRenderer } from "@tiptap/react";
import { CodeBlockComponent } from "./code-block-node-view"; import { CodeBlockComponent } from "./code-block-node-view";
import { CodeBlockLowlight } from "./code-block-lowlight";
export const CustomCodeBlockExtension = CodeBlockLowlight.extend({ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
addNodeView() { addNodeView() {
@ -18,85 +17,100 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
Tab: ({ editor }) => { Tab: ({ editor }) => {
const { state } = editor; try {
const { selection } = state; const { state } = editor;
const { $from, empty } = selection; const { selection } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) { if (!empty || $from.parent.type !== this.type) {
return false;
}
// Use ProseMirror's insertText transaction to insert the tab character
const tr = state.tr.insertText("\t", $from.pos, $from.pos);
editor.view.dispatch(tr);
return true;
} catch (error) {
console.error("Error handling Tab in CustomCodeBlockExtension:", error);
return false; return false;
} }
// Use ProseMirror's insertText transaction to insert the tab character
const tr = state.tr.insertText("\t", $from.pos, $from.pos);
editor.view.dispatch(tr);
return true;
}, },
ArrowUp: ({ editor }) => { ArrowUp: ({ editor }) => {
const { state } = editor; try {
const { selection } = state; const { state } = editor;
const { $from, empty } = selection; const { selection } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) { if (!empty || $from.parent.type !== this.type) {
return false;
}
const isAtStart = $from.parentOffset === 0;
if (!isAtStart) {
return false;
}
// Check if codeBlock is the first node
const isFirstNode = $from.depth === 1 && $from.index($from.depth - 1) === 0;
if (isFirstNode) {
// Insert a new paragraph at the start of the document and move the cursor to it
return editor.commands.command(({ tr }) => {
const node = editor.schema.nodes.paragraph.create();
tr.insert(0, node);
tr.setSelection(Selection.near(tr.doc.resolve(1)));
return true;
});
}
return false;
} catch (error) {
console.error("Error handling ArrowUp in CustomCodeBlockExtension:", error);
return false; return false;
} }
const isAtStart = $from.parentOffset === 0;
if (!isAtStart) {
return false;
}
// Check if codeBlock is the first node
const isFirstNode = $from.depth === 1 && $from.index($from.depth - 1) === 0;
if (isFirstNode) {
// Insert a new paragraph at the start of the document and move the cursor to it
return editor.commands.command(({ tr }) => {
const node = editor.schema.nodes.paragraph.create();
tr.insert(0, node);
tr.setSelection(Selection.near(tr.doc.resolve(1)));
return true;
});
}
return false;
}, },
ArrowDown: ({ editor }) => { ArrowDown: ({ editor }) => {
if (!this.options.exitOnArrowDown) { try {
if (!this.options.exitOnArrowDown) {
return false;
}
const { state } = editor;
const { selection, doc } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
if (!isAtEnd) {
return false;
}
const after = $from.after();
if (after === undefined) {
return false;
}
const nodeAfter = doc.nodeAt(after);
if (nodeAfter) {
return editor.commands.command(({ tr }) => {
tr.setSelection(Selection.near(doc.resolve(after)));
return true;
});
}
return editor.commands.exitCode();
} catch (error) {
console.error("Error handling ArrowDown in CustomCodeBlockExtension:", error);
return false; return false;
} }
const { state } = editor;
const { selection, doc } = state;
const { $from, empty } = selection;
if (!empty || $from.parent.type !== this.type) {
return false;
}
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
if (!isAtEnd) {
return false;
}
const after = $from.after();
if (after === undefined) {
return false;
}
const nodeAfter = doc.nodeAt(after);
if (nodeAfter) {
return editor.commands.command(({ tr }) => {
tr.setSelection(Selection.near(doc.resolve(after)));
return true;
});
}
return editor.commands.exitCode();
}, },
}; };
}, },

View File

@ -0,0 +1,153 @@
import { findChildren } from "@tiptap/core";
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import highlight from "highlight.js/lib/core";
function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] {
return nodes
.map((node) => {
const classes = [...className, ...(node.properties ? node.properties.className : [])];
if (node.children) {
return parseNodes(node.children, classes);
}
return {
text: node.value,
classes,
};
})
.flat();
}
function getHighlightNodes(result: any) {
// `.value` for lowlight v1, `.children` for lowlight v2
return result.value || result.children || [];
}
function registered(aliasOrLanguage: string) {
return Boolean(highlight.getLanguage(aliasOrLanguage));
}
function getDecorations({
doc,
name,
lowlight,
defaultLanguage,
}: {
doc: ProsemirrorNode;
name: string;
lowlight: any;
defaultLanguage: string | null | undefined;
}) {
const decorations: Decoration[] = [];
findChildren(doc, (node) => node.type.name === name).forEach((block) => {
let from = block.pos + 1;
const language = block.node.attrs.language || defaultLanguage;
const languages = lowlight.listLanguages();
const nodes =
language && (languages.includes(language) || registered(language))
? getHighlightNodes(lowlight.highlight(language, block.node.textContent))
: getHighlightNodes(lowlight.highlightAuto(block.node.textContent));
parseNodes(nodes).forEach((node) => {
const to = from + node.text.length;
if (node.classes.length) {
const decoration = Decoration.inline(from, to, {
class: node.classes.join(" "),
});
decorations.push(decoration);
}
from = to;
});
});
return DecorationSet.create(doc, decorations);
}
function isFunction(param: () => any) {
return typeof param === "function";
}
export function LowlightPlugin({
name,
lowlight,
defaultLanguage,
}: {
name: string;
lowlight: any;
defaultLanguage: string | null | undefined;
}) {
if (!["highlight", "highlightAuto", "listLanguages"].every((api) => isFunction(lowlight[api]))) {
throw Error("You should provide an instance of lowlight to use the code-block-lowlight extension");
}
const lowlightPlugin: Plugin<any> = new Plugin({
key: new PluginKey("lowlight"),
state: {
init: (_, { doc }) =>
getDecorations({
doc,
name,
lowlight,
defaultLanguage,
}),
apply: (transaction, decorationSet, oldState, newState) => {
const oldNodeName = oldState.selection.$head.parent.type.name;
const newNodeName = newState.selection.$head.parent.type.name;
const oldNodes = findChildren(oldState.doc, (node) => node.type.name === name);
const newNodes = findChildren(newState.doc, (node) => node.type.name === name);
if (
transaction.docChanged &&
// Apply decorations if:
// selection includes named node,
([oldNodeName, newNodeName].includes(name) ||
// OR transaction adds/removes named node,
newNodes.length !== oldNodes.length ||
// OR transaction has changes that completely encapsulte a node
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some(
(step) =>
// @ts-ignore
step.from !== undefined &&
// @ts-ignore
step.to !== undefined &&
oldNodes.some(
(node) =>
// @ts-ignore
node.pos >= step.from &&
// @ts-ignore
node.pos + node.node.nodeSize <= step.to
)
))
) {
return getDecorations({
doc: transaction.doc,
name,
lowlight,
defaultLanguage,
});
}
return decorationSet.map(transaction.mapping, transaction.doc);
},
},
props: {
decorations(state) {
return lowlightPlugin.getState(state);
},
},
});
return lowlightPlugin;
}

View File

@ -46,19 +46,24 @@ export const ListKeymap = Extension.create<ListKeymapOptions>({
return true; return true;
}, },
Delete: ({ editor }) => { Delete: ({ editor }) => {
let handled = false; try {
let handled = false;
this.options.listTypes.forEach(({ itemName }) => { this.options.listTypes.forEach(({ itemName }) => {
if (editor.state.schema.nodes[itemName] === undefined) { if (editor.state.schema.nodes[itemName] === undefined) {
return; return;
} }
if (handleDelete(editor, itemName)) { if (handleDelete(editor, itemName)) {
handled = true; handled = true;
} }
}); });
return handled; return handled;
} catch (e) {
console.log("error in handling delete:", e);
return false;
}
}, },
"Mod-Delete": ({ editor }) => { "Mod-Delete": ({ editor }) => {
let handled = false; let handled = false;
@ -76,19 +81,24 @@ export const ListKeymap = Extension.create<ListKeymapOptions>({
return handled; return handled;
}, },
Backspace: ({ editor }) => { Backspace: ({ editor }) => {
let handled = false; try {
let handled = false;
this.options.listTypes.forEach(({ itemName, wrapperNames }) => { this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
if (editor.state.schema.nodes[itemName] === undefined) { if (editor.state.schema.nodes[itemName] === undefined) {
return; return;
} }
if (handleBackspace(editor, itemName, wrapperNames)) { if (handleBackspace(editor, itemName, wrapperNames)) {
handled = true; handled = true;
} }
}); });
return handled; return handled;
} catch (e) {
console.log("error in handling Backspace:", e);
return false;
}
}, },
"Mod-Backspace": ({ editor }) => { "Mod-Backspace": ({ editor }) => {
let handled = false; let handled = false;

View File

@ -11,7 +11,7 @@ export const DropHandlerExtension = (uploadFile: UploadImage) =>
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
new Plugin({ new Plugin({
key: new PluginKey("dropHandler"), key: new PluginKey("drop-handler-plugin"),
props: { props: {
handlePaste: (view, event) => { handlePaste: (view, event) => {
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) { if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {

View File

@ -2,44 +2,51 @@ import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { KeyboardShortcutCommand } from "@tiptap/core"; import { KeyboardShortcutCommand } from "@tiptap/core";
export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => { export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => {
const { selection, doc } = editor.state; try {
const { $from, $to } = selection; const { selection, doc } = editor.state;
const { $from, $to } = selection;
let imageNode: ProseMirrorNode | null = null; let imageNode: ProseMirrorNode | null = null;
let imagePos: number | null = null; let imagePos: number | null = null;
// Check if the selection itself is an image node // Check if the selection itself is an image node
doc.nodesBetween($from.pos, $to.pos, (node, pos) => { doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
if (node.type.name === "image") { if (node.type.name === "image") {
imageNode = node; imageNode = node;
imagePos = pos; imagePos = pos;
return false; // Stop iterating once an image node is found return false; // Stop iterating once an image node is found
} }
return true; return true;
}); });
if (imageNode === null || imagePos === null) return false; if (imageNode === null || imagePos === null) return false;
// Since we want to insert above the image, we use the imagePos directly // Since we want to insert above the image, we use the imagePos directly
const insertPos = imagePos; const insertPos = imagePos;
if (insertPos < 0) return false; const docSize = editor.state.doc.content.size;
// Check for an existing node immediately before the image if (insertPos < 0 || insertPos > docSize) return false;
if (insertPos === 0) {
// If the previous node doesn't exist or isn't a paragraph, create and insert a new empty node there
editor.chain().insertContentAt(insertPos, { type: "paragraph" }).run();
editor.chain().setTextSelection(insertPos).run();
} else {
const prevNode = doc.nodeAt(insertPos);
if (prevNode && prevNode.type.name === "paragraph") { // Check for an existing node immediately before the image
// If the previous node is a paragraph, move the cursor there if (insertPos === 0) {
// If the previous node doesn't exist or isn't a paragraph, create and insert a new empty node there
editor.chain().insertContentAt(insertPos, { type: "paragraph" }).run();
editor.chain().setTextSelection(insertPos).run(); editor.chain().setTextSelection(insertPos).run();
} else { } else {
return false; const prevNode = doc.nodeAt(insertPos);
}
}
return true; if (prevNode && prevNode.type.name === "paragraph") {
// If the previous node is a paragraph, move the cursor there
editor.chain().setTextSelection(insertPos).run();
} else {
return false;
}
}
return true;
} catch (error) {
console.error("An error occurred while inserting a line above the image:", error);
return false;
}
}; };

View File

@ -2,45 +2,50 @@ import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { KeyboardShortcutCommand } from "@tiptap/core"; import { KeyboardShortcutCommand } from "@tiptap/core";
export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => { export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => {
const { selection, doc } = editor.state; try {
const { $from, $to } = selection; const { selection, doc } = editor.state;
const { $from, $to } = selection;
let imageNode: ProseMirrorNode | null = null; let imageNode: ProseMirrorNode | null = null;
let imagePos: number | null = null; let imagePos: number | null = null;
// Check if the selection itself is an image node // Check if the selection itself is an image node
doc.nodesBetween($from.pos, $to.pos, (node, pos) => { doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
if (node.type.name === "image") { if (node.type.name === "image") {
imageNode = node; imageNode = node;
imagePos = pos; imagePos = pos;
return false; // Stop iterating once an image node is found return false; // Stop iterating once an image node is found
}
return true;
});
if (imageNode === null || imagePos === null) return false;
const guaranteedImageNode: ProseMirrorNode = imageNode;
const nextNodePos = imagePos + guaranteedImageNode.nodeSize;
// Check for an existing node immediately after the image
const nextNode = doc.nodeAt(nextNodePos);
if (nextNode && nextNode.type.name === "paragraph") {
// If the next node is a paragraph, move the cursor there
const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1;
editor.chain().setTextSelection(endOfParagraphPos).run();
} else if (!nextNode) {
// If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there
editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run();
editor
.chain()
.setTextSelection(nextNodePos + 1)
.run();
} else {
// If the next node is not a paragraph, do not proceed
return false;
} }
return true; return true;
}); } catch (error) {
console.error("An error occurred while inserting a line below the image:", error);
if (imageNode === null || imagePos === null) return false;
const guaranteedImageNode: ProseMirrorNode = imageNode;
const nextNodePos = imagePos + guaranteedImageNode.nodeSize;
// Check for an existing node immediately after the image
const nextNode = doc.nodeAt(nextNodePos);
if (nextNode && nextNode.type.name === "paragraph") {
// If the next node is a paragraph, move the cursor there
const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1;
editor.chain().setTextSelection(endOfParagraphPos).run();
} else if (!nextNode) {
// If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there
editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run();
editor
.chain()
.setTextSelection(nextNodePos + 1)
.run();
} else {
// If the next node is not a paragraph, do not proceed
return false; return false;
} }
return true;
}; };

View File

@ -28,9 +28,9 @@ import { CustomLinkExtension } from "src/ui/extensions/custom-link";
import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline";
import { CustomTypographyExtension } from "src/ui/extensions/typography"; import { CustomTypographyExtension } from "src/ui/extensions/typography";
import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule"; import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule";
import { CustomCodeMarkPlugin } from "./custom-code-inline/inline-code-plugin"; import { CustomCodeMarkPlugin } from "src/ui/extensions/custom-code-inline/inline-code-plugin";
import { UploadImage } from "src/types/upload-image"; import { UploadImage } from "src/types/upload-image";
import { DropHandlerExtension } from "./drop"; import { DropHandlerExtension } from "src/ui/extensions/drop";
type TArguments = { type TArguments = {
mentionConfig: { mentionConfig: {

View File

@ -4,21 +4,26 @@ export const CustomQuoteExtension = Blockquote.extend({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
Enter: () => { Enter: () => {
const { $from, $to, $head } = this.editor.state.selection; try {
const parent = $head.node(-1); const { $from, $to, $head } = this.editor.state.selection;
const parent = $head.node(-1);
if (!parent) return false; if (!parent) return false;
if (parent.type.name !== "blockquote") { if (parent.type.name !== "blockquote") {
return false;
}
if ($from.pos !== $to.pos) return false;
// if ($head.parentOffset < $head.parent.content.size) return false;
// this.editor.commands.insertContentAt(parent.ne);
this.editor.chain().splitBlock().lift(this.name).run();
return true;
} catch (error) {
console.error("Error handling Enter in blockquote:", error);
return false; return false;
} }
if ($from.pos !== $to.pos) return false;
// if ($head.parentOffset < $head.parent.content.size) return false;
// this.editor.commands.insertContentAt(parent.ne);
this.editor.chain().splitBlock().lift(this.name).run();
return true;
}, },
}; };
}, },

View File

@ -5,46 +5,51 @@ export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor })
// Check if the current selection or the closest node is a table // Check if the current selection or the closest node is a table
if (!editor.isActive("table")) return false; if (!editor.isActive("table")) return false;
// Get the current selection try {
const { selection } = editor.state; // Get the current selection
const { selection } = editor.state;
// Find the table node and its position // Find the table node and its position
const tableNode = findParentNodeOfType(selection, "table"); const tableNode = findParentNodeOfType(selection, "table");
if (!tableNode) return false; if (!tableNode) return false;
const tablePos = tableNode.pos; const tablePos = tableNode.pos;
// Determine if the selection is in the first row of the table // Determine if the selection is in the first row of the table
const firstRow = tableNode.node.child(0); const firstRow = tableNode.node.child(0);
const selectionPath = (selection.$anchor as any).path; const selectionPath = (selection.$anchor as any).path;
const selectionInFirstRow = selectionPath.includes(firstRow); const selectionInFirstRow = selectionPath.includes(firstRow);
if (!selectionInFirstRow) return false; if (!selectionInFirstRow) return false;
// Check if the table is at the very start of the document or its parent node // Check if the table is at the very start of the document or its parent node
if (tablePos === 0) { if (tablePos === 0) {
// The table is at the start, so just insert a paragraph at the current position // The table is at the start, so just insert a paragraph at the current position
editor.chain().insertContentAt(tablePos, { type: "paragraph" }).run(); editor.chain().insertContentAt(tablePos, { type: "paragraph" }).run();
editor editor
.chain() .chain()
.setTextSelection(tablePos + 1) .setTextSelection(tablePos + 1)
.run(); .run();
} else {
// The table is not at the start, check for the node immediately before the table
const prevNodePos = tablePos - 1;
if (prevNodePos <= 0) return false;
const prevNode = editor.state.doc.nodeAt(prevNodePos - 1);
if (prevNode && prevNode.type.name === "paragraph") {
// If there's a paragraph before the table, move the cursor to the end of that paragraph
const endOfParagraphPos = tablePos - prevNode.nodeSize;
editor.chain().setTextSelection(endOfParagraphPos).run();
} else { } else {
return false; // The table is not at the start, check for the node immediately before the table
} const prevNodePos = tablePos - 1;
}
return true; if (prevNodePos <= 0) return false;
const prevNode = editor.state.doc.nodeAt(prevNodePos - 1);
if (prevNode && prevNode.type.name === "paragraph") {
// If there's a paragraph before the table, move the cursor to the end of that paragraph
const endOfParagraphPos = tablePos - prevNode.nodeSize;
editor.chain().setTextSelection(endOfParagraphPos).run();
} else {
return false;
}
}
return true;
} catch (e) {
console.error("failed to insert line above table", e);
return false;
}
}; };

View File

@ -5,44 +5,49 @@ export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor })
// Check if the current selection or the closest node is a table // Check if the current selection or the closest node is a table
if (!editor.isActive("table")) return false; if (!editor.isActive("table")) return false;
// Get the current selection try {
const { selection } = editor.state; // Get the current selection
const { selection } = editor.state;
// Find the table node and its position // Find the table node and its position
const tableNode = findParentNodeOfType(selection, "table"); const tableNode = findParentNodeOfType(selection, "table");
if (!tableNode) return false; if (!tableNode) return false;
const tablePos = tableNode.pos; const tablePos = tableNode.pos;
const table = tableNode.node; const table = tableNode.node;
// Determine if the selection is in the last row of the table // Determine if the selection is in the last row of the table
const rowCount = table.childCount; const rowCount = table.childCount;
const lastRow = table.child(rowCount - 1); const lastRow = table.child(rowCount - 1);
const selectionPath = (selection.$anchor as any).path; const selectionPath = (selection.$anchor as any).path;
const selectionInLastRow = selectionPath.includes(lastRow); const selectionInLastRow = selectionPath.includes(lastRow);
if (!selectionInLastRow) return false; if (!selectionInLastRow) return false;
// Calculate the position immediately after the table // Calculate the position immediately after the table
const nextNodePos = tablePos + table.nodeSize; const nextNodePos = tablePos + table.nodeSize;
// Check for an existing node immediately after the table // Check for an existing node immediately after the table
const nextNode = editor.state.doc.nodeAt(nextNodePos); const nextNode = editor.state.doc.nodeAt(nextNodePos);
if (nextNode && nextNode.type.name === "paragraph") { if (nextNode && nextNode.type.name === "paragraph") {
// If the next node is an paragraph, move the cursor there // If the next node is an paragraph, move the cursor there
const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1;
editor.chain().setTextSelection(endOfParagraphPos).run(); editor.chain().setTextSelection(endOfParagraphPos).run();
} else if (!nextNode) { } else if (!nextNode) {
// If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there
editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run();
editor editor
.chain() .chain()
.setTextSelection(nextNodePos + 1) .setTextSelection(nextNodePos + 1)
.run(); .run();
} else { } else {
return false;
}
return true;
} catch (e) {
console.error("failed to insert line above table", e);
return false; return false;
} }
return true;
}; };

View File

@ -1,7 +1,6 @@
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import TiptapUnderline from "@tiptap/extension-underline"; import TiptapUnderline from "@tiptap/extension-underline";
import TextStyle from "@tiptap/extension-text-style"; import TextStyle from "@tiptap/extension-text-style";
import { Color } from "@tiptap/extension-color";
import TaskItem from "@tiptap/extension-task-item"; import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list"; import TaskList from "@tiptap/extension-task-list";
import { Markdown } from "tiptap-markdown"; import { Markdown } from "tiptap-markdown";
@ -50,7 +49,9 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
}), }),
CustomQuoteExtension, CustomQuoteExtension,
CustomHorizontalRule.configure({ CustomHorizontalRule.configure({
HTMLAttributes: { class: "my-4" }, HTMLAttributes: {
class: "my-4 border-custom-border-400",
},
}), }),
CustomLinkExtension.configure({ CustomLinkExtension.configure({
openOnClick: true, openOnClick: true,
@ -71,7 +72,6 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
}), }),
TiptapUnderline, TiptapUnderline,
TextStyle, TextStyle,
Color,
TaskList.configure({ TaskList.configure({
HTMLAttributes: { HTMLAttributes: {
class: "not-prose pl-2 space-y-2", class: "not-prose pl-2 space-y-2",
@ -85,7 +85,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
}), }),
CustomCodeBlockExtension.configure({ CustomCodeBlockExtension.configure({
HTMLAttributes: { HTMLAttributes: {
class: "bg-custom-background-90 text-custom-text-100 rounded-lg p-8 pl-9 pr-4", class: "",
}, },
}), }),
CustomCodeInlineExtension, CustomCodeInlineExtension,

View File

@ -2385,11 +2385,6 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.13.tgz#0a26731ebf98ddfd268884ff1712f7189be7b63c" resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.13.tgz#0a26731ebf98ddfd268884ff1712f7189be7b63c"
integrity sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA== integrity sha512-NkWlQ5bLPUlcROj6G/d4oqAxMf3j3wfndGOPp0z8OoXJtVbVoXl/aMSlLbVgE6n8r6CS8MYxKhXNxrb7Ll2foA==
"@tiptap/extension-code-block-lowlight@^2.1.13":
version "2.1.13"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.13.tgz#91110f44d6cc8a12d95ac92aee0c848fdedefb0d"
integrity sha512-PlU0lzAEbUGqPykl7fYqlAiY7/zFRtQExsbrpi2kctSIzxC+jgMM4vEpWxLS4jZEXl7jVHvBRH6lRNINDHWmQA==
"@tiptap/extension-code-block@^2.1.13": "@tiptap/extension-code-block@^2.1.13":
version "2.1.13" version "2.1.13"
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.1.13.tgz#3e441d171d3ed821e67291dbf4cbad7e2ea29809" resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block/-/extension-code-block-2.1.13.tgz#3e441d171d3ed821e67291dbf4cbad7e2ea29809"
@ -2400,11 +2395,6 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.1.13.tgz#27a5ca5705e59ca97390fad4d6631bf431690480" resolved "https://registry.yarnpkg.com/@tiptap/extension-code/-/extension-code-2.1.13.tgz#27a5ca5705e59ca97390fad4d6631bf431690480"
integrity sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w== integrity sha512-f5fLYlSgliVVa44vd7lQGvo49+peC+Z2H0Fn84TKNCH7tkNZzouoJsHYn0/enLaQ9Sq+24YPfqulfiwlxyiT8w==
"@tiptap/extension-color@^2.1.13":
version "2.1.13"
resolved "https://registry.yarnpkg.com/@tiptap/extension-color/-/extension-color-2.1.13.tgz#f1ea3805db93f308aaf99d8ac80b18fcf13de050"
integrity sha512-T3tJXCIfFxzIlGOhvbPVIZa3y36YZRPYIo2TKsgkTz8LiMob6hRXXNFjsrFDp2Fnu3DrBzyvrorsW7767s4eYg==
"@tiptap/extension-document@^2.1.13": "@tiptap/extension-document@^2.1.13":
version "2.1.13" version "2.1.13"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.13.tgz#5b68fa08e8a79eebd41f1360982db2ddd28ad010" resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.13.tgz#5b68fa08e8a79eebd41f1360982db2ddd28ad010"
@ -2757,7 +2747,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42": "@types/react@*", "@types/react@^18.2.42":
version "18.2.42" version "18.2.42"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA== integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==