diff --git a/libs/shared/lib/components/textEditor/TextEditor.tsx b/libs/shared/lib/components/textEditor/TextEditor.tsx index 597bbf0bf7fd1e163aa66632e20d3722903ae767..9d71b27bbfbc6e76fd168dd97f82e1e8b7f99e26 100644 --- a/libs/shared/lib/components/textEditor/TextEditor.tsx +++ b/libs/shared/lib/components/textEditor/TextEditor.tsx @@ -7,6 +7,7 @@ import { LexicalEditor, EditorState } from 'lexical'; import { $generateHtmlFromNodes } from '@lexical/html'; import { MyOnChangePlugin } from './plugins/StateChangePlugin'; import { ToolbarPlugin } from './plugins/ToolbarPlugin'; +import { PreviewPlugin } from './plugins/PreviewPlugin'; import { InsertVariablesPlugin } from './plugins/InsertVariablesPlugin'; import { ErrorHandler } from './ErrorHandler'; import { Placeholder } from './Placeholder'; @@ -24,13 +25,10 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold function onChange(editorState: EditorState, editor: LexicalEditor) { setEditorState(editorState); editor.read(() => { - const html = $generateHtmlFromNodes(editor as any); // any needed to avoid excessive ts error - text.current = html; + // TODO: }); } - const text = useRef<string>('hai'); - const initialConfig = { namespace: 'MyEditor', editorState: editorState, @@ -38,21 +36,26 @@ export function TextEditor({ editorState, setEditorState, showToolbar, placehold nodes: [VariableNode], }; + const contentEditableRef = useRef(); + return ( <LexicalComposer initialConfig={initialConfig}> - {showToolbar && <ToolbarPlugin />} - <div className="relative"> - <RichTextPlugin - contentEditable={<ContentEditable className="border min-h-24 p-3" />} - placeholder={<Placeholder placeholder={placeholder} />} - ErrorBoundary={LexicalErrorBoundary} - /> + <div className="flex items-center bg-secondary-50 rounded mt-4 space-x-2"> + <PreviewPlugin contentEditable={contentEditableRef} /> + {showToolbar && <ToolbarPlugin />} + </div> + <div className="border p-2"> + <div className="editor relative"> + <RichTextPlugin + contentEditable={<ContentEditable className="border p-3 min-h-24 rounded" ref={contentEditableRef} />} + placeholder={<Placeholder placeholder={placeholder} />} + ErrorBoundary={LexicalErrorBoundary} + /> + </div> + <div className="preview min-h-24 p-3 hidden"></div> </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 index 72ced5ad048f585aa72297d63bcd481f169174a4..5267cba556fe32d908c7befbb10b33eee35749b5 100644 --- a/libs/shared/lib/components/textEditor/VariableNode.tsx +++ b/libs/shared/lib/components/textEditor/VariableNode.tsx @@ -1,15 +1,12 @@ import type { NodeKey, LexicalEditor, DOMExportOutput } from 'lexical'; import { DecoratorNode, EditorConfig } from 'lexical'; - export enum VariableType { - statistic, - visualization -}; - + statistic = 'statistic', + visualization = 'vis', +} export class VariableNode extends DecoratorNode<JSX.Element> { - __name: string; __variableType: VariableType; @@ -45,7 +42,7 @@ export class VariableNode extends DecoratorNode<JSX.Element> { // View createDOM(config: EditorConfig): HTMLElement { - const span = document.createElement("span"); + const span = document.createElement('span'); const theme = config.theme; const className = theme.image; if (className !== undefined) { @@ -62,15 +59,11 @@ export class VariableNode extends DecoratorNode<JSX.Element> { const self = this.getLatest(); return { - element: new Text(self.getTextContent()) + element: new Text(self.getTextContent()), }; } decorate(): JSX.Element { - return ( - <div className="variable-node"> - {this.getName()} - </div> - ); + return <div className="variable-node">{this.getName()}</div>; } -} \ No newline at end of file +} diff --git a/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cb81db1aeb19496310a7a6cc9642885778ae7900 --- /dev/null +++ b/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx @@ -0,0 +1,78 @@ +import { LegacyRef, useRef, useEffect, MutableRefObject } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { FORMAT_TEXT_COMMAND } from 'lexical'; +import { Button } from '../../buttons'; +import { $generateHtmlFromNodes } from '@lexical/html'; + +export function PreviewPlugin({ contentEditable }: { contentEditable: MutableRefObject<HTMLDivElement | undefined> }): JSX.Element { + const [editor] = useLexicalComposerContext(); + + function updatePreview() { + editor.read(() => { + const html = $generateHtmlFromNodes(editor as any); // any needed to avoid excessive ts error + const preview = document.querySelector('.editor + .preview'); + if (preview == null) return; // TODO: nope + preview.innerHTML = html; + }); + } + + const activeClasses = ['text-blue-600', 'bg-light', 'active']; + + function resetStyling(event: React.MouseEvent) { + document.querySelectorAll('.toolbar-preview li button').forEach((btn) => btn.classList.remove('text-blue-600', 'bg-light')); + const button = event.target as HTMLButtonElement; + button.classList.add(...activeClasses); + } + + function showPreview(event: React.MouseEvent) { + resetStyling(event); + + const editor = contentEditable.current?.parentElement; + if (editor == null) return; + + editor.classList.add('hidden'); + let preview = editor.parentElement?.querySelector('.editor + .preview'); + preview?.classList.remove('hidden'); + + updatePreview(); + } + + function showEditor(event: React.MouseEvent) { + resetStyling(event); + + const button = event.target as HTMLButtonElement; + button.classList.add(...activeClasses); + + const editor = contentEditable.current?.parentElement; + if (editor == null) return; + + editor.classList.remove('hidden'); + editor.querySelector('div').focus(); + + let preview = editor.parentElement?.querySelector('.editor + .preview'); + if (preview == null) return; + preview.classList.add('hidden'); + } + + return ( + <ul + className="toolbar-preview flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400 z-10" + style={{ transform: 'translate(0px, 2.5px)' }} + > + <li className="me-1"> + <button + onClick={showEditor} + aria-current="page" + className="inline-block px-3 py-2 text-blue-600 bg-light border border-b-light border-light-200 rounded-t-lg active" + > + Write + </button> + </li> + <li> + <button onClick={showPreview} className="inline-block px-3 py-2 border border-b-0 border-light-200 rounded-t-lg"> + Preview + </button> + </li> + </ul> + ); +} diff --git a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx index 74ded236745eb6bb53d66ce13e88136482294cbd..e4b1ec07e08248fb60e2f9a449c8fdad55faecf3 100644 --- a/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx +++ b/libs/shared/lib/components/textEditor/plugins/ToolbarPlugin.tsx @@ -18,17 +18,31 @@ export function ToolbarPlugin() { editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline'); }; - return ( - <div className="flex bg-secondary-50 rounded mt-4 p-2 space-x-2"> - <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-format-bold]" onClick={formatBold} /> - <Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-format-italic]" onClick={formatItalic} /> - <Button - variantType="secondary" - variant="ghost" - size="xs" - iconComponent="icon-[ic--baseline-format-underlined]" - onClick={formatUnderline} - /> - </div> - ); + return [ + <div style={{ flex: '1 1 auto' }}></div>, + <Button + className="my-2" + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-format-bold]" + onClick={formatBold} + />, + <Button + className="my-2" + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-format-italic]" + onClick={formatItalic} + />, + <Button + className="my-2 me-2" + variantType="secondary" + variant="ghost" + size="xs" + iconComponent="icon-[ic--baseline-format-underlined]" + onClick={formatUnderline} + />, + ]; }