diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index ab6c77724..072dc28c6 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -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", diff --git a/packages/editor/core/src/index.ts b/packages/editor/core/src/index.ts index 9c1c292b2..55f68ac74 100644 --- a/packages/editor/core/src/index.ts +++ b/packages/editor/core/src/index.ts @@ -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"; diff --git a/packages/editor/core/src/lib/editor-commands.ts b/packages/editor/core/src/lib/editor-commands.ts index 8f9e36350..229341f08 100644 --- a/packages/editor/core/src/lib/editor-commands.ts +++ b/packages/editor/core/src/lib/editor-commands.ts @@ -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(); diff --git a/packages/editor/core/src/styles/github-dark.css b/packages/editor/core/src/styles/github-dark.css new file mode 100644 index 000000000..9374de403 --- /dev/null +++ b/packages/editor/core/src/styles/github-dark.css @@ -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; +} diff --git a/packages/editor/core/src/types/validate-image.ts b/packages/editor/core/src/types/validate-image.ts new file mode 100644 index 000000000..d976d5cdb --- /dev/null +++ b/packages/editor/core/src/types/validate-image.ts @@ -0,0 +1 @@ +export type ValidateImage = (assetUrlWithWorkspaceId: string) => Promise; diff --git a/packages/editor/core/src/ui/extensions/code/index.tsx b/packages/editor/core/src/ui/extensions/code/index.tsx new file mode 100644 index 000000000..016cec2c3 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/code/index.tsx @@ -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, +}); diff --git a/packages/editor/core/src/ui/extensions/horizontal-rule.tsx b/packages/editor/core/src/ui/extensions/horizontal-rule.tsx new file mode 100644 index 000000000..0e3b5fe94 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/horizontal-rule.tsx @@ -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; +} + +declare module "@tiptap/core" { + interface Commands { + horizontalRule: { + /** + * Add a horizontal rule + */ + setHorizontalRule: () => ReturnType; + }; + } +} + +export default Node.create({ + 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()); + }, + }), + ]; + }, +}); diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 3f191a912..8106bbd8f 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -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, diff --git a/packages/editor/core/src/ui/extensions/keymap.tsx b/packages/editor/core/src/ui/extensions/keymap.tsx new file mode 100644 index 000000000..0caa194cd --- /dev/null +++ b/packages/editor/core/src/ui/extensions/keymap.tsx @@ -0,0 +1,54 @@ +import { Extension } from "@tiptap/core"; + +declare module "@tiptap/core" { + // eslint-disable-next-line no-unused-vars + interface Commands { + 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; + }, + }; + }, +}); diff --git a/packages/editor/core/src/ui/hooks/useEditor.tsx b/packages/editor/core/src/ui/hooks/useEditor.tsx index 258da8652..56cf83d32 100644 --- a/packages/editor/core/src/ui/hooks/useEditor.tsx +++ b/packages/editor/core/src/ui/hooks/useEditor.tsx @@ -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, diff --git a/packages/editor/core/src/ui/menus/menu-items/index.tsx b/packages/editor/core/src/ui/menus/menu-items/index.tsx index 8a2651d1e..a9a724720 100644 --- a/packages/editor/core/src/ui/menus/menu-items/index.tsx +++ b/packages/editor/core/src/ui/menus/menu-items/index.tsx @@ -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"), diff --git a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx index a4fb0479c..9bca18ef0 100644 --- a/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx +++ b/packages/editor/lite-text-editor/src/ui/menus/fixed-menu/index.tsx @@ -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) => {
- {basicMarkItems.map((item, index) => ( + {basicTextFormattingItems.map((item, index) => ( {item.name}} @@ -130,7 +130,7 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => { ))}
- {listItems.map((item, index) => ( + {listFormattingItems.map((item, index) => ( {item.name}} diff --git a/packages/editor/rich-text-editor/package.json b/packages/editor/rich-text-editor/package.json index db793261c..743a7c9b4 100644 --- a/packages/editor/rich-text-editor/package.json +++ b/packages/editor/rich-text-editor/package.json @@ -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": { diff --git a/packages/editor/rich-text-editor/src/index.ts b/packages/editor/rich-text-editor/src/index.ts index 9ea7f9a39..0b854c0ae 100644 --- a/packages/editor/rich-text-editor/src/index.ts +++ b/packages/editor/rich-text-editor/src/index.ts @@ -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"; diff --git a/packages/editor/rich-text-editor/src/styles/github-dark.css b/packages/editor/rich-text-editor/src/styles/github-dark.css deleted file mode 100644 index 20a7f4e66..000000000 --- a/packages/editor/rich-text-editor/src/styles/github-dark.css +++ /dev/null @@ -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} diff --git a/packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx b/packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx new file mode 100644 index 000000000..69542c1ce --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/extensions/drag-drop.tsx @@ -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; diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx index a28982da3..e1b1e9b3d 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -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, diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 2e98a72aa..ba320a25c 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -11,6 +11,7 @@ import { RichTextEditorExtensions } from "./extensions"; export type UploadImage = (file: File) => Promise; export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise; +export type ValidateImage = (assetUrlWithWorkspaceId: string) => Promise; 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, }); diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx index f9d830599..bb5d1b534 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx @@ -38,21 +38,8 @@ export const EditorBubbleMenu: FC = (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 = (props: any) => { editor={props.editor!} isOpen={isNodeSelectorOpen} setIsOpen={() => { + console.log("setIsNodeSelectorOpen"); setIsNodeSelectorOpen(!isNodeSelectorOpen); setIsLinkSelectorOpen(false); }} @@ -125,6 +113,7 @@ export const EditorBubbleMenu: FC = (props: any) => { editor={props.editor!!} isOpen={isLinkSelectorOpen} setIsOpen={() => { + console.log("setIsLinkSelectorOpen"); setIsLinkSelectorOpen(!isLinkSelectorOpen); setIsNodeSelectorOpen(false); }} diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx index 965e7a42e..7681fbe5b 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/node-selector.tsx @@ -1,12 +1,12 @@ import { BulletListItem, cn, - CodeItem, HeadingOneItem, HeadingThreeItem, HeadingTwoItem, NumberedListItem, QuoteItem, + CodeItem, TodoListItem, } from "@plane/editor-core"; import { Editor } from "@tiptap/react"; diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index 8c6a75d30..e4bb37767 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -151,6 +151,7 @@ export const IssueDescriptionForm: FC = (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) => { diff --git a/web/components/issues/issue-peek-overview/issue-detail.tsx b/web/components/issues/issue-peek-overview/issue-detail.tsx index 875927907..66e267078 100644 --- a/web/components/issues/issue-peek-overview/issue-detail.tsx +++ b/web/components/issues/issue-peek-overview/issue-detail.tsx @@ -140,6 +140,7 @@ export const PeekOverviewIssueDetails: FC = (props) =
{errors.name ? errors.name.message : null} { + // 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 { return async (file: File) => { const formData = new FormData(); diff --git a/web/styles/editor.css b/web/styles/editor.css index 85d881eeb..981f53703 100644 --- a/web/styles/editor.css +++ b/web/styles/editor.css @@ -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)); + } +} diff --git a/yarn.lock b/yarn.lock index f25d1cf7d..eef1655f4 100644 --- a/yarn.lock +++ b/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"