feat: Allow commenting in code (#5953)
* Allow commenting in code marks * Allow commenting in code blocks * Floating comment toolbar in code block
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -129,6 +129,7 @@ const Arrow = styled(ExpandedIcon)`
|
||||
const Label = styled.span`
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: ${s("text")};
|
||||
`;
|
||||
|
||||
export default ToolbarMenu;
|
||||
|
||||
@@ -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: <CodeIcon />,
|
||||
active: isMarkActive(schema.marks.code_inline),
|
||||
visible: !isCodeBlock,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
@@ -166,8 +168,8 @@ export default function formattingMenuItems(
|
||||
name: "comment",
|
||||
tooltip: dictionary.comment,
|
||||
icon: <CommentIcon />,
|
||||
label: isCodeBlock ? dictionary.comment : undefined,
|
||||
active: isMarkActive(schema.marks.comment),
|
||||
visible: !isCode,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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" }],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export default class CodeFence extends Node {
|
||||
},
|
||||
},
|
||||
content: "text*",
|
||||
marks: "",
|
||||
marks: "comment",
|
||||
group: "block",
|
||||
code: true,
|
||||
defining: true,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user