[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": "*",
"@tiptap/core": "^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-list-item": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13",

View File

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

View File

@ -34,33 +34,83 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
else editor.chain().focus().toggleUnderline().run();
};
const replaceCodeBlockWithContent = (editor: Editor) => {
try {
const { schema } = editor.state;
const { paragraph } = schema.nodes;
let replaced = false;
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.");
}
} catch (error) {
console.error("An error occurred while replacing code block content:", error);
}
};
export const toggleCodeBlock = (editor: Editor, range?: Range) => {
// Check if code block is active then toggle code block
try {
if (editor.isActive("codeBlock")) {
if (range) {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
return;
}
editor.chain().focus().toggleCodeBlock().run();
replaceCodeBlockWithContent(editor);
return;
}
// Check if user hasn't selected any text
const isSelectionEmpty = editor.state.selection.empty;
const { from, to } = range || editor.state.selection;
const text = editor.state.doc.textBetween(from, to, "\n");
const isMultiline = text.includes("\n");
if (isSelectionEmpty) {
if (range) {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
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 {
if (range) {
editor.chain().focus().deleteRange(range).toggleCode().run();
return;
}
editor.chain().focus().toggleCode().run();
}
} catch (error) {
console.error("An error occurred while toggling code block:", error);
}
};
export const toggleOrderedList = (editor: Editor, range?: Range) => {

View File

@ -15,6 +15,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
const handleContainerClick = () => {
if (!editor) return;
if (!editor.isEditable) return;
try {
if (editor.isFocused) return; // If editor is already focused, do nothing
const { selection } = editor.state;
@ -45,6 +46,9 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
.chain()
.setTextSelection(endPosition + 1)
.run();
} catch (error) {
console.error("An error occurred while handling container click to insert new empty node at bottom:", error);
}
};
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>
<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>
</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 ts from "highlight.js/lib/languages/typescript";
@ -9,6 +7,7 @@ lowlight.register("ts", ts);
import { Selection } from "@tiptap/pm/state";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { CodeBlockComponent } from "./code-block-node-view";
import { CodeBlockLowlight } from "./code-block-lowlight";
export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
addNodeView() {
@ -18,6 +17,7 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
addKeyboardShortcuts() {
return {
Tab: ({ editor }) => {
try {
const { state } = editor;
const { selection } = state;
const { $from, empty } = selection;
@ -31,8 +31,13 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
editor.view.dispatch(tr);
return true;
} catch (error) {
console.error("Error handling Tab in CustomCodeBlockExtension:", error);
return false;
}
},
ArrowUp: ({ editor }) => {
try {
const { state } = editor;
const { selection } = state;
const { $from, empty } = selection;
@ -61,8 +66,13 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
}
return false;
} catch (error) {
console.error("Error handling ArrowUp in CustomCodeBlockExtension:", error);
return false;
}
},
ArrowDown: ({ editor }) => {
try {
if (!this.options.exitOnArrowDown) {
return false;
}
@ -97,6 +107,10 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
}
return editor.commands.exitCode();
} catch (error) {
console.error("Error handling ArrowDown in CustomCodeBlockExtension:", error);
return false;
}
},
};
},

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,6 +46,7 @@ export const ListKeymap = Extension.create<ListKeymapOptions>({
return true;
},
Delete: ({ editor }) => {
try {
let handled = false;
this.options.listTypes.forEach(({ itemName }) => {
@ -59,6 +60,10 @@ export const ListKeymap = Extension.create<ListKeymapOptions>({
});
return handled;
} catch (e) {
console.log("error in handling delete:", e);
return false;
}
},
"Mod-Delete": ({ editor }) => {
let handled = false;
@ -76,6 +81,7 @@ export const ListKeymap = Extension.create<ListKeymapOptions>({
return handled;
},
Backspace: ({ editor }) => {
try {
let handled = false;
this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
@ -89,6 +95,10 @@ export const ListKeymap = Extension.create<ListKeymapOptions>({
});
return handled;
} catch (e) {
console.log("error in handling Backspace:", e);
return false;
}
},
"Mod-Backspace": ({ editor }) => {
let handled = false;

View File

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

View File

@ -2,6 +2,7 @@ import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { KeyboardShortcutCommand } from "@tiptap/core";
export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => {
try {
const { selection, doc } = editor.state;
const { $from, $to } = selection;
@ -23,7 +24,9 @@ export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor })
// Since we want to insert above the image, we use the imagePos directly
const insertPos = imagePos;
if (insertPos < 0) return false;
const docSize = editor.state.doc.content.size;
if (insertPos < 0 || insertPos > docSize) return false;
// Check for an existing node immediately before the image
if (insertPos === 0) {
@ -42,4 +45,8 @@ export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor })
}
return true;
} catch (error) {
console.error("An error occurred while inserting a line above the image:", error);
return false;
}
};

View File

@ -2,6 +2,7 @@ import { Node as ProseMirrorNode } from "@tiptap/pm/model";
import { KeyboardShortcutCommand } from "@tiptap/core";
export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => {
try {
const { selection, doc } = editor.state;
const { $from, $to } = selection;
@ -43,4 +44,8 @@ export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor })
}
return true;
} catch (error) {
console.error("An error occurred while inserting a line below the image:", error);
return false;
}
};

View File

@ -28,9 +28,9 @@ import { CustomLinkExtension } from "src/ui/extensions/custom-link";
import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline";
import { CustomTypographyExtension } from "src/ui/extensions/typography";
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 { DropHandlerExtension } from "./drop";
import { DropHandlerExtension } from "src/ui/extensions/drop";
type TArguments = {
mentionConfig: {

View File

@ -4,6 +4,7 @@ export const CustomQuoteExtension = Blockquote.extend({
addKeyboardShortcuts() {
return {
Enter: () => {
try {
const { $from, $to, $head } = this.editor.state.selection;
const parent = $head.node(-1);
@ -19,6 +20,10 @@ export const CustomQuoteExtension = Blockquote.extend({
this.editor.chain().splitBlock().lift(this.name).run();
return true;
} catch (error) {
console.error("Error handling Enter in blockquote:", error);
return false;
}
},
};
},

View File

@ -5,6 +5,7 @@ export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor })
// Check if the current selection or the closest node is a table
if (!editor.isActive("table")) return false;
try {
// Get the current selection
const { selection } = editor.state;
@ -47,4 +48,8 @@ export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor })
}
return true;
} catch (e) {
console.error("failed to insert line above table", e);
return false;
}
};

View File

@ -5,6 +5,7 @@ export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor })
// Check if the current selection or the closest node is a table
if (!editor.isActive("table")) return false;
try {
// Get the current selection
const { selection } = editor.state;
@ -45,4 +46,8 @@ export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor })
}
return true;
} catch (e) {
console.error("failed to insert line above table", e);
return false;
}
};

View File

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

View File

@ -2385,11 +2385,6 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.13.tgz#0a26731ebf98ddfd268884ff1712f7189be7b63c"
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":
version "2.1.13"
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"
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":
version "2.1.13"
resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.1.13.tgz#5b68fa08e8a79eebd41f1360982db2ddd28ad010"
@ -2757,7 +2747,7 @@
dependencies:
"@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"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==