From d0f69909f994b63d2a488b5c2c422dc3d9c08cc5 Mon Sep 17 00:00:00 2001 From: Dennis Collaris <d.a.c.collaris@uu.nl> Date: Tue, 8 Oct 2024 09:41:51 +0000 Subject: [PATCH] feat: basic support for inserting variables into editor solves #1208331470355125 --- apps/web/src/main.css | 16 +++++ libs/shared/lib/components/layout/Dialog.tsx | 2 +- .../lib/components/textEditor/Placeholder.tsx | 2 +- .../lib/components/textEditor/TextEditor.tsx | 24 +++++-- .../components/textEditor/VariableNode.tsx | 68 +++++++++++++++++++ .../plugins/InsertVariablesPlugin.tsx | 55 +++++++++++++++ .../textEditor/plugins/StateChangePlugin.tsx | 6 +- .../textEditor/plugins/ToolbarPlugin.tsx | 2 +- 8 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 libs/shared/lib/components/textEditor/VariableNode.tsx create mode 100644 libs/shared/lib/components/textEditor/plugins/InsertVariablesPlugin.tsx diff --git a/apps/web/src/main.css b/apps/web/src/main.css index d4d0780e2..63faa3191 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 06ee3f226..1f6bb731e 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 008398e84..dbe71297b 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 8a95a8042..90f797e20 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 000000000..20fa96795 --- /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 000000000..baa419ee2 --- /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 c4ef00a4e..bf3f902de 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 93c56d75c..74ded2367 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> ); -- GitLab