diff --git a/apps/console/src/components/PolicyEditor.tsx b/apps/console/src/components/PolicyEditor.tsx deleted file mode 100644 index 40215e609..000000000 --- a/apps/console/src/components/PolicyEditor.tsx +++ /dev/null @@ -1,461 +0,0 @@ -import { useEffect, useState, useRef } from "react"; -import { LexicalComposer } from "@lexical/react/LexicalComposer"; -import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; -import { ContentEditable } from "@lexical/react/LexicalContentEditable"; -import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"; -import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin"; -import { HeadingNode, QuoteNode } from "@lexical/rich-text"; -import { TableCellNode, TableNode, TableRowNode } from "@lexical/table"; -import { ListItemNode, ListNode } from "@lexical/list"; -import { CodeHighlightNode, CodeNode } from "@lexical/code"; -import { AutoLinkNode, LinkNode } from "@lexical/link"; -import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; -import { ListPlugin } from "@lexical/react/LexicalListPlugin"; -import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"; -import { TRANSFORMERS } from "@lexical/markdown"; -import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html"; -import { $getRoot, $insertNodes, EditorState, LexicalEditor } from "lexical"; -import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; -import { - Bold, - Italic, - Underline, - List, - ListOrdered, - Heading1, - Heading2, -} from "lucide-react"; -import { $isRangeSelection, FORMAT_TEXT_COMMAND, $getSelection } from "lexical"; -import { $createHeadingNode, $isHeadingNode } from "@lexical/rich-text"; -import { $setBlocksType } from "@lexical/selection"; -import { $createParagraphNode } from "lexical"; -import { - INSERT_ORDERED_LIST_COMMAND, - INSERT_UNORDERED_LIST_COMMAND, -} from "@lexical/list"; - -import "./PolicyEditor.css"; - -// Custom plugin to preserve scroll position -function ScrollPlugin() { - const [editor] = useLexicalComposerContext(); - const scrollRef = useRef(0); - const editorRef = useRef(null); - - useEffect(() => { - return editor.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => { - // Skip if there are no changes - if (dirtyElements.size === 0 && dirtyLeaves.size === 0) { - return; - } - - // Only run after the DOM has been updated - setTimeout(() => { - if (!editorRef.current) { - const rootElement = editor.getRootElement(); - if (rootElement) { - // Find the actual contentEditable element - const contentEditables = rootElement.getElementsByClassName( - "policy-editor-input" - ); - if (contentEditables.length > 0) { - editorRef.current = contentEditables[0] as HTMLElement; - } - } - } - - if (editorRef.current) { - // After updates, restore the scroll position - editorRef.current.scrollTop = scrollRef.current; - } - }, 0); - }); - }, [editor]); - - useEffect(() => { - if (!editorRef.current) { - const rootElement = editor.getRootElement(); - if (rootElement) { - const contentEditables = rootElement.getElementsByClassName( - "policy-editor-input" - ); - if (contentEditables.length > 0) { - editorRef.current = contentEditables[0] as HTMLElement; - } - } - } - - const saveScrollPosition = () => { - if (editorRef.current) { - scrollRef.current = editorRef.current.scrollTop; - } - }; - - if (editorRef.current) { - editorRef.current.addEventListener("scroll", saveScrollPosition); - } - - return () => { - if (editorRef.current) { - editorRef.current.removeEventListener("scroll", saveScrollPosition); - } - }; - }, [editor]); - - return null; -} - -// Theme for the editor -const theme = { - ltr: "ltr", - rtl: "rtl", - paragraph: "editor-paragraph", - quote: "editor-quote", - heading: { - h1: "editor-heading-h1", - h2: "editor-heading-h2", - h3: "editor-heading-h3", - h4: "editor-heading-h4", - h5: "editor-heading-h5", - }, - list: { - nested: { - listitem: "editor-nested-listitem", - }, - ol: "editor-list-ol", - ul: "editor-list-ul", - listitem: "editor-listitem", - listitemChecked: "editor-listitem--checked", - listitemUnchecked: "editor-listitem--unchecked", - }, - image: "editor-image", - link: "editor-link", - text: { - bold: "editor-text-bold", - italic: "editor-text-italic", - underline: "editor-text-underline", - strikethrough: "editor-text-strikethrough", - underlineStrikethrough: "editor-text-underlineStrikethrough", - code: "editor-text-code", - }, - code: "editor-code", - codeHighlight: { - atrule: "editor-tokenAttr", - attr: "editor-tokenAttr", - boolean: "editor-tokenProperty", - builtin: "editor-tokenSelector", - cdata: "editor-tokenComment", - char: "editor-tokenSelector", - class: "editor-tokenFunction", - "class-name": "editor-tokenFunction", - comment: "editor-tokenComment", - constant: "editor-tokenProperty", - deleted: "editor-tokenProperty", - doctype: "editor-tokenComment", - entity: "editor-tokenOperator", - function: "editor-tokenFunction", - important: "editor-tokenVariable", - inserted: "editor-tokenSelector", - keyword: "editor-tokenAttr", - namespace: "editor-tokenVariable", - number: "editor-tokenProperty", - operator: "editor-tokenOperator", - prolog: "editor-tokenComment", - property: "editor-tokenProperty", - punctuation: "editor-tokenPunctuation", - regex: "editor-tokenVariable", - selector: "editor-tokenSelector", - string: "editor-tokenSelector", - symbol: "editor-tokenProperty", - tag: "editor-tokenProperty", - url: "editor-tokenOperator", - variable: "editor-tokenVariable", - }, -}; - -// Simple Toolbar Plugin -function ToolbarPlugin() { - const [editor] = useLexicalComposerContext(); - const [isBold, setIsBold] = useState(false); - const [isItalic, setIsItalic] = useState(false); - const [isUnderline, setIsUnderline] = useState(false); - const [blockType, setBlockType] = useState("paragraph"); - - useEffect(() => { - return editor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - setIsBold(selection.hasFormat("bold")); - setIsItalic(selection.hasFormat("italic")); - setIsUnderline(selection.hasFormat("underline")); - - const anchorNode = selection.anchor.getNode(); - const element = anchorNode.getTopLevelElementOrThrow(); - if ($isHeadingNode(element)) { - setBlockType(element.getTag()); - } else { - setBlockType("paragraph"); - } - } - }); - }); - }, [editor]); - - const formatHeading = (headingSize: "h1" | "h2") => { - if (blockType !== headingSize) { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createHeadingNode(headingSize)); - } - }); - } else { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createParagraphNode()); - } - }); - } - }; - - const formatBulletList = () => { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); - }; - - const formatNumberedList = () => { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); - }; - - return ( -
- - - - -
- - - - -
- - - -
- ); -} - -// Import HTML Plugin -function HtmlImportPlugin({ - initialHtml, - setEditor, -}: { - initialHtml: string; - setEditor: (editor: LexicalEditor) => void; -}) { - const [editor] = useLexicalComposerContext(); - const isInitializedRef = useRef(false); - - useEffect(() => { - console.log( - "HtmlImportPlugin effect running, editor:", - !!editor, - "initialHtml:", - initialHtml - ? initialHtml.substring(0, 50) + (initialHtml.length > 50 ? "..." : "") - : "empty" - ); - - if (editor && !isInitializedRef.current) { - setEditor(editor); - isInitializedRef.current = true; - - editor.update(() => { - try { - if (initialHtml && initialHtml.trim() !== "") { - console.log("Importing HTML content into editor"); - const parser = new DOMParser(); - const dom = parser.parseFromString(initialHtml, "text/html"); - const nodes = $generateNodesFromDOM(editor, dom); - - const root = $getRoot(); - root.clear(); - $insertNodes(nodes); - console.log("HTML content imported successfully"); - } else { - // If no initial content, ensure we have at least a paragraph - const root = $getRoot(); - if (root.getChildrenSize() === 0) { - const paragraph = $createParagraphNode(); - root.append(paragraph); - } - } - } catch (error) { - console.error("Error importing HTML content:", error); - // Ensure we have at least a paragraph on error - const root = $getRoot(); - if (root.getChildrenSize() === 0) { - const paragraph = $createParagraphNode(); - root.append(paragraph); - } - } - }); - } - }, [editor, initialHtml, setEditor]); - - return null; -} - -export interface PolicyEditorProps { - initialContent: string; - onChange: (html: string) => void; -} - -export default function PolicyEditor({ - initialContent, - onChange, -}: PolicyEditorProps) { - const [editor, setEditor] = useState(null); - const initialContentRef = useRef(initialContent); - - // Update reference if initialContent changes - useEffect(() => { - initialContentRef.current = initialContent; - }, [initialContent]); - - console.log("PolicyEditor rendering with initialContent:", initialContent); - - // Function to handle editor changes - const handleEditorChange = (editorState: EditorState) => { - if (!editor) return; - - editorState.read(() => { - const htmlString = $generateHtmlFromNodes(editor); - console.log( - "Editor content changed:", - htmlString.substring(0, 100) + (htmlString.length > 100 ? "..." : "") - ); - onChange(htmlString); - }); - }; - - // Lexical Editor Configuration - const initialConfig = { - namespace: "PolicyEditor", - theme, - onError: (error: Error) => { - console.error("Lexical Editor Error:", error); - }, - nodes: [ - HeadingNode, - QuoteNode, - ListNode, - ListItemNode, - CodeNode, - CodeHighlightNode, - TableNode, - TableCellNode, - TableRowNode, - AutoLinkNode, - LinkNode, - ], - }; - - return ( -
- -
- -
- - } - placeholder={ -
- Enter policy content... -
- } - ErrorBoundary={LexicalErrorBoundary} - /> - - - - - - - - -
-
-
-
- ); -} diff --git a/apps/console/src/components/editor/ToolbarPlugin.tsx b/apps/console/src/components/editor/ToolbarPlugin.tsx deleted file mode 100644 index 7080d567d..000000000 --- a/apps/console/src/components/editor/ToolbarPlugin.tsx +++ /dev/null @@ -1,405 +0,0 @@ -import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { - $createParagraphNode, - $getSelection, - $isRangeSelection, - FORMAT_TEXT_COMMAND, - UNDO_COMMAND, - REDO_COMMAND, - Point, - RangeSelection, -} from "lexical"; -import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode, -} from "@lexical/rich-text"; -import { - $isListNode, - INSERT_ORDERED_LIST_COMMAND, - INSERT_UNORDERED_LIST_COMMAND, - ListNode, - REMOVE_LIST_COMMAND, -} from "@lexical/list"; -import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; -import { - $getSelectionStyleValueForProperty, - $patchStyleText, - $setBlocksType, -} from "@lexical/selection"; -import { $getNearestNodeOfType } from "@lexical/utils"; -import { useCallback, useEffect, useState } from "react"; -import { - Bold, - Italic, - Underline, - Strikethrough, - List, - ListOrdered, - Link, - Quote, - Heading1, - Heading2, - Heading3, - Undo, - Redo, - AlignLeft, - AlignCenter, - AlignRight, - AlignJustify, -} from "lucide-react"; - -export function ToolbarPlugin() { - const [editor] = useLexicalComposerContext(); - const [activeEditor] = useState(editor); - const [isBold, setIsBold] = useState(false); - const [isItalic, setIsItalic] = useState(false); - const [isUnderline, setIsUnderline] = useState(false); - const [isStrikethrough, setIsStrikethrough] = useState(false); - const [isLink, setIsLink] = useState(false); - const [blockType, setBlockType] = useState("paragraph"); - const [, setSelectedElementKey] = useState(null); - const [textAlignment, setTextAlignment] = useState< - "left" | "center" | "right" | "justify" - >("left"); - - const updateToolbar = useCallback(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - setIsBold(selection.hasFormat("bold")); - setIsItalic(selection.hasFormat("italic")); - setIsUnderline(selection.hasFormat("underline")); - setIsStrikethrough(selection.hasFormat("strikethrough")); - - // Check for links - const node = getSelectedNode(selection); - const parent = node.getParent(); - if ($isLinkNode(parent) || $isLinkNode(node)) { - setIsLink(true); - } else { - setIsLink(false); - } - - // Get text alignment - const alignment = $getSelectionStyleValueForProperty( - selection, - "text-align", - "left" - ) as "left" | "center" | "right" | "justify"; - setTextAlignment(alignment); - - // Update block type - const anchorNode = selection.anchor.getNode(); - const element = - anchorNode.getKey() === "root" - ? anchorNode - : anchorNode.getTopLevelElementOrThrow(); - const elementKey = element.getKey(); - const elementDOM = activeEditor.getElementByKey(elementKey); - - setSelectedElementKey(elementKey); - - if (elementDOM !== null) { - if ($isHeadingNode(element)) { - const tag = element.getTag(); - setBlockType(tag); - } else if ($isListNode(element)) { - const parentList = $getNearestNodeOfType( - anchorNode, - ListNode - ); - const listType = parentList - ? parentList.getListType() - : element.getListType(); - setBlockType(listType); - } else { - setBlockType("paragraph"); - } - } - } - }, [activeEditor]); - - useEffect(() => { - return editor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateToolbar(); - }); - }); - }, [editor, updateToolbar]); - - const formatHeading = (headingSize: "h1" | "h2" | "h3") => { - if (blockType !== headingSize) { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createHeadingNode(headingSize)); - } - }); - } else { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createParagraphNode()); - } - }); - } - }; - - const formatQuote = () => { - if (blockType !== "quote") { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createQuoteNode()); - } - }); - } else { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $setBlocksType(selection, () => $createParagraphNode()); - } - }); - } - }; - - const formatBulletList = () => { - if (blockType !== "ul") { - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); - } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); - } - }; - - const formatNumberedList = () => { - if (blockType !== "ol") { - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); - } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); - } - }; - - const formatLink = () => { - if (!isLink) { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - const url = prompt("Enter URL", "https://"); - if (url) { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, url); - } - } - } else { - editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); - } - }; - - const formatTextAlignment = ( - alignment: "left" | "center" | "right" | "justify" - ) => { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $patchStyleText(selection, { - "text-align": alignment, - }); - } - }); - }; - - return ( -
- - - -
- - - - - - - -
- - - - - - - - -
- - - - - -
- ); -} - -function getSelectedNode(selection: RangeSelection) { - const anchor = selection.anchor; - const focus = selection.focus; - const anchorNode = selection.anchor.getNode(); - const focusNode = selection.focus.getNode(); - if (anchorNode === focusNode) { - return anchorNode; - } - const isBackward = selection.isBackward(); - if (isBackward) { - return $isAtNodeEnd(focus) ? anchorNode : focusNode; - } else { - return $isAtNodeEnd(anchor) ? focusNode : anchorNode; - } -} - -function $isAtNodeEnd(point: Point) { - return point.offset === point.getNode().getTextContentSize(); -}