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