forked from github/plane
[feat]: Drag and Drop Handles for all Data Structures (#2745)
* better variable names and comments * drag drop migrated * custom horizontal rule created * init transaction hijack * fixed code block with better contrast, keyboard tripple enter press disabled and syntax highlighting * fixed link selector closing on open behaviour * added better keymaps and syntax highlights * made drag and drop working for code blocks * fixed drag drop for code blocks * moved drag drop only to rich text editor * fixed drag and drop only for description * enabled drag handles for peek overview and main issues * got images to old state
This commit is contained in:
parent
1f61ad141e
commit
34ab188a99
@ -30,6 +30,9 @@
|
||||
"dependencies": {
|
||||
"@blueprintjs/popover2": "^2.0.10",
|
||||
"@tiptap/core": "^2.1.7",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.1.12",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lowlight": "^3.0.0",
|
||||
"@tiptap/extension-color": "^2.1.11",
|
||||
"@tiptap/extension-image": "^2.1.7",
|
||||
"@tiptap/extension-link": "^2.1.7",
|
||||
@ -42,9 +45,8 @@
|
||||
"@tiptap/extension-task-list": "^2.1.7",
|
||||
"@tiptap/extension-text-style": "^2.1.11",
|
||||
"@tiptap/extension-underline": "^2.1.7",
|
||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
"@tiptap/pm": "^2.1.7",
|
||||
"@tiptap/prosemirror-tables": "^1.1.4",
|
||||
"@tiptap/react": "^2.1.7",
|
||||
"@tiptap/starter-kit": "^2.1.10",
|
||||
"@tiptap/suggestion": "^2.0.4",
|
||||
@ -56,7 +58,9 @@
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"jsx-dom-cjs": "^8.0.3",
|
||||
"lucide-react": "^0.244.0",
|
||||
"prosemirror-async-query": "^0.0.4",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-moveable": "^0.54.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
|
@ -1,6 +1,7 @@
|
||||
// styles
|
||||
// import "./styles/tailwind.css";
|
||||
// import "./styles/editor.css";
|
||||
import "./styles/github-dark.css";
|
||||
|
||||
export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
|
||||
|
||||
|
@ -50,10 +50,11 @@ export const toggleUnderline = (editor: Editor, range?: Range) => {
|
||||
else editor.chain().focus().toggleUnderline().run();
|
||||
};
|
||||
|
||||
export const toggleCode = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleCode().run();
|
||||
else editor.chain().focus().toggleCode().run();
|
||||
export const toggleCodeBlock = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
||||
else editor.chain().focus().toggleCodeBlock().run();
|
||||
};
|
||||
|
||||
export const toggleOrderedList = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||
|
85
packages/editor/core/src/styles/github-dark.css
Normal file
85
packages/editor/core/src/styles/github-dark.css
Normal file
@ -0,0 +1,85 @@
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
code.hljs {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
.hljs {
|
||||
color: #c9d1d9;
|
||||
background: #0d1117;
|
||||
}
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
color: #ff7b72;
|
||||
}
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id,
|
||||
.hljs-variable {
|
||||
color: #79c0ff;
|
||||
}
|
||||
.hljs-meta .hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-string {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
color: #ffa657;
|
||||
}
|
||||
.hljs-code,
|
||||
.hljs-comment,
|
||||
.hljs-formula {
|
||||
color: #8b949e;
|
||||
}
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-selector-tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
.hljs-subst {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
.hljs-section {
|
||||
color: #1f6feb;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hljs-bullet {
|
||||
color: #f2cc60;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
color: #c9d1d9;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
color: #c9d1d9;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hljs-addition {
|
||||
color: #aff5b4;
|
||||
background-color: #033a16;
|
||||
}
|
||||
.hljs-deletion {
|
||||
color: #ffdcd7;
|
||||
background-color: #67060c;
|
||||
}
|
1
packages/editor/core/src/types/validate-image.ts
Normal file
1
packages/editor/core/src/types/validate-image.ts
Normal file
@ -0,0 +1 @@
|
||||
export type ValidateImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
29
packages/editor/core/src/ui/extensions/code/index.tsx
Normal file
29
packages/editor/core/src/ui/extensions/code/index.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
|
||||
const lowlight = createLowlight(common);
|
||||
lowlight.register("ts", ts);
|
||||
|
||||
export const CustomCodeBlock = CodeBlockLowlight.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Tab: ({ editor }) => {
|
||||
const { state } = editor;
|
||||
const { selection, doc } = state;
|
||||
const { $from, empty } = selection;
|
||||
|
||||
if (!empty || $from.parent.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.commands.insertContent(" ");
|
||||
},
|
||||
};
|
||||
},
|
||||
}).configure({
|
||||
lowlight,
|
||||
defaultLanguage: "plaintext",
|
||||
exitOnTripleEnter: false,
|
||||
});
|
116
packages/editor/core/src/ui/extensions/horizontal-rule.tsx
Normal file
116
packages/editor/core/src/ui/extensions/horizontal-rule.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
|
||||
import {
|
||||
InputRule,
|
||||
mergeAttributes,
|
||||
Node,
|
||||
nodeInputRule,
|
||||
wrappingInputRule,
|
||||
} from "@tiptap/core";
|
||||
|
||||
/**
|
||||
* Extension based on:
|
||||
* - Tiptap HorizontalRule extension (https://tiptap.dev/api/nodes/horizontal-rule)
|
||||
*/
|
||||
|
||||
export interface HorizontalRuleOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
horizontalRule: {
|
||||
/**
|
||||
* Add a horizontal rule
|
||||
*/
|
||||
setHorizontalRule: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create<HorizontalRuleOptions>({
|
||||
name: "horizontalRule",
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
};
|
||||
},
|
||||
|
||||
group: "block",
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
color: {
|
||||
default: "#dddddd",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: `div[data-type="${this.name}"]`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"div",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
"data-type": this.name,
|
||||
}),
|
||||
["div", {}],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setHorizontalRule:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return (
|
||||
chain()
|
||||
.insertContent({ type: this.name })
|
||||
// set cursor after horizontal rule
|
||||
.command(({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const { $to } = tr.selection;
|
||||
const posAfter = $to.end();
|
||||
|
||||
if ($to.nodeAfter) {
|
||||
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
||||
} else {
|
||||
// add node after horizontal rule if it’s the end of the document
|
||||
const node =
|
||||
$to.parent.type.contentMatch.defaultType?.create();
|
||||
|
||||
if (node) {
|
||||
tr.insert(posAfter, node);
|
||||
tr.setSelection(TextSelection.create(tr.doc, posAfter));
|
||||
}
|
||||
}
|
||||
|
||||
tr.scrollIntoView();
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.run()
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
handler: ({ state, range, match }) => {
|
||||
state.tr.replaceRangeWith(range.from, range.to, this.type.create());
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
@ -6,12 +6,13 @@ 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";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import TableHeader from "./table/table-header/table-header";
|
||||
import Table from "./table/table";
|
||||
import TableCell from "./table/table-cell/table-cell";
|
||||
import TableRow from "./table/table-row/table-row";
|
||||
import DragDrop from "./drag-drop";
|
||||
import HorizontalRule from "./horizontal-rule";
|
||||
|
||||
import ImageExtension from "./image";
|
||||
|
||||
@ -19,6 +20,10 @@ import { DeleteImage } from "../../types/delete-image";
|
||||
import { isValidHttpUrl } from "../../lib/utils";
|
||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
||||
import { Mentions } from "../mentions";
|
||||
import { ValidateImage } from "../../types/validate-image";
|
||||
|
||||
import { CustomKeymap } from "./keymap";
|
||||
import { CustomCodeBlock } from "./code";
|
||||
|
||||
export const CoreEditorExtensions = (
|
||||
mentionConfig: {
|
||||
@ -26,6 +31,7 @@ export const CoreEditorExtensions = (
|
||||
mentionHighlights: string[];
|
||||
},
|
||||
deleteFile: DeleteImage,
|
||||
validateFile?: ValidateImage,
|
||||
cancelUploadImage?: () => any,
|
||||
) => [
|
||||
StarterKit.configure({
|
||||
@ -49,22 +55,15 @@ export const CoreEditorExtensions = (
|
||||
class: "border-l-4 border-custom-border-300",
|
||||
},
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
width: 2,
|
||||
},
|
||||
gapcursor: false,
|
||||
}),
|
||||
Gapcursor,
|
||||
CustomKeymap,
|
||||
TiptapLink.configure({
|
||||
protocols: ["http", "https"],
|
||||
validate: (url) => isValidHttpUrl(url),
|
||||
@ -73,7 +72,7 @@ export const CoreEditorExtensions = (
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
ImageExtension(deleteFile, cancelUploadImage).configure({
|
||||
ImageExtension(deleteFile, validateFile, cancelUploadImage).configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
@ -86,6 +85,7 @@ export const CoreEditorExtensions = (
|
||||
class: "not-prose pl-2",
|
||||
},
|
||||
}),
|
||||
CustomCodeBlock,
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex items-start my-4",
|
||||
@ -95,7 +95,9 @@ export const CoreEditorExtensions = (
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
HorizontalRule,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
|
54
packages/editor/core/src/ui/extensions/keymap.tsx
Normal file
54
packages/editor/core/src/ui/extensions/keymap.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
interface Commands<ReturnType> {
|
||||
customkeymap: {
|
||||
/**
|
||||
* Select text between node boundaries
|
||||
*/
|
||||
selectTextWithinNodeBoundaries: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomKeymap = Extension.create({
|
||||
name: "CustomKeymap",
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
selectTextWithinNodeBoundaries:
|
||||
() =>
|
||||
({ editor, commands }) => {
|
||||
const { state } = editor;
|
||||
const { tr } = state;
|
||||
const startNodePos = tr.selection.$from.start();
|
||||
const endNodePos = tr.selection.$to.end();
|
||||
return commands.setTextSelection({
|
||||
from: startNodePos,
|
||||
to: endNodePos,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
"Mod-a": ({ editor }) => {
|
||||
const { state } = editor;
|
||||
const { tr } = state;
|
||||
const startSelectionPos = tr.selection.from;
|
||||
const endSelectionPos = tr.selection.to;
|
||||
const startNodePos = tr.selection.$from.start();
|
||||
const endNodePos = tr.selection.$to.end();
|
||||
const isCurrentTextSelectionNotExtendedToNodeBoundaries =
|
||||
startSelectionPos > startNodePos || endSelectionPos < endNodePos;
|
||||
if (isCurrentTextSelectionNotExtendedToNodeBoundaries) {
|
||||
editor.chain().selectTextWithinNodeBoundaries().run();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
@ -6,6 +6,7 @@ import {
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { DeleteImage } from "../../types/delete-image";
|
||||
import { ValidateImage } from "../../types/validate-image";
|
||||
import { CoreEditorProps } from "../props";
|
||||
import { CoreEditorExtensions } from "../extensions";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
@ -16,6 +17,7 @@ import { IMentionSuggestion } from "../../types/mention-suggestion";
|
||||
|
||||
interface CustomEditorProps {
|
||||
uploadFile: UploadImage;
|
||||
validateFile?: ValidateImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
@ -35,6 +37,7 @@ interface CustomEditorProps {
|
||||
export const useEditor = ({
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
validateFile,
|
||||
cancelUploadImage,
|
||||
editorProps = {},
|
||||
value,
|
||||
@ -59,6 +62,7 @@ export const useEditor = ({
|
||||
mentionHighlights: mentionHighlights ?? [],
|
||||
},
|
||||
deleteFile,
|
||||
validateFile,
|
||||
cancelUploadImage,
|
||||
),
|
||||
...extensions,
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
toggleBlockquote,
|
||||
toggleBold,
|
||||
toggleBulletList,
|
||||
toggleCode,
|
||||
toggleCodeBlock,
|
||||
toggleHeadingOne,
|
||||
toggleHeadingThree,
|
||||
toggleHeadingTwo,
|
||||
@ -89,13 +89,6 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
|
||||
icon: StrikethroughIcon,
|
||||
});
|
||||
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||
name: "code",
|
||||
isActive: () => editor?.isActive("code"),
|
||||
command: () => toggleCode(editor),
|
||||
icon: CodeIcon,
|
||||
});
|
||||
|
||||
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
||||
name: "bullet-list",
|
||||
isActive: () => editor?.isActive("bulletList"),
|
||||
@ -110,6 +103,13 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({
|
||||
icon: CheckSquare,
|
||||
});
|
||||
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||
name: "code",
|
||||
isActive: () => editor?.isActive("code"),
|
||||
command: () => toggleCodeBlock(editor),
|
||||
icon: CodeIcon,
|
||||
});
|
||||
|
||||
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
||||
name: "ordered-list",
|
||||
isActive: () => editor?.isActive("orderedList"),
|
||||
|
@ -46,14 +46,14 @@ type EditorBubbleMenuProps = {
|
||||
};
|
||||
|
||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
const basicMarkItems: BubbleMenuItem[] = [
|
||||
const basicTextFormattingItems: BubbleMenuItem[] = [
|
||||
BoldItem(props.editor),
|
||||
ItalicItem(props.editor),
|
||||
UnderLineItem(props.editor),
|
||||
StrikeThroughItem(props.editor),
|
||||
];
|
||||
|
||||
const listItems: BubbleMenuItem[] = [
|
||||
const listFormattingItems: BubbleMenuItem[] = [
|
||||
BulletListItem(props.editor),
|
||||
NumberedListItem(props.editor),
|
||||
];
|
||||
@ -103,7 +103,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
<div className="flex items-stretch justify-between gap-2 w-full border-[0.5px] border-custom-border-200 bg-custom-background-90 rounded p-1">
|
||||
<div className="flex items-stretch">
|
||||
<div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200">
|
||||
{basicMarkItems.map((item, index) => (
|
||||
{basicTextFormattingItems.map((item, index) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||
@ -130,7 +130,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200">
|
||||
{listItems.map((item, index) => (
|
||||
{listFormattingItems.map((item, index) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||
|
@ -30,14 +30,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/editor-core": "*",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.1.11",
|
||||
"@tiptap/extension-horizontal-rule": "^2.1.11",
|
||||
"@tiptap/extension-placeholder": "^2.1.11",
|
||||
"@tiptap/suggestion": "^2.1.7",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^1.2.1",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lowlight": "^3.0.0",
|
||||
"lucide-react": "^0.244.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,5 +1,3 @@
|
||||
import "./styles/github-dark.css";
|
||||
|
||||
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
|
||||
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
|
||||
export type { IMentionSuggestion, IMentionHighlight } from "./ui";
|
||||
|
@ -1,2 +0,0 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}
|
||||
.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
252
packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx
Normal file
252
packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
import { PluginKey, NodeSelection, Plugin } from "@tiptap/pm/state";
|
||||
// @ts-ignore
|
||||
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||
|
||||
export interface DragHandleOptions {
|
||||
dragHandleWidth: number;
|
||||
}
|
||||
|
||||
function absoluteRect(node: Element) {
|
||||
const data = node.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: data.top,
|
||||
left: data.left,
|
||||
width: data.width,
|
||||
};
|
||||
}
|
||||
|
||||
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
||||
return document
|
||||
.elementsFromPoint(coords.x, coords.y)
|
||||
.find((elem: Element) => {
|
||||
return (
|
||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||
elem.matches(
|
||||
[
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
"[data-type=horizontalRule]",
|
||||
".tableWrapper",
|
||||
].join(", "),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function nodePosAtDOM(node: Element, view: EditorView) {
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
|
||||
if (node.nodeName === "IMG") {
|
||||
return view.posAtCoords({
|
||||
left: boundingRect.left + 1,
|
||||
top: boundingRect.top + 1,
|
||||
})?.pos;
|
||||
}
|
||||
|
||||
if (node.nodeName === "PRE") {
|
||||
return (
|
||||
view.posAtCoords({
|
||||
left: boundingRect.left + 1,
|
||||
top: boundingRect.top + 1,
|
||||
})?.pos! - 1
|
||||
);
|
||||
}
|
||||
|
||||
return view.posAtCoords({
|
||||
left: boundingRect.left + 1,
|
||||
top: boundingRect.top + 1,
|
||||
})?.inside;
|
||||
}
|
||||
|
||||
function DragHandle(options: DragHandleOptions) {
|
||||
function handleDragStart(event: DragEvent, view: EditorView) {
|
||||
view.focus();
|
||||
|
||||
if (!event.dataTransfer) return;
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + options.dragHandleWidth + 50,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
if (!(node instanceof Element)) return;
|
||||
|
||||
const nodePos = nodePosAtDOM(node, view);
|
||||
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||
);
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData("text/html", dom.innerHTML);
|
||||
event.dataTransfer.setData("text/plain", text);
|
||||
event.dataTransfer.effectAllowed = "copyMove";
|
||||
|
||||
event.dataTransfer.setDragImage(node, 0, 0);
|
||||
|
||||
view.dragging = { slice, move: event.ctrlKey };
|
||||
}
|
||||
|
||||
function handleClick(event: MouseEvent, view: EditorView) {
|
||||
view.focus();
|
||||
|
||||
view.dom.classList.remove("dragging");
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + 50 + options.dragHandleWidth,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
if (!(node instanceof Element)) return;
|
||||
|
||||
const nodePos = nodePosAtDOM(node, view);
|
||||
|
||||
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||
);
|
||||
}
|
||||
|
||||
let dragHandleElement: HTMLElement | null = null;
|
||||
|
||||
function hideDragHandle() {
|
||||
if (dragHandleElement) {
|
||||
dragHandleElement.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function showDragHandle() {
|
||||
if (dragHandleElement) {
|
||||
dragHandleElement.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey("dragHandle"),
|
||||
view: (view) => {
|
||||
dragHandleElement = document.createElement("div");
|
||||
dragHandleElement.draggable = true;
|
||||
dragHandleElement.dataset.dragHandle = "";
|
||||
dragHandleElement.classList.add("drag-handle");
|
||||
|
||||
const dragHandleContainer = document.createElement("div");
|
||||
dragHandleContainer.classList.add("drag-handle-container");
|
||||
dragHandleElement.appendChild(dragHandleContainer);
|
||||
|
||||
const dotsContainer = document.createElement("div");
|
||||
dotsContainer.classList.add("drag-handle-dots");
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const spanElement = document.createElement("span");
|
||||
spanElement.classList.add("drag-handle-dot");
|
||||
dotsContainer.appendChild(spanElement);
|
||||
}
|
||||
|
||||
dragHandleContainer.appendChild(dotsContainer);
|
||||
dragHandleElement.addEventListener("dragstart", (e) => {
|
||||
handleDragStart(e, view);
|
||||
});
|
||||
dragHandleElement.addEventListener("click", (e) => {
|
||||
handleClick(e, view);
|
||||
});
|
||||
|
||||
dragHandleElement.addEventListener("dragstart", (e) => {
|
||||
handleDragStart(e, view);
|
||||
});
|
||||
dragHandleElement.addEventListener("click", (e) => {
|
||||
handleClick(e, view);
|
||||
});
|
||||
|
||||
hideDragHandle();
|
||||
|
||||
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
dragHandleElement?.remove?.();
|
||||
dragHandleElement = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mousemove: (view, event) => {
|
||||
if (!view.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + options.dragHandleWidth,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
if (!(node instanceof Element)) {
|
||||
hideDragHandle();
|
||||
return;
|
||||
}
|
||||
|
||||
const compStyle = window.getComputedStyle(node);
|
||||
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
||||
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
||||
|
||||
const rect = absoluteRect(node);
|
||||
|
||||
rect.top += (lineHeight - 24) / 2;
|
||||
rect.top += paddingTop;
|
||||
// Li markers
|
||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||
rect.left -= options.dragHandleWidth;
|
||||
}
|
||||
rect.width = options.dragHandleWidth;
|
||||
|
||||
if (!dragHandleElement) return;
|
||||
|
||||
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
||||
dragHandleElement.style.top = `${rect.top}px`;
|
||||
showDragHandle();
|
||||
},
|
||||
keydown: () => {
|
||||
hideDragHandle();
|
||||
},
|
||||
wheel: () => {
|
||||
hideDragHandle();
|
||||
},
|
||||
// dragging className is used for CSS
|
||||
dragstart: (view) => {
|
||||
view.dom.classList.add("dragging");
|
||||
},
|
||||
drop: (view) => {
|
||||
view.dom.classList.remove("dragging");
|
||||
},
|
||||
dragend: (view) => {
|
||||
view.dom.classList.remove("dragging");
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const DragAndDrop = Extension.create({
|
||||
name: "dragAndDrop",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
DragHandle({
|
||||
dragHandleWidth: 24,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export default DragAndDrop;
|
@ -1,50 +1,18 @@
|
||||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
import { common, createLowlight } from "lowlight";
|
||||
import { InputRule } from "@tiptap/core";
|
||||
|
||||
import ts from "highlight.js/lib/languages/typescript";
|
||||
|
||||
import SlashCommand from "./slash-command";
|
||||
import { UploadImage } from "../";
|
||||
|
||||
const lowlight = createLowlight(common);
|
||||
lowlight.register("ts", ts);
|
||||
import DragAndDrop from "./drag-drop";
|
||||
|
||||
export const RichTextEditorExtensions = (
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
dragDropEnabled?: boolean,
|
||||
) => [
|
||||
HorizontalRule.extend({
|
||||
addInputRules() {
|
||||
return [
|
||||
new InputRule({
|
||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||
handler: ({ state, range, commands }) => {
|
||||
commands.splitBlock();
|
||||
|
||||
const attributes = {};
|
||||
const { tr } = state;
|
||||
const start = range.from;
|
||||
const end = range.to;
|
||||
// @ts-ignore
|
||||
tr.replaceWith(start - 1, end, this.type.create(attributes));
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
}).configure({
|
||||
HTMLAttributes: {
|
||||
class: "mb-6 border-t border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
SlashCommand(uploadFile, setIsSubmitting),
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight,
|
||||
}),
|
||||
dragDropEnabled === true && DragAndDrop,
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
@ -53,7 +21,9 @@ export const RichTextEditorExtensions = (
|
||||
if (node.type.name === "image" || node.type.name === "table") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (node.type.name === "codeBlock") {
|
||||
return "Type in your code here...";
|
||||
}
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
|
@ -11,6 +11,7 @@ import { RichTextEditorExtensions } from "./extensions";
|
||||
|
||||
export type UploadImage = (file: File) => Promise<string>;
|
||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
export type ValidateImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
|
||||
export type IMentionSuggestion = {
|
||||
id: string;
|
||||
@ -25,8 +26,10 @@ export type IMentionHighlight = string;
|
||||
|
||||
interface IRichTextEditor {
|
||||
value: string;
|
||||
dragDropEnabled?: boolean;
|
||||
uploadFile: UploadImage;
|
||||
deleteFile: DeleteImage;
|
||||
validateFile?: ValidateImage;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
cancelUploadImage?: () => any;
|
||||
@ -54,6 +57,7 @@ interface EditorHandle {
|
||||
|
||||
const RichTextEditor = ({
|
||||
onChange,
|
||||
dragDropEnabled,
|
||||
debouncedUpdatesEnabled,
|
||||
setIsSubmitting,
|
||||
setShouldShowAlert,
|
||||
@ -61,6 +65,7 @@ const RichTextEditor = ({
|
||||
value,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
validateFile,
|
||||
noBorder,
|
||||
cancelUploadImage,
|
||||
borderOnFocus,
|
||||
@ -77,9 +82,14 @@ const RichTextEditor = ({
|
||||
value,
|
||||
uploadFile,
|
||||
cancelUploadImage,
|
||||
validateFile,
|
||||
deleteFile,
|
||||
forwardedRef,
|
||||
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting),
|
||||
extensions: RichTextEditorExtensions(
|
||||
uploadFile,
|
||||
setIsSubmitting,
|
||||
dragDropEnabled,
|
||||
),
|
||||
mentionHighlights,
|
||||
mentionSuggestions,
|
||||
});
|
||||
|
@ -38,21 +38,8 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
const { selection } = state;
|
||||
|
||||
const { empty } = selection;
|
||||
const hasEditorFocus = view.hasFocus();
|
||||
|
||||
// if (typeof window !== "undefined") {
|
||||
// const selection: any = window?.getSelection();
|
||||
// if (selection.rangeCount !== 0) {
|
||||
// const range = selection.getRangeAt(0);
|
||||
// if (findTableAncestor(range.startContainer)) {
|
||||
// console.log("table");
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (
|
||||
!hasEditorFocus ||
|
||||
empty ||
|
||||
!editor.isEditable ||
|
||||
editor.isActive("image") ||
|
||||
@ -116,6 +103,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
editor={props.editor!}
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
console.log("setIsNodeSelectorOpen");
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
@ -125,6 +113,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
editor={props.editor!!}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
console.log("setIsLinkSelectorOpen");
|
||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
}}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import {
|
||||
BulletListItem,
|
||||
cn,
|
||||
CodeItem,
|
||||
HeadingOneItem,
|
||||
HeadingThreeItem,
|
||||
HeadingTwoItem,
|
||||
NumberedListItem,
|
||||
QuoteItem,
|
||||
CodeItem,
|
||||
TodoListItem,
|
||||
} from "@plane/editor-core";
|
||||
import { Editor } from "@tiptap/react";
|
||||
|
@ -151,6 +151,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||
value={value}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
dragDropEnabled={true}
|
||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
||||
noBorder={!isAllowed}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
|
@ -140,6 +140,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
||||
</div>
|
||||
<span>{errors.name ? errors.name.message : null}</span>
|
||||
<RichTextEditor
|
||||
dragDropEnabled={true}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
|
@ -34,6 +34,7 @@ export class FileService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
this.uploadFile = this.uploadFile.bind(this);
|
||||
// this.validateFile = this.validateFile.bind(this);
|
||||
this.deleteImage = this.deleteImage.bind(this);
|
||||
this.cancelUpload = this.cancelUpload.bind(this);
|
||||
}
|
||||
@ -52,6 +53,7 @@ export class FileService extends APIService {
|
||||
if (axios.isCancel(error)) {
|
||||
console.log(error.message);
|
||||
} else {
|
||||
console.log(error);
|
||||
throw error?.response?.data;
|
||||
}
|
||||
});
|
||||
@ -61,6 +63,14 @@ export class FileService extends APIService {
|
||||
this.cancelSource.cancel("Upload cancelled");
|
||||
}
|
||||
|
||||
// async validateFile(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||
// console.log("bruh", assetUrlWithWorkspaceId);
|
||||
// const res = await this.get(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`);
|
||||
// const data = res?.data;
|
||||
// console.log("data inside fucntion");
|
||||
// return data.status;
|
||||
// }
|
||||
//
|
||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||
return async (file: File) => {
|
||||
const formData = new FormData();
|
||||
|
@ -229,3 +229,101 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
.ProseMirror table * .is-empty::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
background: rgba(var(--color-background-80));
|
||||
border-radius: 0.5rem;
|
||||
color: rgba(var(--color-text-100));
|
||||
font-family: "JetBrainsMono", monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror pre code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(img):not(pre) {
|
||||
outline: none !important;
|
||||
border-radius: 0.2rem;
|
||||
background-color: rgb(var(--color-background-90));
|
||||
border: 1px solid #5abbf7;
|
||||
padding: 4px 2px 4px 2px;
|
||||
transition: background-color 0.2s;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: fixed;
|
||||
opacity: 1;
|
||||
transition: opacity ease-in 0.2s;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 20px;
|
||||
width: 15px;
|
||||
z-index: 10;
|
||||
cursor: grab;
|
||||
border-radius: 2px;
|
||||
background-color: rgb(var(--color-background-90));
|
||||
&:hover {
|
||||
background-color: rgb(var(--color-background-80));
|
||||
}
|
||||
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
background-color: #0d0d0d 10;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.drag-handle.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.drag-handle {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle-container {
|
||||
height: 20px;
|
||||
width: 15px;
|
||||
cursor: grab;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.drag-handle-dots {
|
||||
height: 100%;
|
||||
width: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.drag-handle-dot {
|
||||
height: 2.75px;
|
||||
width: 3px;
|
||||
background-color: rgba(var(--color-text-100));
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
div[data-type="horizontalRule"] {
|
||||
line-height: 0;
|
||||
padding: 0.25rem 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
& > div {
|
||||
border-bottom: 1px solid rgb(var(--color-text-100));
|
||||
}
|
||||
}
|
||||
|
11
yarn.lock
11
yarn.lock
@ -2376,7 +2376,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-bullet-list/-/extension-bullet-list-2.1.12.tgz#7c905a577ce30ef2cb335870a23f9d24fd26f6aa"
|
||||
integrity sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==
|
||||
|
||||
"@tiptap/extension-code-block-lowlight@^2.1.11":
|
||||
"@tiptap/extension-code-block-lowlight@^2.1.12":
|
||||
version "2.1.12"
|
||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.1.12.tgz#ccbca5d0d92bee373dc8e2e2ae6c27f62f66437c"
|
||||
integrity sha512-dtIbpI9QrWa9TzNO4v5q/zf7+d83wpy5i9PEccdJAVtRZ0yOI8JIZAWzG5ex3zAoCA0CnQFdsPSVykYSDdxtDA==
|
||||
@ -6497,7 +6497,7 @@ mz@^2.7.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nanoid@^3.3.4, nanoid@^3.3.6:
|
||||
nanoid@^3.1.30, nanoid@^3.3.4, nanoid@^3.3.6:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
@ -7065,6 +7065,13 @@ property-information@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.0.tgz#6bc4c618b0c2d68b3bb8b552cbb97f8e300a0f82"
|
||||
integrity sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==
|
||||
|
||||
prosemirror-async-query@^0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-async-query/-/prosemirror-async-query-0.0.4.tgz#4fedbee082692e659ab1f472645aac7765133b1d"
|
||||
integrity sha512-eliJ722n+fVuChcvoZeS3pE/mpN/TJnqMkhIfVSTAH8Vd9S7aGfT9t31idD+mwnptgIc7OUPy56UdYN+ph++TQ==
|
||||
dependencies:
|
||||
nanoid "^3.1.30"
|
||||
|
||||
prosemirror-changeset@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
|
||||
|
Loading…
Reference in New Issue
Block a user