forked from github/plane
[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:
parent
59772be014
commit
27db8699c8
@ -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",
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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]
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
@ -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>
|
||||||
);
|
);
|
||||||
|
346
packages/editor/core/src/ui/extensions/code/code-block.ts
Normal file
346
packages/editor/core/src/ui/extensions/code/code-block.ts
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
@ -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();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
153
packages/editor/core/src/ui/extensions/code/lowlight-plugin.ts
Normal file
153
packages/editor/core/src/ui/extensions/code/lowlight-plugin.ts
Normal 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;
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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]) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
@ -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: {
|
||||||
|
@ -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;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -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==
|
||||||
|
Loading…
Reference in New Issue
Block a user