diff --git a/apps/web/src/main.css b/apps/web/src/main.css index d4d0780e21e06bcf52273ff6eb6a671f4a8865a2..63faa31914c2f06e0ca4e7ecd9524020c9039f56 100644 --- a/apps/web/src/main.css +++ b/apps/web/src/main.css @@ -78,3 +78,19 @@ body { cursor: se-resize; } */ + +.variable-node { + display: inline-block; + user-select: none; + padding: 3px 7px; + margin: 0px 7px; + font-size: 11px; + border-radius: 5px; + background: #e0e7f5; + color: #6e788e; + border: 1px solid #6e788e80; +} + +p > span:first-child > .variable-node { + margin-left: 0px; +} \ No newline at end of file diff --git a/libs/shared/lib/components/layout/Dialog.tsx b/libs/shared/lib/components/layout/Dialog.tsx index 06ee3f226cdb4ed57c0d2d07e27d13916a315736..1f6bb731e679143a156b347d1267ded18b4d665d 100644 --- a/libs/shared/lib/components/layout/Dialog.tsx +++ b/libs/shared/lib/components/layout/Dialog.tsx @@ -141,7 +141,7 @@ export const DialogContent = React.forwardRef<HTMLDivElement, React.HTMLProps<HT return ( <FloatingPortal> - <FloatingOverlay className="grid place-items-center z-50" lockScroll style={{ backgroundColor: 'rgba(0, 0, 0, 0.4)' }}> + <FloatingOverlay className="grid place-items-center" lockScroll style={{ backgroundColor: 'rgba(0, 0, 0, 0.4)' }}> <FloatingFocusManager context={context.floatingContext}> <div ref={ref} diff --git a/libs/shared/lib/components/textEditor/Placeholder.tsx b/libs/shared/lib/components/textEditor/Placeholder.tsx index 008398e8455f3f084e5e683e6948442c86f4d0a8..dbe71297b0b3dd5ee82b6ca0eee77a731379d110 100644 --- a/libs/shared/lib/components/textEditor/Placeholder.tsx +++ b/libs/shared/lib/components/textEditor/Placeholder.tsx @@ -1,5 +1,5 @@ import React from 'react'; export function Placeholder({ placeholder }: { placeholder?: string }) { - return placeholder && <div className="absolute inset-0 pointer-events-none flex items-center px-2 text-secondary-400">{placeholder}</div>; + return placeholder && <div className="absolute inset-0 pointer-events-none flex p-3 text-secondary-400">{placeholder}</div>; } diff --git a/libs/shared/lib/components/textEditor/TextEditor.tsx b/libs/shared/lib/components/textEditor/TextEditor.tsx index 8a95a80426a9b722c291c7cdf80b1c019b9bf77f..90f797e20f3f761638acf32f8bad7ef76ff58b6e 100644 --- a/libs/shared/lib/components/textEditor/TextEditor.tsx +++ b/libs/shared/lib/components/textEditor/TextEditor.tsx @@ -1,13 +1,17 @@ -import React from 'react'; +import { useRef } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { EditorState } from 'lexical'; +import { LexicalEditor, EditorState } from 'lexical'; +import { $generateHtmlFromNodes } from "@lexical/html"; import { MyOnChangePlugin } from './plugins/StateChangePlugin'; import { ToolbarPlugin } from './plugins/ToolbarPlugin'; +import { InsertVariablesPlugin } from './plugins/InsertVariablesPlugin'; import { ErrorHandler } from './ErrorHandler'; import { Placeholder } from './Placeholder'; +import { VariableNode } from './VariableNode'; +import { fontFamily } from 'html2canvas/dist/types/css/property-descriptors/font-family'; type TextEditorProps = { editorState: EditorState | undefined; @@ -17,14 +21,21 @@ type TextEditorProps = { }; export function TextEditor({ editorState, setEditorState, showToolbar, placeholder }: TextEditorProps) { - function onChange(editorState: EditorState) { + function onChange(editorState: EditorState, editor: LexicalEditor) { setEditorState(editorState); + editor.read(() => { + const html = $generateHtmlFromNodes(editor) + text.current = html; + }); } + const text = useRef<string>("hai"); + const initialConfig = { namespace: 'MyEditor', editorState: editorState, onError: ErrorHandler, + nodes: [ VariableNode ], }; return ( @@ -32,12 +43,17 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold {showToolbar && <ToolbarPlugin />} <div className="relative"> <RichTextPlugin - contentEditable={<ContentEditable className="border min-h-24" />} + contentEditable={<ContentEditable className="border min-h-24 p-3" />} placeholder={<Placeholder placeholder={placeholder} />} ErrorBoundary={LexicalErrorBoundary} /> </div> <MyOnChangePlugin onChange={onChange} /> + <InsertVariablesPlugin /> + <br /><b>Debug:</b> + <div style={{fontFamily: 'monospace'}}> + { text.current } + </div> </LexicalComposer> ); } diff --git a/libs/shared/lib/components/textEditor/VariableNode.tsx b/libs/shared/lib/components/textEditor/VariableNode.tsx new file mode 100644 index 0000000000000000000000000000000000000000..20fa9679572f1b8041e401affdc18df4ab5c384d --- /dev/null +++ b/libs/shared/lib/components/textEditor/VariableNode.tsx @@ -0,0 +1,68 @@ +import type { NodeKey, LexicalEditor, DOMExportOutput } from 'lexical'; +import { DecoratorNode, EditorConfig } from 'lexical'; + + +export class VariableNode extends DecoratorNode<JSX.Element> { + + __name: string; + + static getType(): string { + return 'variable-node'; + } + + static clone(node: VariableNode): VariableNode { + return new VariableNode(node.__name, node.__key); + } + + constructor(name: string, key?: NodeKey) { + super(key); + this.__name = name; + } + + setName(name: string) { + const self = this.getWritable(); + this.__name = name; + } + + getName(): string { + const self = this.getLatest(); + return self.__name; + } + + getTextContent(): string { + const self = this.getLatest(); + return `{{${self.__name}}}`; + } + + // View + + createDOM(config: EditorConfig): HTMLElement { + const span = document.createElement("span"); + const theme = config.theme; + const className = theme.image; + if (className !== undefined) { + span.className = `${className}`; + } + return span; + } + + updateDOM(): false { + return false; + } + + exportDOM(editor: LexicalEditor): DOMExportOutput { + const self = this.getLatest(); + + return { + element: new Text(self.getTextContent()) + }; + } + + decorate(): JSX.Element { + return ( + <div className="variable-node"> + {this.getName()} + </div> + ); + } +} \ No newline at end of file diff --git a/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx new file mode 100644 index 0000000000000000000000000000000000000000..baa419ee21901e969d84f266020b926bacb65744 --- /dev/null +++ b/libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx @@ -0,0 +1,55 @@ +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $copyNode, + $getRoot, + $isParagraphNode, + $isElementNode, + $getNodeByKey, + $getSelection, + type BaseSelection, + $parseSerializedNode +} from "lexical"; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { VariableNode } from '../VariableNode'; +import { useState } from 'react'; +import { useGraphQueryResult } from '@graphpolaris/shared/lib/data-access'; + +export const InsertVariablesPlugin = () => { + const [editor] = useLexicalComposerContext(); + + const onChange = (value: string | number) => { + editor.update(() => { + const selection = $getSelection() as BaseSelection; + + const node = new VariableNode(String(value)); + + selection.insertNodes([node]); + + // TODO: enable drag and dropping nodes + }); + }; + + const result = useGraphQueryResult(); + + const nodeTypes = Object.keys(result.metaData?.nodes.types || {}); + function optionsForType(type: string) { + const typeObj = result.metaData?.nodes.types[type] ?? {}; + + if (!('attributes' in typeObj)) { + return []; + } + + + return Object.entries(typeObj.attributes).map(([k,v]) => Object.keys(v).map(x => `${k}_${x}`)).flat(); + } + + return nodeTypes.map((nodeType) => + <Input + type="dropdown" + label={`${nodeType} variable`} + value="" + options={optionsForType(nodeType)} + onChange={onChange} + /> + ); +}; \ No newline at end of file diff --git a/libs/shared/lib/components/textEditor/plugins/StateChangePlugin.tsx b/libs/shared/lib/components/textEditor/plugins/StateChangePlugin.tsx index c4ef00a4e0f172acc57bec4d7b53416b25c70524..bf3f902de691a8a2862d0b371313013646d7d477 100644 --- a/libs/shared/lib/components/textEditor/plugins/StateChangePlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/StateChangePlugin.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { EditorState } from 'lexical'; +import { LexicalEditor, EditorState } from 'lexical'; -export function MyOnChangePlugin({ onChange }: { onChange: (val: EditorState) => void }) { +export function MyOnChangePlugin({ onChange }: { onChange: (val: EditorState, editor: LexicalEditor) => void }) { const [editor] = useLexicalComposerContext(); useEffect(() => { return editor.registerUpdateListener(({ editorState }) => { - onChange(editorState); + onChange(editorState, editor); }); }, [editor, onChange]); diff --git a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx index 93c56d75cff092de4ed746d9d3465fdc0e57ed5a..74ded236745eb6bb53d66ce13e88136482294cbd 100644 --- a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx @@ -27,7 +27,7 @@ export function ToolbarPlugin() { variant="ghost" size="xs" iconComponent="icon-[ic--baseline-format-underlined]" - onClick={formatItalic} + onClick={formatUnderline} /> </div> );