[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:
M. Palanikannan 2023-11-17 12:29:30 +05:30 committed by sriram veeraghanta
parent 1f61ad141e
commit 34ab188a99
25 changed files with 716 additions and 88 deletions

View File

@ -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",

View File

@ -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";

View File

@ -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();

View 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;
}

View File

@ -0,0 +1 @@
export type ValidateImage = (assetUrlWithWorkspaceId: string) => Promise<any>;

View 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,
});

View 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 its 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());
},
}),
];
},
});

View File

@ -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,

View 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;
},
};
},
});

View File

@ -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,

View File

@ -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"),

View File

@ -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>}

View File

@ -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": {

View File

@ -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";

View File

@ -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}

View 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;

View File

@ -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,

View File

@ -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,
});

View File

@ -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);
}}

View File

@ -1,12 +1,12 @@
import {
BulletListItem,
cn,
CodeItem,
HeadingOneItem,
HeadingThreeItem,
HeadingTwoItem,
NumberedListItem,
QuoteItem,
CodeItem,
TodoListItem,
} from "@plane/editor-core";
import { Editor } from "@tiptap/react";

View File

@ -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) => {

View File

@ -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}

View File

@ -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();

View File

@ -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));
}
}

View File

@ -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"