forked from github/plane
[WEB-434] feat: add support to insert a new empty line on clicking at bottom of the editor (#3856)
* fix: horizontal rule no more causes issues on last node * fixed the mismatched transaction by using native tiptap stuff * added support to add new line onclick at bottom if last node is an image TODO: blockquote at last node * fix: simplified adding node at end of the document logic * feat: rewrite entire logic handling all cases * feat: arrow down and arrow up keys add empty node at top and bottom of doc from first/last row's cells * feat: added arrow up and down key support to images too * remove unnecessary console logs * chore: formatting components * fix: reduced bottom padding to increase onclick area --------- Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
parent
8997ee2e3e
commit
899771a678
@ -1,3 +1,4 @@
|
|||||||
|
import { Selection } from "@tiptap/pm/state";
|
||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
interface EditorClassNames {
|
interface EditorClassNames {
|
||||||
@ -18,6 +19,19 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to find the parent node of a specific type
|
||||||
|
export function findParentNodeOfType(selection: Selection, typeName: string) {
|
||||||
|
let depth = selection.$anchor.depth;
|
||||||
|
while (depth > 0) {
|
||||||
|
const node = selection.$anchor.node(depth);
|
||||||
|
if (node.type.name === typeName) {
|
||||||
|
return { node, pos: selection.$anchor.start(depth) - 1 };
|
||||||
|
}
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
||||||
while (node !== null && node.nodeName !== "TABLE") {
|
while (node !== null && node.nodeName !== "TABLE") {
|
||||||
node = node.parentNode;
|
node = node.parentNode;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Editor } from "@tiptap/react";
|
import { Editor } from "@tiptap/react";
|
||||||
import { ReactNode } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
|
|
||||||
interface EditorContainerProps {
|
interface EditorContainerProps {
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
@ -8,17 +8,54 @@ interface EditorContainerProps {
|
|||||||
hideDragHandle?: () => void;
|
hideDragHandle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, children }: EditorContainerProps) => (
|
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||||
<div
|
const { editor, editorClassNames, hideDragHandle, children } = props;
|
||||||
id="editor-container"
|
|
||||||
onClick={() => {
|
const handleContainerClick = () => {
|
||||||
editor?.chain().focus(undefined, { scrollIntoView: false }).run();
|
if (!editor) return;
|
||||||
}}
|
if (!editor.isEditable) return;
|
||||||
onMouseLeave={() => {
|
if (editor.isFocused) return; // If editor is already focused, do nothing
|
||||||
hideDragHandle?.();
|
|
||||||
}}
|
const { selection } = editor.state;
|
||||||
className={`cursor-text ${editorClassNames}`}
|
const currentNode = selection.$from.node();
|
||||||
>
|
|
||||||
{children}
|
editor?.chain().focus("end", { scrollIntoView: false }).run(); // Focus the editor at the end
|
||||||
</div>
|
|
||||||
);
|
if (
|
||||||
|
currentNode.content.size === 0 && // Check if the current node is empty
|
||||||
|
!(
|
||||||
|
editor.isActive("orderedList") ||
|
||||||
|
editor.isActive("bulletList") ||
|
||||||
|
editor.isActive("taskItem") ||
|
||||||
|
editor.isActive("table") ||
|
||||||
|
editor.isActive("blockquote") ||
|
||||||
|
editor.isActive("codeBlock")
|
||||||
|
) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a new paragraph at the end of the document
|
||||||
|
const endPosition = editor?.state.doc.content.size;
|
||||||
|
editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run();
|
||||||
|
|
||||||
|
// Focus the newly added paragraph for immediate editing
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection(endPosition + 1)
|
||||||
|
.run();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="editor-container"
|
||||||
|
onClick={handleContainerClick}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
hideDragHandle?.();
|
||||||
|
}}
|
||||||
|
className={`cursor-text ${editorClassNames}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -5,6 +5,8 @@ import ImageExt from "@tiptap/extension-image";
|
|||||||
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
|
import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image";
|
||||||
import { DeleteImage } from "src/types/delete-image";
|
import { DeleteImage } from "src/types/delete-image";
|
||||||
import { RestoreImage } from "src/types/restore-image";
|
import { RestoreImage } from "src/types/restore-image";
|
||||||
|
import { insertLineBelowImageAction } from "./utilities/insert-line-below-image";
|
||||||
|
import { insertLineAboveImageAction } from "./utilities/insert-line-above-image";
|
||||||
|
|
||||||
interface ImageNode extends ProseMirrorNode {
|
interface ImageNode extends ProseMirrorNode {
|
||||||
attrs: {
|
attrs: {
|
||||||
@ -18,6 +20,12 @@ const IMAGE_NODE_TYPE = "image";
|
|||||||
|
|
||||||
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
||||||
ImageExt.extend({
|
ImageExt.extend({
|
||||||
|
addKeyboardShortcuts() {
|
||||||
|
return {
|
||||||
|
ArrowDown: insertLineBelowImageAction,
|
||||||
|
ArrowUp: insertLineAboveImageAction,
|
||||||
|
};
|
||||||
|
},
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
UploadImagesPlugin(cancelUploadImage),
|
UploadImagesPlugin(cancelUploadImage),
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
|
import { KeyboardShortcutCommand } from "@tiptap/core";
|
||||||
|
|
||||||
|
export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => {
|
||||||
|
const { selection, doc } = editor.state;
|
||||||
|
const { $from, $to } = selection;
|
||||||
|
|
||||||
|
let imageNode: ProseMirrorNode | null = null;
|
||||||
|
let imagePos: number | null = null;
|
||||||
|
|
||||||
|
// Check if the selection itself is an image node
|
||||||
|
doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
||||||
|
if (node.type.name === "image") {
|
||||||
|
imageNode = node;
|
||||||
|
imagePos = pos;
|
||||||
|
return false; // Stop iterating once an image node is found
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (imageNode === null || imagePos === null) return false;
|
||||||
|
|
||||||
|
// Since we want to insert above the image, we use the imagePos directly
|
||||||
|
const insertPos = imagePos;
|
||||||
|
|
||||||
|
if (insertPos < 0) return false;
|
||||||
|
|
||||||
|
// Check for an existing node immediately before the image
|
||||||
|
if (insertPos === 0) {
|
||||||
|
// If the previous node doesn't exist or isn't a paragraph, create and insert a new empty node there
|
||||||
|
editor.chain().insertContentAt(insertPos, { type: "paragraph" }).run();
|
||||||
|
editor.chain().setTextSelection(insertPos).run();
|
||||||
|
} else {
|
||||||
|
const prevNode = doc.nodeAt(insertPos);
|
||||||
|
|
||||||
|
if (prevNode && prevNode.type.name === "paragraph") {
|
||||||
|
// If the previous node is a paragraph, move the cursor there
|
||||||
|
editor.chain().setTextSelection(insertPos).run();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -0,0 +1,46 @@
|
|||||||
|
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
|
import { KeyboardShortcutCommand } from "@tiptap/core";
|
||||||
|
|
||||||
|
export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => {
|
||||||
|
const { selection, doc } = editor.state;
|
||||||
|
const { $from, $to } = selection;
|
||||||
|
|
||||||
|
let imageNode: ProseMirrorNode | null = null;
|
||||||
|
let imagePos: number | null = null;
|
||||||
|
|
||||||
|
// Check if the selection itself is an image node
|
||||||
|
doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
|
||||||
|
if (node.type.name === "image") {
|
||||||
|
imageNode = node;
|
||||||
|
imagePos = pos;
|
||||||
|
return false; // Stop iterating once an image node is found
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (imageNode === null || imagePos === null) return false;
|
||||||
|
|
||||||
|
const guaranteedImageNode: ProseMirrorNode = imageNode;
|
||||||
|
const nextNodePos = imagePos + guaranteedImageNode.nodeSize;
|
||||||
|
|
||||||
|
// Check for an existing node immediately after the image
|
||||||
|
const nextNode = doc.nodeAt(nextNodePos);
|
||||||
|
|
||||||
|
if (nextNode && nextNode.type.name === "paragraph") {
|
||||||
|
// If the next node is a paragraph, move the cursor there
|
||||||
|
const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1;
|
||||||
|
editor.chain().setTextSelection(endOfParagraphPos).run();
|
||||||
|
} else if (!nextNode) {
|
||||||
|
// If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there
|
||||||
|
editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run();
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection(nextNodePos + 1)
|
||||||
|
.run();
|
||||||
|
} else {
|
||||||
|
// If the next node is not a paragraph, do not proceed
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -25,6 +25,8 @@ import { tableControls } from "src/ui/extensions/table/table/table-controls";
|
|||||||
import { TableView } from "src/ui/extensions/table/table/table-view";
|
import { TableView } from "src/ui/extensions/table/table/table-view";
|
||||||
import { createTable } from "src/ui/extensions/table/table/utilities/create-table";
|
import { createTable } from "src/ui/extensions/table/table/utilities/create-table";
|
||||||
import { deleteTableWhenAllCellsSelected } from "src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected";
|
import { deleteTableWhenAllCellsSelected } from "src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected";
|
||||||
|
import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action";
|
||||||
|
import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action";
|
||||||
|
|
||||||
export interface TableOptions {
|
export interface TableOptions {
|
||||||
HTMLAttributes: Record<string, any>;
|
HTMLAttributes: Record<string, any>;
|
||||||
@ -231,6 +233,8 @@ export const Table = Node.create({
|
|||||||
"Mod-Backspace": deleteTableWhenAllCellsSelected,
|
"Mod-Backspace": deleteTableWhenAllCellsSelected,
|
||||||
Delete: deleteTableWhenAllCellsSelected,
|
Delete: deleteTableWhenAllCellsSelected,
|
||||||
"Mod-Delete": deleteTableWhenAllCellsSelected,
|
"Mod-Delete": deleteTableWhenAllCellsSelected,
|
||||||
|
ArrowDown: insertLineBelowTableAction,
|
||||||
|
ArrowUp: insertLineAboveTableAction,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
import { KeyboardShortcutCommand } from "@tiptap/core";
|
||||||
|
import { findParentNodeOfType } from "src/lib/utils";
|
||||||
|
|
||||||
|
export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) => {
|
||||||
|
// Check if the current selection or the closest node is a table
|
||||||
|
if (!editor.isActive("table")) return false;
|
||||||
|
|
||||||
|
// Get the current selection
|
||||||
|
const { selection } = editor.state;
|
||||||
|
|
||||||
|
// Find the table node and its position
|
||||||
|
const tableNode = findParentNodeOfType(selection, "table");
|
||||||
|
if (!tableNode) return false;
|
||||||
|
|
||||||
|
const tablePos = tableNode.pos;
|
||||||
|
|
||||||
|
// Determine if the selection is in the first row of the table
|
||||||
|
const firstRow = tableNode.node.child(0);
|
||||||
|
const selectionPath = (selection.$anchor as any).path;
|
||||||
|
const selectionInFirstRow = selectionPath.includes(firstRow);
|
||||||
|
|
||||||
|
if (!selectionInFirstRow) return false;
|
||||||
|
|
||||||
|
// Check if the table is at the very start of the document or its parent node
|
||||||
|
if (tablePos === 0) {
|
||||||
|
// The table is at the start, so just insert a paragraph at the current position
|
||||||
|
editor.chain().insertContentAt(tablePos, { type: "paragraph" }).run();
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection(tablePos + 1)
|
||||||
|
.run();
|
||||||
|
} else {
|
||||||
|
// The table is not at the start, check for the node immediately before the table
|
||||||
|
const prevNodePos = tablePos - 1;
|
||||||
|
|
||||||
|
if (prevNodePos <= 0) return false;
|
||||||
|
|
||||||
|
const prevNode = editor.state.doc.nodeAt(prevNodePos - 1);
|
||||||
|
|
||||||
|
if (prevNode && prevNode.type.name === "paragraph") {
|
||||||
|
// If there's a paragraph before the table, move the cursor to the end of that paragraph
|
||||||
|
const endOfParagraphPos = tablePos - prevNode.nodeSize;
|
||||||
|
editor.chain().setTextSelection(endOfParagraphPos).run();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
import { KeyboardShortcutCommand } from "@tiptap/core";
|
||||||
|
import { findParentNodeOfType } from "src/lib/utils";
|
||||||
|
|
||||||
|
export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) => {
|
||||||
|
// Check if the current selection or the closest node is a table
|
||||||
|
if (!editor.isActive("table")) return false;
|
||||||
|
|
||||||
|
// Get the current selection
|
||||||
|
const { selection } = editor.state;
|
||||||
|
|
||||||
|
// Find the table node and its position
|
||||||
|
const tableNode = findParentNodeOfType(selection, "table");
|
||||||
|
if (!tableNode) return false;
|
||||||
|
|
||||||
|
const tablePos = tableNode.pos;
|
||||||
|
const table = tableNode.node;
|
||||||
|
|
||||||
|
// Determine if the selection is in the last row of the table
|
||||||
|
const rowCount = table.childCount;
|
||||||
|
const lastRow = table.child(rowCount - 1);
|
||||||
|
const selectionPath = (selection.$anchor as any).path;
|
||||||
|
const selectionInLastRow = selectionPath.includes(lastRow);
|
||||||
|
|
||||||
|
if (!selectionInLastRow) return false;
|
||||||
|
|
||||||
|
// Calculate the position immediately after the table
|
||||||
|
const nextNodePos = tablePos + table.nodeSize;
|
||||||
|
|
||||||
|
// Check for an existing node immediately after the table
|
||||||
|
const nextNode = editor.state.doc.nodeAt(nextNodePos);
|
||||||
|
|
||||||
|
if (nextNode && nextNode.type.name === "paragraph") {
|
||||||
|
// If the next node is an paragraph, move the cursor there
|
||||||
|
const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1;
|
||||||
|
editor.chain().setTextSelection(endOfParagraphPos).run();
|
||||||
|
} else if (!nextNode) {
|
||||||
|
// If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there
|
||||||
|
editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run();
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.setTextSelection(nextNodePos + 1)
|
||||||
|
.run();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -152,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full pb-64 md:pl-7 pl-3 pt-5 page-renderer">
|
<div className="w-full h-full pb-20 pl-7 pt-5 page-renderer">
|
||||||
{!readonly ? (
|
{!readonly ? (
|
||||||
<input
|
<input
|
||||||
onChange={(e) => handlePageTitleChange(e.target.value)}
|
onChange={(e) => handlePageTitleChange(e.target.value)}
|
||||||
|
@ -100,10 +100,10 @@ const RichTextEditor = ({
|
|||||||
customClassName,
|
customClassName,
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
// React.useEffect(() => {
|
||||||
if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue);
|
// if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue);
|
||||||
}, [editor, initialValue]);
|
// }, [editor, initialValue]);
|
||||||
|
//
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -121,7 +121,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
<button
|
<button
|
||||||
key={item.name}
|
key={item.name}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={item.command}
|
onClick={(e) => {
|
||||||
|
item.command();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"p-2 text-custom-text-300 transition-colors hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5",
|
"p-2 text-custom-text-300 transition-colors hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5",
|
||||||
{
|
{
|
||||||
|
@ -33,8 +33,9 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
"flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100",
|
"flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100",
|
||||||
{ "bg-custom-background-100": isOpen }
|
{ "bg-custom-background-100": isOpen }
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-base">↗</p>
|
<p className="text-base">↗</p>
|
||||||
@ -60,6 +61,9 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="Paste a link"
|
placeholder="Paste a link"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
|
className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
|
||||||
defaultValue={editor.getAttributes("link").href || ""}
|
defaultValue={editor.getAttributes("link").href || ""}
|
||||||
/>
|
/>
|
||||||
@ -67,9 +71,10 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
|
className="flex items-center rounded-sm p-1 text-red-600 transition-all hover:bg-red-100 dark:hover:bg-red-800"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
unsetLinkEditor(editor);
|
unsetLinkEditor(editor);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
@ -78,7 +83,8 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
<button
|
<button
|
||||||
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
|
className="flex items-center rounded-sm p-1 text-custom-text-300 transition-all hover:bg-custom-background-90"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
onLinkSubmit();
|
onLinkSubmit();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -47,7 +47,10 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={(e) => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
className="flex h-full items-center gap-1 whitespace-nowrap p-2 text-sm font-medium text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5"
|
className="flex h-full items-center gap-1 whitespace-nowrap p-2 text-sm font-medium text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5"
|
||||||
>
|
>
|
||||||
<span>{activeItem?.name}</span>
|
<span>{activeItem?.name}</span>
|
||||||
@ -60,9 +63,10 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
<button
|
<button
|
||||||
key={item.name}
|
key={item.name}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
item.command();
|
item.command();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100",
|
"flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100",
|
||||||
|
Loading…
Reference in New Issue
Block a user