From 9924fa6621df34099e1957fe93dbcf72104ccd35 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 6 Oct 2023 09:56:59 -0400 Subject: [PATCH] feat: Allow commenting in code (#5953) * Allow commenting in code marks * Allow commenting in code blocks * Floating comment toolbar in code block --- app/editor/components/FloatingToolbar.tsx | 2 +- app/editor/components/SelectionToolbar.tsx | 7 ++--- app/editor/components/ToolbarButton.tsx | 2 +- app/editor/components/ToolbarMenu.tsx | 1 + app/editor/menus/formatting.tsx | 4 ++- shared/editor/marks/Code.ts | 2 +- shared/editor/marks/Comment.ts | 12 ++++---- shared/editor/nodes/CodeFence.ts | 2 +- shared/editor/nodes/index.ts | 2 +- shared/editor/queries/isInCode.ts | 35 ++++++++++++++-------- 10 files changed, 42 insertions(+), 27 deletions(-) diff --git a/app/editor/components/FloatingToolbar.tsx b/app/editor/components/FloatingToolbar.tsx index 49a184aa5..d451adeb6 100644 --- a/app/editor/components/FloatingToolbar.tsx +++ b/app/editor/components/FloatingToolbar.tsx @@ -74,7 +74,7 @@ function usePosition({ // position at the top right of code blocks const codeBlock = findParentNode(isCode)(view.state.selection); - if (codeBlock) { + if (codeBlock && view.state.selection.empty) { const element = view.nodeDOM(codeBlock.pos); const bounds = (element as HTMLElement).getBoundingClientRect(); selectionBounds.top = bounds.top; diff --git a/app/editor/components/SelectionToolbar.tsx b/app/editor/components/SelectionToolbar.tsx index ac9e2c001..3d8f82402 100644 --- a/app/editor/components/SelectionToolbar.tsx +++ b/app/editor/components/SelectionToolbar.tsx @@ -4,6 +4,7 @@ import * as React from "react"; import createAndInsertLink from "@shared/editor/commands/createAndInsertLink"; import filterExcessSeparators from "@shared/editor/lib/filterExcessSeparators"; import getMarkRange from "@shared/editor/queries/getMarkRange"; +import isInCode from "@shared/editor/queries/isInCode"; import isMarkActive from "@shared/editor/queries/isMarkActive"; import isNodeActive from "@shared/editor/queries/isNodeActive"; import { getColumnIndex, getRowIndex } from "@shared/editor/queries/table"; @@ -216,13 +217,11 @@ export default function SelectionToolbar(props: Props) { const range = getMarkRange(selection.$from, state.schema.marks.link); const isImageSelection = selection instanceof NodeSelection && selection.node.type.name === "image"; - const isCodeSelection = - isNodeActive(state.schema.nodes.code_block)(state) || - isNodeActive(state.schema.nodes.code_fence)(state); + const isCodeSelection = isInCode(state, { onlyBlock: true }); let items: MenuItem[] = []; - if (isCodeSelection) { + if (isCodeSelection && selection.empty) { items = getCodeMenuItems(state, readOnly, dictionary); } else if (isTableSelection) { items = getTableMenuItems(dictionary); diff --git a/app/editor/components/ToolbarButton.tsx b/app/editor/components/ToolbarButton.tsx index 0ffbb79b1..bcd3b608e 100644 --- a/app/editor/components/ToolbarButton.tsx +++ b/app/editor/components/ToolbarButton.tsx @@ -23,7 +23,7 @@ export default styled.button.attrs((props) => ({ background: none; transition: opacity 100ms ease-in-out; padding: 0; - opacity: 0.7; + opacity: 0.8; outline: none; pointer-events: all; position: relative; diff --git a/app/editor/components/ToolbarMenu.tsx b/app/editor/components/ToolbarMenu.tsx index e572b71cd..16a6bd2dd 100644 --- a/app/editor/components/ToolbarMenu.tsx +++ b/app/editor/components/ToolbarMenu.tsx @@ -129,6 +129,7 @@ const Arrow = styled(ExpandedIcon)` const Label = styled.span` font-size: 15px; font-weight: 500; + color: ${s("text")}; `; export default ToolbarMenu; diff --git a/app/editor/menus/formatting.tsx b/app/editor/menus/formatting.tsx index a786b5e7e..4e52e12d7 100644 --- a/app/editor/menus/formatting.tsx +++ b/app/editor/menus/formatting.tsx @@ -36,6 +36,7 @@ export default function formattingMenuItems( const isTable = isInTable(state); const isList = isInList(state); const isCode = isInCode(state); + const isCodeBlock = isInCode(state, { onlyBlock: true }); const allowBlocks = !isTable && !isList; return [ @@ -83,6 +84,7 @@ export default function formattingMenuItems( tooltip: dictionary.codeInline, icon: , active: isMarkActive(schema.marks.code_inline), + visible: !isCodeBlock, }, { name: "separator", @@ -166,8 +168,8 @@ export default function formattingMenuItems( name: "comment", tooltip: dictionary.comment, icon: , + label: isCodeBlock ? dictionary.comment : undefined, active: isMarkActive(schema.marks.comment), - visible: !isCode, }, ]; } diff --git a/shared/editor/marks/Code.ts b/shared/editor/marks/Code.ts index 185f51f46..7f0011ce9 100644 --- a/shared/editor/marks/Code.ts +++ b/shared/editor/marks/Code.ts @@ -41,7 +41,7 @@ export default class Code extends Mark { get schema(): MarkSpec { return { - excludes: "comment mention link placeholder highlight em strong", + excludes: "mention link placeholder highlight em strong", parseDOM: [{ tag: "code.inline", preserveWhitespace: true }], toDOM: () => ["code", { class: "inline", spellCheck: "false" }], }; diff --git a/shared/editor/marks/Comment.ts b/shared/editor/marks/Comment.ts index c9ff8431a..a58a5ea18 100644 --- a/shared/editor/marks/Comment.ts +++ b/shared/editor/marks/Comment.ts @@ -109,14 +109,16 @@ export default class Comment extends Mark { props: { handleDOMEvents: { mouseup: (_view, event: MouseEvent) => { - if ( - !(event.target instanceof HTMLSpanElement) || - !event.target.classList.contains("comment-marker") - ) { + if (!(event.target instanceof HTMLElement)) { return false; } - const commentId = event.target.id.replace("comment-", ""); + const comment = event.target.closest(".comment-marker"); + if (!comment) { + return false; + } + + const commentId = comment.id.replace("comment-", ""); if (commentId) { this.options?.onClickCommentMark?.(commentId); } diff --git a/shared/editor/nodes/CodeFence.ts b/shared/editor/nodes/CodeFence.ts index 9b553f142..5c11babd9 100644 --- a/shared/editor/nodes/CodeFence.ts +++ b/shared/editor/nodes/CodeFence.ts @@ -150,7 +150,7 @@ export default class CodeFence extends Node { }, }, content: "text*", - marks: "", + marks: "comment", group: "block", code: true, defining: true, diff --git a/shared/editor/nodes/index.ts b/shared/editor/nodes/index.ts index 0634028cb..6ef1263b1 100644 --- a/shared/editor/nodes/index.ts +++ b/shared/editor/nodes/index.ts @@ -118,4 +118,4 @@ export const richExtensions: Nodes = [ /** * Add commenting and mentions to a set of nodes */ -export const withComments = (nodes: Nodes) => [Mention, Comment, ...nodes]; +export const withComments = (nodes: Nodes) => [...nodes, Mention, Comment]; diff --git a/shared/editor/queries/isInCode.ts b/shared/editor/queries/isInCode.ts index 70c7706df..e9c6ba30b 100644 --- a/shared/editor/queries/isInCode.ts +++ b/shared/editor/queries/isInCode.ts @@ -1,29 +1,40 @@ import { EditorState } from "prosemirror-state"; import isMarkActive from "./isMarkActive"; +import isNodeActive from "./isNodeActive"; + +type Options = { + /** Only check if the selection is inside a code block. */ + onlyBlock?: boolean; + /** Only check if the selection is inside a code mark. */ + onlyMark?: boolean; +}; /** * Returns true if the selection is inside a code block or code mark. * * @param state The editor state. + * @param options The options. * @returns True if the selection is inside a code block or code mark. */ -export default function isInCode(state: EditorState): boolean { +export default function isInCode( + state: EditorState, + options?: Options +): boolean { const { nodes, marks } = state.schema; - if (nodes.code_block || nodes.code_fence) { - const $head = state.selection.$head; - for (let d = $head.depth; d > 0; d--) { - if (nodes.code_block && $head.node(d).type === nodes.code_block) { - return true; - } - if (nodes.code_fence && $head.node(d).type === nodes.code_fence) { - return true; - } + if (!options?.onlyMark) { + if (nodes.code_block && isNodeActive(nodes.code_block)(state)) { + return true; + } + if (nodes.code_fence && isNodeActive(nodes.code_fence)(state)) { + return true; } } - if (marks.code_inline) { - return isMarkActive(marks.code_inline)(state); + if (!options?.onlyBlock) { + if (marks.code_inline) { + return isMarkActive(marks.code_inline)(state); + } } return false;