From 59e5ea92cb046f2d55814506573141cf0013b13e Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 1 Mar 2023 10:39:39 +0100 Subject: [PATCH 01/57] feat: add color action to heading and paragraph toolbar --- packages/lexical-editor/package.json | 1 + .../components/ColorPicker/ColorPicker.tsx | 5 ++ .../src/components/Toolbar/Toolbar.css | 3 + .../ToolbarActions/FontColorAction.tsx | 86 +++++++++++++++++++ .../ToolbarPresets/HeadingToolbarPreset.tsx | 2 + .../ToolbarPresets/ParagraphToolbarPreset.tsx | 2 + .../src/images/icons/font-color.svg | 1 + packages/lexical-editor/tsconfig.build.json | 5 +- packages/lexical-editor/tsconfig.json | 6 +- yarn.lock | 1 + 10 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx create mode 100644 packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx create mode 100644 packages/lexical-editor/src/images/icons/font-color.svg diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index d9644fbb4fe..f67ef6d2667 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -18,6 +18,7 @@ "@lexical/selection": "0.8.1", "@lexical/utils": "0.8.1", "@webiny/react-composition": "0.0.0", + "@webiny/ui": "0.0.0", "lexical": "0.8.1", "react": "^17.0.2", "react-dom": "^17.0.2" diff --git a/packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx b/packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx new file mode 100644 index 00000000000..a4bc17fc2bc --- /dev/null +++ b/packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx @@ -0,0 +1,5 @@ +export interface ColorPicker { + onChange: (color: string) => void; + color: string; + open: boolean; +} diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.css b/packages/lexical-editor/src/components/Toolbar/Toolbar.css index 4c1d6733752..2aeae709981 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.css +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.css @@ -194,6 +194,9 @@ i.chevron-down { background-color: rgb(223, 232, 250); } +i.font-color { + background-image: url("../../images/icons/font-color.svg"); +} .floating-text-format-popup button.toolbar-item { border: 0; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx new file mode 100644 index 00000000000..de7e38cd03f --- /dev/null +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -0,0 +1,86 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $getSelection, + $isRangeSelection, + COMMAND_PRIORITY_CRITICAL, + SELECTION_CHANGE_COMMAND +} from "lexical"; +import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection"; +import { DropDown } from "~/ui/DropDown"; +import { makeComposable } from "@webiny/react-composition"; +import { ColorPicker } from "@webiny/ui/ColorPicker"; + +interface FontColorPicker { + onChange: (value: string) => void; +} + +export const FontColorPicker = makeComposable( + "FontColorPicker", + ({ onChange }: FontColorPicker): JSX.Element => { + return ( + +
+ + Some color picker thing +
+
+ ); + } +); + +export const FontColorAction = () => { + const [editor] = useLexicalComposerContext(); + const [activeEditor, setActiveEditor] = useState(editor); + const [fontColor, setFontColor] = useState("#000"); + + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + } + }, [activeEditor]); + + useEffect(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + setFontColor($getSelectionStyleValueForProperty(selection, "color", fontColor)); + } + }, [activeEditor]); + + const applyStyleText = useCallback( + (styles: Record) => { + activeEditor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + $patchStyleText(selection, styles); + } + }); + }, + [activeEditor] + ); + + const onFontColorSelect = useCallback( + (value: string) => { + applyStyleText({ color: value }); + }, + [applyStyleText] + ); + + useEffect(() => { + return editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + setActiveEditor(newEditor); + return false; + }, + COMMAND_PRIORITY_CRITICAL + ); + }, [editor, updateToolbar]); + + return ; +}; diff --git a/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx b/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx index 22425682144..e15c8497cf2 100644 --- a/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx +++ b/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx @@ -7,11 +7,13 @@ import { CodeHighlightAction } from "~/components/ToolbarActions/CodeHighlightAc import { LinkAction } from "~/components/ToolbarActions/LinkAction"; import { FontSizeAction } from "~/components/ToolbarActions/FontSizeAction"; import { Divider } from "~/ui/Divider"; +import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; export const HeadingToolbarPreset = () => { return ( <> } type={"heading"} /> + } type={"heading"} /> } type={"heading"} /> } type={"heading"} /> } type={"heading"} /> diff --git a/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx b/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx index 146e4ae9fce..7b39dbe3e07 100644 --- a/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx +++ b/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx @@ -10,11 +10,13 @@ import { Divider } from "~/ui/Divider"; import { NumberedListAction } from "~/components/ToolbarActions/NumberedListAction"; import { BulletListAction } from "~/components/ToolbarActions/BulletListAction"; import { QuoteAction } from "~/components/ToolbarActions/QuoteAction"; +import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; export const ParagraphToolbarPreset = () => { return ( <> } type={"paragraph"} /> + } type={"paragraph"} /> } type={"paragraph"} /> } type={"paragraph"} /> } type={"paragraph"} /> diff --git a/packages/lexical-editor/src/images/icons/font-color.svg b/packages/lexical-editor/src/images/icons/font-color.svg new file mode 100644 index 00000000000..1ac53f7ac5c --- /dev/null +++ b/packages/lexical-editor/src/images/icons/font-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/lexical-editor/tsconfig.build.json b/packages/lexical-editor/tsconfig.build.json index d0eb9513334..02fe5316e8c 100644 --- a/packages/lexical-editor/tsconfig.build.json +++ b/packages/lexical-editor/tsconfig.build.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.build.json", "include": ["src"], - "references": [{ "path": "../react-composition/tsconfig.build.json" }], + "references": [ + { "path": "../react-composition/tsconfig.build.json" }, + { "path": "../ui/tsconfig.build.json" } + ], "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/packages/lexical-editor/tsconfig.json b/packages/lexical-editor/tsconfig.json index b8f98f630e1..cbd69943ac9 100644 --- a/packages/lexical-editor/tsconfig.json +++ b/packages/lexical-editor/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "include": ["src", "__tests__/**/*.ts"], - "references": [{ "path": "../react-composition" }], + "references": [{ "path": "../react-composition" }, { "path": "../ui" }], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], "outDir": "./dist", @@ -10,7 +10,9 @@ "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], "@webiny/react-composition/*": ["../react-composition/src/*"], - "@webiny/react-composition": ["../react-composition/src"] + "@webiny/react-composition": ["../react-composition/src"], + "@webiny/ui/*": ["../ui/src/*"], + "@webiny/ui": ["../ui/src"] }, "baseUrl": "." } diff --git a/yarn.lock b/yarn.lock index 89220519594..22a289a2da1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15173,6 +15173,7 @@ __metadata: "@webiny/cli": ^5.33.1 "@webiny/project-utils": ^5.33.1 "@webiny/react-composition": 0.0.0 + "@webiny/ui": 0.0.0 lexical: 0.8.1 react: ^17.0.2 react-dom: ^17.0.2 From 6f2ab410d6c8c6c5ba15418aa865cc2f39c7ea99 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 1 Mar 2023 13:05:48 +0100 Subject: [PATCH 02/57] wip: add color picker element to toolbar color action --- .../src/components/LexicalColorPicker.tsx | 6 +++ .../lexical-editor-pb-element/src/index.tsx | 2 +- .../src/components/AddColorPickerDropDown.tsx | 10 +++++ .../components/ColorPicker/ColorPicker.tsx | 5 --- .../ColorPickerDropdown.tsx | 34 ++++++++++++++++ .../ToolbarActions/FontColorAction.tsx | 39 +++++++------------ 6 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx create mode 100644 packages/lexical-editor/src/components/AddColorPickerDropDown.tsx delete mode 100644 packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx create mode 100644 packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx diff --git a/packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx b/packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx new file mode 100644 index 00000000000..8906cf851f5 --- /dev/null +++ b/packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx @@ -0,0 +1,6 @@ +const LexicalColorPicker = () => { +return
+} + + + diff --git a/packages/lexical-editor-pb-element/src/index.tsx b/packages/lexical-editor-pb-element/src/index.tsx index 494f2b71439..5f5634196a8 100644 --- a/packages/lexical-editor-pb-element/src/index.tsx +++ b/packages/lexical-editor-pb-element/src/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ParagraphToolbarPreset, HeadingToolbarPreset } from "@webiny/lexical-editor"; +import {ParagraphToolbarPreset, HeadingToolbarPreset, AddToolbarAction} from "@webiny/lexical-editor"; import { PeTextPlugin } from "~/plugins/PeTextPlugin"; import { HeadingPlugin } from "~/plugins/HeadingPlugin"; import { ParagraphPlugin } from "~/plugins/ParagraphPlugin"; diff --git a/packages/lexical-editor/src/components/AddColorPickerDropDown.tsx b/packages/lexical-editor/src/components/AddColorPickerDropDown.tsx new file mode 100644 index 00000000000..9070c9589c3 --- /dev/null +++ b/packages/lexical-editor/src/components/AddColorPickerDropDown.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +interface AddColorPickerDropDown { + target: "font-color-action" | string; + element: JSX.Element; +} + +export const AddColorPickerDropDown: React.FC = ({ target, element }): JSX.Element => { + return
; +}; diff --git a/packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx b/packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx deleted file mode 100644 index a4bc17fc2bc..00000000000 --- a/packages/lexical-editor/src/components/ColorPicker/ColorPicker.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export interface ColorPicker { - onChange: (color: string) => void; - color: string; - open: boolean; -} diff --git a/packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx b/packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx new file mode 100644 index 00000000000..860379b3a87 --- /dev/null +++ b/packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx @@ -0,0 +1,34 @@ +import {makeComposable} from "@webiny/react-composition"; +import {DropDown} from "~/ui/DropDown"; +import React from "react"; + + +interface ColorPickerElement { + onChange: (color: string) => void; +} + +interface ColorPickerDropdown { + onChange: (color: string) => void; + value: string; + ColorPickerElement?: React.ElementType; +} + +export const ColorPickerDropdown = makeComposable( + "FontColorPicker", + ({ value, onChange, ColorPickerElement }: ColorPickerDropdown): JSX.Element => { + return ( + +
+ {ColorPickerElement && } + {value} +
+
+ ); + } +); + + diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index de7e38cd03f..3a00217ec33 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -7,33 +7,20 @@ import { SELECTION_CHANGE_COMMAND } from "lexical"; import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection"; -import { DropDown } from "~/ui/DropDown"; -import { makeComposable } from "@webiny/react-composition"; -import { ColorPicker } from "@webiny/ui/ColorPicker"; +import {ColorPickerDropdown} from "~/components/ColorPickerDropdown/ColorPickerDropdown"; +import {makeComposable} from "@webiny/react-composition"; -interface FontColorPicker { - onChange: (value: string) => void; + +interface ColorPickerElement { + onChange: (color: string) => void; + value: string; } -export const FontColorPicker = makeComposable( - "FontColorPicker", - ({ onChange }: FontColorPicker): JSX.Element => { - return ( - -
- - Some color picker thing -
-
- ); - } -); +interface FontColorAction { + ColorPickerElement?: JSX.Element; +} -export const FontColorAction = () => { +export const FontColorAction = makeComposable("FontColorAction", (colorPickerElement) => { const [editor] = useLexicalComposerContext(); const [activeEditor, setActiveEditor] = useState(editor); const [fontColor, setFontColor] = useState("#000"); @@ -47,7 +34,7 @@ export const FontColorAction = () => { useEffect(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { - setFontColor($getSelectionStyleValueForProperty(selection, "color", fontColor)); + setFontColor($getSelectionStyleValueForProperty(selection, "color", "#000")); } }, [activeEditor]); @@ -82,5 +69,5 @@ export const FontColorAction = () => { ); }, [editor, updateToolbar]); - return ; -}; + return ; +}); From ba156f5985e30cf7df8385673ba74fb7c7cbc75e Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 1 Mar 2023 20:44:07 +0100 Subject: [PATCH 03/57] wip: implement lexical configuration interface for font color action --- apps/admin/package.json | 1 + apps/admin/src/App.tsx | 7 +++ .../src/lexicalEditor/ColorPickerDropdown.tsx | 33 ++++++++++++ .../src/components/LexicalColorPicker.tsx | 6 --- .../lexical-editor-pb-element/src/index.tsx | 5 +- packages/lexical-editor/package.json | 1 - .../src/components/AddColorPickerDropDown.tsx | 10 ---- .../ColorPickerDropdown.tsx | 34 ------------- .../LexicalEditorConfig.tsx | 12 +++++ .../ToolbarActions/FontColorAction.tsx | 50 +++++++++++++++---- packages/lexical-editor/src/index.tsx | 2 + packages/lexical-editor/src/types.ts | 1 + packages/lexical-editor/tsconfig.build.json | 5 +- packages/lexical-editor/tsconfig.json | 6 +-- yarn.lock | 2 +- 15 files changed, 104 insertions(+), 71 deletions(-) create mode 100644 apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx delete mode 100644 packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx delete mode 100644 packages/lexical-editor/src/components/AddColorPickerDropDown.tsx delete mode 100644 packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx create mode 100644 packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx diff --git a/apps/admin/package.json b/apps/admin/package.json index c23f31f6803..672e392ad6b 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -16,6 +16,7 @@ "@webiny/app-page-builder-editor": "0.0.0", "@webiny/app-serverless-cms": "0.0.0", "@webiny/cli": "0.0.0", + "@webiny/lexical-editor": "0.0.0", "@webiny/plugins": "0.0.0", "@webiny/react-properties": "0.0.0", "@webiny/serverless-cms-aws": "0.0.0", diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 0015d2d546b..f3fb82baef5 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -2,11 +2,18 @@ import React from "react"; import { Admin } from "@webiny/app-serverless-cms"; import { Cognito } from "@webiny/app-admin-users-cognito"; import "./App.scss"; +import { LexicalEditorConfig } from "@webiny/lexical-editor"; +import { FontColorPickerDropdown } from "./lexicalEditor/ColorPickerDropdown"; export const App: React.FC = () => { return ( + + + ); }; diff --git a/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx b/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx new file mode 100644 index 00000000000..b01fe9afbeb --- /dev/null +++ b/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx @@ -0,0 +1,33 @@ +import React, { useEffect } from "react"; +import { DropDown } from "@webiny/lexical-editor"; +import { FontColorPicker } from "@webiny/lexical-editor/types"; +import { makeComposable } from "@webiny/app-admin"; + +export const FontColorPickerDropdown = makeComposable( + "FontColorPickerDropdown", + ({ value, onChange }): JSX.Element => { + useEffect(() => { + console.log("FontColorPickerDropdown"); + }, []); + + return ( + +
+ {value && value} + {onChange && ( +
onChange("#0F52BA")} + > + Blue color +
+ )} +
+
+ ); + } +); diff --git a/packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx b/packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx deleted file mode 100644 index 8906cf851f5..00000000000 --- a/packages/lexical-editor-pb-element/src/components/LexicalColorPicker.tsx +++ /dev/null @@ -1,6 +0,0 @@ -const LexicalColorPicker = () => { -return
-} - - - diff --git a/packages/lexical-editor-pb-element/src/index.tsx b/packages/lexical-editor-pb-element/src/index.tsx index 5f5634196a8..278a7180e79 100644 --- a/packages/lexical-editor-pb-element/src/index.tsx +++ b/packages/lexical-editor-pb-element/src/index.tsx @@ -1,5 +1,8 @@ import React from "react"; -import {ParagraphToolbarPreset, HeadingToolbarPreset, AddToolbarAction} from "@webiny/lexical-editor"; +import { + ParagraphToolbarPreset, + HeadingToolbarPreset +} from "@webiny/lexical-editor"; import { PeTextPlugin } from "~/plugins/PeTextPlugin"; import { HeadingPlugin } from "~/plugins/HeadingPlugin"; import { ParagraphPlugin } from "~/plugins/ParagraphPlugin"; diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index f67ef6d2667..d9644fbb4fe 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -18,7 +18,6 @@ "@lexical/selection": "0.8.1", "@lexical/utils": "0.8.1", "@webiny/react-composition": "0.0.0", - "@webiny/ui": "0.0.0", "lexical": "0.8.1", "react": "^17.0.2", "react-dom": "^17.0.2" diff --git a/packages/lexical-editor/src/components/AddColorPickerDropDown.tsx b/packages/lexical-editor/src/components/AddColorPickerDropDown.tsx deleted file mode 100644 index 9070c9589c3..00000000000 --- a/packages/lexical-editor/src/components/AddColorPickerDropDown.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -interface AddColorPickerDropDown { - target: "font-color-action" | string; - element: JSX.Element; -} - -export const AddColorPickerDropDown: React.FC = ({ target, element }): JSX.Element => { - return
; -}; diff --git a/packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx b/packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx deleted file mode 100644 index 860379b3a87..00000000000 --- a/packages/lexical-editor/src/components/ColorPickerDropdown/ColorPickerDropdown.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import {makeComposable} from "@webiny/react-composition"; -import {DropDown} from "~/ui/DropDown"; -import React from "react"; - - -interface ColorPickerElement { - onChange: (color: string) => void; -} - -interface ColorPickerDropdown { - onChange: (color: string) => void; - value: string; - ColorPickerElement?: React.ElementType; -} - -export const ColorPickerDropdown = makeComposable( - "FontColorPicker", - ({ value, onChange, ColorPickerElement }: ColorPickerDropdown): JSX.Element => { - return ( - -
- {ColorPickerElement && } - {value} -
-
- ); - } -); - - diff --git a/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx b/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx new file mode 100644 index 00000000000..0699e2baf1e --- /dev/null +++ b/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; + +interface LexicalEditorConfig extends React.FC { + FontColorAction: typeof FontColorAction; +} + +export const LexicalEditorConfig: LexicalEditorConfig = ({ children }) => { + return <>{children}; +}; + +LexicalEditorConfig.FontColorAction = FontColorAction; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 3a00217ec33..1cce8474041 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -7,20 +7,45 @@ import { SELECTION_CHANGE_COMMAND } from "lexical"; import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection"; -import {ColorPickerDropdown} from "~/components/ColorPickerDropdown/ColorPickerDropdown"; -import {makeComposable} from "@webiny/react-composition"; +import { createComponentPlugin, makeComposable } from "@webiny/react-composition"; +export interface FontColorPicker { + value?: string; + onChange?: (value: string) => void; +} + +/* + * Composable Color Picker component that is mounted on toolbar action. + * Note: Toa add custom component access trough @see LexicalEditorConfig API + * */ +export const FontColorPicker = makeComposable( + "FontColorPicker", + (): JSX.Element | null => { + useEffect(() => { + console.log("Default FontColorPicker, please add your own component"); + }, []); + return null; + } +); -interface ColorPickerElement { - onChange: (color: string) => void; - value: string; +interface FontActionColorPicker { + Element: typeof FontColorPicker; } -interface FontColorAction { - ColorPickerElement?: JSX.Element; +const FontActionColorPicker: React.FC = ({ Element }): JSX.Element => { + const FontColorPickerPlugin = createComponentPlugin(FontColorPicker, () => { + return function FontColorPickerPlugin({ value, onChange }): JSX.Element { + return ; + }; + }); + return ; +}; + +export interface FontColorAction extends React.FC { + ColorPicker: typeof FontActionColorPicker; } -export const FontColorAction = makeComposable("FontColorAction", (colorPickerElement) => { +export const FontColorAction: FontColorAction = () => { const [editor] = useLexicalComposerContext(); const [activeEditor, setActiveEditor] = useState(editor); const [fontColor, setFontColor] = useState("#000"); @@ -69,5 +94,10 @@ export const FontColorAction = makeComposable("FontColorAction" ); }, [editor, updateToolbar]); - return ; -}); + return ; +}; + +{ + /* Color action settings */ +} +FontColorAction.ColorPicker = FontActionColorPicker; diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 30778eea9ed..e3f1cf6ee8f 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -43,3 +43,5 @@ export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue export { isValidLexicalData } from "~/utils/isValidLexicalData"; // types export * as types from "./types"; +// config +export { LexicalEditorConfig } from "~/components/LexicalEditorConfig/LexicalEditorConfig"; diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index bf231ddd7b2..d8472382d99 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -1,2 +1,3 @@ export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; +export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; diff --git a/packages/lexical-editor/tsconfig.build.json b/packages/lexical-editor/tsconfig.build.json index 02fe5316e8c..d0eb9513334 100644 --- a/packages/lexical-editor/tsconfig.build.json +++ b/packages/lexical-editor/tsconfig.build.json @@ -1,10 +1,7 @@ { "extends": "../../tsconfig.build.json", "include": ["src"], - "references": [ - { "path": "../react-composition/tsconfig.build.json" }, - { "path": "../ui/tsconfig.build.json" } - ], + "references": [{ "path": "../react-composition/tsconfig.build.json" }], "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/packages/lexical-editor/tsconfig.json b/packages/lexical-editor/tsconfig.json index cbd69943ac9..b8f98f630e1 100644 --- a/packages/lexical-editor/tsconfig.json +++ b/packages/lexical-editor/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "include": ["src", "__tests__/**/*.ts"], - "references": [{ "path": "../react-composition" }, { "path": "../ui" }], + "references": [{ "path": "../react-composition" }], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], "outDir": "./dist", @@ -10,9 +10,7 @@ "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], "@webiny/react-composition/*": ["../react-composition/src/*"], - "@webiny/react-composition": ["../react-composition/src"], - "@webiny/ui/*": ["../ui/src/*"], - "@webiny/ui": ["../ui/src"] + "@webiny/react-composition": ["../react-composition/src"] }, "baseUrl": "." } diff --git a/yarn.lock b/yarn.lock index 22a289a2da1..6a546789004 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15173,7 +15173,6 @@ __metadata: "@webiny/cli": ^5.33.1 "@webiny/project-utils": ^5.33.1 "@webiny/react-composition": 0.0.0 - "@webiny/ui": 0.0.0 lexical: 0.8.1 react: ^17.0.2 react-dom: ^17.0.2 @@ -16208,6 +16207,7 @@ __metadata: "@webiny/app-page-builder-editor": 0.0.0 "@webiny/app-serverless-cms": 0.0.0 "@webiny/cli": 0.0.0 + "@webiny/lexical-editor": 0.0.0 "@webiny/plugins": 0.0.0 "@webiny/react-properties": 0.0.0 "@webiny/serverless-cms-aws": 0.0.0 From bec6be244f8e5dd058bf6941cdb360592652f72b Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 1 Mar 2023 20:46:27 +0100 Subject: [PATCH 04/57] refactor: prettier fix --- packages/lexical-editor-pb-element/src/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/lexical-editor-pb-element/src/index.tsx b/packages/lexical-editor-pb-element/src/index.tsx index 278a7180e79..494f2b71439 100644 --- a/packages/lexical-editor-pb-element/src/index.tsx +++ b/packages/lexical-editor-pb-element/src/index.tsx @@ -1,8 +1,5 @@ import React from "react"; -import { - ParagraphToolbarPreset, - HeadingToolbarPreset -} from "@webiny/lexical-editor"; +import { ParagraphToolbarPreset, HeadingToolbarPreset } from "@webiny/lexical-editor"; import { PeTextPlugin } from "~/plugins/PeTextPlugin"; import { HeadingPlugin } from "~/plugins/HeadingPlugin"; import { ParagraphPlugin } from "~/plugins/ParagraphPlugin"; From dd86ba1b6fb44febfbdcc8343ac41a0629749b2b Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Thu, 2 Mar 2023 11:08:15 +0100 Subject: [PATCH 05/57] wip: brainstorming from the 1:1 --- apps/admin/src/App.tsx | 6 +- .../src/lexicalEditor/ColorPickerDropdown.tsx | 57 +++++++++---------- .../ToolbarActions/FontColorAction.tsx | 39 +++++-------- 3 files changed, 45 insertions(+), 57 deletions(-) diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index f3fb82baef5..fa0e534001b 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -5,14 +5,14 @@ import "./App.scss"; import { LexicalEditorConfig } from "@webiny/lexical-editor"; import { FontColorPickerDropdown } from "./lexicalEditor/ColorPickerDropdown"; +const { FontColorAction } = LexicalEditorConfig; + export const App: React.FC = () => { return ( - + } /> ); diff --git a/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx b/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx index b01fe9afbeb..735c90da5d6 100644 --- a/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx +++ b/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx @@ -1,33 +1,30 @@ import React, { useEffect } from "react"; -import { DropDown } from "@webiny/lexical-editor"; -import { FontColorPicker } from "@webiny/lexical-editor/types"; -import { makeComposable } from "@webiny/app-admin"; +import { DropDown, useFontColorPicker } from "@webiny/lexical-editor"; -export const FontColorPickerDropdown = makeComposable( - "FontColorPickerDropdown", - ({ value, onChange }): JSX.Element => { - useEffect(() => { - console.log("FontColorPickerDropdown"); - }, []); +export const FontColorPickerDropdown = () => { + const { value, onChange } = useFontColorPicker(); - return ( - -
- {value && value} - {onChange && ( -
onChange("#0F52BA")} - > - Blue color -
- )} -
-
- ); - } -); + useEffect(() => { + console.log("FontColorPickerDropdown"); + }, []); + + return ( + +
+ {value && value} + {onChange && ( +
onChange("#0F52BA")} + > + Blue color +
+ )} +
+
+ ); +}; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 1cce8474041..7be15bc443a 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -7,38 +7,25 @@ import { SELECTION_CHANGE_COMMAND } from "lexical"; import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection"; -import { createComponentPlugin, makeComposable } from "@webiny/react-composition"; - -export interface FontColorPicker { - value?: string; - onChange?: (value: string) => void; -} +import { Compose, makeComposable } from "@webiny/react-composition"; /* * Composable Color Picker component that is mounted on toolbar action. * Note: Toa add custom component access trough @see LexicalEditorConfig API * */ -export const FontColorPicker = makeComposable( - "FontColorPicker", - (): JSX.Element | null => { - useEffect(() => { - console.log("Default FontColorPicker, please add your own component"); - }, []); - return null; - } -); +export const FontColorPicker = makeComposable("FontColorPicker", (): JSX.Element | null => { + useEffect(() => { + console.log("Default FontColorPicker, please add your own component"); + }, []); + return null; +}); interface FontActionColorPicker { - Element: typeof FontColorPicker; + element: JSX.Element; } -const FontActionColorPicker: React.FC = ({ Element }): JSX.Element => { - const FontColorPickerPlugin = createComponentPlugin(FontColorPicker, () => { - return function FontColorPickerPlugin({ value, onChange }): JSX.Element { - return ; - }; - }); - return ; +const FontActionColorPicker: React.FC = ({ element }): JSX.Element => { + return () => element} />; }; export interface FontColorAction extends React.FC { @@ -94,7 +81,11 @@ export const FontColorAction: FontColorAction = () => { ); }, [editor, updateToolbar]); - return ; + return ( + // + + // + ); }; { From 168045879f5bf26af3303aac8d05e18857404220 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 2 Mar 2023 12:52:05 +0100 Subject: [PATCH 06/57] wip: add font color acton context --- apps/admin/src/App.tsx | 2 +- .../components/ToolbarActions/FontColorAction.tsx | 7 ++++--- .../src/context/FontColorActionContext.tsx | 10 ++++++++++ .../lexical-editor/src/hooks/useFontColorPicker.ts | 13 +++++++++++++ packages/lexical-editor/src/index.tsx | 1 + 5 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 packages/lexical-editor/src/context/FontColorActionContext.tsx create mode 100644 packages/lexical-editor/src/hooks/useFontColorPicker.ts diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index fa0e534001b..b6b521bf21a 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -12,7 +12,7 @@ export const App: React.FC = () => { - } /> + } /> ); diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 7be15bc443a..9822562fd33 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -8,6 +8,7 @@ import { } from "lexical"; import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection"; import { Compose, makeComposable } from "@webiny/react-composition"; +import { FontColorActionContext } from "~/context/FontColorActionContext"; /* * Composable Color Picker component that is mounted on toolbar action. @@ -82,9 +83,9 @@ export const FontColorAction: FontColorAction = () => { }, [editor, updateToolbar]); return ( - // - - // + + + ); }; diff --git a/packages/lexical-editor/src/context/FontColorActionContext.tsx b/packages/lexical-editor/src/context/FontColorActionContext.tsx new file mode 100644 index 00000000000..f400a3ac66a --- /dev/null +++ b/packages/lexical-editor/src/context/FontColorActionContext.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +export interface FontColorActionContext { + value: string; + onChange: (value: string) => void; +} + +export const FontColorActionContext = React.createContext( + undefined +); diff --git a/packages/lexical-editor/src/hooks/useFontColorPicker.ts b/packages/lexical-editor/src/hooks/useFontColorPicker.ts new file mode 100644 index 00000000000..5378a279ae6 --- /dev/null +++ b/packages/lexical-editor/src/hooks/useFontColorPicker.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { FontColorActionContext } from "~/context/FontColorActionContext"; + +export function useFontColorPicker() { + const context = useContext(FontColorActionContext); + if (!context) { + throw Error( + `Missing FontColorActionContext in the component hierarchy. Are you using "useFontColorPicker()" in the right place?` + ); + } + + return context; +} diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index e3f1cf6ee8f..70c5a6e6f1d 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -2,6 +2,7 @@ export { LexicalHtmlRenderer } from "~/components/LexicalHtmlRenderer"; // hooks export { useRichTextEditor } from "~/hooks/useRichTextEditor"; +export { useFontColorPicker } from "~/hooks/useFontColorPicker"; // UI elements export { Divider } from "~/ui/Divider"; export { DropDownItem } from "~/ui/DropDown"; From f419315a0ea4f47a2f0231bb544b6bf02c07fab1 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 7 Mar 2023 16:08:16 +0100 Subject: [PATCH 07/57] wip: implmenting theme into lexical font color action node --- apps/admin/package.json | 4 + apps/admin/src/App.tsx | 4 +- .../src/lexicalEditor/ColorPickerDropdown.tsx | 30 --- .../LexicalColorPicker/LexicalColorPicker.tsx | 225 ++++++++++++++++++ .../LexicalColorPicker/StyledComponents.ts | 128 ++++++++++ .../LexicalColorPicker/colorize.svg | 1 + .../round-color_lens-24px.svg | 19 ++ .../LexicalColorPicker/unselected.svg | 10 + .../LexicalColorPickerDropdown.tsx | 28 +++ apps/theme/theme.ts | 2 +- packages/app-page-builder/package.json | 2 +- .../components/ColorPicker/ColorPicker.tsx | 3 +- .../ColorPicker/StyledComponents.ts | 128 ++++++++++ packages/lexical-editor/package.json | 1 + .../src/components/Editor/RichTextEditor.tsx | 2 + .../src/components/Toolbar/Toolbar.css | 2 +- .../ToolbarActions/FontColorAction.tsx | 58 ++--- .../src/context/FontColorActionContext.tsx | 10 +- packages/lexical-editor/src/index.tsx | 1 + .../lexical-editor/src/nodes/FontColorNode.ts | 149 ++++++++++++ .../lexical-editor/src/nodes/nodesFactory.ts | 31 +++ .../lexical-editor/src/nodes/webinyNodes.ts | 17 +- .../FontColorPlugin/FontColorPlugin.tsx | 51 ++++ .../src/themes/webinyLexicalTheme.ts | 18 +- .../src/ui/ToolbarActionDialog.tsx | 116 +++++++++ yarn.lock | 11 +- 26 files changed, 949 insertions(+), 102 deletions(-) delete mode 100644 apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx create mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx create mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts create mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg create mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/round-color_lens-24px.svg create mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/unselected.svg create mode 100644 apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx create mode 100644 packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts create mode 100644 packages/lexical-editor/src/nodes/FontColorNode.ts create mode 100644 packages/lexical-editor/src/nodes/nodesFactory.ts create mode 100644 packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx create mode 100644 packages/lexical-editor/src/ui/ToolbarActionDialog.tsx diff --git a/apps/admin/package.json b/apps/admin/package.json index 672e392ad6b..90bda63ac8a 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -14,16 +14,20 @@ "@webiny/app-form-builder": "0.0.0", "@webiny/app-page-builder": "0.0.0", "@webiny/app-page-builder-editor": "0.0.0", + "@webiny/app-page-builder-elements": "0.0.0", "@webiny/app-serverless-cms": "0.0.0", + "@webiny/app-theme-manager": "0.0.0", "@webiny/cli": "0.0.0", "@webiny/lexical-editor": "0.0.0", "@webiny/plugins": "0.0.0", "@webiny/react-properties": "0.0.0", "@webiny/serverless-cms-aws": "0.0.0", + "@webiny/ui": "0.0.0", "core-js": "^3.0.1", "cross-fetch": "^3.0.4", "prop-types": "^15.7.2", "react": "17.0.2", + "react-color": "^2.19.3", "react-dom": "17.0.2", "regenerator-runtime": "^0.13.5", "theme": "^0.1.0", diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index b6b521bf21a..8e055a9103d 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -3,7 +3,7 @@ import { Admin } from "@webiny/app-serverless-cms"; import { Cognito } from "@webiny/app-admin-users-cognito"; import "./App.scss"; import { LexicalEditorConfig } from "@webiny/lexical-editor"; -import { FontColorPickerDropdown } from "./lexicalEditor/ColorPickerDropdown"; +import { LexicalColorPickerDropdown } from "./lexicalEditor/LexicalColorPickerDropdown"; const { FontColorAction } = LexicalEditorConfig; @@ -12,7 +12,7 @@ export const App: React.FC = () => { - } /> + } /> ); diff --git a/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx b/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx deleted file mode 100644 index 735c90da5d6..00000000000 --- a/apps/admin/src/lexicalEditor/ColorPickerDropdown.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useEffect } from "react"; -import { DropDown, useFontColorPicker } from "@webiny/lexical-editor"; - -export const FontColorPickerDropdown = () => { - const { value, onChange } = useFontColorPicker(); - - useEffect(() => { - console.log("FontColorPickerDropdown"); - }, []); - - return ( - -
- {value && value} - {onChange && ( -
onChange("#0F52BA")} - > - Blue color -
- )} -
-
- ); -}; diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx b/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx new file mode 100644 index 00000000000..d4adbc39d09 --- /dev/null +++ b/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx @@ -0,0 +1,225 @@ +import React, { useCallback, useState } from "react"; +import styled from "@emotion/styled"; +import { css } from "emotion"; +import { COLORS } from "./StyledComponents"; +import { usePageBuilder } from "@webiny/app-page-builder/hooks/usePageBuilder"; +import { usePageElements } from "@webiny/app-page-builder-elements/hooks/usePageElements"; +import { PbTheme } from "@webiny/app-page-builder/types"; +import { isLegacyRenderingEngine } from "@webiny/app-page-builder/utils"; +import classnames from "classnames"; +import { ChromePicker } from "react-color"; + +// Icons +import { ReactComponent as IconPalette } from "./round-color_lens-24px.svg"; + +const ColorPickerStyle = styled("div")({ + display: "flex", + flexWrap: "wrap", + justifyContent: "space-between", + width: 240, + padding: 5, + backgroundColor: "#fff" +}); + +const ColorBox = styled("div")({ + cursor: "pointer", + width: 50, + height: 40, + margin: 10, + borderRadius: 2, + boxSizing: "border-box", + transition: "transform 0.2s", + color: "var(--mdc-theme-text-secondary-on-background)" +}); + +const Color = styled("button")({ + cursor: "pointer", + width: 40, + height: 30, + border: "1px solid var(--mdc-theme-on-background)", + transition: "transform 0.2s, scale 0.2s", + display: "flex", + alignItems: "center", + "&::after": { + boxShadow: "0 0.25rem 0.125rem 0 rgba(0,0,0,0.05)", + transition: "opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1)", + content: '""', + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + zIndex: -1, + opacity: 0 + }, + "&:hover": { + transform: "scale(1.25)", + "&::after": { + opacity: 1 + } + } +}); + +const transparent = css({ + backgroundSize: "10px 10px", + backgroundImage: + "linear-gradient( 45deg, #555 25%, transparent 25%, transparent)," + + "linear-gradient(-45deg, #555 25%, transparent 25%, transparent)," + + "linear-gradient( 45deg, transparent 75%, #555 75%)," + + "linear-gradient(-45deg, transparent 75%, #555 75%)" +}); + +const iconPaletteStyle = css({ + height: 20, + width: "100%", + marginTop: 1, + color: "var(--mdc-theme-secondary)" +}); + +const styles = { + selectedColor: css({ + boxShadow: "0px 0px 0px 2px var(--mdc-theme-secondary)" + }), + button: css({ + cursor: "pointer", + height: 30, + boxSizing: "border-box", + "&:hover:not(:disabled)": { backgroundColor: COLORS.gray }, + "&:focus:not(:disabled)": { + outline: "none" + }, + "&:disabled": { + opacity: 0.5, + cursor: "not-allowed" + }, + "& svg": { + width: 16, + height: 16 + } + }), + color: css({ + width: "40px", + height: "100%" + }) +}; + +interface LexicalColorPickerProps { + value: string; + onChange: Function; + onChangeComplete: Function; + handlerClassName?: string; +} + +export const LexicalColorPicker: React.FC = ({ + value, + onChange, + onChangeComplete +}) => { + const [showPicker, setShowPicker] = useState(false); + // Either a custom color or a color coming from the theme object. + const [actualSelectedColor, setActualSelectedColor] = useState(value || "#fff"); + let themeColor = false; + + const getColorValue = useCallback(rgb => { + return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`; + }, []); + + const onColorChange = useCallback( + (color, event) => { + setActualSelectedColor(value); + onChange(getColorValue(color.rgb)); + event.preventDefault(); + }, + [onChange] + ); + + const onColorChangeComplete = useCallback( + ({ rgb }, event) => { + setActualSelectedColor(value); + onChangeComplete(getColorValue(rgb)); + event.preventDefault(); + }, + [onChangeComplete] + ); + + const togglePicker = useCallback(e => { + debugger; + e.stopPropagation(); + setShowPicker(!showPicker); + }, []); + + const { theme } = usePageBuilder(); + + const pageElements = usePageElements(); + + let themeColors: Record = {}; + + if (isLegacyRenderingEngine) { + const legacyTheme = theme as PbTheme; + themeColors = legacyTheme?.colors || {}; + } else { + const colors = pageElements.theme?.styles?.colors; + if (colors) { + for (const key in colors) { + if (colors[key]) { + themeColors[key] = colors[key]; + } + } + } + } + + return ( + + {Object.keys(themeColors).map((key, index) => { + const color = themeColors[key]; + + if (color === value || value === "transparent") { + themeColor = true; + } + return ( + + { + // With page elements implementation, we want to store the color key and + // then the actual color will be retrieved from the theme object. + let value = color; + if (!isLegacyRenderingEngine) { + value = key; + } + + onChangeComplete(value); + }} + /> + + ); + })} + + + { + onChangeComplete("transparent"); + }} + /> + + + + + + + + + {showPicker && ( + + )} + + ); +}; diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts b/apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts new file mode 100644 index 00000000000..c330dbee19f --- /dev/null +++ b/apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts @@ -0,0 +1,128 @@ +import styled from "@emotion/styled"; +import { css } from "emotion"; + +export const classes = { + simpleGrid: css({ + "&.mdc-layout-grid": { + padding: 0, + margin: "0px 0px 16px" + } + }), + grid: css({ + "& .mdc-layout-grid": { + padding: 0, + margin: "0px 0px 16px" + } + }) +}; + +export const Footer = styled("div")({ + backgroundColor: "var(--mdc-theme-background)", + paddingBottom: 10, + margin: "0 -15px -15px -15px", + ".mdc-layout-grid": { + padding: "15px 10px 10px 15px", + ".mdc-layout-grid__cell.mdc-layout-grid__cell--span-4": { + paddingRight: 10 + } + } +}); + +interface InputContainerProps { + width?: number | string; + margin?: number | string; +} + +export const InputContainer = styled<"div", InputContainerProps>("div")(props => ({ + "> .mdc-text-field.mdc-text-field--upgraded": { + height: "30px !important", + width: props.width || 50, + margin: props.hasOwnProperty("margin") ? props.margin : "0 0 0 18px", + ".mdc-text-field__input": { + paddingTop: 16 + } + } +})); + +type ContentWrapperProps = { + direction?: "row" | "row-reverse" | "column" | "column-reverse"; +}; + +export const ContentWrapper = styled<"div", ContentWrapperProps>("div")(props => ({ + display: "flex", + flexDirection: props.direction || "row" +})); + +export const COLORS = { + lightGray: "hsla(0, 0%, 97%, 1)", + gray: "hsla(300, 2%, 92%, 1)", + darkGray: "hsla(0, 0%, 70%, 1)", + darkestGray: "hsla(0, 0%, 20%, 1)", + black: "hsla(208, 100%, 5%, 1)" +}; + +export const TopLeft = styled("div")({ + gridArea: "topLeft" +}); +export const Top = styled("div")({ + gridArea: "top" +}); +export const TopRight = styled("div")({ + gridArea: "topRight" +}); +export const Left = styled("div")({ + gridArea: "left" +}); +export const Center = styled("div")({ + gridArea: "center", + backgroundColor: "rgb(204,229,255)", + border: "1px dashed rgb(0,64,133)" +}); +export const Right = styled("div")({ + gridArea: "right" +}); +export const BottomLeft = styled("div")({ + gridArea: "bottomLeft" +}); +export const Bottom = styled("div")({ + gridArea: "bottom" +}); +export const BottomRight = styled("div")({ + gridArea: "bottomRight" +}); +export const SpacingGrid = styled("div")({ + display: "grid", + gridTemplateColumns: "1fr 2fr 1fr", + gridTemplateRows: "1fr 1fr 1fr", + gap: "0px 0px", + gridTemplateAreas: + '"topLeft top topRight"' + '"left center right"' + '"bottomLeft bottom bottomRight"', + border: "1px dashed rgb(21,87,36)", + backgroundColor: COLORS.lightGray, + + "& .text": { + fontSize: 11, + padding: "4px 8px" + }, + "& .mono": { + fontFamily: "monospace" + }, + "& .align-center": { + display: "flex", + justifyContent: "center" + } +}); +export const SimpleButton = styled("button")({ + boxSizing: "border-box", + border: "1px solid var(--mdc-theme-on-background)", + borderRadius: 1, + backgroundColor: "transparent", + padding: "8px 16px", + cursor: "pointer" +}); +export const ButtonContainer = styled("div")({ + marginTop: 16 +}); +export const justifySelfEndStyle = css({ + justifySelf: "end" +}); diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg b/apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg new file mode 100644 index 00000000000..0732ed51c37 --- /dev/null +++ b/apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/round-color_lens-24px.svg b/apps/admin/src/lexicalEditor/LexicalColorPicker/round-color_lens-24px.svg new file mode 100644 index 00000000000..a233bc4007b --- /dev/null +++ b/apps/admin/src/lexicalEditor/LexicalColorPicker/round-color_lens-24px.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/unselected.svg b/apps/admin/src/lexicalEditor/LexicalColorPicker/unselected.svg new file mode 100644 index 00000000000..3361e960b44 --- /dev/null +++ b/apps/admin/src/lexicalEditor/LexicalColorPicker/unselected.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx b/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx new file mode 100644 index 00000000000..90868264c0e --- /dev/null +++ b/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { ToolbarActionDialog, useFontColorPicker } from "@webiny/lexical-editor"; +import { LexicalColorPicker } from "./LexicalColorPicker/LexicalColorPicker"; + +export const LexicalColorPickerDropdown = () => { + const { value, applyColor } = useFontColorPicker(); + + return ( + +
+ +
+
+ ); +}; diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index f7e57671226..023c60477e4 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -11,7 +11,7 @@ export const breakpoints = { // Colors. export const colors = { - color1: "#fa5723", // Primary. + color1: "#c45560", //#fa5723", // Primary. color2: "#00ccb0", // Secondary. color3: "#0a0a0a", // Text primary. color4: "#616161", // Text secondary. diff --git a/packages/app-page-builder/package.json b/packages/app-page-builder/package.json index 3e52fdaa97f..68c512ac5a4 100644 --- a/packages/app-page-builder/package.json +++ b/packages/app-page-builder/package.json @@ -63,7 +63,7 @@ "platform": "^1.3.5", "prop-types": "^15.7.2", "react": "17.0.2", - "react-color": "^2.14.1", + "react-color": "^2.19.3", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "17.0.2", diff --git a/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx b/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx index 31e5c75c02c..f1c06902ae6 100644 --- a/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx +++ b/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx @@ -14,8 +14,9 @@ import { ReactComponent as IconPalette } from "../../assets/icons/round-color_le import { ReactComponent as ColorizeIcon } from "./colorize.svg"; import { ReactComponent as NoColorSelectedIcon } from "./unselected.svg"; import { COLORS } from "../../plugins/elementSettings/components/StyledComponents"; -import { isLegacyRenderingEngine } from "~/utils"; + import { PbTheme } from "~/types"; +import { isLegacyRenderingEngine } from "~/utils"; const ColorPickerStyle = styled("div")({ display: "flex", diff --git a/packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts b/packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts new file mode 100644 index 00000000000..c330dbee19f --- /dev/null +++ b/packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts @@ -0,0 +1,128 @@ +import styled from "@emotion/styled"; +import { css } from "emotion"; + +export const classes = { + simpleGrid: css({ + "&.mdc-layout-grid": { + padding: 0, + margin: "0px 0px 16px" + } + }), + grid: css({ + "& .mdc-layout-grid": { + padding: 0, + margin: "0px 0px 16px" + } + }) +}; + +export const Footer = styled("div")({ + backgroundColor: "var(--mdc-theme-background)", + paddingBottom: 10, + margin: "0 -15px -15px -15px", + ".mdc-layout-grid": { + padding: "15px 10px 10px 15px", + ".mdc-layout-grid__cell.mdc-layout-grid__cell--span-4": { + paddingRight: 10 + } + } +}); + +interface InputContainerProps { + width?: number | string; + margin?: number | string; +} + +export const InputContainer = styled<"div", InputContainerProps>("div")(props => ({ + "> .mdc-text-field.mdc-text-field--upgraded": { + height: "30px !important", + width: props.width || 50, + margin: props.hasOwnProperty("margin") ? props.margin : "0 0 0 18px", + ".mdc-text-field__input": { + paddingTop: 16 + } + } +})); + +type ContentWrapperProps = { + direction?: "row" | "row-reverse" | "column" | "column-reverse"; +}; + +export const ContentWrapper = styled<"div", ContentWrapperProps>("div")(props => ({ + display: "flex", + flexDirection: props.direction || "row" +})); + +export const COLORS = { + lightGray: "hsla(0, 0%, 97%, 1)", + gray: "hsla(300, 2%, 92%, 1)", + darkGray: "hsla(0, 0%, 70%, 1)", + darkestGray: "hsla(0, 0%, 20%, 1)", + black: "hsla(208, 100%, 5%, 1)" +}; + +export const TopLeft = styled("div")({ + gridArea: "topLeft" +}); +export const Top = styled("div")({ + gridArea: "top" +}); +export const TopRight = styled("div")({ + gridArea: "topRight" +}); +export const Left = styled("div")({ + gridArea: "left" +}); +export const Center = styled("div")({ + gridArea: "center", + backgroundColor: "rgb(204,229,255)", + border: "1px dashed rgb(0,64,133)" +}); +export const Right = styled("div")({ + gridArea: "right" +}); +export const BottomLeft = styled("div")({ + gridArea: "bottomLeft" +}); +export const Bottom = styled("div")({ + gridArea: "bottom" +}); +export const BottomRight = styled("div")({ + gridArea: "bottomRight" +}); +export const SpacingGrid = styled("div")({ + display: "grid", + gridTemplateColumns: "1fr 2fr 1fr", + gridTemplateRows: "1fr 1fr 1fr", + gap: "0px 0px", + gridTemplateAreas: + '"topLeft top topRight"' + '"left center right"' + '"bottomLeft bottom bottomRight"', + border: "1px dashed rgb(21,87,36)", + backgroundColor: COLORS.lightGray, + + "& .text": { + fontSize: 11, + padding: "4px 8px" + }, + "& .mono": { + fontFamily: "monospace" + }, + "& .align-center": { + display: "flex", + justifyContent: "center" + } +}); +export const SimpleButton = styled("button")({ + boxSizing: "border-box", + border: "1px solid var(--mdc-theme-on-background)", + borderRadius: 1, + backgroundColor: "transparent", + padding: "8px 16px", + cursor: "pointer" +}); +export const ButtonContainer = styled("div")({ + marginTop: 16 +}); +export const justifySelfEndStyle = css({ + justifySelf: "end" +}); diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index d9644fbb4fe..0ed3906e19f 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -17,6 +17,7 @@ "@lexical/rich-text": "0.8.1", "@lexical/selection": "0.8.1", "@lexical/utils": "0.8.1", + "@webiny/app-page-builder-elements": "0.0.0", "@webiny/react-composition": "0.0.0", "lexical": "0.8.1", "react": "^17.0.2", diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index a2827f9e516..a77a095c512 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -18,6 +18,7 @@ import { RichTextEditorProvider } from "~/context/RichTextEditorContext"; import { isValidLexicalData } from "~/utils/isValidLexicalData"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; +import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; export interface RichTextEditorProps { toolbar?: React.ReactNode; @@ -91,6 +92,7 @@ const BaseRichTextEditor: React.FC = ({ {value && } + {/* Events */} {onBlur && } {focus && } diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.css b/packages/lexical-editor/src/components/Toolbar/Toolbar.css index 2aeae709981..585933abe0c 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.css +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.css @@ -194,7 +194,7 @@ i.chevron-down { background-color: rgb(223, 232, 250); } -i.font-color { +i.font-color, .icon.font-color { background-image: url("../../images/icons/font-color.svg"); } diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 9822562fd33..d23dfb1b4db 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -1,14 +1,10 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { - $getSelection, - $isRangeSelection, - COMMAND_PRIORITY_CRITICAL, - SELECTION_CHANGE_COMMAND -} from "lexical"; -import { $getSelectionStyleValueForProperty, $patchStyleText } from "@lexical/selection"; +import { $getSelection, $isRangeSelection, LexicalCommand } from "lexical"; +import { $getSelectionStyleValueForProperty } from "@lexical/selection"; import { Compose, makeComposable } from "@webiny/react-composition"; import { FontColorActionContext } from "~/context/FontColorActionContext"; +import { ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; /* * Composable Color Picker component that is mounted on toolbar action. @@ -38,12 +34,6 @@ export const FontColorAction: FontColorAction = () => { const [activeEditor, setActiveEditor] = useState(editor); const [fontColor, setFontColor] = useState("#000"); - const updateToolbar = useCallback(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - } - }, [activeEditor]); - useEffect(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { @@ -51,39 +41,19 @@ export const FontColorAction: FontColorAction = () => { } }, [activeEditor]); - const applyStyleText = useCallback( - (styles: Record) => { - activeEditor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - $patchStyleText(selection, styles); - } - }); - }, - [activeEditor] - ); - - const onFontColorSelect = useCallback( - (value: string) => { - applyStyleText({ color: value }); - }, - [applyStyleText] - ); - - useEffect(() => { - return editor.registerCommand( - SELECTION_CHANGE_COMMAND, - (_payload, newEditor) => { - updateToolbar(); - setActiveEditor(newEditor); - return false; - }, - COMMAND_PRIORITY_CRITICAL - ); - }, [editor, updateToolbar]); + const onFontColorSelect = useCallback((value: string) => { + editor.dispatchCommand>(ADD_FONT_COLOR_COMMAND, { + themeColor: value + }); + }, []); return ( - + ); diff --git a/packages/lexical-editor/src/context/FontColorActionContext.tsx b/packages/lexical-editor/src/context/FontColorActionContext.tsx index f400a3ac66a..b37555d3be1 100644 --- a/packages/lexical-editor/src/context/FontColorActionContext.tsx +++ b/packages/lexical-editor/src/context/FontColorActionContext.tsx @@ -1,8 +1,16 @@ import React from "react"; export interface FontColorActionContext { + /* + * @desc Current selected color value + * */ value: string; - onChange: (value: string) => void; + + /* + * @desc Apply color to selected text. + * @params: value + */ + applyColor: (value: string) => void; } export const FontColorActionContext = React.createContext( diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 70c5a6e6f1d..273fa2f2f29 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -7,6 +7,7 @@ export { useFontColorPicker } from "~/hooks/useFontColorPicker"; export { Divider } from "~/ui/Divider"; export { DropDownItem } from "~/ui/DropDown"; export { DropDown } from "~/ui/DropDown"; +export { ToolbarActionDialog } from "~/ui/ToolbarActionDialog"; // actions export { BoldAction } from "~/components/ToolbarActions/BoldAction"; export { BulletListAction } from "~/components/ToolbarActions/BulletListAction"; diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts new file mode 100644 index 00000000000..bf1776d051d --- /dev/null +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -0,0 +1,149 @@ +import { + createCommand, + DOMConversionMap, + DOMConversionOutput, + EditorConfig, + LexicalCommand, + LexicalEditor, + LexicalNode, + NodeKey, + SerializedTextNode, + Spread, + TextNode +} from "lexical"; + +export const ADD_FONT_COLOR_COMMAND: LexicalCommand = + createCommand("ADD_FONT_COLOR_COMMAND"); +export const FontColorNodeType = "font-color-node"; +const FontColorNodeAttribute = "font-color-theme"; + +export interface FontColorPayload { + themeColor: string; + color?: string; + caption?: LexicalEditor; + key?: NodeKey; +} + +export type SerializedFontColorNode = Spread< + { + themeColor: string; + color: string; + type: "font-color-node"; + version: 1; + }, + SerializedTextNode +>; + +function convertFontColorElement(domNode: HTMLElement): DOMConversionOutput | null { + const textContent = domNode.textContent; + //TODO: apply theme + const themeColor = domNode.attributes.getNamedItem(FontColorNodeAttribute)?.value || "#000"; + const color = domNode.style.color || "#000"; + if (textContent !== null) { + const node = $createFontColorNode(textContent, color, themeColor); + return { + node + }; + } + + return null; +} + +/** + * Main responsibility of this node is to apply custom or Webiny theme color to selected text. + * Extends the original TextNode node to add additional transformation and support for webiny theme font color. + */ +export class FontColorTextNode extends TextNode { + /** + * @description Webiny theme color property name. Example: color1, color2... + * If not specified, specified color will be applied or will remain undefined. + */ + __themeColor: string; + + /** + * @description Color to be applied on text. + * If theme color is specified that as priority will be applied to this variable or #000 will be set. + */ + __color: string; + + constructor(text: string, color: string, themeColor: string, key?: NodeKey) { + super(text, key); + this.__themeColor = themeColor; + this.__color = color; + } + + static override getType(): string { + return FontColorNodeType; + } + + static override clone(node: FontColorTextNode): FontColorTextNode { + return new FontColorTextNode(node.__text, node.__themeColor, node.__key); + } + + static override importJSON(serializedNode: SerializedFontColorNode): FontColorTextNode { + const node = $createFontColorNode( + serializedNode.text, + serializedNode.color, + serializedNode.themeColor + ); + node.setTextContent(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + override exportJSON(): SerializedFontColorNode { + return { + ...super.exportJSON(), + themeColor: this.__themeColor, + color: this.__color, + type: "font-color-node", + version: 1 + }; + } + + override updateDOM( + prevNode: FontColorTextNode, + dom: HTMLElement, + config: EditorConfig + ): boolean { + const isUpdated = super.updateDOM(prevNode, dom, config); + dom.setAttribute("theme-font-color", this.__themeColor); + dom.style.color = this.__color; + return isUpdated; + } + + override createDOM(config: EditorConfig): HTMLElement { + const element = super.createDOM(config); + element.setAttribute("theme-font-color", this.__themeColor); + element.style.color = this.__color; + return element; + } + + override canInsertTextBefore(): boolean { + return false; + } + + override canInsertTextAfter(): boolean { + return false; + } + + override isTextEntity(): true { + return true; + } +} + +export function $createFontColorNode( + text: string, + color: string, + themeColor: string, + key?: NodeKey +): FontColorTextNode { + return new FontColorTextNode(text, color, themeColor, key); +} + +export function $isFontColorTextNode(node: LexicalNode | null | undefined | undefined): boolean { + return node instanceof FontColorTextNode; +} diff --git a/packages/lexical-editor/src/nodes/nodesFactory.ts b/packages/lexical-editor/src/nodes/nodesFactory.ts new file mode 100644 index 00000000000..55489d54537 --- /dev/null +++ b/packages/lexical-editor/src/nodes/nodesFactory.ts @@ -0,0 +1,31 @@ +import { Klass, LexicalNode } from "lexical"; +import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { ListItemNode, ListNode } from "@lexical/list"; +import { CodeHighlightNode, CodeNode } from "@lexical/code"; +import { HashtagNode } from "@lexical/hashtag"; +import { AutoLinkNode, LinkNode } from "@lexical/link"; +import { OverflowNode } from "@lexical/overflow"; +import { MarkNode } from "@lexical/mark"; +import { FontColorTextNode } from "~/nodes/FontColorNode"; + +/* + * @description create nodes + * */ +const nodesFactory = (theme: unknown): Array> => { + return [ + HeadingNode, + ListNode, + ListItemNode, + QuoteNode, + CodeNode, + HashtagNode, + CodeHighlightNode, + AutoLinkNode, + LinkNode, + OverflowNode, + MarkNode, + FontColorTextNode + ]; +}; + +export const getNodeInstanceWithTheme = (node: LexicalNode, theme: unknown) => {}; diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/webinyNodes.ts index 4e252bfdb2d..ae06e0cb046 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/webinyNodes.ts @@ -7,8 +7,20 @@ import { ListItemNode, ListNode } from "@lexical/list"; import { MarkNode } from "@lexical/mark"; import { OverflowNode } from "@lexical/overflow"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { FontColorTextNode } from "~/nodes/FontColorNode"; -export const WebinyNodes: Array> = [ +export const WebinyNodes: + | Array> + | { + replace: Klass; + with: < + T extends { + new (...args: any): any; + } + >( + node: InstanceType + ) => LexicalNode; + } = [ HeadingNode, ListNode, ListItemNode, @@ -19,5 +31,6 @@ export const WebinyNodes: Array> = [ AutoLinkNode, LinkNode, OverflowNode, - MarkNode + MarkNode, + FontColorTextNode ]; diff --git a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx new file mode 100644 index 00000000000..276d0aa5e9e --- /dev/null +++ b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { usePageElements } from "@webiny/app-page-builder-elements"; +import { + $createFontColorNode, + ADD_FONT_COLOR_COMMAND, + FontColorPayload +} from "~/nodes/FontColorNode"; +import { + $createParagraphNode, + $getSelection, + $insertNodes, + $isRangeSelection, + $isRootOrShadowRoot, + COMMAND_PRIORITY_EDITOR +} from "lexical"; +import { $wrapNodeInElement } from "@lexical/utils"; + +export const FontColorPlugin: React.FC = () => { + const [editor] = useLexicalComposerContext(); + const pageElements = usePageElements(); + + useEffect(() => { + return editor.registerCommand( + ADD_FONT_COLOR_COMMAND, + payload => { + editor.update(() => { + const { themeColor } = payload; + const selection = editor.getEditorState().read($getSelection); + if ($isRangeSelection(selection) && !selection.isCollapsed()) { + const colors = pageElements.theme?.styles?.colors; + const applyColor = colors[themeColor] ?? themeColor; + const fontColorNode = $createFontColorNode( + selection.getTextContent(), + applyColor, + themeColor + ); + $insertNodes([fontColorNode]); + if ($isRootOrShadowRoot(fontColorNode.getParentOrThrow())) { + $wrapNodeInElement(fontColorNode, $createParagraphNode).selectEnd(); + } + } + }); + return true; + }, + COMMAND_PRIORITY_EDITOR + ); + }, [editor]); + + return null; +}; diff --git a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts index 9602ac92c92..a4990b84dfa 100644 --- a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts +++ b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts @@ -58,7 +58,6 @@ export const theme: EditorThemeClasses = { h5: "WebinyLexical__h5", h6: "WebinyLexical__h6" }, - image: "editor-image", link: "WebinyLexical__link", list: { listitem: "WebinyLexical__listItem", @@ -82,20 +81,6 @@ export const theme: EditorThemeClasses = { paragraph: "WebinyLexical__paragraph", quote: "WebinyLexical__quote", rtl: "WebinyLexical__rtl", - table: "WebinyLexical__table", - tableAddColumns: "WebinyLexical__tableAddColumns", - tableAddRows: "WebinyLexical__tableAddRows", - tableCell: "WebinyLexical__tableCell", - tableCellActionButton: "WebinyLexical__tableCellActionButton", - tableCellActionButtonContainer: "WebinyLexical__tableCellActionButtonContainer", - tableCellEditing: "WebinyLexical__tableCellEditing", - tableCellHeader: "WebinyLexical__tableCellHeader", - tableCellPrimarySelected: "WebinyLexical__tableCellPrimarySelected", - tableCellResizer: "WebinyLexical__tableCellResizer", - tableCellSelected: "WebinyLexical__tableCellSelected", - tableCellSortedIndicator: "WebinyLexical__tableCellSortedIndicator", - tableResizeRuler: "WebinyLexical__tableCellResizeRuler", - tableSelected: "WebinyLexical__tableSelected", text: { bold: "WebinyLexical__textBold", code: "WebinyLexical__textCode", @@ -105,5 +90,6 @@ export const theme: EditorThemeClasses = { superscript: "WebinyLexical__textSuperscript", underline: "WebinyLexical__textUnderline", underlineStrikethrough: "WebinyLexical__textUnderlineStrikethrough" - } + }, + fontColorText: "WebinyLexical__fontColorText" }; diff --git a/packages/lexical-editor/src/ui/ToolbarActionDialog.tsx b/packages/lexical-editor/src/ui/ToolbarActionDialog.tsx new file mode 100644 index 00000000000..35bd9d79ced --- /dev/null +++ b/packages/lexical-editor/src/ui/ToolbarActionDialog.tsx @@ -0,0 +1,116 @@ +import { useEffect, useRef, useState } from "react"; +import * as React from "react"; + +function MenuContainer({ + children, + menuContainerRef, + onClose +}: { + children: React.ReactNode | React.ReactNode[]; + menuContainerRef?: React.Ref; + onClose: () => void; +}) { + const handleKeyDown = (event: React.KeyboardEvent) => { + const key = event.key; + + if (["Escape", "ArrowUp", "ArrowDown", "Tab"].includes(key)) { + event.preventDefault(); + } + + if (key === "Escape" || key === "Tab") { + onClose(); + } + }; + + const handleContainerClick = (e: React.MouseEvent) => { + e.preventDefault(); + console.log("click", e); + }; + + return ( +
+
handleContainerClick(e)} + style={{ + position: "absolute", + top: -10, + left: 0, + width: 240, + backgroundColor: "#fff" + }} + ref={menuContainerRef ?? null} + onKeyDown={handleKeyDown} + > + {children} +
+
+ ); +} +interface ToolbarActionDialogProps { + disabled: boolean; + buttonLabel?: string; + buttonAriaLabel: string; + buttonClassName: string; + buttonIconClassName: string; + children: React.ReactNode | React.ReactNode[]; + stopCloseOnClickSelf?: boolean; +} + +export const ToolbarActionDialog: React.FC = ({ + disabled, + buttonAriaLabel, + buttonClassName, + buttonIconClassName, + buttonLabel, + children, + stopCloseOnClickSelf +}): JSX.Element => { + const menuWindowRef = useRef(null); + const [showDropDown, setShowDropDown] = useState(false); + + const handleClose = () => { + debugger; + if (menuWindowRef && menuWindowRef.current) { + setShowDropDown(false); + menuWindowRef.current.focus(); + } + }; + + useEffect(() => { + if (!showDropDown) { + return; + } + + const handle = (event: MouseEvent) => { + /* const target = event.target; + if (!button.contains(target as Node)) { + setShowDropDown(false); + }*/ + console.log("handle", event); + }; + document.addEventListener("click", handle); + + return () => { + document.removeEventListener("click", handle); + }; + }, [showDropDown, stopCloseOnClickSelf]); + + return ( +
+ + {showDropDown && {children}} +
+ ); +}; diff --git a/yarn.lock b/yarn.lock index 6a546789004..edf32db203a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13895,7 +13895,7 @@ __metadata: platform: ^1.3.5 prop-types: ^15.7.2 react: 17.0.2 - react-color: ^2.14.1 + react-color: ^2.19.3 react-dnd: ^11.1.3 react-dnd-html5-backend: ^11.1.3 react-dom: 17.0.2 @@ -14121,7 +14121,7 @@ __metadata: languageName: unknown linkType: soft -"@webiny/app-theme-manager@workspace:packages/app-theme-manager": +"@webiny/app-theme-manager@0.0.0, @webiny/app-theme-manager@workspace:packages/app-theme-manager": version: 0.0.0-use.local resolution: "@webiny/app-theme-manager@workspace:packages/app-theme-manager" dependencies: @@ -15170,6 +15170,7 @@ __metadata: "@lexical/rich-text": 0.8.1 "@lexical/selection": 0.8.1 "@lexical/utils": 0.8.1 + "@webiny/app-page-builder-elements": 0.0.0 "@webiny/cli": ^5.33.1 "@webiny/project-utils": ^5.33.1 "@webiny/react-composition": 0.0.0 @@ -16205,17 +16206,21 @@ __metadata: "@webiny/app-form-builder": 0.0.0 "@webiny/app-page-builder": 0.0.0 "@webiny/app-page-builder-editor": 0.0.0 + "@webiny/app-page-builder-elements": 0.0.0 "@webiny/app-serverless-cms": 0.0.0 + "@webiny/app-theme-manager": 0.0.0 "@webiny/cli": 0.0.0 "@webiny/lexical-editor": 0.0.0 "@webiny/plugins": 0.0.0 "@webiny/react-properties": 0.0.0 "@webiny/serverless-cms-aws": 0.0.0 + "@webiny/ui": 0.0.0 core-js: ^3.0.1 cross-env: ^5.0.2 cross-fetch: ^3.0.4 prop-types: ^15.7.2 react: 17.0.2 + react-color: ^2.19.3 react-dom: 17.0.2 regenerator-runtime: ^0.13.5 theme: ^0.1.0 @@ -36142,7 +36147,7 @@ __metadata: languageName: node linkType: hard -"react-color@npm:^2.14.1, react-color@npm:^2.17.0": +"react-color@npm:^2.14.1, react-color@npm:^2.17.0, react-color@npm:^2.19.3": version: 2.19.3 resolution: "react-color@npm:2.19.3" dependencies: From 4f29319534c86c17eefb13630b61cfbb9b2756e5 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 9 Mar 2023 15:21:05 +0100 Subject: [PATCH 08/57] wip: get lexical conetnt text and lexical color node update --- packages/lexical-editor/package.json | 1 + .../src/components/Editor/RichTextEditor.tsx | 7 +- .../src/components/LexicalHtmlRenderer.tsx | 5 +- .../ToolbarActions/FontColorAction.tsx | 2 +- .../lexical-editor/src/nodes/FontColorNode.ts | 82 +++++++++++-------- .../lexical-editor/src/nodes/nodesFactory.ts | 11 +-- .../lexical-editor/src/nodes/webinyNodes.ts | 17 +--- .../FontColorPlugin/FontColorPlugin.tsx | 37 +++++++-- .../src/utils/getLexicalContentText.ts | 30 +++++++ yarn.lock | 10 +++ 10 files changed, 137 insertions(+), 65 deletions(-) create mode 100644 packages/lexical-editor/src/utils/getLexicalContentText.ts diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index 0ed3906e19f..660b7f2b508 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -9,6 +9,7 @@ "dependencies": { "@lexical/code": "0.8.1", "@lexical/hashtag": "0.8.1", + "@lexical/headless": "^0.8.1", "@lexical/link": "0.8.1", "@lexical/list": "0.8.1", "@lexical/mark": "0.8.1", diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index a77a095c512..ab51fba5de0 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -2,8 +2,6 @@ import React, { useRef, useState } from "react"; import { LexicalValue } from "~/types"; import { Placeholder } from "~/ui/Placeholder"; import { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; -import { WebinyNodes } from "~/nodes/webinyNodes"; -import { theme } from "~/themes/webinyLexicalTheme"; import { EditorState } from "lexical/LexicalEditorState"; import { Klass, LexicalEditor, LexicalNode } from "lexical"; import { LexicalComposer } from "@lexical/react/LexicalComposer"; @@ -19,6 +17,8 @@ import { isValidLexicalData } from "~/utils/isValidLexicalData"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; +import { usePageElements } from "@webiny/app-page-builder-elements"; +import { nodesFactory } from "~/nodes/nodesFactory"; export interface RichTextEditorProps { toolbar?: React.ReactNode; @@ -54,6 +54,7 @@ const BaseRichTextEditor: React.FC = ({ const [floatingAnchorElem, setFloatingAnchorElem] = useState( undefined ); + const { theme } = usePageElements(); const onRef = (_floatingAnchorElem: HTMLDivElement) => { if (_floatingAnchorElem !== null) { @@ -72,7 +73,7 @@ const BaseRichTextEditor: React.FC = ({ onError: (error: Error) => { throw error; }, - nodes: [...WebinyNodes, ...(nodes || [])], + nodes: [...nodesFactory(theme.styles), ...(nodes || [])], theme: theme }; diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 6e4b4895e29..b7267b230c8 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -10,6 +10,8 @@ import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; +import { nodesFactory } from "~/nodes/nodesFactory"; +import { usePageElements } from "@webiny/app-page-builder-elements"; interface LexicalHtmlRendererProps { nodes?: Klass[]; @@ -17,6 +19,7 @@ interface LexicalHtmlRendererProps { } export const LexicalHtmlRenderer: React.FC = ({ nodes, value }) => { + const { theme } = usePageElements(); const initialConfig = { editorState: isValidLexicalData(value) ? value : generateInitialLexicalValue(), namespace: "webiny", @@ -24,7 +27,7 @@ export const LexicalHtmlRenderer: React.FC = ({ nodes, throw error; }, editable: false, - nodes: [...WebinyNodes, ...(nodes || [])], + nodes: [...nodesFactory(theme.styles), ...(nodes || [])], theme: theme }; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index d23dfb1b4db..448c9e9dcd0 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -43,7 +43,7 @@ export const FontColorAction: FontColorAction = () => { const onFontColorSelect = useCallback((value: string) => { editor.dispatchCommand>(ADD_FONT_COLOR_COMMAND, { - themeColor: value + color: value }); }, []); diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index bf1776d051d..41343104e8f 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -1,25 +1,21 @@ import { createCommand, - DOMConversionMap, - DOMConversionOutput, EditorConfig, LexicalCommand, LexicalEditor, - LexicalNode, NodeKey, SerializedTextNode, Spread, TextNode } from "lexical"; +import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; export const ADD_FONT_COLOR_COMMAND: LexicalCommand = createCommand("ADD_FONT_COLOR_COMMAND"); -export const FontColorNodeType = "font-color-node"; -const FontColorNodeAttribute = "font-color-theme"; +const FontColorNodeAttrName = "font-color-theme"; export interface FontColorPayload { - themeColor: string; - color?: string; + color: string; caption?: LexicalEditor; key?: NodeKey; } @@ -34,57 +30,61 @@ export type SerializedFontColorNode = Spread< SerializedTextNode >; -function convertFontColorElement(domNode: HTMLElement): DOMConversionOutput | null { +/*function convertFontColorElement(domNode: HTMLElement): DOMConversionOutput | null { const textContent = domNode.textContent; //TODO: apply theme const themeColor = domNode.attributes.getNamedItem(FontColorNodeAttribute)?.value || "#000"; const color = domNode.style.color || "#000"; if (textContent !== null) { - const node = $createFontColorNode(textContent, color, themeColor); + const node = $createFontColorNode(textContent); return { node }; } return null; -} +}*/ + +// export const createFontColorNodeClass = (themeStyles: ThemeStyles) => { /** * Main responsibility of this node is to apply custom or Webiny theme color to selected text. * Extends the original TextNode node to add additional transformation and support for webiny theme font color. */ export class FontColorTextNode extends TextNode { + __themeStyles: ThemeStyles; + __themeColor: string; + /** * @description Webiny theme color property name. Example: color1, color2... * If not specified, specified color will be applied or will remain undefined. */ - __themeColor: string; - /** * @description Color to be applied on text. * If theme color is specified that as priority will be applied to this variable or #000 will be set. */ __color: string; - constructor(text: string, color: string, themeColor: string, key?: NodeKey) { + constructor(text: string, color: string, themeStyles: ThemeStyles, key?: NodeKey) { super(text, key); - this.__themeColor = themeColor; - this.__color = color; + this.__themeStyles = themeStyles; + this.__themeColor = this.getThemeColorName(color); + this.__color = this.getColorValue(color); } static override getType(): string { - return FontColorNodeType; + return "font-color-node"; } static override clone(node: FontColorTextNode): FontColorTextNode { - return new FontColorTextNode(node.__text, node.__themeColor, node.__key); + return new FontColorTextNode(node.__text, node.__color, node.__themeStyles, node.__key); } - static override importJSON(serializedNode: SerializedFontColorNode): FontColorTextNode { + importJSON(serializedNode: SerializedFontColorNode): TextNode { const node = $createFontColorNode( serializedNode.text, serializedNode.color, - serializedNode.themeColor + this.__themeColors ); node.setTextContent(serializedNode.text); node.setFormat(serializedNode.format); @@ -104,22 +104,40 @@ export class FontColorTextNode extends TextNode { }; } + getThemeColorName(color: string) { + return this.isColorThemeStyleName(color) ? color : "custom"; + } + + getColorValue(color: string) { + return this.isColorThemeStyleName(color) ? this.__themeStyles?.colors[color] : color; + } + + isColorThemeStyleName(color: string) { + return !!this.__themeStyles?.colors[color]; + } + + addColorValueToHTMLElement(element: HTMLElement): HTMLElement { + if (!element) { + return element; + } + element.setAttribute(FontColorNodeAttrName, this.__themeColor); + element.style.color = this.__color; + return element; + } + override updateDOM( prevNode: FontColorTextNode, dom: HTMLElement, config: EditorConfig ): boolean { const isUpdated = super.updateDOM(prevNode, dom, config); - dom.setAttribute("theme-font-color", this.__themeColor); - dom.style.color = this.__color; + this.addColorValueToHTMLElement(dom); return isUpdated; } override createDOM(config: EditorConfig): HTMLElement { const element = super.createDOM(config); - element.setAttribute("theme-font-color", this.__themeColor); - element.style.color = this.__color; - return element; + return this.addColorValueToHTMLElement(element); } override canInsertTextBefore(): boolean { @@ -135,15 +153,13 @@ export class FontColorTextNode extends TextNode { } } -export function $createFontColorNode( +export const $createFontColorNode = ( text: string, color: string, - themeColor: string, + themeColors: ThemeStyles, key?: NodeKey -): FontColorTextNode { - return new FontColorTextNode(text, color, themeColor, key); -} - -export function $isFontColorTextNode(node: LexicalNode | null | undefined | undefined): boolean { - return node instanceof FontColorTextNode; -} +): TextNode => { + return new FontColorTextNode(text, color, themeColors); + /* const fontColor = new FontColorActionNode(text, color); + return fontColor;*/ +}; diff --git a/packages/lexical-editor/src/nodes/nodesFactory.ts b/packages/lexical-editor/src/nodes/nodesFactory.ts index 55489d54537..f479fbb542b 100644 --- a/packages/lexical-editor/src/nodes/nodesFactory.ts +++ b/packages/lexical-editor/src/nodes/nodesFactory.ts @@ -1,4 +1,4 @@ -import { Klass, LexicalNode } from "lexical"; +import { Klass, LexicalNode, TextNode } from "lexical"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; import { ListItemNode, ListNode } from "@lexical/list"; import { CodeHighlightNode, CodeNode } from "@lexical/code"; @@ -7,11 +7,14 @@ import { AutoLinkNode, LinkNode } from "@lexical/link"; import { OverflowNode } from "@lexical/overflow"; import { MarkNode } from "@lexical/mark"; import { FontColorTextNode } from "~/nodes/FontColorNode"; +import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; +// import {FontColorAction} from "~/components/ToolbarActions/FontColorAction"; /* * @description create nodes - * */ -const nodesFactory = (theme: unknown): Array> => { + */ +export const nodesFactory = (theme: ThemeStyles): Klass[] => { + // const FontActionColorNode = createFontColorNodeClass(theme); return [ HeadingNode, ListNode, @@ -27,5 +30,3 @@ const nodesFactory = (theme: unknown): Array> => { FontColorTextNode ]; }; - -export const getNodeInstanceWithTheme = (node: LexicalNode, theme: unknown) => {}; diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/webinyNodes.ts index ae06e0cb046..4e252bfdb2d 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/webinyNodes.ts @@ -7,20 +7,8 @@ import { ListItemNode, ListNode } from "@lexical/list"; import { MarkNode } from "@lexical/mark"; import { OverflowNode } from "@lexical/overflow"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; -import { FontColorTextNode } from "~/nodes/FontColorNode"; -export const WebinyNodes: - | Array> - | { - replace: Klass; - with: < - T extends { - new (...args: any): any; - } - >( - node: InstanceType - ) => LexicalNode; - } = [ +export const WebinyNodes: Array> = [ HeadingNode, ListNode, ListItemNode, @@ -31,6 +19,5 @@ export const WebinyNodes: AutoLinkNode, LinkNode, OverflowNode, - MarkNode, - FontColorTextNode + MarkNode ]; diff --git a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx index 276d0aa5e9e..492f6a98aa6 100644 --- a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx +++ b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx @@ -4,10 +4,13 @@ import { usePageElements } from "@webiny/app-page-builder-elements"; import { $createFontColorNode, ADD_FONT_COLOR_COMMAND, - FontColorPayload + FontColorPayload, + FontColorTextNode } from "~/nodes/FontColorNode"; import { $createParagraphNode, + $createTextNode, + $getRoot, $getSelection, $insertNodes, $isRangeSelection, @@ -18,22 +21,20 @@ import { $wrapNodeInElement } from "@lexical/utils"; export const FontColorPlugin: React.FC = () => { const [editor] = useLexicalComposerContext(); - const pageElements = usePageElements(); + const { theme } = usePageElements(); useEffect(() => { return editor.registerCommand( ADD_FONT_COLOR_COMMAND, payload => { editor.update(() => { - const { themeColor } = payload; + const { color } = payload; const selection = editor.getEditorState().read($getSelection); if ($isRangeSelection(selection) && !selection.isCollapsed()) { - const colors = pageElements.theme?.styles?.colors; - const applyColor = colors[themeColor] ?? themeColor; const fontColorNode = $createFontColorNode( selection.getTextContent(), - applyColor, - themeColor + color, + theme.styles ); $insertNodes([fontColorNode]); if ($isRootOrShadowRoot(fontColorNode.getParentOrThrow())) { @@ -47,5 +48,27 @@ export const FontColorPlugin: React.FC = () => { ); }, [editor]); + useEffect(() => { + return editor.registerMutationListener(FontColorTextNode, mutatedNodes => { + // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation. + for (const [nodeKey, mutation] of mutatedNodes) { + console.log(nodeKey, mutation); + console.log(mutatedNodes.get(nodeKey)); + } + }); + }, [editor]); + + useEffect(() => { + return editor.registerNodeTransform(FontColorTextNode, paragraph => { + // Triggers + editor.update(() => { + const paragraph = $getRoot().getFirstChild(); + if (paragraph) { + paragraph.append($createTextNode("foo")); + } + }); + }); + }, [editor]); + return null; }; diff --git a/packages/lexical-editor/src/utils/getLexicalContentText.ts b/packages/lexical-editor/src/utils/getLexicalContentText.ts new file mode 100644 index 00000000000..4d22b9fb664 --- /dev/null +++ b/packages/lexical-editor/src/utils/getLexicalContentText.ts @@ -0,0 +1,30 @@ +import { LexicalValue } from "~/types"; +import { $getRoot } from "lexical"; +import { createHeadlessEditor } from "@lexical/headless"; +import { WebinyNodes } from "~/nodes/webinyNodes"; +import { theme } from "~/themes/webinyLexicalTheme"; +import { isValidLexicalData } from "~/utils/isValidLexicalData"; + +export const getLexicalContentText = (value: LexicalValue | null): string | null => { + if (!value) { + return value; + } + + if (!isValidLexicalData(value)) { + return value; + } + + const config = { + namespace: "webiny", + nodes: [...WebinyNodes], + onError: (error: Error) => { + throw error; + }, + theme: theme + }; + + const editor = createHeadlessEditor(config); + const newEditorState = editor.parseEditorState(value); + + return newEditorState.read(() => $getRoot().getTextContent()); +}; diff --git a/yarn.lock b/yarn.lock index edf32db203a..cd1125d45a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6540,6 +6540,15 @@ __metadata: languageName: node linkType: hard +"@lexical/headless@npm:^0.8.1": + version: 0.8.1 + resolution: "@lexical/headless@npm:0.8.1" + peerDependencies: + lexical: 0.8.1 + checksum: f9b65d6c0bed4c9dc2d0fea74c2a920b47e59f8a504037dde9f781913f01be2b5119646fac444249eafe61459b0120ef2e4e32b3d12bec068a1116d118724daf + languageName: node + linkType: hard + "@lexical/history@npm:0.8.1": version: 0.8.1 resolution: "@lexical/history@npm:0.8.1" @@ -15162,6 +15171,7 @@ __metadata: dependencies: "@lexical/code": 0.8.1 "@lexical/hashtag": 0.8.1 + "@lexical/headless": ^0.8.1 "@lexical/link": 0.8.1 "@lexical/list": 0.8.1 "@lexical/mark": 0.8.1 From 01d08a61f9cdaa32571066e82b2196b01cf3c46a Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Fri, 10 Mar 2023 16:10:04 +0100 Subject: [PATCH 09/57] wip: updated node with factory class --- .../src/components/Editor/RichTextEditor.tsx | 11 +- .../src/components/LexicalHtmlRenderer.tsx | 2 - .../ToolbarActions/FontColorAction.tsx | 7 - .../lexical-editor/src/nodes/FontColorNode.ts | 210 ++++++++---------- .../lexical-editor/src/nodes/nodesFactory.ts | 6 +- .../FontColorPlugin/FontColorPlugin.tsx | 38 ++-- .../src/themes/webinyLexicalTheme.ts | 3 +- 7 files changed, 119 insertions(+), 158 deletions(-) diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index ab51fba5de0..43ca8baae56 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -19,6 +19,7 @@ import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; import { usePageElements } from "@webiny/app-page-builder-elements"; import { nodesFactory } from "~/nodes/nodesFactory"; +import { createFontColorNodeClass } from "~/nodes/FontColorNode"; export interface RichTextEditorProps { toolbar?: React.ReactNode; @@ -51,11 +52,16 @@ const BaseRichTextEditor: React.FC = ({ }: RichTextEditorProps) => { const placeholderElem = {placeholder || "Enter text..."}; const scrollRef = useRef(null); + const { theme } = usePageElements(); const [floatingAnchorElem, setFloatingAnchorElem] = useState( undefined ); - const { theme } = usePageElements(); + const FontActionNodeClassRef = useRef>( + createFontColorNodeClass(theme.styles) + ); + // console.log(new FontActionNodeClassRef.current()); + debugger; const onRef = (_floatingAnchorElem: HTMLDivElement) => { if (_floatingAnchorElem !== null) { setFloatingAnchorElem(_floatingAnchorElem); @@ -81,6 +87,7 @@ const BaseRichTextEditor: React.FC = ({ editorState.read(() => { if (typeof onChange === "function") { const editorState = editor.getEditorState(); + //TODO: send plain JSON object onChange(JSON.stringify(editorState.toJSON())); } }); @@ -93,7 +100,7 @@ const BaseRichTextEditor: React.FC = ({ {value && } - + {/* Events */} {onBlur && } {focus && } diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index b7267b230c8..0161d4d4b6b 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -2,8 +2,6 @@ import React from "react"; import { LexicalValue } from "~/types"; import { isValidLexicalData } from "~/utils/isValidLexicalData"; import { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; -import { WebinyNodes } from "~/nodes/webinyNodes"; -import { theme } from "~/themes/webinyLexicalTheme"; import { LexicalComposer } from "@lexical/react/LexicalComposer"; import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"; import { ContentEditable } from "@lexical/react/LexicalContentEditable"; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 448c9e9dcd0..775cf98e18c 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -34,13 +34,6 @@ export const FontColorAction: FontColorAction = () => { const [activeEditor, setActiveEditor] = useState(editor); const [fontColor, setFontColor] = useState("#000"); - useEffect(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection)) { - setFontColor($getSelectionStyleValueForProperty(selection, "color", "#000")); - } - }, [activeEditor]); - const onFontColorSelect = useCallback((value: string) => { editor.dispatchCommand>(ADD_FONT_COLOR_COMMAND, { color: value diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index 41343104e8f..a3ab980cc8b 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -30,136 +30,104 @@ export type SerializedFontColorNode = Spread< SerializedTextNode >; -/*function convertFontColorElement(domNode: HTMLElement): DOMConversionOutput | null { - const textContent = domNode.textContent; - //TODO: apply theme - const themeColor = domNode.attributes.getNamedItem(FontColorNodeAttribute)?.value || "#000"; - const color = domNode.style.color || "#000"; - if (textContent !== null) { - const node = $createFontColorNode(textContent); - return { - node - }; - } - - return null; -}*/ - -// export const createFontColorNodeClass = (themeStyles: ThemeStyles) => { - -/** - * Main responsibility of this node is to apply custom or Webiny theme color to selected text. - * Extends the original TextNode node to add additional transformation and support for webiny theme font color. - */ -export class FontColorTextNode extends TextNode { - __themeStyles: ThemeStyles; - __themeColor: string; +export const createFontColorNodeClass = (themeStyles: ThemeStyles) => { + console.log("CRETE COLOR NODE"); /** - * @description Webiny theme color property name. Example: color1, color2... - * If not specified, specified color will be applied or will remain undefined. + * Main responsibility of this node is to apply custom or Webiny theme color to selected text. + * Extends the original TextNode node to add additional transformation and support for webiny theme font color. */ - /** - * @description Color to be applied on text. - * If theme color is specified that as priority will be applied to this variable or #000 will be set. - */ - __color: string; - - constructor(text: string, color: string, themeStyles: ThemeStyles, key?: NodeKey) { - super(text, key); - this.__themeStyles = themeStyles; - this.__themeColor = this.getThemeColorName(color); - this.__color = this.getColorValue(color); - } - - static override getType(): string { - return "font-color-node"; - } - - static override clone(node: FontColorTextNode): FontColorTextNode { - return new FontColorTextNode(node.__text, node.__color, node.__themeStyles, node.__key); - } - - importJSON(serializedNode: SerializedFontColorNode): TextNode { - const node = $createFontColorNode( - serializedNode.text, - serializedNode.color, - this.__themeColors - ); - node.setTextContent(serializedNode.text); - node.setFormat(serializedNode.format); - node.setDetail(serializedNode.detail); - node.setMode(serializedNode.mode); - node.setStyle(serializedNode.style); - return node; - } - - override exportJSON(): SerializedFontColorNode { - return { - ...super.exportJSON(), - themeColor: this.__themeColor, - color: this.__color, - type: "font-color-node", - version: 1 - }; - } - - getThemeColorName(color: string) { - return this.isColorThemeStyleName(color) ? color : "custom"; - } - - getColorValue(color: string) { - return this.isColorThemeStyleName(color) ? this.__themeStyles?.colors[color] : color; - } - - isColorThemeStyleName(color: string) { - return !!this.__themeStyles?.colors[color]; - } - - addColorValueToHTMLElement(element: HTMLElement): HTMLElement { - if (!element) { + return class FontColorTextNode extends TextNode { + __themeStyles: ThemeStyles; + __themeColor: string; + __color: string; + + constructor(text: string, color: string, key?: NodeKey) { + super(text, key); + this.__themeStyles = themeStyles; + this.__themeColor = this.getThemeColorName(color); + this.__color = this.getThemeColorValue(color); + } + + static override getType(): string { + return "font-color-node"; + } + + static override clone(node: FontColorTextNode): FontColorTextNode { + const nodeCLone = new FontColorTextNode(node.__text, node.__color, node.__key); + return nodeCLone; + } + + static override importJSON(serializedNode: SerializedFontColorNode): TextNode { + debugger; + const color = + serializedNode.themeColor === "custom" + ? serializedNode.color + : serializedNode.themeColor; + const node = new FontColorTextNode(serializedNode.text, color); + node.setTextContent(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + override exportJSON(): SerializedFontColorNode { + return { + ...super.exportJSON(), + themeColor: this.__themeColor, + color: this.__color, + type: "font-color-node", + version: 1 + }; + } + + getThemeColorName(colorName: string) { + return this.isThemeColor(colorName) ? colorName : "custom"; + } + + getThemeColorValue(color: string) { + return this.isThemeColor(color) ? this.__themeStyles?.colors[color] : color; + } + + isThemeColor(color: string) { + return !!this.__themeStyles?.colors[color]; + } + + addColorValueToHTMLElement(element: HTMLElement): HTMLElement { + element.setAttribute(FontColorNodeAttrName, this.__themeColor); + element.style.color = this.__color; + debugger; return element; } - element.setAttribute(FontColorNodeAttrName, this.__themeColor); - element.style.color = this.__color; - return element; - } - - override updateDOM( - prevNode: FontColorTextNode, - dom: HTMLElement, - config: EditorConfig - ): boolean { - const isUpdated = super.updateDOM(prevNode, dom, config); - this.addColorValueToHTMLElement(dom); - return isUpdated; - } - - override createDOM(config: EditorConfig): HTMLElement { - const element = super.createDOM(config); - return this.addColorValueToHTMLElement(element); - } - - override canInsertTextBefore(): boolean { - return false; - } - - override canInsertTextAfter(): boolean { - return false; - } - - override isTextEntity(): true { - return true; - } -} -export const $createFontColorNode = ( + override updateDOM( + prevNode: FontColorTextNode, + dom: HTMLElement, + config: EditorConfig + ): boolean { + debugger; + const isUpdated = super.updateDOM(prevNode, dom, config); + this.addColorValueToHTMLElement(dom); + return isUpdated; + } + + override createDOM(config: EditorConfig): HTMLElement { + debugger; + const element = super.createDOM(config); + return this.addColorValueToHTMLElement(element); + } + }; +}; + +/*export const $createFontColorNode = ( text: string, color: string, themeColors: ThemeStyles, key?: NodeKey ): TextNode => { return new FontColorTextNode(text, color, themeColors); - /* const fontColor = new FontColorActionNode(text, color); - return fontColor;*/ -}; + /!* const fontColor = new FontColorActionNode(text, color); + return fontColor;*!/ +};*/ diff --git a/packages/lexical-editor/src/nodes/nodesFactory.ts b/packages/lexical-editor/src/nodes/nodesFactory.ts index f479fbb542b..8c678ed999f 100644 --- a/packages/lexical-editor/src/nodes/nodesFactory.ts +++ b/packages/lexical-editor/src/nodes/nodesFactory.ts @@ -6,15 +6,15 @@ import { HashtagNode } from "@lexical/hashtag"; import { AutoLinkNode, LinkNode } from "@lexical/link"; import { OverflowNode } from "@lexical/overflow"; import { MarkNode } from "@lexical/mark"; -import { FontColorTextNode } from "~/nodes/FontColorNode"; import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; +import { createFontColorNodeClass } from "~/nodes/FontColorNode"; // import {FontColorAction} from "~/components/ToolbarActions/FontColorAction"; /* * @description create nodes */ export const nodesFactory = (theme: ThemeStyles): Klass[] => { - // const FontActionColorNode = createFontColorNodeClass(theme); + const FontAction = createFontColorNodeClass(theme); return [ HeadingNode, ListNode, @@ -27,6 +27,6 @@ export const nodesFactory = (theme: ThemeStyles): Klass[] => { LinkNode, OverflowNode, MarkNode, - FontColorTextNode + FontAction ]; }; diff --git a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx index 492f6a98aa6..25f8827fa6e 100644 --- a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx +++ b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx @@ -1,27 +1,24 @@ import React, { useEffect } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { usePageElements } from "@webiny/app-page-builder-elements"; -import { - $createFontColorNode, - ADD_FONT_COLOR_COMMAND, - FontColorPayload, - FontColorTextNode -} from "~/nodes/FontColorNode"; +import { ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; import { $createParagraphNode, - $createTextNode, - $getRoot, $getSelection, $insertNodes, $isRangeSelection, $isRootOrShadowRoot, - COMMAND_PRIORITY_EDITOR + COMMAND_PRIORITY_EDITOR, + Klass, + LexicalNode } from "lexical"; import { $wrapNodeInElement } from "@lexical/utils"; -export const FontColorPlugin: React.FC = () => { +interface FontColorPlugin { + NodeFactoryClass: Klass; +} + +export const FontColorPlugin: React.FC = ({ NodeFactoryClass }) => { const [editor] = useLexicalComposerContext(); - const { theme } = usePageElements(); useEffect(() => { return editor.registerCommand( @@ -29,12 +26,11 @@ export const FontColorPlugin: React.FC = () => { payload => { editor.update(() => { const { color } = payload; - const selection = editor.getEditorState().read($getSelection); - if ($isRangeSelection(selection) && !selection.isCollapsed()) { - const fontColorNode = $createFontColorNode( + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const fontColorNode = new NodeFactoryClass( selection.getTextContent(), - color, - theme.styles + color ); $insertNodes([fontColorNode]); if ($isRootOrShadowRoot(fontColorNode.getParentOrThrow())) { @@ -48,7 +44,7 @@ export const FontColorPlugin: React.FC = () => { ); }, [editor]); - useEffect(() => { + /* useEffect(() => { return editor.registerMutationListener(FontColorTextNode, mutatedNodes => { // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation. for (const [nodeKey, mutation] of mutatedNodes) { @@ -63,12 +59,10 @@ export const FontColorPlugin: React.FC = () => { // Triggers editor.update(() => { const paragraph = $getRoot().getFirstChild(); - if (paragraph) { - paragraph.append($createTextNode("foo")); - } + console.log("transform"); }); }); - }, [editor]); + }, [editor]);*/ return null; }; diff --git a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts index a4990b84dfa..4206628949f 100644 --- a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts +++ b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts @@ -91,5 +91,6 @@ export const theme: EditorThemeClasses = { underline: "WebinyLexical__textUnderline", underlineStrikethrough: "WebinyLexical__textUnderlineStrikethrough" }, - fontColorText: "WebinyLexical__fontColorText" + fontColorText: "WebinyLexical__fontColorText", + colors: {} }; From c2d8ac21023cfad569414cb849af6c3bdcf80a21 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 13 Mar 2023 10:47:19 +0100 Subject: [PATCH 10/57] wip: text theme color updates on change --- packages/lexical-editor/package.json | 1 - .../src/components/Editor/RichTextEditor.tsx | 15 +- .../src/components/LexicalHtmlRenderer.tsx | 4 +- .../ToolbarActions/FontColorAction.tsx | 32 ++- .../lexical-editor/src/nodes/FontColorNode.ts | 186 ++++++++---------- .../lexical-editor/src/nodes/nodesFactory.ts | 32 --- .../lexical-editor/src/nodes/webinyNodes.ts | 4 +- .../FontColorPlugin/FontColorPlugin.tsx | 19 +- .../src/themes/webinyLexicalTheme.ts | 21 +- .../src/utils/getLexicalContentText.ts | 30 --- 10 files changed, 139 insertions(+), 205 deletions(-) delete mode 100644 packages/lexical-editor/src/nodes/nodesFactory.ts delete mode 100644 packages/lexical-editor/src/utils/getLexicalContentText.ts diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index 660b7f2b508..0ed3906e19f 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -9,7 +9,6 @@ "dependencies": { "@lexical/code": "0.8.1", "@lexical/hashtag": "0.8.1", - "@lexical/headless": "^0.8.1", "@lexical/link": "0.8.1", "@lexical/list": "0.8.1", "@lexical/mark": "0.8.1", diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index 43ca8baae56..67405947470 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -18,8 +18,8 @@ import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; import { usePageElements } from "@webiny/app-page-builder-elements"; -import { nodesFactory } from "~/nodes/nodesFactory"; -import { createFontColorNodeClass } from "~/nodes/FontColorNode"; +import { webinyLexicalTheme } from "~/themes/webinyLexicalTheme"; +import { WebinyNodes } from "~/nodes/webinyNodes"; export interface RichTextEditorProps { toolbar?: React.ReactNode; @@ -57,11 +57,6 @@ const BaseRichTextEditor: React.FC = ({ undefined ); - const FontActionNodeClassRef = useRef>( - createFontColorNodeClass(theme.styles) - ); - // console.log(new FontActionNodeClassRef.current()); - debugger; const onRef = (_floatingAnchorElem: HTMLDivElement) => { if (_floatingAnchorElem !== null) { setFloatingAnchorElem(_floatingAnchorElem); @@ -79,8 +74,8 @@ const BaseRichTextEditor: React.FC = ({ onError: (error: Error) => { throw error; }, - nodes: [...nodesFactory(theme.styles), ...(nodes || [])], - theme: theme + nodes: [...WebinyNodes, ...(nodes || [])], + theme: { ...webinyLexicalTheme, styles: theme.styles } }; function handleOnChange(editorState: EditorState, editor: LexicalEditor) { @@ -100,7 +95,7 @@ const BaseRichTextEditor: React.FC = ({ {value && } - + {/* Events */} {onBlur && } {focus && } diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 0161d4d4b6b..86eb53875b4 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -8,8 +8,8 @@ import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; -import { nodesFactory } from "~/nodes/nodesFactory"; import { usePageElements } from "@webiny/app-page-builder-elements"; +import {WebinyNodes} from "~/nodes/webinyNodes"; interface LexicalHtmlRendererProps { nodes?: Klass[]; @@ -25,7 +25,7 @@ export const LexicalHtmlRenderer: React.FC = ({ nodes, throw error; }, editable: false, - nodes: [...nodesFactory(theme.styles), ...(nodes || [])], + nodes: [...WebinyNodes, ...(nodes || [])], theme: theme }; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 775cf98e18c..50a58f02deb 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { $getSelection, $isRangeSelection, LexicalCommand } from "lexical"; -import { $getSelectionStyleValueForProperty } from "@lexical/selection"; +import { LexicalCommand } from "lexical"; import { Compose, makeComposable } from "@webiny/react-composition"; import { FontColorActionContext } from "~/context/FontColorActionContext"; import { ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; +import { usePageElements } from "@webiny/app-page-builder-elements"; /* * Composable Color Picker component that is mounted on toolbar action. @@ -31,13 +31,31 @@ export interface FontColorAction extends React.FC { export const FontColorAction: FontColorAction = () => { const [editor] = useLexicalComposerContext(); - const [activeEditor, setActiveEditor] = useState(editor); const [fontColor, setFontColor] = useState("#000"); + const { theme } = usePageElements(); - const onFontColorSelect = useCallback((value: string) => { - editor.dispatchCommand>(ADD_FONT_COLOR_COMMAND, { - color: value - }); + const isThemeColorName = (color: string): boolean => { + return !!theme?.styles?.colors[color]; + }; + + const getThemeColor = (colorValue: string): string => { + return isThemeColorName(colorValue) ? theme?.styles?.colors[colorValue] : colorValue; + }; + + const onFontColorSelect = useCallback((colorValue: string) => { + const color = getThemeColor(colorValue); + const isThemeColor = isThemeColorName(colorValue); + const themeColorName = isThemeColor ? colorValue : undefined; + setFontColor(colorValue); + const payloadData = { + color, + themeColorName, + isThemeColor + }; + editor.dispatchCommand>( + ADD_FONT_COLOR_COMMAND, + payloadData + ); }, []); return ( diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index a3ab980cc8b..0c56b6e62cc 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -8,7 +8,7 @@ import { Spread, TextNode } from "lexical"; -import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; +import { WebinyLexicalTheme } from "~/themes/webinyLexicalTheme"; export const ADD_FONT_COLOR_COMMAND: LexicalCommand = createCommand("ADD_FONT_COLOR_COMMAND"); @@ -16,13 +16,18 @@ const FontColorNodeAttrName = "font-color-theme"; export interface FontColorPayload { color: string; + isThemeColor: boolean; + themeColorName: string | undefined; caption?: LexicalEditor; key?: NodeKey; } +type ThemeStyleColorName = string; +type ThemeColor = "custom" | ThemeStyleColorName; + export type SerializedFontColorNode = Spread< { - themeColor: string; + themeColor: ThemeColor; color: string; type: "font-color-node"; version: 1; @@ -30,104 +35,87 @@ export type SerializedFontColorNode = Spread< SerializedTextNode >; -export const createFontColorNodeClass = (themeStyles: ThemeStyles) => { - console.log("CRETE COLOR NODE"); - - /** - * Main responsibility of this node is to apply custom or Webiny theme color to selected text. - * Extends the original TextNode node to add additional transformation and support for webiny theme font color. - */ - return class FontColorTextNode extends TextNode { - __themeStyles: ThemeStyles; - __themeColor: string; - __color: string; - - constructor(text: string, color: string, key?: NodeKey) { - super(text, key); - this.__themeStyles = themeStyles; - this.__themeColor = this.getThemeColorName(color); - this.__color = this.getThemeColorValue(color); - } - - static override getType(): string { - return "font-color-node"; - } - - static override clone(node: FontColorTextNode): FontColorTextNode { - const nodeCLone = new FontColorTextNode(node.__text, node.__color, node.__key); - return nodeCLone; - } - - static override importJSON(serializedNode: SerializedFontColorNode): TextNode { - debugger; - const color = - serializedNode.themeColor === "custom" - ? serializedNode.color - : serializedNode.themeColor; - const node = new FontColorTextNode(serializedNode.text, color); - node.setTextContent(serializedNode.text); - node.setFormat(serializedNode.format); - node.setDetail(serializedNode.detail); - node.setMode(serializedNode.mode); - node.setStyle(serializedNode.style); - return node; - } - - override exportJSON(): SerializedFontColorNode { - return { - ...super.exportJSON(), - themeColor: this.__themeColor, - color: this.__color, - type: "font-color-node", - version: 1 - }; - } - - getThemeColorName(colorName: string) { - return this.isThemeColor(colorName) ? colorName : "custom"; +// export const createFontColorNodeClass = (themeStyles: ThemeStyles) => { + +/** + * Main responsibility of this node is to apply custom or Webiny theme color to selected text. + * Extends the original TextNode node to add additional transformation and support for webiny theme font color. + */ +export class FontColorNode extends TextNode { + __themeColor: ThemeColor; + __color: string; + + constructor(text: string, color: string, themeColor?: ThemeColor, key?: NodeKey) { + super(text, key); + this.__themeColor = themeColor || "custom"; + this.__color = color; + } + + static override getType(): string { + return "font-color-node"; + } + + static override clone(node: FontColorNode): FontColorNode { + const nodeCLone = new FontColorNode( + node.__text, + node.__color, + node.__themeColor, + node.__key + ); + return nodeCLone; + } + + static override importJSON(serializedNode: SerializedFontColorNode): TextNode { + const node = new FontColorNode( + serializedNode.text, + serializedNode.color, + serializedNode.themeColor + ); + node.setTextContent(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + override exportJSON(): SerializedFontColorNode { + return { + ...super.exportJSON(), + themeColor: this.__themeColor, + color: this.__color, + type: "font-color-node", + version: 1 + }; + } + + addColorValueToHTMLElement(element: HTMLElement, theme: WebinyLexicalTheme): HTMLElement { + const hasThemeColor = this.__themeColor !== "custom"; + // get the updated color from webiny theme + if (hasThemeColor && theme?.styles?.colors) { + this.__color = theme.styles.colors[this.__themeColor]; } - getThemeColorValue(color: string) { - return this.isThemeColor(color) ? this.__themeStyles?.colors[color] : color; - } - - isThemeColor(color: string) { - return !!this.__themeStyles?.colors[color]; - } - - addColorValueToHTMLElement(element: HTMLElement): HTMLElement { - element.setAttribute(FontColorNodeAttrName, this.__themeColor); - element.style.color = this.__color; - debugger; - return element; - } - - override updateDOM( - prevNode: FontColorTextNode, - dom: HTMLElement, - config: EditorConfig - ): boolean { - debugger; - const isUpdated = super.updateDOM(prevNode, dom, config); - this.addColorValueToHTMLElement(dom); - return isUpdated; - } + element.setAttribute(FontColorNodeAttrName, this.__themeColor); + element.style.color = this.__color; + debugger; + return element; + } + + override updateDOM(prevNode: FontColorNode, dom: HTMLElement, config: EditorConfig): boolean { + debugger; + const isUpdated = super.updateDOM(prevNode, dom, config); + this.addColorValueToHTMLElement(dom, config.theme); + return isUpdated; + } + + override createDOM(config: EditorConfig): HTMLElement { + debugger; + const element = super.createDOM(config); + return this.addColorValueToHTMLElement(element, config.theme); + } +} - override createDOM(config: EditorConfig): HTMLElement { - debugger; - const element = super.createDOM(config); - return this.addColorValueToHTMLElement(element); - } - }; +export const $createFontColorNode = (text: string, color: string, key?: NodeKey): TextNode => { + return new FontColorNode(text, color, key); }; - -/*export const $createFontColorNode = ( - text: string, - color: string, - themeColors: ThemeStyles, - key?: NodeKey -): TextNode => { - return new FontColorTextNode(text, color, themeColors); - /!* const fontColor = new FontColorActionNode(text, color); - return fontColor;*!/ -};*/ diff --git a/packages/lexical-editor/src/nodes/nodesFactory.ts b/packages/lexical-editor/src/nodes/nodesFactory.ts deleted file mode 100644 index 8c678ed999f..00000000000 --- a/packages/lexical-editor/src/nodes/nodesFactory.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Klass, LexicalNode, TextNode } from "lexical"; -import { HeadingNode, QuoteNode } from "@lexical/rich-text"; -import { ListItemNode, ListNode } from "@lexical/list"; -import { CodeHighlightNode, CodeNode } from "@lexical/code"; -import { HashtagNode } from "@lexical/hashtag"; -import { AutoLinkNode, LinkNode } from "@lexical/link"; -import { OverflowNode } from "@lexical/overflow"; -import { MarkNode } from "@lexical/mark"; -import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; -import { createFontColorNodeClass } from "~/nodes/FontColorNode"; -// import {FontColorAction} from "~/components/ToolbarActions/FontColorAction"; - -/* - * @description create nodes - */ -export const nodesFactory = (theme: ThemeStyles): Klass[] => { - const FontAction = createFontColorNodeClass(theme); - return [ - HeadingNode, - ListNode, - ListItemNode, - QuoteNode, - CodeNode, - HashtagNode, - CodeHighlightNode, - AutoLinkNode, - LinkNode, - OverflowNode, - MarkNode, - FontAction - ]; -}; diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/webinyNodes.ts index 4e252bfdb2d..e23cb6860e1 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/webinyNodes.ts @@ -7,6 +7,7 @@ import { ListItemNode, ListNode } from "@lexical/list"; import { MarkNode } from "@lexical/mark"; import { OverflowNode } from "@lexical/overflow"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; +import { FontColorNode } from "~/nodes/FontColorNode"; export const WebinyNodes: Array> = [ HeadingNode, @@ -19,5 +20,6 @@ export const WebinyNodes: Array> = [ AutoLinkNode, LinkNode, OverflowNode, - MarkNode + MarkNode, + FontColorNode ]; diff --git a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx index 25f8827fa6e..3fe26c98fa5 100644 --- a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx +++ b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx @@ -1,23 +1,17 @@ import React, { useEffect } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; +import { ADD_FONT_COLOR_COMMAND, FontColorNode, FontColorPayload } from "~/nodes/FontColorNode"; import { $createParagraphNode, $getSelection, $insertNodes, $isRangeSelection, $isRootOrShadowRoot, - COMMAND_PRIORITY_EDITOR, - Klass, - LexicalNode + COMMAND_PRIORITY_EDITOR } from "lexical"; import { $wrapNodeInElement } from "@lexical/utils"; -interface FontColorPlugin { - NodeFactoryClass: Klass; -} - -export const FontColorPlugin: React.FC = ({ NodeFactoryClass }) => { +export const FontColorPlugin: React.FC = () => { const [editor] = useLexicalComposerContext(); useEffect(() => { @@ -25,12 +19,13 @@ export const FontColorPlugin: React.FC = ({ NodeFactoryClass }) ADD_FONT_COLOR_COMMAND, payload => { editor.update(() => { - const { color } = payload; + const { color, themeColorName } = payload; const selection = $getSelection(); if ($isRangeSelection(selection)) { - const fontColorNode = new NodeFactoryClass( + const fontColorNode = new FontColorNode( selection.getTextContent(), - color + color, + themeColorName ); $insertNodes([fontColorNode]); if ($isRootOrShadowRoot(fontColorNode.getParentOrThrow())) { diff --git a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts index 4206628949f..deaf9437cf0 100644 --- a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts +++ b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts @@ -1,16 +1,16 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - import type { EditorThemeClasses } from "lexical"; import "./webinyLexicalTheme.css"; +import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; + +export type WebinyTheme = { + styles?: ThemeStyles; +}; + +export type WebinyLexicalTheme = WebinyTheme & EditorThemeClasses; -export const theme: EditorThemeClasses = { +export const webinyLexicalTheme: WebinyLexicalTheme = { + styles: undefined, characterLimit: "WebinyLexical__characterLimit", code: "WebinyLexical__code", codeHighlight: { @@ -91,6 +91,5 @@ export const theme: EditorThemeClasses = { underline: "WebinyLexical__textUnderline", underlineStrikethrough: "WebinyLexical__textUnderlineStrikethrough" }, - fontColorText: "WebinyLexical__fontColorText", - colors: {} + fontColorText: "WebinyLexical__fontColorText" }; diff --git a/packages/lexical-editor/src/utils/getLexicalContentText.ts b/packages/lexical-editor/src/utils/getLexicalContentText.ts deleted file mode 100644 index 4d22b9fb664..00000000000 --- a/packages/lexical-editor/src/utils/getLexicalContentText.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { LexicalValue } from "~/types"; -import { $getRoot } from "lexical"; -import { createHeadlessEditor } from "@lexical/headless"; -import { WebinyNodes } from "~/nodes/webinyNodes"; -import { theme } from "~/themes/webinyLexicalTheme"; -import { isValidLexicalData } from "~/utils/isValidLexicalData"; - -export const getLexicalContentText = (value: LexicalValue | null): string | null => { - if (!value) { - return value; - } - - if (!isValidLexicalData(value)) { - return value; - } - - const config = { - namespace: "webiny", - nodes: [...WebinyNodes], - onError: (error: Error) => { - throw error; - }, - theme: theme - }; - - const editor = createHeadlessEditor(config); - const newEditorState = editor.parseEditorState(value); - - return newEditorState.read(() => $getRoot().getTextContent()); -}; From efc6f246d1d5951374c513fc53a982bd0830cd5f Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 13 Mar 2023 10:54:30 +0100 Subject: [PATCH 11/57] wip: lexical html renderer support text theme change --- .../src/components/LexicalHtmlRenderer.tsx | 3 ++- yarn.lock | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 86eb53875b4..7b2dc3d4297 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -10,6 +10,7 @@ import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; import { usePageElements } from "@webiny/app-page-builder-elements"; import {WebinyNodes} from "~/nodes/webinyNodes"; +import {webinyLexicalTheme} from "~/themes/webinyLexicalTheme"; interface LexicalHtmlRendererProps { nodes?: Klass[]; @@ -26,7 +27,7 @@ export const LexicalHtmlRenderer: React.FC = ({ nodes, }, editable: false, nodes: [...WebinyNodes, ...(nodes || [])], - theme: theme + theme: { ...webinyLexicalTheme, styles: theme.styles } }; return ( diff --git a/yarn.lock b/yarn.lock index cd1125d45a6..edf32db203a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6540,15 +6540,6 @@ __metadata: languageName: node linkType: hard -"@lexical/headless@npm:^0.8.1": - version: 0.8.1 - resolution: "@lexical/headless@npm:0.8.1" - peerDependencies: - lexical: 0.8.1 - checksum: f9b65d6c0bed4c9dc2d0fea74c2a920b47e59f8a504037dde9f781913f01be2b5119646fac444249eafe61459b0120ef2e4e32b3d12bec068a1116d118724daf - languageName: node - linkType: hard - "@lexical/history@npm:0.8.1": version: 0.8.1 resolution: "@lexical/history@npm:0.8.1" @@ -15171,7 +15162,6 @@ __metadata: dependencies: "@lexical/code": 0.8.1 "@lexical/hashtag": 0.8.1 - "@lexical/headless": ^0.8.1 "@lexical/link": 0.8.1 "@lexical/list": 0.8.1 "@lexical/mark": 0.8.1 From 93992a6b02c142a3dbf029a47cb7b78ab95b0cb9 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 13 Mar 2023 12:21:44 +0100 Subject: [PATCH 12/57] wip: update style and format from range selection to new font color node --- .../src/components/LexicalHtmlRenderer.tsx | 4 +-- .../lexical-editor/src/nodes/FontColorNode.ts | 36 +++++++++++++++---- .../FontColorPlugin/FontColorPlugin.tsx | 32 ++++++----------- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 7b2dc3d4297..49dd964b05e 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -9,8 +9,8 @@ import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; import { usePageElements } from "@webiny/app-page-builder-elements"; -import {WebinyNodes} from "~/nodes/webinyNodes"; -import {webinyLexicalTheme} from "~/themes/webinyLexicalTheme"; +import { WebinyNodes } from "~/nodes/webinyNodes"; +import { webinyLexicalTheme } from "~/themes/webinyLexicalTheme"; interface LexicalHtmlRendererProps { nodes?: Klass[]; diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index 0c56b6e62cc..1151ebbc6a7 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -3,12 +3,17 @@ import { EditorConfig, LexicalCommand, LexicalEditor, + LexicalNode, NodeKey, + NodeSelection, + RangeSelection, SerializedTextNode, Spread, + TextFormatType, TextNode } from "lexical"; import { WebinyLexicalTheme } from "~/themes/webinyLexicalTheme"; +import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; export const ADD_FONT_COLOR_COMMAND: LexicalCommand = createCommand("ADD_FONT_COLOR_COMMAND"); @@ -98,24 +103,43 @@ export class FontColorNode extends TextNode { element.setAttribute(FontColorNodeAttrName, this.__themeColor); element.style.color = this.__color; - debugger; return element; } override updateDOM(prevNode: FontColorNode, dom: HTMLElement, config: EditorConfig): boolean { - debugger; + const theme = config.theme; const isUpdated = super.updateDOM(prevNode, dom, config); - this.addColorValueToHTMLElement(dom, config.theme); + const hasThemeColor = this.__themeColor !== "custom"; + // get the updated color from webiny theme + if (hasThemeColor && theme?.styles?.colors) { + this.__color = theme.styles.colors[this.__themeColor]; + } + + dom.setAttribute(FontColorNodeAttrName, this.__themeColor); + dom.style.color = this.__color; return isUpdated; } override createDOM(config: EditorConfig): HTMLElement { - debugger; const element = super.createDOM(config); return this.addColorValueToHTMLElement(element, config.theme); } } -export const $createFontColorNode = (text: string, color: string, key?: NodeKey): TextNode => { - return new FontColorNode(text, color, key); +export const $createFontColorNode = ( + text: string, + color: string, + themeColor?: ThemeColor, + key?: NodeKey +): FontColorNode => { + return new FontColorNode(text, color, themeColor, key); }; + +export function $isFontColorNode(node: LexicalNode | null | undefined): node is FontColorNode { + return node instanceof FontColorNode; +} + +export function $applyStylesToNode(node: FontColorNode, nodeStyleProvider: RangeSelection) { + node.setFormat(nodeStyleProvider.format); + node.setStyle(nodeStyleProvider.style); +} diff --git a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx index 3fe26c98fa5..a01bfc5cbfe 100644 --- a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx +++ b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx @@ -1,7 +1,13 @@ import React, { useEffect } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { ADD_FONT_COLOR_COMMAND, FontColorNode, FontColorPayload } from "~/nodes/FontColorNode"; import { + $applyStylesToNode, + $createFontColorNode, + ADD_FONT_COLOR_COMMAND, + FontColorPayload +} from "~/nodes/FontColorNode"; +import { + $createNodeSelection, $createParagraphNode, $getSelection, $insertNodes, @@ -21,12 +27,14 @@ export const FontColorPlugin: React.FC = () => { editor.update(() => { const { color, themeColorName } = payload; const selection = $getSelection(); + if ($isRangeSelection(selection)) { - const fontColorNode = new FontColorNode( + const fontColorNode = $createFontColorNode( selection.getTextContent(), color, themeColorName ); + $applyStylesToNode(fontColorNode, selection); $insertNodes([fontColorNode]); if ($isRootOrShadowRoot(fontColorNode.getParentOrThrow())) { $wrapNodeInElement(fontColorNode, $createParagraphNode).selectEnd(); @@ -39,25 +47,5 @@ export const FontColorPlugin: React.FC = () => { ); }, [editor]); - /* useEffect(() => { - return editor.registerMutationListener(FontColorTextNode, mutatedNodes => { - // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation. - for (const [nodeKey, mutation] of mutatedNodes) { - console.log(nodeKey, mutation); - console.log(mutatedNodes.get(nodeKey)); - } - }); - }, [editor]); - - useEffect(() => { - return editor.registerNodeTransform(FontColorTextNode, paragraph => { - // Triggers - editor.update(() => { - const paragraph = $getRoot().getFirstChild(); - console.log("transform"); - }); - }); - }, [editor]);*/ - return null; }; From 7e7776f9cafa7ea625b14ba16d4a8cca5b13467f Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 13 Mar 2023 16:10:12 +0100 Subject: [PATCH 13/57] wip: color picker dropdown supports selection of color without closing itself on mouse move --- .../LexicalColorPicker/LexicalColorPicker.tsx | 36 +++-- .../LexicalColorPicker/StyledComponents.ts | 128 ------------------ .../LexicalColorPicker/colorize.svg | 1 - .../LexicalColorPickerDropdown.tsx | 18 +-- .../src/components/Toolbar/Toolbar.css | 6 + .../ToolbarActions/FontColorAction.tsx | 35 ++++- packages/lexical-editor/src/index.tsx | 1 - .../lexical-editor/src/nodes/FontColorNode.ts | 24 ++-- .../FontColorPlugin/FontColorPlugin.tsx | 1 - packages/lexical-editor/src/ui/DropDown.tsx | 16 ++- 10 files changed, 90 insertions(+), 176 deletions(-) delete mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts delete mode 100644 apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx b/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx index d4adbc39d09..55d2101fa01 100644 --- a/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx +++ b/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useState } from "react"; import styled from "@emotion/styled"; import { css } from "emotion"; -import { COLORS } from "./StyledComponents"; import { usePageBuilder } from "@webiny/app-page-builder/hooks/usePageBuilder"; import { usePageElements } from "@webiny/app-page-builder-elements/hooks/usePageElements"; import { PbTheme } from "@webiny/app-page-builder/types"; @@ -13,11 +12,12 @@ import { ChromePicker } from "react-color"; import { ReactComponent as IconPalette } from "./round-color_lens-24px.svg"; const ColorPickerStyle = styled("div")({ + position: "relative", display: "flex", flexWrap: "wrap", justifyContent: "space-between", - width: 240, - padding: 5, + width: 225, + padding: 0, backgroundColor: "#fff" }); @@ -26,10 +26,7 @@ const ColorBox = styled("div")({ width: 50, height: 40, margin: 10, - borderRadius: 2, - boxSizing: "border-box", - transition: "transform 0.2s", - color: "var(--mdc-theme-text-secondary-on-background)" + borderRadius: 2 }); const Color = styled("button")({ @@ -76,6 +73,14 @@ const iconPaletteStyle = css({ color: "var(--mdc-theme-secondary)" }); +const COLORS = { + lightGray: "hsla(0, 0%, 97%, 1)", + gray: "hsla(300, 2%, 92%, 1)", + darkGray: "hsla(0, 0%, 70%, 1)", + darkestGray: "hsla(0, 0%, 20%, 1)", + black: "hsla(208, 100%, 5%, 1)" +}; + const styles = { selectedColor: css({ boxShadow: "0px 0px 0px 2px var(--mdc-theme-secondary)" @@ -105,7 +110,7 @@ const styles = { interface LexicalColorPickerProps { value: string; - onChange: Function; + onChange?: Function; onChangeComplete: Function; handlerClassName?: string; } @@ -126,9 +131,13 @@ export const LexicalColorPicker: React.FC = ({ const onColorChange = useCallback( (color, event) => { - setActualSelectedColor(value); - onChange(getColorValue(color.rgb)); event.preventDefault(); + // controls of the picker are updated as user moves the mouse + const customColor = getColorValue(color.rgb); + setActualSelectedColor(customColor); + if (typeof onChange === "function") { + onChange(customColor); + } }, [onChange] ); @@ -143,7 +152,6 @@ export const LexicalColorPicker: React.FC = ({ ); const togglePicker = useCallback(e => { - debugger; e.stopPropagation(); setShowPicker(!showPicker); }, []); @@ -218,7 +226,11 @@ export const LexicalColorPicker: React.FC = ({ {showPicker && ( - + )} ); diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts b/apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts deleted file mode 100644 index c330dbee19f..00000000000 --- a/apps/admin/src/lexicalEditor/LexicalColorPicker/StyledComponents.ts +++ /dev/null @@ -1,128 +0,0 @@ -import styled from "@emotion/styled"; -import { css } from "emotion"; - -export const classes = { - simpleGrid: css({ - "&.mdc-layout-grid": { - padding: 0, - margin: "0px 0px 16px" - } - }), - grid: css({ - "& .mdc-layout-grid": { - padding: 0, - margin: "0px 0px 16px" - } - }) -}; - -export const Footer = styled("div")({ - backgroundColor: "var(--mdc-theme-background)", - paddingBottom: 10, - margin: "0 -15px -15px -15px", - ".mdc-layout-grid": { - padding: "15px 10px 10px 15px", - ".mdc-layout-grid__cell.mdc-layout-grid__cell--span-4": { - paddingRight: 10 - } - } -}); - -interface InputContainerProps { - width?: number | string; - margin?: number | string; -} - -export const InputContainer = styled<"div", InputContainerProps>("div")(props => ({ - "> .mdc-text-field.mdc-text-field--upgraded": { - height: "30px !important", - width: props.width || 50, - margin: props.hasOwnProperty("margin") ? props.margin : "0 0 0 18px", - ".mdc-text-field__input": { - paddingTop: 16 - } - } -})); - -type ContentWrapperProps = { - direction?: "row" | "row-reverse" | "column" | "column-reverse"; -}; - -export const ContentWrapper = styled<"div", ContentWrapperProps>("div")(props => ({ - display: "flex", - flexDirection: props.direction || "row" -})); - -export const COLORS = { - lightGray: "hsla(0, 0%, 97%, 1)", - gray: "hsla(300, 2%, 92%, 1)", - darkGray: "hsla(0, 0%, 70%, 1)", - darkestGray: "hsla(0, 0%, 20%, 1)", - black: "hsla(208, 100%, 5%, 1)" -}; - -export const TopLeft = styled("div")({ - gridArea: "topLeft" -}); -export const Top = styled("div")({ - gridArea: "top" -}); -export const TopRight = styled("div")({ - gridArea: "topRight" -}); -export const Left = styled("div")({ - gridArea: "left" -}); -export const Center = styled("div")({ - gridArea: "center", - backgroundColor: "rgb(204,229,255)", - border: "1px dashed rgb(0,64,133)" -}); -export const Right = styled("div")({ - gridArea: "right" -}); -export const BottomLeft = styled("div")({ - gridArea: "bottomLeft" -}); -export const Bottom = styled("div")({ - gridArea: "bottom" -}); -export const BottomRight = styled("div")({ - gridArea: "bottomRight" -}); -export const SpacingGrid = styled("div")({ - display: "grid", - gridTemplateColumns: "1fr 2fr 1fr", - gridTemplateRows: "1fr 1fr 1fr", - gap: "0px 0px", - gridTemplateAreas: - '"topLeft top topRight"' + '"left center right"' + '"bottomLeft bottom bottomRight"', - border: "1px dashed rgb(21,87,36)", - backgroundColor: COLORS.lightGray, - - "& .text": { - fontSize: 11, - padding: "4px 8px" - }, - "& .mono": { - fontFamily: "monospace" - }, - "& .align-center": { - display: "flex", - justifyContent: "center" - } -}); -export const SimpleButton = styled("button")({ - boxSizing: "border-box", - border: "1px solid var(--mdc-theme-on-background)", - borderRadius: 1, - backgroundColor: "transparent", - padding: "8px 16px", - cursor: "pointer" -}); -export const ButtonContainer = styled("div")({ - marginTop: 16 -}); -export const justifySelfEndStyle = css({ - justifySelf: "end" -}); diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg b/apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg deleted file mode 100644 index 0732ed51c37..00000000000 --- a/apps/admin/src/lexicalEditor/LexicalColorPicker/colorize.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx b/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx index 90868264c0e..599b07e2019 100644 --- a/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx +++ b/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx @@ -1,28 +1,20 @@ import React from "react"; -import { ToolbarActionDialog, useFontColorPicker } from "@webiny/lexical-editor"; +import { useFontColorPicker, DropDown } from "@webiny/lexical-editor"; import { LexicalColorPicker } from "./LexicalColorPicker/LexicalColorPicker"; export const LexicalColorPickerDropdown = () => { const { value, applyColor } = useFontColorPicker(); return ( - -
- -
-
+ + ); }; diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.css b/packages/lexical-editor/src/components/Toolbar/Toolbar.css index 585933abe0c..1e6ccf71956 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.css +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.css @@ -323,6 +323,12 @@ i.font-color, .icon.font-color { overflow-y: auto; } +.lexical-dropdown.no-scroll { + max-height: inherit; + overflow: auto; + overflow-y: auto; +} + .lexical-dropdown .item { margin: 0 8px 0 8px; padding: 8px; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 50a58f02deb..002785970f7 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -1,10 +1,11 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { LexicalCommand } from "lexical"; +import { $getSelection, $isRangeSelection, LexicalCommand } from "lexical"; import { Compose, makeComposable } from "@webiny/react-composition"; import { FontColorActionContext } from "~/context/FontColorActionContext"; -import { ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; +import { $isFontColorNode, ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; import { usePageElements } from "@webiny/app-page-builder-elements"; +import { getSelectedNode } from "~/utils/getSelectedNode"; /* * Composable Color Picker component that is mounted on toolbar action. @@ -42,11 +43,18 @@ export const FontColorAction: FontColorAction = () => { return isThemeColorName(colorValue) ? theme?.styles?.colors[colorValue] : colorValue; }; + const setFontColorSelect = useCallback( + (fontColorValue: string) => { + setFontColor(fontColorValue); + }, + [fontColor] + ); + const onFontColorSelect = useCallback((colorValue: string) => { const color = getThemeColor(colorValue); const isThemeColor = isThemeColorName(colorValue); const themeColorName = isThemeColor ? colorValue : undefined; - setFontColor(colorValue); + setFontColorSelect(colorValue); const payloadData = { color, themeColorName, @@ -58,6 +66,27 @@ export const FontColorAction: FontColorAction = () => { ); }, []); + const updatePopup = useCallback(() => { + editor.getEditorState().read(() => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return; + } + const node = getSelectedNode(selection); + if ($isFontColorNode(node)) { + const colorStyle = node.getColorStyle(); + setFontColor(colorStyle.color); + } + }); + }, [editor]); + + useEffect(() => { + document.addEventListener("selectionchange", updatePopup); + return () => { + document.removeEventListener("selectionchange", updatePopup); + }; + }, [updatePopup]); + return ( = createCommand("ADD_FONT_COLOR_COMMAND"); @@ -40,8 +37,6 @@ export type SerializedFontColorNode = Spread< SerializedTextNode >; -// export const createFontColorNodeClass = (themeStyles: ThemeStyles) => { - /** * Main responsibility of this node is to apply custom or Webiny theme color to selected text. * Extends the original TextNode node to add additional transformation and support for webiny theme font color. @@ -61,13 +56,7 @@ export class FontColorNode extends TextNode { } static override clone(node: FontColorNode): FontColorNode { - const nodeCLone = new FontColorNode( - node.__text, - node.__color, - node.__themeColor, - node.__key - ); - return nodeCLone; + return new FontColorNode(node.__text, node.__color, node.__themeColor, node.__key); } static override importJSON(serializedNode: SerializedFontColorNode): TextNode { @@ -120,6 +109,13 @@ export class FontColorNode extends TextNode { return isUpdated; } + getColorStyle(): { color: string; themeColor: ThemeColor } { + return { + color: this.__color, + themeColor: this.__themeColor + }; + } + override createDOM(config: EditorConfig): HTMLElement { const element = super.createDOM(config); return this.addColorValueToHTMLElement(element, config.theme); @@ -135,9 +131,9 @@ export const $createFontColorNode = ( return new FontColorNode(text, color, themeColor, key); }; -export function $isFontColorNode(node: LexicalNode | null | undefined): node is FontColorNode { +export const $isFontColorNode = (node: LexicalNode): boolean => { return node instanceof FontColorNode; -} +}; export function $applyStylesToNode(node: FontColorNode, nodeStyleProvider: RangeSelection) { node.setFormat(nodeStyleProvider.format); diff --git a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx index a01bfc5cbfe..e5a21bdff29 100644 --- a/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx +++ b/packages/lexical-editor/src/plugins/FontColorPlugin/FontColorPlugin.tsx @@ -7,7 +7,6 @@ import { FontColorPayload } from "~/nodes/FontColorNode"; import { - $createNodeSelection, $createParagraphNode, $getSelection, $insertNodes, diff --git a/packages/lexical-editor/src/ui/DropDown.tsx b/packages/lexical-editor/src/ui/DropDown.tsx index ee324ecd1c2..7841afb49e1 100644 --- a/packages/lexical-editor/src/ui/DropDown.tsx +++ b/packages/lexical-editor/src/ui/DropDown.tsx @@ -52,10 +52,12 @@ export function DropDownItem({ function DropDownItems({ children, dropDownRef, + showScroll = true, onClose }: { children: React.ReactNode; dropDownRef?: React.Ref; + showScroll?: boolean; onClose: () => void; }) { const [items, setItems] = useState[]>(); @@ -118,7 +120,11 @@ function DropDownItems({ return ( -
+
{children}
@@ -132,7 +138,8 @@ export function DropDown({ buttonClassName, buttonIconClassName, children, - stopCloseOnClickSelf + stopCloseOnClickSelf, + showScroll = true }: { disabled?: boolean; buttonAriaLabel?: string; @@ -141,6 +148,7 @@ export function DropDown({ buttonLabel?: string; children: ReactNode; stopCloseOnClickSelf?: boolean; + showScroll?: boolean; }): JSX.Element { const buttonRef = useRef(null); const [showDropDown, setShowDropDown] = useState(false); @@ -185,7 +193,9 @@ export function DropDown({ {showDropDown && (
- {children} + + {children} +
)} From d0612efa6ff9764f1d6d9894a824fc4a5f011c29 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 13 Mar 2023 18:48:45 +0100 Subject: [PATCH 14/57] wip: add lexical editor config package with configuration plugin --- apps/admin/package.json | 5 ---- apps/admin/src/App.tsx | 8 ----- packages/app-page-builder/package.json | 2 +- packages/lexical-editor-config/.babelrc.js | 1 + packages/lexical-editor-config/LICENSE | 21 +++++++++++++ packages/lexical-editor-config/README.md | 15 ++++++++++ packages/lexical-editor-config/package.json | 28 +++++++++++++++++ .../src/LexicalEditorConfigurationPlugin.tsx | 23 ++++++++++++++ .../LexicalColorPicker/LexicalColorPicker.tsx | 0 .../round-color_lens-24px.svg | 0 .../LexicalColorPicker/unselected.svg | 0 .../LexicalColorPickerDropdown.tsx | 2 +- packages/lexical-editor-config/src/index.ts | 1 + .../lexical-editor-config/tsconfig.build.json | 17 +++++++++++ packages/lexical-editor-config/tsconfig.json | 28 +++++++++++++++++ .../lexical-editor-config/webiny.config.js | 8 +++++ .../src/components/Editor/RichTextEditor.tsx | 4 +-- packages/lexical-editor/src/index.tsx | 3 +- .../lexical-editor/src/nodes/FontColorNode.ts | 4 +-- .../src/themes/webinyLexicalTheme.ts | 4 +-- packages/lexical-editor/tsconfig.build.json | 5 +++- packages/lexical-editor/tsconfig.json | 4 ++- yarn.lock | 30 ++++++++++++++----- 23 files changed, 181 insertions(+), 32 deletions(-) create mode 100644 packages/lexical-editor-config/.babelrc.js create mode 100644 packages/lexical-editor-config/LICENSE create mode 100644 packages/lexical-editor-config/README.md create mode 100644 packages/lexical-editor-config/package.json create mode 100644 packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx rename {apps/admin/src/lexicalEditor => packages/lexical-editor-config/src/components}/LexicalColorPicker/LexicalColorPicker.tsx (100%) rename {apps/admin/src/lexicalEditor => packages/lexical-editor-config/src/components}/LexicalColorPicker/round-color_lens-24px.svg (100%) rename {apps/admin/src/lexicalEditor => packages/lexical-editor-config/src/components}/LexicalColorPicker/unselected.svg (100%) rename {apps/admin/src/lexicalEditor => packages/lexical-editor-config/src/components}/LexicalColorPickerDropdown.tsx (87%) create mode 100644 packages/lexical-editor-config/src/index.ts create mode 100644 packages/lexical-editor-config/tsconfig.build.json create mode 100644 packages/lexical-editor-config/tsconfig.json create mode 100644 packages/lexical-editor-config/webiny.config.js diff --git a/apps/admin/package.json b/apps/admin/package.json index 90bda63ac8a..c23f31f6803 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -14,20 +14,15 @@ "@webiny/app-form-builder": "0.0.0", "@webiny/app-page-builder": "0.0.0", "@webiny/app-page-builder-editor": "0.0.0", - "@webiny/app-page-builder-elements": "0.0.0", "@webiny/app-serverless-cms": "0.0.0", - "@webiny/app-theme-manager": "0.0.0", "@webiny/cli": "0.0.0", - "@webiny/lexical-editor": "0.0.0", "@webiny/plugins": "0.0.0", "@webiny/react-properties": "0.0.0", "@webiny/serverless-cms-aws": "0.0.0", - "@webiny/ui": "0.0.0", "core-js": "^3.0.1", "cross-fetch": "^3.0.4", "prop-types": "^15.7.2", "react": "17.0.2", - "react-color": "^2.19.3", "react-dom": "17.0.2", "regenerator-runtime": "^0.13.5", "theme": "^0.1.0", diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 8e055a9103d..79c8c468c57 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -2,18 +2,10 @@ import React from "react"; import { Admin } from "@webiny/app-serverless-cms"; import { Cognito } from "@webiny/app-admin-users-cognito"; import "./App.scss"; -import { LexicalEditorConfig } from "@webiny/lexical-editor"; -import { LexicalColorPickerDropdown } from "./lexicalEditor/LexicalColorPickerDropdown"; - -const { FontColorAction } = LexicalEditorConfig; - export const App: React.FC = () => { return ( - - } /> - ); }; diff --git a/packages/app-page-builder/package.json b/packages/app-page-builder/package.json index 68c512ac5a4..27540508d37 100644 --- a/packages/app-page-builder/package.json +++ b/packages/app-page-builder/package.json @@ -63,7 +63,7 @@ "platform": "^1.3.5", "prop-types": "^15.7.2", "react": "17.0.2", - "react-color": "^2.19.3", + "react-color": "^2.14.3", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "17.0.2", diff --git a/packages/lexical-editor-config/.babelrc.js b/packages/lexical-editor-config/.babelrc.js new file mode 100644 index 00000000000..bec58b263bd --- /dev/null +++ b/packages/lexical-editor-config/.babelrc.js @@ -0,0 +1 @@ +module.exports = require("@webiny/project-utils").createBabelConfigForReact({ path: __dirname }); diff --git a/packages/lexical-editor-config/LICENSE b/packages/lexical-editor-config/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/lexical-editor-config/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Webiny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/lexical-editor-config/README.md b/packages/lexical-editor-config/README.md new file mode 100644 index 00000000000..0bbc2d317a0 --- /dev/null +++ b/packages/lexical-editor-config/README.md @@ -0,0 +1,15 @@ +# @webiny/lexical-editor-config +[![](https://img.shields.io/npm/dw/@webiny/app-page-builder.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +[![](https://img.shields.io/npm/v/@webiny/app-page-builder.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + + +## About + +This package provides a configuration plugins for lexical editor. + + +## Where is it used? + +Currently, this packaged is used in [@webiny/app-serverless-cms](../app-serverless-cms). diff --git a/packages/lexical-editor-config/package.json b/packages/lexical-editor-config/package.json new file mode 100644 index 00000000000..64d3556ea15 --- /dev/null +++ b/packages/lexical-editor-config/package.json @@ -0,0 +1,28 @@ +{ + "name": "@webiny/lexical-editor-config", + "version": "0.0.0", + "dependencies": { + "@webiny/lexical-editor": "0.0.0", + "@webiny/app-page-builder": "0.0.0", + "@webiny/app-page-builder-elements": "0.0.0", + "@webiny/react-composition": "0.0.0", + "@emotion/styled": "^10.0.27", + "emotion": "^10.0.27", + "classnames": "^2.3.1", + "react-color": "^2.14.3", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@webiny/cli": "0.0.0", + "@webiny/project-utils": "0.0.0" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "scripts": { + "build": "yarn webiny run build", + "watch": "yarn webiny run watch" + } +} diff --git a/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx b/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx new file mode 100644 index 00000000000..b95bb1bde2b --- /dev/null +++ b/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx @@ -0,0 +1,23 @@ +import {LexicalEditorConfig} from "@webiny/lexical-editor"; +import React from "react"; +import {LexicalColorPickerDropdown} from "~/components/LexicalColorPickerDropdown"; +import { FontColorAction } from "@webiny/lexical-editor"; +import { makeComposable, createComponentPlugin } from "@webiny/react-composition"; + + + +const LexicalEditorConfiguration = makeComposable("LexicalEditorConfiguration", () => { + return ( + + } /> + + ) +}); + +export const LexicalEditorConfigurationPlugin = createComponentPlugin(LexicalEditorConfiguration, Original => { + return function LexicalEditorConfigurationPlugin() { + return + } +}) + + diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx b/packages/lexical-editor-config/src/components/LexicalColorPicker/LexicalColorPicker.tsx similarity index 100% rename from apps/admin/src/lexicalEditor/LexicalColorPicker/LexicalColorPicker.tsx rename to packages/lexical-editor-config/src/components/LexicalColorPicker/LexicalColorPicker.tsx diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/round-color_lens-24px.svg b/packages/lexical-editor-config/src/components/LexicalColorPicker/round-color_lens-24px.svg similarity index 100% rename from apps/admin/src/lexicalEditor/LexicalColorPicker/round-color_lens-24px.svg rename to packages/lexical-editor-config/src/components/LexicalColorPicker/round-color_lens-24px.svg diff --git a/apps/admin/src/lexicalEditor/LexicalColorPicker/unselected.svg b/packages/lexical-editor-config/src/components/LexicalColorPicker/unselected.svg similarity index 100% rename from apps/admin/src/lexicalEditor/LexicalColorPicker/unselected.svg rename to packages/lexical-editor-config/src/components/LexicalColorPicker/unselected.svg diff --git a/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx b/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx similarity index 87% rename from apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx rename to packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx index 599b07e2019..5f4e3b06c8d 100644 --- a/apps/admin/src/lexicalEditor/LexicalColorPickerDropdown.tsx +++ b/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useFontColorPicker, DropDown } from "@webiny/lexical-editor"; -import { LexicalColorPicker } from "./LexicalColorPicker/LexicalColorPicker"; +import {LexicalColorPicker} from "~/components/LexicalColorPicker/LexicalColorPicker"; export const LexicalColorPickerDropdown = () => { const { value, applyColor } = useFontColorPicker(); diff --git a/packages/lexical-editor-config/src/index.ts b/packages/lexical-editor-config/src/index.ts new file mode 100644 index 00000000000..a03b2541b4b --- /dev/null +++ b/packages/lexical-editor-config/src/index.ts @@ -0,0 +1 @@ +export { LexicalEditorConfigurationPlugin } from "~/LexicalEditorConfigurationPlugin"; diff --git a/packages/lexical-editor-config/tsconfig.build.json b/packages/lexical-editor-config/tsconfig.build.json new file mode 100644 index 00000000000..6a28bd1e313 --- /dev/null +++ b/packages/lexical-editor-config/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "references": [ + { "path": "../lexical-editor/tsconfig.build.json" }, + { "path": "../app-page-builder/tsconfig.build.json" }, + { "path": "../app-page-builder-elements/tsconfig.build.json" }, + { "path": "../react-composition/tsconfig.build.json" } + ], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"] }, + "baseUrl": "." + } +} diff --git a/packages/lexical-editor-config/tsconfig.json b/packages/lexical-editor-config/tsconfig.json new file mode 100644 index 00000000000..a2359655b3a --- /dev/null +++ b/packages/lexical-editor-config/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "__tests__/**/*.ts"], + "references": [ + { "path": "../lexical-editor" }, + { "path": "../app-page-builder" }, + { "path": "../app-page-builder-elements" }, + { "path": "../react-composition" } + ], + "compilerOptions": { + "rootDirs": ["./src", "./__tests__"], + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { + "~/*": ["./src/*"], + "~tests/*": ["./__tests__/*"], + "@webiny/lexical-editor/*": ["../lexical-editor/src/*"], + "@webiny/lexical-editor": ["../lexical-editor/src"], + "@webiny/app-page-builder/*": ["../app-page-builder/src/*"], + "@webiny/app-page-builder": ["../app-page-builder/src"], + "@webiny/app-page-builder-elements/*": ["../app-page-builder-elements/src/*"], + "@webiny/app-page-builder-elements": ["../app-page-builder-elements/src"], + "@webiny/react-composition/*": ["../react-composition/src/*"], + "@webiny/react-composition": ["../react-composition/src"] + }, + "baseUrl": "." + } +} diff --git a/packages/lexical-editor-config/webiny.config.js b/packages/lexical-editor-config/webiny.config.js new file mode 100644 index 00000000000..6dff86766c9 --- /dev/null +++ b/packages/lexical-editor-config/webiny.config.js @@ -0,0 +1,8 @@ +const { createWatchPackage, createBuildPackage } = require("@webiny/project-utils"); + +module.exports = { + commands: { + build: createBuildPackage({ cwd: __dirname }), + watch: createWatchPackage({ cwd: __dirname }) + } +}; diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index 67405947470..9af14c22126 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -18,7 +18,7 @@ import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; import { usePageElements } from "@webiny/app-page-builder-elements"; -import { webinyLexicalTheme } from "~/themes/webinyLexicalTheme"; +import { webinyEditorTheme } from "~/themes/webinyLexicalTheme"; import { WebinyNodes } from "~/nodes/webinyNodes"; export interface RichTextEditorProps { @@ -75,7 +75,7 @@ const BaseRichTextEditor: React.FC = ({ throw error; }, nodes: [...WebinyNodes, ...(nodes || [])], - theme: { ...webinyLexicalTheme, styles: theme.styles } + theme: { ...webinyEditorTheme, styles: theme.styles } }; function handleOnChange(editorState: EditorState, editor: LexicalEditor) { diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 70c5a6e6f1d..7aa779d9fcd 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -12,6 +12,7 @@ export { BoldAction } from "~/components/ToolbarActions/BoldAction"; export { BulletListAction } from "~/components/ToolbarActions/BulletListAction"; export { CodeHighlightAction } from "~/components/ToolbarActions/CodeHighlightAction"; export { FontSizeAction } from "~/components/ToolbarActions/FontSizeAction"; +export { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; export { ItalicAction } from "~/components/ToolbarActions/ItalicAction"; export { LinkAction } from "~/components/ToolbarActions/LinkAction"; export { NumberedListAction } from "~/components/ToolbarActions/NumberedListAction"; @@ -34,7 +35,7 @@ export { FloatingLinkEditorPlugin } from "~/plugins/FloatingLinkEditorPlugin/Flo export { CodeHighlightPlugin } from "~/plugins/CodeHighlightPlugin/CodeHighlightPlugin"; export { ClickableLinkPlugin } from "~/plugins/ClickableLinkPlugin/ClickableLinkPlugin"; export { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; -export { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; +export { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin/LexicalUpdateStatePlugin"; // composition export { AddToolbarAction } from "~/components/AddToolbarAction"; export { AddRichTextEditorPlugin } from "~/components/AddRichTextEditorPlugin"; diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index 89cab820ec8..d1c757a4423 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -10,7 +10,7 @@ import { Spread, TextNode } from "lexical"; -import { WebinyLexicalTheme } from "~/themes/webinyLexicalTheme"; +import {WebinyEditorTheme} from "~/themes/webinyLexicalTheme"; export const ADD_FONT_COLOR_COMMAND: LexicalCommand = createCommand("ADD_FONT_COLOR_COMMAND"); @@ -83,7 +83,7 @@ export class FontColorNode extends TextNode { }; } - addColorValueToHTMLElement(element: HTMLElement, theme: WebinyLexicalTheme): HTMLElement { + addColorValueToHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { const hasThemeColor = this.__themeColor !== "custom"; // get the updated color from webiny theme if (hasThemeColor && theme?.styles?.colors) { diff --git a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts index deaf9437cf0..ba14db5ecab 100644 --- a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts +++ b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts @@ -7,9 +7,9 @@ export type WebinyTheme = { styles?: ThemeStyles; }; -export type WebinyLexicalTheme = WebinyTheme & EditorThemeClasses; +export type WebinyEditorTheme = WebinyTheme & EditorThemeClasses; -export const webinyLexicalTheme: WebinyLexicalTheme = { +export const webinyEditorTheme: WebinyEditorTheme = { styles: undefined, characterLimit: "WebinyLexical__characterLimit", code: "WebinyLexical__code", diff --git a/packages/lexical-editor/tsconfig.build.json b/packages/lexical-editor/tsconfig.build.json index d0eb9513334..e33639c5d56 100644 --- a/packages/lexical-editor/tsconfig.build.json +++ b/packages/lexical-editor/tsconfig.build.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.build.json", "include": ["src"], - "references": [{ "path": "../react-composition/tsconfig.build.json" }], + "references": [ + { "path": "../app-page-builder-elements/tsconfig.build.json" }, + { "path": "../react-composition/tsconfig.build.json" } + ], "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/packages/lexical-editor/tsconfig.json b/packages/lexical-editor/tsconfig.json index b8f98f630e1..3db8cd87535 100644 --- a/packages/lexical-editor/tsconfig.json +++ b/packages/lexical-editor/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "include": ["src", "__tests__/**/*.ts"], - "references": [{ "path": "../react-composition" }], + "references": [{ "path": "../app-page-builder-elements" }, { "path": "../react-composition" }], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], "outDir": "./dist", @@ -9,6 +9,8 @@ "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], + "@webiny/app-page-builder-elements/*": ["../app-page-builder-elements/src/*"], + "@webiny/app-page-builder-elements": ["../app-page-builder-elements/src"], "@webiny/react-composition/*": ["../react-composition/src/*"], "@webiny/react-composition": ["../react-composition/src"] }, diff --git a/yarn.lock b/yarn.lock index edf32db203a..455f3a4cb77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13895,7 +13895,7 @@ __metadata: platform: ^1.3.5 prop-types: ^15.7.2 react: 17.0.2 - react-color: ^2.19.3 + react-color: ^2.14.3 react-dnd: ^11.1.3 react-dnd-html5-backend: ^11.1.3 react-dom: 17.0.2 @@ -14121,7 +14121,7 @@ __metadata: languageName: unknown linkType: soft -"@webiny/app-theme-manager@0.0.0, @webiny/app-theme-manager@workspace:packages/app-theme-manager": +"@webiny/app-theme-manager@workspace:packages/app-theme-manager": version: 0.0.0-use.local resolution: "@webiny/app-theme-manager@workspace:packages/app-theme-manager" dependencies: @@ -15136,6 +15136,25 @@ __metadata: languageName: unknown linkType: soft +"@webiny/lexical-editor-config@workspace:packages/lexical-editor-config": + version: 0.0.0-use.local + resolution: "@webiny/lexical-editor-config@workspace:packages/lexical-editor-config" + dependencies: + "@emotion/styled": ^10.0.27 + "@webiny/app-page-builder": 0.0.0 + "@webiny/app-page-builder-elements": 0.0.0 + "@webiny/cli": 0.0.0 + "@webiny/lexical-editor": 0.0.0 + "@webiny/project-utils": 0.0.0 + "@webiny/react-composition": 0.0.0 + classnames: ^2.3.1 + emotion: ^10.0.27 + react: ^17.0.2 + react-color: ^2.14.3 + react-dom: ^17.0.2 + languageName: unknown + linkType: soft + "@webiny/lexical-editor-pb-element@0.0.0, @webiny/lexical-editor-pb-element@workspace:packages/lexical-editor-pb-element": version: 0.0.0-use.local resolution: "@webiny/lexical-editor-pb-element@workspace:packages/lexical-editor-pb-element" @@ -16206,21 +16225,16 @@ __metadata: "@webiny/app-form-builder": 0.0.0 "@webiny/app-page-builder": 0.0.0 "@webiny/app-page-builder-editor": 0.0.0 - "@webiny/app-page-builder-elements": 0.0.0 "@webiny/app-serverless-cms": 0.0.0 - "@webiny/app-theme-manager": 0.0.0 "@webiny/cli": 0.0.0 - "@webiny/lexical-editor": 0.0.0 "@webiny/plugins": 0.0.0 "@webiny/react-properties": 0.0.0 "@webiny/serverless-cms-aws": 0.0.0 - "@webiny/ui": 0.0.0 core-js: ^3.0.1 cross-env: ^5.0.2 cross-fetch: ^3.0.4 prop-types: ^15.7.2 react: 17.0.2 - react-color: ^2.19.3 react-dom: 17.0.2 regenerator-runtime: ^0.13.5 theme: ^0.1.0 @@ -36147,7 +36161,7 @@ __metadata: languageName: node linkType: hard -"react-color@npm:^2.14.1, react-color@npm:^2.17.0, react-color@npm:^2.19.3": +"react-color@npm:^2.14.1, react-color@npm:^2.14.3, react-color@npm:^2.17.0": version: 2.19.3 resolution: "react-color@npm:2.19.3" dependencies: From 8e9b51c0f3b4e7f679f8f7ff74c16270766d035b Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 14 Mar 2023 09:21:33 +0100 Subject: [PATCH 15/57] wip: package updates --- packages/lexical-editor-config/package.json | 8 ++--- .../src/LexicalEditorConfigurationPlugin.tsx | 29 +++++++++---------- .../components/LexicalColorPickerDropdown.tsx | 2 +- .../lexical-editor/src/nodes/FontColorNode.ts | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/lexical-editor-config/package.json b/packages/lexical-editor-config/package.json index 64d3556ea15..20d5e60fd9c 100644 --- a/packages/lexical-editor-config/package.json +++ b/packages/lexical-editor-config/package.json @@ -2,15 +2,15 @@ "name": "@webiny/lexical-editor-config", "version": "0.0.0", "dependencies": { - "@webiny/lexical-editor": "0.0.0", + "@emotion/styled": "^10.0.27", "@webiny/app-page-builder": "0.0.0", "@webiny/app-page-builder-elements": "0.0.0", + "@webiny/lexical-editor": "0.0.0", "@webiny/react-composition": "0.0.0", - "@emotion/styled": "^10.0.27", - "emotion": "^10.0.27", "classnames": "^2.3.1", - "react-color": "^2.14.3", + "emotion": "^10.0.27", "react": "^17.0.2", + "react-color": "^2.14.3", "react-dom": "^17.0.2" }, "devDependencies": { diff --git a/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx b/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx index b95bb1bde2b..97ef5253732 100644 --- a/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx +++ b/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx @@ -1,23 +1,22 @@ -import {LexicalEditorConfig} from "@webiny/lexical-editor"; +import { LexicalEditorConfig } from "@webiny/lexical-editor"; import React from "react"; -import {LexicalColorPickerDropdown} from "~/components/LexicalColorPickerDropdown"; +import { LexicalColorPickerDropdown } from "~/components/LexicalColorPickerDropdown"; import { FontColorAction } from "@webiny/lexical-editor"; import { makeComposable, createComponentPlugin } from "@webiny/react-composition"; - - const LexicalEditorConfiguration = makeComposable("LexicalEditorConfiguration", () => { - return ( - - } /> - - ) + return ( + + } /> + + ); }); -export const LexicalEditorConfigurationPlugin = createComponentPlugin(LexicalEditorConfiguration, Original => { - return function LexicalEditorConfigurationPlugin() { - return +export const LexicalEditorConfigurationPlugin = createComponentPlugin( + LexicalEditorConfiguration, + Original => { + return function LexicalEditorConfigurationPlugin() { + return ; + }; } -}) - - +); diff --git a/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx b/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx index 5f4e3b06c8d..c866aa099de 100644 --- a/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx +++ b/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useFontColorPicker, DropDown } from "@webiny/lexical-editor"; -import {LexicalColorPicker} from "~/components/LexicalColorPicker/LexicalColorPicker"; +import { LexicalColorPicker } from "~/components/LexicalColorPicker/LexicalColorPicker"; export const LexicalColorPickerDropdown = () => { const { value, applyColor } = useFontColorPicker(); diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index d1c757a4423..3bad01c2f64 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -10,7 +10,7 @@ import { Spread, TextNode } from "lexical"; -import {WebinyEditorTheme} from "~/themes/webinyLexicalTheme"; +import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; export const ADD_FONT_COLOR_COMMAND: LexicalCommand = createCommand("ADD_FONT_COLOR_COMMAND"); From a302c5fd54de23eddd055fe69faa0fb6fdf7586c Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Tue, 14 Mar 2023 10:08:56 +0100 Subject: [PATCH 16/57] wip: resolve circular deps --- packages/app-page-builder-elements/package.json | 2 +- .../.babelrc.js | 0 .../LICENSE | 0 .../README.md | 8 ++++---- .../package.json | 2 +- .../src/LexicalEditorConfigurationPlugin.tsx | 0 .../components/LexicalColorPicker/LexicalColorPicker.tsx | 0 .../LexicalColorPicker/round-color_lens-24px.svg | 0 .../src/components/LexicalColorPicker/unselected.svg | 0 .../src/components/LexicalColorPickerDropdown.tsx | 0 .../src/index.ts | 0 .../tsconfig.build.json | 0 .../tsconfig.json | 0 .../webiny.config.js | 0 packages/lexical-editor/package.json | 1 - .../src/components/Editor/RichTextEditor.tsx | 5 +++-- .../lexical-editor/src/components/LexicalHtmlRenderer.tsx | 8 ++++---- .../src/components/ToolbarActions/FontColorAction.tsx | 4 ++-- packages/lexical-editor/src/themes/webinyLexicalTheme.ts | 3 +-- packages/lexical-editor/tsconfig.build.json | 1 - packages/lexical-editor/tsconfig.json | 4 +--- yarn.lock | 1 - 22 files changed, 17 insertions(+), 22 deletions(-) rename packages/{lexical-editor-config => lexical-editor-actions}/.babelrc.js (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/LICENSE (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/README.md (53%) rename packages/{lexical-editor-config => lexical-editor-actions}/package.json (93%) rename packages/{lexical-editor-config => lexical-editor-actions}/src/LexicalEditorConfigurationPlugin.tsx (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/src/components/LexicalColorPicker/LexicalColorPicker.tsx (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/src/components/LexicalColorPicker/round-color_lens-24px.svg (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/src/components/LexicalColorPicker/unselected.svg (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/src/components/LexicalColorPickerDropdown.tsx (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/src/index.ts (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/tsconfig.build.json (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/tsconfig.json (100%) rename packages/{lexical-editor-config => lexical-editor-actions}/webiny.config.js (100%) diff --git a/packages/app-page-builder-elements/package.json b/packages/app-page-builder-elements/package.json index 3702351d2d1..82147b89d1b 100644 --- a/packages/app-page-builder-elements/package.json +++ b/packages/app-page-builder-elements/package.json @@ -17,6 +17,7 @@ "@babel/runtime": "^7.19.0", "@emotion/core": "^10.0.17", "@emotion/styled": "^10.0.17", + "@webiny/lexical-editor": "0.0.0", "@webiny/theme": "0.0.0" }, "peerDependencies": { @@ -54,7 +55,6 @@ "@types/react": "17.0.39", "@types/resize-observer-browser": "^0.1.4", "@webiny/cli": "0.0.0", - "@webiny/lexical-editor": "0.0.0", "@webiny/project-utils": "0.0.0", "babel-plugin-lodash": "^3.3.4", "execa": "^5.0.0", diff --git a/packages/lexical-editor-config/.babelrc.js b/packages/lexical-editor-actions/.babelrc.js similarity index 100% rename from packages/lexical-editor-config/.babelrc.js rename to packages/lexical-editor-actions/.babelrc.js diff --git a/packages/lexical-editor-config/LICENSE b/packages/lexical-editor-actions/LICENSE similarity index 100% rename from packages/lexical-editor-config/LICENSE rename to packages/lexical-editor-actions/LICENSE diff --git a/packages/lexical-editor-config/README.md b/packages/lexical-editor-actions/README.md similarity index 53% rename from packages/lexical-editor-config/README.md rename to packages/lexical-editor-actions/README.md index 0bbc2d317a0..623f87fe26d 100644 --- a/packages/lexical-editor-config/README.md +++ b/packages/lexical-editor-actions/README.md @@ -1,13 +1,13 @@ -# @webiny/lexical-editor-config -[![](https://img.shields.io/npm/dw/@webiny/app-page-builder.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) -[![](https://img.shields.io/npm/v/@webiny/app-page-builder.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +# @webiny/lexical-editor-actions +[![](https://img.shields.io/npm/dw/@webiny/lexical-editor-actions.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +[![](https://img.shields.io/npm/v/@webiny/lexical-editor-actions.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ## About -This package provides a configuration plugins for lexical editor. +This package provides actions plugins for Lexical editor. ## Where is it used? diff --git a/packages/lexical-editor-config/package.json b/packages/lexical-editor-actions/package.json similarity index 93% rename from packages/lexical-editor-config/package.json rename to packages/lexical-editor-actions/package.json index 20d5e60fd9c..26af37ad804 100644 --- a/packages/lexical-editor-config/package.json +++ b/packages/lexical-editor-actions/package.json @@ -1,5 +1,5 @@ { - "name": "@webiny/lexical-editor-config", + "name": "@webiny/lexical-editor-actions", "version": "0.0.0", "dependencies": { "@emotion/styled": "^10.0.27", diff --git a/packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx b/packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx similarity index 100% rename from packages/lexical-editor-config/src/LexicalEditorConfigurationPlugin.tsx rename to packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx diff --git a/packages/lexical-editor-config/src/components/LexicalColorPicker/LexicalColorPicker.tsx b/packages/lexical-editor-actions/src/components/LexicalColorPicker/LexicalColorPicker.tsx similarity index 100% rename from packages/lexical-editor-config/src/components/LexicalColorPicker/LexicalColorPicker.tsx rename to packages/lexical-editor-actions/src/components/LexicalColorPicker/LexicalColorPicker.tsx diff --git a/packages/lexical-editor-config/src/components/LexicalColorPicker/round-color_lens-24px.svg b/packages/lexical-editor-actions/src/components/LexicalColorPicker/round-color_lens-24px.svg similarity index 100% rename from packages/lexical-editor-config/src/components/LexicalColorPicker/round-color_lens-24px.svg rename to packages/lexical-editor-actions/src/components/LexicalColorPicker/round-color_lens-24px.svg diff --git a/packages/lexical-editor-config/src/components/LexicalColorPicker/unselected.svg b/packages/lexical-editor-actions/src/components/LexicalColorPicker/unselected.svg similarity index 100% rename from packages/lexical-editor-config/src/components/LexicalColorPicker/unselected.svg rename to packages/lexical-editor-actions/src/components/LexicalColorPicker/unselected.svg diff --git a/packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx b/packages/lexical-editor-actions/src/components/LexicalColorPickerDropdown.tsx similarity index 100% rename from packages/lexical-editor-config/src/components/LexicalColorPickerDropdown.tsx rename to packages/lexical-editor-actions/src/components/LexicalColorPickerDropdown.tsx diff --git a/packages/lexical-editor-config/src/index.ts b/packages/lexical-editor-actions/src/index.ts similarity index 100% rename from packages/lexical-editor-config/src/index.ts rename to packages/lexical-editor-actions/src/index.ts diff --git a/packages/lexical-editor-config/tsconfig.build.json b/packages/lexical-editor-actions/tsconfig.build.json similarity index 100% rename from packages/lexical-editor-config/tsconfig.build.json rename to packages/lexical-editor-actions/tsconfig.build.json diff --git a/packages/lexical-editor-config/tsconfig.json b/packages/lexical-editor-actions/tsconfig.json similarity index 100% rename from packages/lexical-editor-config/tsconfig.json rename to packages/lexical-editor-actions/tsconfig.json diff --git a/packages/lexical-editor-config/webiny.config.js b/packages/lexical-editor-actions/webiny.config.js similarity index 100% rename from packages/lexical-editor-config/webiny.config.js rename to packages/lexical-editor-actions/webiny.config.js diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index 0ed3906e19f..d9644fbb4fe 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -17,7 +17,6 @@ "@lexical/rich-text": "0.8.1", "@lexical/selection": "0.8.1", "@lexical/utils": "0.8.1", - "@webiny/app-page-builder-elements": "0.0.0", "@webiny/react-composition": "0.0.0", "lexical": "0.8.1", "react": "^17.0.2", diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index 9af14c22126..2ac4e30c021 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -17,7 +17,6 @@ import { isValidLexicalData } from "~/utils/isValidLexicalData"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; -import { usePageElements } from "@webiny/app-page-builder-elements"; import { webinyEditorTheme } from "~/themes/webinyLexicalTheme"; import { WebinyNodes } from "~/nodes/webinyNodes"; @@ -52,7 +51,9 @@ const BaseRichTextEditor: React.FC = ({ }: RichTextEditorProps) => { const placeholderElem = {placeholder || "Enter text..."}; const scrollRef = useRef(null); - const { theme } = usePageElements(); + // const { theme } = usePageElements(); + const theme = { styles: {}}; + const [floatingAnchorElem, setFloatingAnchorElem] = useState( undefined ); diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 49dd964b05e..1e9902c6af0 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -8,9 +8,8 @@ import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; -import { usePageElements } from "@webiny/app-page-builder-elements"; import { WebinyNodes } from "~/nodes/webinyNodes"; -import { webinyLexicalTheme } from "~/themes/webinyLexicalTheme"; +import { webinyEditorTheme } from "~/themes/webinyLexicalTheme"; interface LexicalHtmlRendererProps { nodes?: Klass[]; @@ -18,7 +17,8 @@ interface LexicalHtmlRendererProps { } export const LexicalHtmlRenderer: React.FC = ({ nodes, value }) => { - const { theme } = usePageElements(); + // const { theme } = usePageElements(); + const theme = { styles: {}}; const initialConfig = { editorState: isValidLexicalData(value) ? value : generateInitialLexicalValue(), namespace: "webiny", @@ -27,7 +27,7 @@ export const LexicalHtmlRenderer: React.FC = ({ nodes, }, editable: false, nodes: [...WebinyNodes, ...(nodes || [])], - theme: { ...webinyLexicalTheme, styles: theme.styles } + theme: { ...webinyEditorTheme, styles: theme.styles } }; return ( diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 002785970f7..01d30143922 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -4,7 +4,6 @@ import { $getSelection, $isRangeSelection, LexicalCommand } from "lexical"; import { Compose, makeComposable } from "@webiny/react-composition"; import { FontColorActionContext } from "~/context/FontColorActionContext"; import { $isFontColorNode, ADD_FONT_COLOR_COMMAND, FontColorPayload } from "~/nodes/FontColorNode"; -import { usePageElements } from "@webiny/app-page-builder-elements"; import { getSelectedNode } from "~/utils/getSelectedNode"; /* @@ -33,7 +32,8 @@ export interface FontColorAction extends React.FC { export const FontColorAction: FontColorAction = () => { const [editor] = useLexicalComposerContext(); const [fontColor, setFontColor] = useState("#000"); - const { theme } = usePageElements(); + // const { theme } = usePageElements(); + const theme = { styles: {}} as any; const isThemeColorName = (color: string): boolean => { return !!theme?.styles?.colors[color]; diff --git a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts index ba14db5ecab..fb37335a99b 100644 --- a/packages/lexical-editor/src/themes/webinyLexicalTheme.ts +++ b/packages/lexical-editor/src/themes/webinyLexicalTheme.ts @@ -1,10 +1,9 @@ import type { EditorThemeClasses } from "lexical"; import "./webinyLexicalTheme.css"; -import { ThemeStyles } from "@webiny/app-page-builder-elements/types"; export type WebinyTheme = { - styles?: ThemeStyles; + styles?: Record; }; export type WebinyEditorTheme = WebinyTheme & EditorThemeClasses; diff --git a/packages/lexical-editor/tsconfig.build.json b/packages/lexical-editor/tsconfig.build.json index e33639c5d56..08fc5934245 100644 --- a/packages/lexical-editor/tsconfig.build.json +++ b/packages/lexical-editor/tsconfig.build.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.build.json", "include": ["src"], "references": [ - { "path": "../app-page-builder-elements/tsconfig.build.json" }, { "path": "../react-composition/tsconfig.build.json" } ], "compilerOptions": { diff --git a/packages/lexical-editor/tsconfig.json b/packages/lexical-editor/tsconfig.json index 3db8cd87535..b8f98f630e1 100644 --- a/packages/lexical-editor/tsconfig.json +++ b/packages/lexical-editor/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "include": ["src", "__tests__/**/*.ts"], - "references": [{ "path": "../app-page-builder-elements" }, { "path": "../react-composition" }], + "references": [{ "path": "../react-composition" }], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], "outDir": "./dist", @@ -9,8 +9,6 @@ "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], - "@webiny/app-page-builder-elements/*": ["../app-page-builder-elements/src/*"], - "@webiny/app-page-builder-elements": ["../app-page-builder-elements/src"], "@webiny/react-composition/*": ["../react-composition/src/*"], "@webiny/react-composition": ["../react-composition/src"] }, diff --git a/yarn.lock b/yarn.lock index 455f3a4cb77..eb9c699e9c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15189,7 +15189,6 @@ __metadata: "@lexical/rich-text": 0.8.1 "@lexical/selection": 0.8.1 "@lexical/utils": 0.8.1 - "@webiny/app-page-builder-elements": 0.0.0 "@webiny/cli": ^5.33.1 "@webiny/project-utils": ^5.33.1 "@webiny/react-composition": 0.0.0 From 2af283c950427c3dab0e9f43d72544dfc2b7a1bd Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 14 Mar 2023 11:57:35 +0100 Subject: [PATCH 17/57] refactor: remove dependency of the circular dependency on app-page-builder-elements --- apps/theme/theme.ts | 2 +- .../src/renderers/heading.tsx | 4 +- .../src/renderers/paragraph.tsx | 4 +- .../tsconfig.build.json | 4 +- .../app-page-builder-elements/tsconfig.json | 8 +- packages/app-page-builder/package.json | 2 +- .../components/ColorPicker/ColorPicker.tsx | 2 +- .../ColorPicker/StyledComponents.ts | 128 ------------------ packages/app-serverless-cms/package.json | 1 + packages/app-serverless-cms/src/Admin.tsx | 2 + .../app-serverless-cms/tsconfig.build.json | 1 + packages/app-serverless-cms/tsconfig.json | 3 + packages/lexical-editor-actions/package.json | 3 +- .../src/LexicalEditorConfigurationPlugin.tsx | 14 +- .../LexicalColorPicker/LexicalColorPicker.tsx | 7 +- packages/lexical-editor-actions/src/index.ts | 2 +- .../tsconfig.build.json | 3 +- packages/lexical-editor-actions/tsconfig.json | 9 +- .../src/LexicalEditor.tsx | 8 +- .../src/components/PeTextRenderer.tsx | 5 +- .../plugins/TextElementRendererPlugin.tsx | 4 +- .../components/AddRichTextEditorPlugin.tsx | 8 +- .../src/components/Editor/RichTextEditor.tsx | 14 +- .../src/components/LexicalHtmlRenderer.tsx | 14 +- .../ToolbarActions/FontColorAction.tsx | 35 ++--- .../src/context/FontColorActionContext.tsx | 2 +- .../lexical-editor/src/nodes/FontColorNode.ts | 3 +- packages/lexical-editor/tsconfig.build.json | 4 +- yarn.lock | 12 +- 29 files changed, 92 insertions(+), 216 deletions(-) delete mode 100644 packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index 023c60477e4..f7e57671226 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -11,7 +11,7 @@ export const breakpoints = { // Colors. export const colors = { - color1: "#c45560", //#fa5723", // Primary. + color1: "#fa5723", // Primary. color2: "#00ccb0", // Secondary. color3: "#0a0a0a", // Text primary. color4: "#616161", // Text secondary. diff --git a/packages/app-page-builder-elements/src/renderers/heading.tsx b/packages/app-page-builder-elements/src/renderers/heading.tsx index 40572dca174..a78b5cb70e5 100644 --- a/packages/app-page-builder-elements/src/renderers/heading.tsx +++ b/packages/app-page-builder-elements/src/renderers/heading.tsx @@ -2,6 +2,7 @@ import React from "react"; import { createRenderer } from "~/createRenderer"; import { useRenderer } from "~/hooks/useRenderer"; import { isValidLexicalData, LexicalHtmlRenderer } from "@webiny/lexical-editor"; +import { usePageElements } from "~/hooks/usePageElements"; export type HeadingRenderer = ReturnType; @@ -9,12 +10,13 @@ export const createHeading = () => { return createRenderer(() => { const { getElement } = useRenderer(); const element = getElement(); + const { theme } = usePageElements(); const tag = element.data.text.desktop.tag || "h1"; const __html = element.data.text.data.text; if (isValidLexicalData(__html)) { - return ; + return ; } return React.createElement(tag, { dangerouslySetInnerHTML: { __html } diff --git a/packages/app-page-builder-elements/src/renderers/paragraph.tsx b/packages/app-page-builder-elements/src/renderers/paragraph.tsx index 871e47646c3..c33828bfb3e 100644 --- a/packages/app-page-builder-elements/src/renderers/paragraph.tsx +++ b/packages/app-page-builder-elements/src/renderers/paragraph.tsx @@ -2,15 +2,17 @@ import React from "react"; import { createRenderer } from "~/createRenderer"; import { useRenderer } from "~/hooks/useRenderer"; import { isValidLexicalData, LexicalHtmlRenderer } from "@webiny/lexical-editor"; +import { usePageElements } from "~/hooks/usePageElements"; export const createParagraph = () => { return createRenderer(() => { const { getElement } = useRenderer(); const element = getElement(); + const { theme } = usePageElements(); const __html = element.data.text.data.text; if (isValidLexicalData(__html)) { - return ; + return ; } // If the text already contains `p` tags (happens when c/p-ing text into the editor), diff --git a/packages/app-page-builder-elements/tsconfig.build.json b/packages/app-page-builder-elements/tsconfig.build.json index d0da46b7fcc..58a9bc11a38 100644 --- a/packages/app-page-builder-elements/tsconfig.build.json +++ b/packages/app-page-builder-elements/tsconfig.build.json @@ -2,8 +2,8 @@ "extends": "../../tsconfig.build.json", "include": ["src"], "references": [ - { "path": "../theme/tsconfig.build.json" }, - { "path": "../lexical-editor/tsconfig.build.json" } + { "path": "../lexical-editor/tsconfig.build.json" }, + { "path": "../theme/tsconfig.build.json" } ], "compilerOptions": { "rootDir": "./src", diff --git a/packages/app-page-builder-elements/tsconfig.json b/packages/app-page-builder-elements/tsconfig.json index d4a76a65572..6c9dfba9019 100644 --- a/packages/app-page-builder-elements/tsconfig.json +++ b/packages/app-page-builder-elements/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "include": ["src", "__tests__/**/*.ts"], - "references": [{ "path": "../theme" }, { "path": "../lexical-editor" }], + "references": [{ "path": "../lexical-editor" }, { "path": "../theme" }], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], "outDir": "./dist", @@ -9,10 +9,10 @@ "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], - "@webiny/theme/*": ["../theme/src/*"], - "@webiny/theme": ["../theme/src"], "@webiny/lexical-editor/*": ["../lexical-editor/src/*"], - "@webiny/lexical-editor": ["../lexical-editor/src"] + "@webiny/lexical-editor": ["../lexical-editor/src"], + "@webiny/theme/*": ["../theme/src/*"], + "@webiny/theme": ["../theme/src"] }, "baseUrl": "." } diff --git a/packages/app-page-builder/package.json b/packages/app-page-builder/package.json index 27540508d37..3e52fdaa97f 100644 --- a/packages/app-page-builder/package.json +++ b/packages/app-page-builder/package.json @@ -63,7 +63,7 @@ "platform": "^1.3.5", "prop-types": "^15.7.2", "react": "17.0.2", - "react-color": "^2.14.3", + "react-color": "^2.14.1", "react-dnd": "^11.1.3", "react-dnd-html5-backend": "^11.1.3", "react-dom": "17.0.2", diff --git a/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx b/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx index f1c06902ae6..8fac7314a63 100644 --- a/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx +++ b/packages/app-page-builder/src/editor/components/ColorPicker/ColorPicker.tsx @@ -14,9 +14,9 @@ import { ReactComponent as IconPalette } from "../../assets/icons/round-color_le import { ReactComponent as ColorizeIcon } from "./colorize.svg"; import { ReactComponent as NoColorSelectedIcon } from "./unselected.svg"; import { COLORS } from "../../plugins/elementSettings/components/StyledComponents"; +import { isLegacyRenderingEngine } from "~/utils"; import { PbTheme } from "~/types"; -import { isLegacyRenderingEngine } from "~/utils"; const ColorPickerStyle = styled("div")({ display: "flex", diff --git a/packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts b/packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts deleted file mode 100644 index c330dbee19f..00000000000 --- a/packages/app-page-builder/src/editor/components/ColorPicker/StyledComponents.ts +++ /dev/null @@ -1,128 +0,0 @@ -import styled from "@emotion/styled"; -import { css } from "emotion"; - -export const classes = { - simpleGrid: css({ - "&.mdc-layout-grid": { - padding: 0, - margin: "0px 0px 16px" - } - }), - grid: css({ - "& .mdc-layout-grid": { - padding: 0, - margin: "0px 0px 16px" - } - }) -}; - -export const Footer = styled("div")({ - backgroundColor: "var(--mdc-theme-background)", - paddingBottom: 10, - margin: "0 -15px -15px -15px", - ".mdc-layout-grid": { - padding: "15px 10px 10px 15px", - ".mdc-layout-grid__cell.mdc-layout-grid__cell--span-4": { - paddingRight: 10 - } - } -}); - -interface InputContainerProps { - width?: number | string; - margin?: number | string; -} - -export const InputContainer = styled<"div", InputContainerProps>("div")(props => ({ - "> .mdc-text-field.mdc-text-field--upgraded": { - height: "30px !important", - width: props.width || 50, - margin: props.hasOwnProperty("margin") ? props.margin : "0 0 0 18px", - ".mdc-text-field__input": { - paddingTop: 16 - } - } -})); - -type ContentWrapperProps = { - direction?: "row" | "row-reverse" | "column" | "column-reverse"; -}; - -export const ContentWrapper = styled<"div", ContentWrapperProps>("div")(props => ({ - display: "flex", - flexDirection: props.direction || "row" -})); - -export const COLORS = { - lightGray: "hsla(0, 0%, 97%, 1)", - gray: "hsla(300, 2%, 92%, 1)", - darkGray: "hsla(0, 0%, 70%, 1)", - darkestGray: "hsla(0, 0%, 20%, 1)", - black: "hsla(208, 100%, 5%, 1)" -}; - -export const TopLeft = styled("div")({ - gridArea: "topLeft" -}); -export const Top = styled("div")({ - gridArea: "top" -}); -export const TopRight = styled("div")({ - gridArea: "topRight" -}); -export const Left = styled("div")({ - gridArea: "left" -}); -export const Center = styled("div")({ - gridArea: "center", - backgroundColor: "rgb(204,229,255)", - border: "1px dashed rgb(0,64,133)" -}); -export const Right = styled("div")({ - gridArea: "right" -}); -export const BottomLeft = styled("div")({ - gridArea: "bottomLeft" -}); -export const Bottom = styled("div")({ - gridArea: "bottom" -}); -export const BottomRight = styled("div")({ - gridArea: "bottomRight" -}); -export const SpacingGrid = styled("div")({ - display: "grid", - gridTemplateColumns: "1fr 2fr 1fr", - gridTemplateRows: "1fr 1fr 1fr", - gap: "0px 0px", - gridTemplateAreas: - '"topLeft top topRight"' + '"left center right"' + '"bottomLeft bottom bottomRight"', - border: "1px dashed rgb(21,87,36)", - backgroundColor: COLORS.lightGray, - - "& .text": { - fontSize: 11, - padding: "4px 8px" - }, - "& .mono": { - fontFamily: "monospace" - }, - "& .align-center": { - display: "flex", - justifyContent: "center" - } -}); -export const SimpleButton = styled("button")({ - boxSizing: "border-box", - border: "1px solid var(--mdc-theme-on-background)", - borderRadius: 1, - backgroundColor: "transparent", - padding: "8px 16px", - cursor: "pointer" -}); -export const ButtonContainer = styled("div")({ - marginTop: 16 -}); -export const justifySelfEndStyle = css({ - justifySelf: "end" -}); diff --git a/packages/app-serverless-cms/package.json b/packages/app-serverless-cms/package.json index 25c0b23ec06..d63320b48a7 100644 --- a/packages/app-serverless-cms/package.json +++ b/packages/app-serverless-cms/package.json @@ -28,6 +28,7 @@ "@webiny/app-security-access-management": "0.0.0", "@webiny/app-tenancy": "0.0.0", "@webiny/app-tenant-manager": "0.0.0", + "@webiny/lexical-editor-actions": "0.0.0", "@webiny/lexical-editor-pb-element": "0.0.0", "@webiny/plugins": "0.0.0", "apollo-cache": "^1.3.5", diff --git a/packages/app-serverless-cms/src/Admin.tsx b/packages/app-serverless-cms/src/Admin.tsx index 9896c4b47ea..8c386e6926b 100644 --- a/packages/app-serverless-cms/src/Admin.tsx +++ b/packages/app-serverless-cms/src/Admin.tsx @@ -26,6 +26,7 @@ import { createViewCompositionProvider } from "@webiny/app-admin/base/providers/ import { AdvancedPublishingWorkflow } from "@webiny/app-apw"; import { TenantManager } from "@webiny/app-tenant-manager"; import { LexicalEditorPlugin } from "@webiny/lexical-editor-pb-element"; +import { LexicalEditorActions } from "@webiny/lexical-editor-actions"; import { Module as MailerSettings } from "@webiny/app-mailer"; export interface AdminProps extends Omit { @@ -52,6 +53,7 @@ const App = (props: AdminProps) => { + diff --git a/packages/app-serverless-cms/tsconfig.build.json b/packages/app-serverless-cms/tsconfig.build.json index 2c93765436c..df8a2f38fb5 100644 --- a/packages/app-serverless-cms/tsconfig.build.json +++ b/packages/app-serverless-cms/tsconfig.build.json @@ -19,6 +19,7 @@ { "path": "../app-security-access-management/tsconfig.build.json" }, { "path": "../app-tenancy/tsconfig.build.json" }, { "path": "../app-tenant-manager/tsconfig.build.json" }, + { "path": "../lexical-editor-actions/tsconfig.build.json" }, { "path": "../lexical-editor-pb-element/tsconfig.build.json" }, { "path": "../plugins/tsconfig.build.json" } ], diff --git a/packages/app-serverless-cms/tsconfig.json b/packages/app-serverless-cms/tsconfig.json index 3912cc68d88..41524f8bff2 100644 --- a/packages/app-serverless-cms/tsconfig.json +++ b/packages/app-serverless-cms/tsconfig.json @@ -19,6 +19,7 @@ { "path": "../app-security-access-management" }, { "path": "../app-tenancy" }, { "path": "../app-tenant-manager" }, + { "path": "../lexical-editor-actions" }, { "path": "../lexical-editor-pb-element" }, { "path": "../plugins" } ], @@ -63,6 +64,8 @@ "@webiny/app-tenancy": ["../app-tenancy/src"], "@webiny/app-tenant-manager/*": ["../app-tenant-manager/src/*"], "@webiny/app-tenant-manager": ["../app-tenant-manager/src"], + "@webiny/lexical-editor-actions/*": ["../lexical-editor-actions/src/*"], + "@webiny/lexical-editor-actions": ["../lexical-editor-actions/src"], "@webiny/lexical-editor-pb-element/*": ["../lexical-editor-pb-element/src/*"], "@webiny/lexical-editor-pb-element": ["../lexical-editor-pb-element/src"], "@webiny/plugins/*": ["../plugins/src/*"], diff --git a/packages/lexical-editor-actions/package.json b/packages/lexical-editor-actions/package.json index 26af37ad804..360fbffaa8e 100644 --- a/packages/lexical-editor-actions/package.json +++ b/packages/lexical-editor-actions/package.json @@ -6,11 +6,10 @@ "@webiny/app-page-builder": "0.0.0", "@webiny/app-page-builder-elements": "0.0.0", "@webiny/lexical-editor": "0.0.0", - "@webiny/react-composition": "0.0.0", "classnames": "^2.3.1", "emotion": "^10.0.27", "react": "^17.0.2", - "react-color": "^2.14.3", + "react-color": "^2.14.1", "react-dom": "^17.0.2" }, "devDependencies": { diff --git a/packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx b/packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx index 97ef5253732..e4d27bfbc8e 100644 --- a/packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx +++ b/packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx @@ -2,21 +2,11 @@ import { LexicalEditorConfig } from "@webiny/lexical-editor"; import React from "react"; import { LexicalColorPickerDropdown } from "~/components/LexicalColorPickerDropdown"; import { FontColorAction } from "@webiny/lexical-editor"; -import { makeComposable, createComponentPlugin } from "@webiny/react-composition"; -const LexicalEditorConfiguration = makeComposable("LexicalEditorConfiguration", () => { +export const LexicalEditorActions = () => { return ( } /> ); -}); - -export const LexicalEditorConfigurationPlugin = createComponentPlugin( - LexicalEditorConfiguration, - Original => { - return function LexicalEditorConfigurationPlugin() { - return ; - }; - } -); +}; diff --git a/packages/lexical-editor-actions/src/components/LexicalColorPicker/LexicalColorPicker.tsx b/packages/lexical-editor-actions/src/components/LexicalColorPicker/LexicalColorPicker.tsx index 55d2101fa01..6ee65cdccb1 100644 --- a/packages/lexical-editor-actions/src/components/LexicalColorPicker/LexicalColorPicker.tsx +++ b/packages/lexical-editor-actions/src/components/LexicalColorPicker/LexicalColorPicker.tsx @@ -193,11 +193,14 @@ export const LexicalColorPicker: React.FC = ({ // With page elements implementation, we want to store the color key and // then the actual color will be retrieved from the theme object. let value = color; + let themeColorName; if (!isLegacyRenderingEngine) { - value = key; + const colors = pageElements.theme?.styles?.colors; + themeColorName = key; + value = colors[key]; } - onChangeComplete(value); + onChangeComplete(value, themeColorName); }} /> diff --git a/packages/lexical-editor-actions/src/index.ts b/packages/lexical-editor-actions/src/index.ts index a03b2541b4b..17d263b6fd7 100644 --- a/packages/lexical-editor-actions/src/index.ts +++ b/packages/lexical-editor-actions/src/index.ts @@ -1 +1 @@ -export { LexicalEditorConfigurationPlugin } from "~/LexicalEditorConfigurationPlugin"; +export { LexicalEditorActions } from "~/LexicalEditorConfigurationPlugin"; diff --git a/packages/lexical-editor-actions/tsconfig.build.json b/packages/lexical-editor-actions/tsconfig.build.json index 6a28bd1e313..3e8742f1c5c 100644 --- a/packages/lexical-editor-actions/tsconfig.build.json +++ b/packages/lexical-editor-actions/tsconfig.build.json @@ -2,10 +2,9 @@ "extends": "../../tsconfig.build.json", "include": ["src"], "references": [ - { "path": "../lexical-editor/tsconfig.build.json" }, { "path": "../app-page-builder/tsconfig.build.json" }, { "path": "../app-page-builder-elements/tsconfig.build.json" }, - { "path": "../react-composition/tsconfig.build.json" } + { "path": "../lexical-editor/tsconfig.build.json" } ], "compilerOptions": { "rootDir": "./src", diff --git a/packages/lexical-editor-actions/tsconfig.json b/packages/lexical-editor-actions/tsconfig.json index a2359655b3a..b0f8095efea 100644 --- a/packages/lexical-editor-actions/tsconfig.json +++ b/packages/lexical-editor-actions/tsconfig.json @@ -2,10 +2,9 @@ "extends": "../../tsconfig.json", "include": ["src", "__tests__/**/*.ts"], "references": [ - { "path": "../lexical-editor" }, { "path": "../app-page-builder" }, { "path": "../app-page-builder-elements" }, - { "path": "../react-composition" } + { "path": "../lexical-editor" } ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], @@ -14,14 +13,12 @@ "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"], - "@webiny/lexical-editor/*": ["../lexical-editor/src/*"], - "@webiny/lexical-editor": ["../lexical-editor/src"], "@webiny/app-page-builder/*": ["../app-page-builder/src/*"], "@webiny/app-page-builder": ["../app-page-builder/src"], "@webiny/app-page-builder-elements/*": ["../app-page-builder-elements/src/*"], "@webiny/app-page-builder-elements": ["../app-page-builder-elements/src"], - "@webiny/react-composition/*": ["../react-composition/src/*"], - "@webiny/react-composition": ["../react-composition/src"] + "@webiny/lexical-editor/*": ["../lexical-editor/src/*"], + "@webiny/lexical-editor": ["../lexical-editor/src"] }, "baseUrl": "." } diff --git a/packages/lexical-editor-pb-element/src/LexicalEditor.tsx b/packages/lexical-editor-pb-element/src/LexicalEditor.tsx index e66d4e273d8..92c394bb422 100644 --- a/packages/lexical-editor-pb-element/src/LexicalEditor.tsx +++ b/packages/lexical-editor-pb-element/src/LexicalEditor.tsx @@ -3,6 +3,7 @@ import { HeadingEditor, ParagraphEditor } from "@webiny/lexical-editor"; import { LexicalValue } from "@webiny/lexical-editor/types"; import { isHeadingTag } from "~/utils/isHeadingTag"; import { isParagraphTag } from "~/utils/isParagraphTag"; +import { usePageElements } from "@webiny/app-page-builder-elements"; interface LexicalEditorProps { tag: string | [string, Record]; @@ -15,11 +16,14 @@ interface LexicalEditorProps { } export const LexicalEditor: React.FC = ({ tag, value, onChange, ...rest }) => { + const { theme } = usePageElements(); return ( <> - {isHeadingTag(tag) && } + {isHeadingTag(tag) && ( + + )} {isParagraphTag(tag) ? ( - + ) : null} ); diff --git a/packages/lexical-editor-pb-element/src/components/PeTextRenderer.tsx b/packages/lexical-editor-pb-element/src/components/PeTextRenderer.tsx index 4c79eb03ff5..2456808f2c2 100644 --- a/packages/lexical-editor-pb-element/src/components/PeTextRenderer.tsx +++ b/packages/lexical-editor-pb-element/src/components/PeTextRenderer.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { createRenderer, useRenderer } from "@webiny/app-page-builder-elements"; +import { createRenderer, usePageElements, useRenderer } from "@webiny/app-page-builder-elements"; import { useElementVariableValue } from "@webiny/app-page-builder/editor/hooks/useElementVariableValue"; import { LexicalHtmlRenderer } from "@webiny/lexical-editor/components/LexicalHtmlRenderer"; @@ -10,6 +10,7 @@ export const PeTextRenderer = createRenderer(() => { const { getElement } = useRenderer(); const element = getElement(); const variableValue = useElementVariableValue(element); + const { theme } = usePageElements(); const __html = variableValue || element.data.text.data.text; - return ; + return ; }); diff --git a/packages/lexical-editor-pb-element/src/render/plugins/TextElementRendererPlugin.tsx b/packages/lexical-editor-pb-element/src/render/plugins/TextElementRendererPlugin.tsx index 378df1d9056..e64c524c3da 100644 --- a/packages/lexical-editor-pb-element/src/render/plugins/TextElementRendererPlugin.tsx +++ b/packages/lexical-editor-pb-element/src/render/plugins/TextElementRendererPlugin.tsx @@ -4,6 +4,7 @@ import { createComponentPlugin } from "@webiny/react-composition"; import TextElement from "@webiny/app-page-builder/render/components/Text"; import { isValidLexicalData, LexicalHtmlRenderer } from "@webiny/lexical-editor"; import { LexicalValue } from "@webiny/lexical-editor/types"; +import { usePageElements } from "@webiny/app-page-builder-elements"; const DATA_NAMESPACE = "data.text"; @@ -15,8 +16,9 @@ const DATA_NAMESPACE = "data.text"; export const TextElementRendererPlugin = createComponentPlugin(TextElement, Original => { return function TextElementRendererPlugin({ element, rootClassName }): JSX.Element { const textContent = get(element, `${DATA_NAMESPACE}.data.text`) as LexicalValue; + const { theme } = usePageElements(); return isValidLexicalData(textContent) ? ( - + ) : ( ); diff --git a/packages/lexical-editor/src/components/AddRichTextEditorPlugin.tsx b/packages/lexical-editor/src/components/AddRichTextEditorPlugin.tsx index b44d6bf91fc..28ec094466d 100644 --- a/packages/lexical-editor/src/components/AddRichTextEditorPlugin.tsx +++ b/packages/lexical-editor/src/components/AddRichTextEditorPlugin.tsx @@ -2,12 +2,17 @@ import React, { FC } from "react"; import { createComponentPlugin } from "@webiny/react-composition"; import { RichTextEditor } from "~/components/Editor/RichTextEditor"; import { LexicalValue } from "~/types"; +import { WebinyTheme } from "~/themes/webinyLexicalTheme"; interface AddRichTextEditorProps { toolbar: React.ReactNode; placeholder?: string; value: LexicalValue; children?: React.ReactNode; + /* + * @description Theme to be injected into lexical editor + */ + theme: WebinyTheme; } export const AddRichTextEditorPlugin: FC = ({ @@ -17,7 +22,7 @@ export const AddRichTextEditorPlugin: FC = ({ }) => { const RichTextEditorPlugin = React.memo( createComponentPlugin(RichTextEditor, Original => { - return function RichTextEditorElem({ tag, value, onChange }): JSX.Element { + return function RichTextEditorElem({ tag, value, onChange, theme }): JSX.Element { return ( = ({ placeholder={placeholder} value={value} onChange={onChange} + theme={theme} > {children} diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index 2ac4e30c021..501e34df39c 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -17,7 +17,7 @@ import { isValidLexicalData } from "~/utils/isValidLexicalData"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; -import { webinyEditorTheme } from "~/themes/webinyLexicalTheme"; +import { webinyEditorTheme, WebinyTheme } from "~/themes/webinyLexicalTheme"; import { WebinyNodes } from "~/nodes/webinyNodes"; export interface RichTextEditorProps { @@ -35,6 +35,10 @@ export interface RichTextEditorProps { onBlur?: (editorState: LexicalValue) => void; height?: number | string; width?: number | string; + /* + * @description Theme to be injected into lexical editor + */ + theme: WebinyTheme; } const BaseRichTextEditor: React.FC = ({ @@ -47,13 +51,11 @@ const BaseRichTextEditor: React.FC = ({ onBlur, focus, width, - height + height, + theme }: RichTextEditorProps) => { const placeholderElem = {placeholder || "Enter text..."}; const scrollRef = useRef(null); - // const { theme } = usePageElements(); - const theme = { styles: {}}; - const [floatingAnchorElem, setFloatingAnchorElem] = useState( undefined ); @@ -76,7 +78,7 @@ const BaseRichTextEditor: React.FC = ({ throw error; }, nodes: [...WebinyNodes, ...(nodes || [])], - theme: { ...webinyEditorTheme, styles: theme.styles } + theme: { ...webinyEditorTheme, styles: theme?.styles } }; function handleOnChange(editorState: EditorState, editor: LexicalEditor) { diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 1e9902c6af0..a8cdb7af474 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -9,16 +9,22 @@ import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; import { WebinyNodes } from "~/nodes/webinyNodes"; -import { webinyEditorTheme } from "~/themes/webinyLexicalTheme"; +import { webinyEditorTheme, WebinyTheme } from "~/themes/webinyLexicalTheme"; interface LexicalHtmlRendererProps { nodes?: Klass[]; value: LexicalValue | null; + /* + * @description Theme to be injected into lexical editor + */ + theme: WebinyTheme; } -export const LexicalHtmlRenderer: React.FC = ({ nodes, value }) => { - // const { theme } = usePageElements(); - const theme = { styles: {}}; +export const LexicalHtmlRenderer: React.FC = ({ + nodes, + value, + theme +}) => { const initialConfig = { editorState: isValidLexicalData(value) ? value : generateInitialLexicalValue(), namespace: "webiny", diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 01d30143922..95939e3d401 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -32,16 +32,6 @@ export interface FontColorAction extends React.FC { export const FontColorAction: FontColorAction = () => { const [editor] = useLexicalComposerContext(); const [fontColor, setFontColor] = useState("#000"); - // const { theme } = usePageElements(); - const theme = { styles: {}} as any; - - const isThemeColorName = (color: string): boolean => { - return !!theme?.styles?.colors[color]; - }; - - const getThemeColor = (colorValue: string): string => { - return isThemeColorName(colorValue) ? theme?.styles?.colors[colorValue] : colorValue; - }; const setFontColorSelect = useCallback( (fontColorValue: string) => { @@ -50,21 +40,16 @@ export const FontColorAction: FontColorAction = () => { [fontColor] ); - const onFontColorSelect = useCallback((colorValue: string) => { - const color = getThemeColor(colorValue); - const isThemeColor = isThemeColorName(colorValue); - const themeColorName = isThemeColor ? colorValue : undefined; - setFontColorSelect(colorValue); - const payloadData = { - color, - themeColorName, - isThemeColor - }; - editor.dispatchCommand>( - ADD_FONT_COLOR_COMMAND, - payloadData - ); - }, []); + const onFontColorSelect = useCallback( + (colorValue: string, themeColorName: string | undefined) => { + setFontColorSelect(colorValue); + editor.dispatchCommand>(ADD_FONT_COLOR_COMMAND, { + color: colorValue, + themeColorName + }); + }, + [] + ); const updatePopup = useCallback(() => { editor.getEditorState().read(() => { diff --git a/packages/lexical-editor/src/context/FontColorActionContext.tsx b/packages/lexical-editor/src/context/FontColorActionContext.tsx index b37555d3be1..6a62f61a6ed 100644 --- a/packages/lexical-editor/src/context/FontColorActionContext.tsx +++ b/packages/lexical-editor/src/context/FontColorActionContext.tsx @@ -10,7 +10,7 @@ export interface FontColorActionContext { * @desc Apply color to selected text. * @params: value */ - applyColor: (value: string) => void; + applyColor: (value: string, themeColorName: string | undefined) => void; } export const FontColorActionContext = React.createContext( diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index 3bad01c2f64..653300e9d72 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -17,8 +17,9 @@ export const ADD_FONT_COLOR_COMMAND: LexicalCommand = const FontColorNodeAttrName = "font-color-theme"; export interface FontColorPayload { + // This color can be hex string color: string; - isThemeColor: boolean; + // webiny theme color variable like color1, color2... themeColorName: string | undefined; caption?: LexicalEditor; key?: NodeKey; diff --git a/packages/lexical-editor/tsconfig.build.json b/packages/lexical-editor/tsconfig.build.json index 08fc5934245..d0eb9513334 100644 --- a/packages/lexical-editor/tsconfig.build.json +++ b/packages/lexical-editor/tsconfig.build.json @@ -1,9 +1,7 @@ { "extends": "../../tsconfig.build.json", "include": ["src"], - "references": [ - { "path": "../react-composition/tsconfig.build.json" } - ], + "references": [{ "path": "../react-composition/tsconfig.build.json" }], "compilerOptions": { "rootDir": "./src", "outDir": "./dist", diff --git a/yarn.lock b/yarn.lock index eb9c699e9c0..3c1bb9f582c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13895,7 +13895,7 @@ __metadata: platform: ^1.3.5 prop-types: ^15.7.2 react: 17.0.2 - react-color: ^2.14.3 + react-color: ^2.14.1 react-dnd: ^11.1.3 react-dnd-html5-backend: ^11.1.3 react-dom: 17.0.2 @@ -14028,6 +14028,7 @@ __metadata: "@webiny/app-tenancy": 0.0.0 "@webiny/app-tenant-manager": 0.0.0 "@webiny/cli": 0.0.0 + "@webiny/lexical-editor-actions": 0.0.0 "@webiny/lexical-editor-pb-element": 0.0.0 "@webiny/plugins": 0.0.0 "@webiny/project-utils": 0.0.0 @@ -15136,9 +15137,9 @@ __metadata: languageName: unknown linkType: soft -"@webiny/lexical-editor-config@workspace:packages/lexical-editor-config": +"@webiny/lexical-editor-actions@0.0.0, @webiny/lexical-editor-actions@workspace:packages/lexical-editor-actions": version: 0.0.0-use.local - resolution: "@webiny/lexical-editor-config@workspace:packages/lexical-editor-config" + resolution: "@webiny/lexical-editor-actions@workspace:packages/lexical-editor-actions" dependencies: "@emotion/styled": ^10.0.27 "@webiny/app-page-builder": 0.0.0 @@ -15146,11 +15147,10 @@ __metadata: "@webiny/cli": 0.0.0 "@webiny/lexical-editor": 0.0.0 "@webiny/project-utils": 0.0.0 - "@webiny/react-composition": 0.0.0 classnames: ^2.3.1 emotion: ^10.0.27 react: ^17.0.2 - react-color: ^2.14.3 + react-color: ^2.14.1 react-dom: ^17.0.2 languageName: unknown linkType: soft @@ -36160,7 +36160,7 @@ __metadata: languageName: node linkType: hard -"react-color@npm:^2.14.1, react-color@npm:^2.14.3, react-color@npm:^2.17.0": +"react-color@npm:^2.14.1, react-color@npm:^2.17.0": version: 2.19.3 resolution: "react-color@npm:2.19.3" dependencies: From 38ce5cd5ab1486e5f3a5e5dcc9685f26549fd223 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 14 Mar 2023 12:39:14 +0100 Subject: [PATCH 18/57] refactor: rename to lexical editor action to match the component name --- ...alEditorConfigurationPlugin.tsx => LexicalEditorActions.tsx} | 0 packages/lexical-editor-actions/src/index.ts | 2 +- .../src/components/ToolbarActions/FontColorAction.tsx | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename packages/lexical-editor-actions/src/{LexicalEditorConfigurationPlugin.tsx => LexicalEditorActions.tsx} (100%) diff --git a/packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx b/packages/lexical-editor-actions/src/LexicalEditorActions.tsx similarity index 100% rename from packages/lexical-editor-actions/src/LexicalEditorConfigurationPlugin.tsx rename to packages/lexical-editor-actions/src/LexicalEditorActions.tsx diff --git a/packages/lexical-editor-actions/src/index.ts b/packages/lexical-editor-actions/src/index.ts index 17d263b6fd7..30c7805ba88 100644 --- a/packages/lexical-editor-actions/src/index.ts +++ b/packages/lexical-editor-actions/src/index.ts @@ -1 +1 @@ -export { LexicalEditorActions } from "~/LexicalEditorConfigurationPlugin"; +export { LexicalEditorActions } from "~/LexicalEditorActions"; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 95939e3d401..6b34c07974d 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -59,6 +59,7 @@ export const FontColorAction: FontColorAction = () => { } const node = getSelectedNode(selection); if ($isFontColorNode(node)) { + debugger; const colorStyle = node.getColorStyle(); setFontColor(colorStyle.color); } From 5b0004c3b2d0701ca49760fb57d416751072f19a Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 14 Mar 2023 21:58:28 +0100 Subject: [PATCH 19/57] wip: inital implementation of the typography action --- apps/theme/theme.ts | 14 +- .../src/LexicalEditorActions.tsx | 4 +- .../src/components/TypographyDropDown.tsx | 77 ++++++++++ .../src/components/Editor/RichTextEditor.tsx | 2 + .../LexicalEditorConfig.tsx | 3 + .../ToolbarActions/FontColorAction.tsx | 1 - .../ToolbarActions/TypographyAction.tsx | 97 +++++++++++++ .../ToolbarPresets/HeadingToolbarPreset.tsx | 2 + .../ToolbarPresets/ParagraphToolbarPreset.tsx | 2 + .../src/context/TypographyActionContext.tsx | 27 ++++ .../src/hooks/useTypographyAction.ts | 13 ++ packages/lexical-editor/src/index.tsx | 2 + .../src/nodes/TypographyNode.ts | 136 ++++++++++++++++++ .../lexical-editor/src/nodes/webinyNodes.ts | 4 +- .../TypographyPlugin/TypographyPlugin.tsx | 50 +++++++ packages/lexical-editor/src/types.ts | 1 + .../src/ui/ToolbarActionDialog.tsx | 116 --------------- .../src/utils/styleObjectToString.ts | 22 +++ 18 files changed, 447 insertions(+), 126 deletions(-) create mode 100644 packages/lexical-editor-actions/src/components/TypographyDropDown.tsx create mode 100644 packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx create mode 100644 packages/lexical-editor/src/context/TypographyActionContext.tsx create mode 100644 packages/lexical-editor/src/hooks/useTypographyAction.ts create mode 100644 packages/lexical-editor/src/nodes/TypographyNode.ts create mode 100644 packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx delete mode 100644 packages/lexical-editor/src/ui/ToolbarActionDialog.tsx create mode 100644 packages/lexical-editor/src/utils/styleObjectToString.ts diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index f7e57671226..5a31b2a985c 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -44,13 +44,13 @@ const paragraphs = { }; export const typography = { - heading1: { ...headings, fontWeight: "bold", fontSize: 48 }, - heading2: { ...headings, fontSize: 36 }, - heading3: { ...headings, fontSize: 30 }, - heading4: { ...headings, fontSize: 24 }, - heading5: { ...headings, fontSize: 20 }, - heading6: { ...headings, fontSize: 18, lineHeight: "1.75rem" }, - paragraph1: { ...paragraphs, fontSize: 16.5 }, + heading1: { ...headings, fontWeight: "bold", fontSize: "48px" }, + heading2: { ...headings, fontSize: "36px" }, + heading3: { ...headings, fontSize: "30px" }, + heading4: { ...headings, fontSize: "24px" }, + heading5: { ...headings, fontSize: "20px" }, + heading6: { ...headings, fontSize: "18px", lineHeight: "1.75rem" }, + paragraph1: { ...paragraphs, fontSize: "16.5px" }, paragraph2: { ...paragraphs, fontSize: 12.5, diff --git a/packages/lexical-editor-actions/src/LexicalEditorActions.tsx b/packages/lexical-editor-actions/src/LexicalEditorActions.tsx index e4d27bfbc8e..52f98ee0fd7 100644 --- a/packages/lexical-editor-actions/src/LexicalEditorActions.tsx +++ b/packages/lexical-editor-actions/src/LexicalEditorActions.tsx @@ -1,12 +1,14 @@ -import { LexicalEditorConfig } from "@webiny/lexical-editor"; +import { LexicalEditorConfig, TypographyAction } from "@webiny/lexical-editor"; import React from "react"; import { LexicalColorPickerDropdown } from "~/components/LexicalColorPickerDropdown"; import { FontColorAction } from "@webiny/lexical-editor"; +import { TypographyDropDown } from "~/components/TypographyDropDown"; export const LexicalEditorActions = () => { return ( } /> + } /> ); }; diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx new file mode 100644 index 00000000000..8e03d2e52de --- /dev/null +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { DropDown, DropDownItem, useTypographyAction } from "@webiny/lexical-editor"; +import { usePageElements } from "@webiny/app-page-builder-elements"; +import { TypographyValue } from "@webiny/lexical-editor/types"; + +const TYPOGRAPHY_DISPLAY_NAMES: Record = { + normal: "Normal", + heading1: "Heading 1", + heading2: "Heading 2", + heading3: "Heading 3", + heading4: "Heading 4", + heading5: "Heading 5", + heading6: "Heading 6", + paragraph1: "Paragraph 1", + paragraph2: "Paragraph 2" +}; + +export const TypographyDropDown = () => { + const { value, applyTypography } = useTypographyAction(); + const { theme } = usePageElements(); + const typographyStyles = theme.styles?.typography; + + const getTypographyDisplayName = (themeTypographyName: string): string => { + const name = TYPOGRAPHY_DISPLAY_NAMES[themeTypographyName]; + return name ? name : themeTypographyName; + }; + + const hasTypographyStyles = (): boolean => { + return !!typographyStyles; + }; + + const typographyList = (): TypographyValue[] => { + const list: TypographyValue[] = []; + for (const key in typographyStyles) { + const styleObject = typographyStyles[key]; + // filter only headings and paragraphs + if (key.includes("heading") || key.includes("paragraph")) { + const typographyValue = { + styleObject, + themeTypographyName: key, + displayName: getTypographyDisplayName(key) + }; + list.push(typographyValue); + } + } + return list; + }; + + return ( + <> + {theme && hasTypographyStyles() ? ( + + {typographyList().map(option => ( + applyTypography(option)} + key={option.themeTypographyName} + > + {option.displayName} + + ))} + + ) : null} + + ); +}; diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index 501e34df39c..10ee8f9fa72 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -19,6 +19,7 @@ import { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin"; import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; import { webinyEditorTheme, WebinyTheme } from "~/themes/webinyLexicalTheme"; import { WebinyNodes } from "~/nodes/webinyNodes"; +import { TypographyPlugin } from "~/plugins/TypographyPlugin/TypographyPlugin"; export interface RichTextEditorProps { toolbar?: React.ReactNode; @@ -99,6 +100,7 @@ const BaseRichTextEditor: React.FC = ({ {value && } + {/* Events */} {onBlur && } {focus && } diff --git a/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx b/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx index 0699e2baf1e..e602a890908 100644 --- a/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx +++ b/packages/lexical-editor/src/components/LexicalEditorConfig/LexicalEditorConfig.tsx @@ -1,8 +1,10 @@ import React from "react"; import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; +import { TypographyAction } from "~/components/ToolbarActions/TypographyAction"; interface LexicalEditorConfig extends React.FC { FontColorAction: typeof FontColorAction; + TypographyAction: typeof TypographyAction; } export const LexicalEditorConfig: LexicalEditorConfig = ({ children }) => { @@ -10,3 +12,4 @@ export const LexicalEditorConfig: LexicalEditorConfig = ({ children }) => { }; LexicalEditorConfig.FontColorAction = FontColorAction; +LexicalEditorConfig.TypographyAction = TypographyAction; diff --git a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx index 6b34c07974d..95939e3d401 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/FontColorAction.tsx @@ -59,7 +59,6 @@ export const FontColorAction: FontColorAction = () => { } const node = getSelectedNode(selection); if ($isFontColorNode(node)) { - debugger; const colorStyle = node.getColorStyle(); setFontColor(colorStyle.color); } diff --git a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx new file mode 100644 index 00000000000..e5970bafde5 --- /dev/null +++ b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx @@ -0,0 +1,97 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { $getSelection, $isRangeSelection, LexicalCommand } from "lexical"; +import { Compose, makeComposable } from "@webiny/react-composition"; +import { getSelectedNode } from "~/utils/getSelectedNode"; +import { TypographyActionContext, TypographyValue } from "~/context/TypographyActionContext"; +import { + $isTypographyNode, + ADD_TYPOGRAPHY_COMMAND, + TypographyPayload +} from "~/nodes/TypographyNode"; + +/* + * Base composable action component that is mounted on toolbar action as a placeholder for the custom toolbar action. + * Note: Toa add custom component access trough @see LexicalEditorConfig API + * */ +export const BaseTypographyActionDropDown = makeComposable( + "BaseTypographyActionDropDown", + (): JSX.Element | null => { + useEffect(() => { + console.log("Default BaseTypographyActionDropDown, please add your own component"); + }, []); + return null; + } +); + +interface TypographyActionDropdownProps { + element: JSX.Element; +} + +const TypographyActionDropDown: React.FC = ({ + element +}): JSX.Element => { + return () => element} />; +}; + +export interface TypographyAction extends React.FC { + TypographyDropDown: typeof TypographyActionDropDown; +} + +export const TypographyAction: TypographyAction = () => { + const [editor] = useLexicalComposerContext(); + const [typography, setTypography] = useState(); + + const setTypographySelect = useCallback( + (value: TypographyValue) => { + setTypography(value); + }, + [typography] + ); + + const onTypographySelect = useCallback((value: TypographyValue) => { + console.log("Value", value); + setTypographySelect(value); + editor.dispatchCommand>(ADD_TYPOGRAPHY_COMMAND, { + value + }); + }, []); + + /*const updatePopup = useCallback(() => { + editor.getEditorState().read(() => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return; + } + const node = getSelectedNode(selection); + if ($isTypographyNode(node)) { + // const colorStyle = node.getColorStyle(); + // setFontColor(colorStyle.color); + console.log("$isTypographyNode", node.getTypographyValue()) + } + }); + }, [editor]); + + useEffect(() => { + document.addEventListener("selectionchange", updatePopup); + return () => { + document.removeEventListener("selectionchange", updatePopup); + }; + }, [updatePopup])*/ + + return ( + + + + ); +}; + +{ + /* Color action settings */ +} +TypographyAction.TypographyDropDown = TypographyActionDropDown; diff --git a/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx b/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx index e15c8497cf2..fd3a95bfd72 100644 --- a/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx +++ b/packages/lexical-editor/src/components/ToolbarPresets/HeadingToolbarPreset.tsx @@ -8,12 +8,14 @@ import { LinkAction } from "~/components/ToolbarActions/LinkAction"; import { FontSizeAction } from "~/components/ToolbarActions/FontSizeAction"; import { Divider } from "~/ui/Divider"; import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; +import { TypographyAction } from "~/components/ToolbarActions/TypographyAction"; export const HeadingToolbarPreset = () => { return ( <> } type={"heading"} /> } type={"heading"} /> + } type={"heading"} /> } type={"heading"} /> } type={"heading"} /> } type={"heading"} /> diff --git a/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx b/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx index 7b39dbe3e07..61d04dfd460 100644 --- a/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx +++ b/packages/lexical-editor/src/components/ToolbarPresets/ParagraphToolbarPreset.tsx @@ -11,12 +11,14 @@ import { NumberedListAction } from "~/components/ToolbarActions/NumberedListActi import { BulletListAction } from "~/components/ToolbarActions/BulletListAction"; import { QuoteAction } from "~/components/ToolbarActions/QuoteAction"; import { FontColorAction } from "~/components/ToolbarActions/FontColorAction"; +import { TypographyAction } from "~/components/ToolbarActions/TypographyAction"; export const ParagraphToolbarPreset = () => { return ( <> } type={"paragraph"} /> } type={"paragraph"} /> + } type={"paragraph"} /> } type={"paragraph"} /> } type={"paragraph"} /> } type={"paragraph"} /> diff --git a/packages/lexical-editor/src/context/TypographyActionContext.tsx b/packages/lexical-editor/src/context/TypographyActionContext.tsx new file mode 100644 index 00000000000..db76d7fb6db --- /dev/null +++ b/packages/lexical-editor/src/context/TypographyActionContext.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +export type TypographyValue = { + // CSSObject type + styleObject: Record; + // variable name defined in the theme + themeTypographyName: string; + // Show on UI + displayName: string; +}; + +export interface TypographyActionContextProps { + /* + * @desc Current selected typography + * */ + value: TypographyValue | undefined; + + /* + * @desc Apply font family to selected text. + * @params: value + */ + applyTypography: (value: TypographyValue) => void; +} + +export const TypographyActionContext = React.createContext< + TypographyActionContextProps | undefined +>(undefined); diff --git a/packages/lexical-editor/src/hooks/useTypographyAction.ts b/packages/lexical-editor/src/hooks/useTypographyAction.ts new file mode 100644 index 00000000000..930408efa24 --- /dev/null +++ b/packages/lexical-editor/src/hooks/useTypographyAction.ts @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { TypographyActionContext } from "~/context/TypographyActionContext"; + +export function useTypographyAction() { + const context = useContext(TypographyActionContext); + if (!context) { + throw Error( + `Missing TypographyActionContext in the component hierarchy. Are you using "useTypographyAction()" in the right place?` + ); + } + + return context; +} diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 7aa779d9fcd..c9421b48480 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -3,6 +3,7 @@ export { LexicalHtmlRenderer } from "~/components/LexicalHtmlRenderer"; // hooks export { useRichTextEditor } from "~/hooks/useRichTextEditor"; export { useFontColorPicker } from "~/hooks/useFontColorPicker"; +export { useTypographyAction } from "~/hooks/useTypographyAction"; // UI elements export { Divider } from "~/ui/Divider"; export { DropDownItem } from "~/ui/DropDown"; @@ -18,6 +19,7 @@ export { LinkAction } from "~/components/ToolbarActions/LinkAction"; export { NumberedListAction } from "~/components/ToolbarActions/NumberedListAction"; export { QuoteAction } from "~/components/ToolbarActions/QuoteAction"; export { UnderlineAction } from "~/components/ToolbarActions/UnderlineAction"; +export { TypographyAction } from "~/components/ToolbarActions/TypographyAction"; // toolbars export { HeadingToolbar } from "~/components/Toolbar/HeadingToolbar"; export { ParagraphToolbar } from "~/components/Toolbar/ParagraphToolbar"; diff --git a/packages/lexical-editor/src/nodes/TypographyNode.ts b/packages/lexical-editor/src/nodes/TypographyNode.ts new file mode 100644 index 00000000000..0760988622d --- /dev/null +++ b/packages/lexical-editor/src/nodes/TypographyNode.ts @@ -0,0 +1,136 @@ +import { + createCommand, + EditorConfig, + LexicalCommand, + LexicalEditor, + LexicalNode, + NodeKey, + RangeSelection, + SerializedTextNode, + Spread, + TextNode +} from "lexical"; +import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; +import { TypographyValue } from "~/context/TypographyActionContext"; +import { styleObjectToString } from "~/utils/styleObjectToString"; + +export const ADD_TYPOGRAPHY_COMMAND: LexicalCommand = + createCommand("ADD_TYPOGRAPHY_COMMAND"); +const TypographyNodeAttrName = "typography-theme"; + +export interface TypographyPayload { + value: TypographyValue; + caption?: LexicalEditor; + key?: NodeKey; +} + +type ThemeTypographyName = "normal" | string; + +export type SerializedTypographyNode = Spread< + { + themeTypographyName: ThemeTypographyName; + typographyStyles: Record; + type: "typography-node"; + version: 1; + }, + SerializedTextNode +>; + +/** + * Main responsibility of this node is to apply custom or Webiny theme typography to selected text. + * Extends the original TextNode node to add additional transformation and support for webiny theme typography. + */ +export class TypographyNode extends TextNode { + __themeTypographyName: ThemeTypographyName; + __typographyStyles: Record; + + constructor( + text: string, + typographyStyles: Record, + themeTypographyName?: ThemeTypographyName, + key?: NodeKey + ) { + super(text, key); + this.__themeTypographyName = themeTypographyName || "default"; + this.__typographyStyles = typographyStyles; + } + + static override getType(): string { + return "typography-node"; + } + + static override clone(node: TypographyNode): TypographyNode { + return new TypographyNode(node.__text, node.__color, node.__themeColor, node.__key); + } + + static override importJSON(serializedNode: SerializedTypographyNode): TextNode { + const node = new TypographyNode( + serializedNode.text, + serializedNode.typographyStyles, + serializedNode.themeTypographyName + ); + node.setTextContent(serializedNode.text); + node.setFormat(serializedNode.format); + node.setDetail(serializedNode.detail); + node.setMode(serializedNode.mode); + node.setStyle(serializedNode.style); + return node; + } + + override exportJSON(): SerializedTypographyNode { + return { + ...super.exportJSON(), + themeTypographyName: this.__themeTypographyName, + typographyStyles: this.__typographyStyles, + type: "typography-node", + version: 1 + }; + } + + addStylesHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { + // if theme is available get the latest typography value + if (theme?.styles?.typography) { + this.__typographyStyles = theme.styles.typography[this.__themeTypographyName]; + } + + element.setAttribute(TypographyNodeAttrName, this.__themeTypographyName); + element.style.cssText = styleObjectToString(this.__typographyStyles); + return element; + } + + override updateDOM(prevNode: TypographyNode, dom: HTMLElement, config: EditorConfig): boolean { + const isUpdated = super.updateDOM(prevNode, dom, config); + dom = this.addStylesHTMLElement(dom, config.theme); + return isUpdated; + } + + getTypographyValue(): { themeTypographyName: string; styleObject: Record } { + return { + themeTypographyName: this.__themeTypographyName, + styleObject: this.__typographyStyles + }; + } + + override createDOM(config: EditorConfig): HTMLElement { + const element = super.createDOM(config); + return this.addStylesHTMLElement(element, config.theme); + } +} + +export const $createTypographyNode = ( + text: string, + typographyStyles: Record, + themeTypographyName?: ThemeTypographyName, + key?: NodeKey +): TypographyNode => { + return new TypographyNode(text, typographyStyles, themeTypographyName, key); +}; + +export const $isTypographyNode = (node: LexicalNode): boolean => { + return node instanceof TypographyNode; +}; + +export function $applyStylesToNode(node: TypographyNode, nodeStyleProvider: RangeSelection) { + node.setFormat(nodeStyleProvider.format); + node.setStyle(nodeStyleProvider.style); +} diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/webinyNodes.ts index e23cb6860e1..1572693f5bd 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/webinyNodes.ts @@ -8,6 +8,7 @@ import { MarkNode } from "@lexical/mark"; import { OverflowNode } from "@lexical/overflow"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; import { FontColorNode } from "~/nodes/FontColorNode"; +import { TypographyNode } from "~/nodes/TypographyNode"; export const WebinyNodes: Array> = [ HeadingNode, @@ -21,5 +22,6 @@ export const WebinyNodes: Array> = [ LinkNode, OverflowNode, MarkNode, - FontColorNode + FontColorNode, + TypographyNode ]; diff --git a/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx b/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx new file mode 100644 index 00000000000..05487c2045b --- /dev/null +++ b/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { + $createParagraphNode, + $getSelection, + $insertNodes, + $isRangeSelection, + $isRootOrShadowRoot, + COMMAND_PRIORITY_EDITOR +} from "lexical"; +import { $wrapNodeInElement } from "@lexical/utils"; +import { + $applyStylesToNode, + $createTypographyNode, + ADD_TYPOGRAPHY_COMMAND, + TypographyPayload +} from "~/nodes/TypographyNode"; + +export const TypographyPlugin: React.FC = () => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerCommand( + ADD_TYPOGRAPHY_COMMAND, + payload => { + editor.update(() => { + const { value } = payload; + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + const typographyNode = $createTypographyNode( + selection.getTextContent(), + value.styleObject, + value.themeTypographyName + ); + $applyStylesToNode(typographyNode, selection); + $insertNodes([typographyNode]); + if ($isRootOrShadowRoot(typographyNode.getParentOrThrow())) { + $wrapNodeInElement(typographyNode, $createParagraphNode).selectEnd(); + } + } + }); + return true; + }, + COMMAND_PRIORITY_EDITOR + ); + }, [editor]); + + return null; +}; diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index d8472382d99..3b2592866b1 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -1,3 +1,4 @@ export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; +export { TypographyValue } from "~/context/TypographyActionContext"; diff --git a/packages/lexical-editor/src/ui/ToolbarActionDialog.tsx b/packages/lexical-editor/src/ui/ToolbarActionDialog.tsx deleted file mode 100644 index 35bd9d79ced..00000000000 --- a/packages/lexical-editor/src/ui/ToolbarActionDialog.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import * as React from "react"; - -function MenuContainer({ - children, - menuContainerRef, - onClose -}: { - children: React.ReactNode | React.ReactNode[]; - menuContainerRef?: React.Ref; - onClose: () => void; -}) { - const handleKeyDown = (event: React.KeyboardEvent) => { - const key = event.key; - - if (["Escape", "ArrowUp", "ArrowDown", "Tab"].includes(key)) { - event.preventDefault(); - } - - if (key === "Escape" || key === "Tab") { - onClose(); - } - }; - - const handleContainerClick = (e: React.MouseEvent) => { - e.preventDefault(); - console.log("click", e); - }; - - return ( -
-
handleContainerClick(e)} - style={{ - position: "absolute", - top: -10, - left: 0, - width: 240, - backgroundColor: "#fff" - }} - ref={menuContainerRef ?? null} - onKeyDown={handleKeyDown} - > - {children} -
-
- ); -} -interface ToolbarActionDialogProps { - disabled: boolean; - buttonLabel?: string; - buttonAriaLabel: string; - buttonClassName: string; - buttonIconClassName: string; - children: React.ReactNode | React.ReactNode[]; - stopCloseOnClickSelf?: boolean; -} - -export const ToolbarActionDialog: React.FC = ({ - disabled, - buttonAriaLabel, - buttonClassName, - buttonIconClassName, - buttonLabel, - children, - stopCloseOnClickSelf -}): JSX.Element => { - const menuWindowRef = useRef(null); - const [showDropDown, setShowDropDown] = useState(false); - - const handleClose = () => { - debugger; - if (menuWindowRef && menuWindowRef.current) { - setShowDropDown(false); - menuWindowRef.current.focus(); - } - }; - - useEffect(() => { - if (!showDropDown) { - return; - } - - const handle = (event: MouseEvent) => { - /* const target = event.target; - if (!button.contains(target as Node)) { - setShowDropDown(false); - }*/ - console.log("handle", event); - }; - document.addEventListener("click", handle); - - return () => { - document.removeEventListener("click", handle); - }; - }, [showDropDown, stopCloseOnClickSelf]); - - return ( -
- - {showDropDown && {children}} -
- ); -}; diff --git a/packages/lexical-editor/src/utils/styleObjectToString.ts b/packages/lexical-editor/src/utils/styleObjectToString.ts new file mode 100644 index 00000000000..ba722c1f93f --- /dev/null +++ b/packages/lexical-editor/src/utils/styleObjectToString.ts @@ -0,0 +1,22 @@ +/* + * Converts CSS style objects to string + * Example: + * { fontSize: '10px' } => "font-size: 10px" + * */ +export const styleObjectToString = (styleObject: Record): string => { + if (!styleObject) { + return styleObject; + } + return Object.keys(styleObject).reduce( + (acc, key) => + acc + + key + .split(/(?=[A-Z])/) + .join("-") + .toLowerCase() + + ":" + + styleObject[key] + + ";", + "" + ); +}; From 1ddeda3281493c07ec98069fdb48ce5dd53cd9c5 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 15 Mar 2023 15:32:08 +0100 Subject: [PATCH 20/57] wip: change typography text node with element node, support heading paragrap, selection recognition --- .../src/components/TypographyDropDown.tsx | 54 +++--- .../ToolbarActions/TypographyAction.tsx | 91 +++++++--- .../src/context/TypographyActionContext.tsx | 10 +- packages/lexical-editor/src/index.tsx | 1 + .../src/nodes/TypographyElementNode.ts | 166 ++++++++++++++++++ .../src/nodes/TypographyNode.ts | 136 -------------- .../lexical-editor/src/nodes/webinyNodes.ts | 4 +- .../TypographyPlugin/TypographyPlugin.tsx | 39 +--- packages/lexical-editor/src/types.ts | 10 +- .../lexical-editor/src/utils/typography.ts | 28 +++ 10 files changed, 312 insertions(+), 227 deletions(-) create mode 100644 packages/lexical-editor/src/nodes/TypographyElementNode.ts delete mode 100644 packages/lexical-editor/src/nodes/TypographyNode.ts create mode 100644 packages/lexical-editor/src/utils/typography.ts diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx index 8e03d2e52de..f473d119fbb 100644 --- a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -1,18 +1,18 @@ import React from "react"; -import { DropDown, DropDownItem, useTypographyAction } from "@webiny/lexical-editor"; +import { + DropDown, + DropDownItem, + getTypographyMetaByName, + useTypographyAction +} from "@webiny/lexical-editor"; import { usePageElements } from "@webiny/app-page-builder-elements"; -import { TypographyValue } from "@webiny/lexical-editor/types"; +import { TypographyHTMLTag } from "@webiny/lexical-editor/types"; -const TYPOGRAPHY_DISPLAY_NAMES: Record = { - normal: "Normal", - heading1: "Heading 1", - heading2: "Heading 2", - heading3: "Heading 3", - heading4: "Heading 4", - heading5: "Heading 5", - heading6: "Heading 6", - paragraph1: "Paragraph 1", - paragraph2: "Paragraph 2" +type TypographyDropDownItem = { + styleObject: Record; + themeTypographyName: string; + htmlTag: TypographyHTMLTag; + displayName: string; }; export const TypographyDropDown = () => { @@ -20,27 +20,23 @@ export const TypographyDropDown = () => { const { theme } = usePageElements(); const typographyStyles = theme.styles?.typography; - const getTypographyDisplayName = (themeTypographyName: string): string => { - const name = TYPOGRAPHY_DISPLAY_NAMES[themeTypographyName]; - return name ? name : themeTypographyName; - }; - const hasTypographyStyles = (): boolean => { return !!typographyStyles; }; - const typographyList = (): TypographyValue[] => { - const list: TypographyValue[] = []; + const typographyList = (): TypographyDropDownItem[] => { + const list: TypographyDropDownItem[] = []; for (const key in typographyStyles) { const styleObject = typographyStyles[key]; + const metadata = getTypographyMetaByName(key); // filter only headings and paragraphs if (key.includes("heading") || key.includes("paragraph")) { - const typographyValue = { + list.push({ styleObject, themeTypographyName: key, - displayName: getTypographyDisplayName(key) - }; - list.push(typographyValue); + htmlTag: metadata.htmlTag, + displayName: metadata.displayName + }); } } return list; @@ -52,7 +48,9 @@ export const TypographyDropDown = () => { { ? "active dropdown-item-active" : "" }`} - onClick={() => applyTypography(option)} + onClick={() => + applyTypography({ + styleObject: option.styleObject, + themeTypographyName: option.themeTypographyName, + htmlTag: option.htmlTag + }) + } key={option.themeTypographyName} > {option.displayName} diff --git a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx index e5970bafde5..3ab7dfd772c 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx @@ -1,14 +1,29 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { $getSelection, $isRangeSelection, LexicalCommand } from "lexical"; +import { + $getSelection, + $isNodeSelection, + $isRangeSelection, + $isRootOrShadowRoot, + COMMAND_PRIORITY_CRITICAL, + LexicalCommand, + SELECTION_CHANGE_COMMAND +} from "lexical"; import { Compose, makeComposable } from "@webiny/react-composition"; -import { getSelectedNode } from "~/utils/getSelectedNode"; -import { TypographyActionContext, TypographyValue } from "~/context/TypographyActionContext"; +import { TypographyActionContext } from "~/context/TypographyActionContext"; + +import { TypographyValue } from "~/types"; import { - $isTypographyNode, - ADD_TYPOGRAPHY_COMMAND, + $isTypographyElementNode, + ADD_TYPOGRAPHY_ELEMENT_COMMAND, + TypographyElementNode, TypographyPayload -} from "~/nodes/TypographyNode"; +} from "~/nodes/TypographyElementNode"; +import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { getSelectedNode } from "~/utils/getSelectedNode"; +import { ListNode } from "@lexical/list"; +import { useLexicalEditor } from "@lexical/react/DEPRECATED_useLexicalEditor"; +import { useRichTextEditor } from "~/hooks/useRichTextEditor"; /* * Base composable action component that is mounted on toolbar action as a placeholder for the custom toolbar action. @@ -40,6 +55,7 @@ export interface TypographyAction extends React.FC { export const TypographyAction: TypographyAction = () => { const [editor] = useLexicalComposerContext(); + const [activeEditor, setActiveEditor] = useState(editor); const [typography, setTypography] = useState(); const setTypographySelect = useCallback( @@ -50,34 +66,61 @@ export const TypographyAction: TypographyAction = () => { ); const onTypographySelect = useCallback((value: TypographyValue) => { - console.log("Value", value); setTypographySelect(value); - editor.dispatchCommand>(ADD_TYPOGRAPHY_COMMAND, { + editor.dispatchCommand>(ADD_TYPOGRAPHY_ELEMENT_COMMAND, { value }); }, []); - /*const updatePopup = useCallback(() => { - editor.getEditorState().read(() => { - const selection = $getSelection(); - if (!$isRangeSelection(selection)) { - return; + const updateToolbar = useCallback(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + let element = + anchorNode.getKey() === "root" + ? anchorNode + : $findMatchingParent(anchorNode, e => { + const parent = e.getParent(); + return parent !== null && $isRootOrShadowRoot(parent); + }); + + if (element === null) { + element = anchorNode.getTopLevelElementOrThrow(); } + + // Update links const node = getSelectedNode(selection); - if ($isTypographyNode(node)) { - // const colorStyle = node.getColorStyle(); - // setFontColor(colorStyle.color); - console.log("$isTypographyNode", node.getTypographyValue()) + const parent = node.getParent(); + + if ($isTypographyElementNode(parent)) { + const el = element as TypographyElementNode; + setTypography(el.getTypographyValue()); } - }); - }, [editor]); + } + }, [activeEditor]); + + useEffect(() => { + return mergeRegister( + activeEditor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + updateToolbar(); + }); + }) + ); + }, [activeEditor, editor, updateToolbar]); useEffect(() => { - document.addEventListener("selectionchange", updatePopup); - return () => { - document.removeEventListener("selectionchange", updatePopup); - }; - }, [updatePopup])*/ + return editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, newEditor) => { + updateToolbar(); + setActiveEditor(newEditor); + return false; + }, + COMMAND_PRIORITY_CRITICAL + ); + }, [editor, updateToolbar]); return ( ; - // variable name defined in the theme - themeTypographyName: string; - // Show on UI - displayName: string; -}; +import { TypographyValue } from "~/types"; export interface TypographyActionContextProps { /* diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index c9421b48480..ec83b8e3503 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -45,6 +45,7 @@ export { AddRichTextEditorNodeType } from "~/components/AddRichTextEditorNodeTyp // utils export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; export { isValidLexicalData } from "~/utils/isValidLexicalData"; +export { getTypographyMetaByName } from "~/utils/typography"; // types export * as types from "./types"; // config diff --git a/packages/lexical-editor/src/nodes/TypographyElementNode.ts b/packages/lexical-editor/src/nodes/TypographyElementNode.ts new file mode 100644 index 00000000000..b6f02c89534 --- /dev/null +++ b/packages/lexical-editor/src/nodes/TypographyElementNode.ts @@ -0,0 +1,166 @@ +import { + $createParagraphNode, + createCommand, + DOMConversionOutput, + EditorConfig, + ElementNode, + LexicalCommand, + LexicalEditor, + LexicalNode, + NodeKey, + RangeSelection, + SerializedElementNode, + Spread +} from "lexical"; +import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; +import { styleObjectToString } from "~/utils/styleObjectToString"; +import { TypographyHTMLTag, TypographyValue } from "~/types"; +import { $createHeadingNode } from "@lexical/rich-text"; + +// Command and payload +export const ADD_TYPOGRAPHY_ELEMENT_COMMAND: LexicalCommand = createCommand( + "ADD_TYPOGRAPHY_ELEMENT_COMMAND" +); +const TypographyNodeAttrName = "typography-el-theme"; + +export interface TypographyPayload { + value: TypographyValue; + caption?: LexicalEditor; + key?: NodeKey; +} + +// Node +type ThemeTypographyName = "normal" | string; +export type SerializedTypographyNode = Spread< + { + tag: TypographyHTMLTag; + themeTypographyName: ThemeTypographyName; + typographyStyles: Record; + type: "typography-el-node"; + version: 1; + }, + SerializedElementNode +>; + +function convertElement(domNode: Node): DOMConversionOutput { + const nodeName = domNode.nodeName.toLowerCase(); + let node = null; + if ( + nodeName === "h1" || + nodeName === "h2" || + nodeName === "h3" || + nodeName === "h4" || + nodeName === "h5" || + nodeName === "h6" + ) { + node = $createHeadingNode(nodeName); + } + + if (nodeName === "p") { + node = $createParagraphNode(); + } + return { node }; +} + +/** + * Main responsibility of this node is to apply custom or Webiny theme typography to selected text. + * Extends the original ElementNode node to add additional transformation and support for webiny theme typography. + */ +export class TypographyElementNode extends ElementNode { + __tag: TypographyHTMLTag; + __themeTypographyName: ThemeTypographyName; + __typographyStyles: Record; + + constructor( + tag: TypographyHTMLTag, + typographyStyles: Record, + themeTypographyName?: ThemeTypographyName, + key?: NodeKey + ) { + super(key); + this.__tag = tag; + this.__themeTypographyName = themeTypographyName || "default"; + this.__typographyStyles = typographyStyles; + } + + static override getType(): string { + return "typography-el-node"; + } + + static override clone(node: TypographyElementNode): TypographyElementNode { + return new TypographyElementNode( + node.__tag, + node.__typographyStyles, + node.__themeTypographyName, + node.__key + ); + } + + getTypographyValue(): TypographyValue { + return { + htmlTag: this.__tag, + styleObject: this.__typographyStyles, + themeTypographyName: this.__themeTypographyName + }; + } + + addStylesHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { + // if theme is available get the latest typography value + if (theme?.styles?.typography) { + this.__typographyStyles = theme.styles.typography[this.__themeTypographyName]; + } + + element.setAttribute(TypographyNodeAttrName, this.__themeTypographyName); + element.style.cssText = styleObjectToString(this.__typographyStyles); + return element; + } + + override exportJSON(): SerializedTypographyNode { + return { + ...super.exportJSON(), + tag: this.__tag, + themeTypographyName: this.__themeTypographyName, + typographyStyles: this.__typographyStyles, + type: "typography-el-node", + version: 1 + }; + } + + static override importJSON(serializedNode: SerializedTypographyNode): TypographyElementNode { + const node = new TypographyElementNode( + serializedNode.tag, + serializedNode.typographyStyles, + serializedNode.themeTypographyName + ); + node.setFormat(serializedNode.format); + node.setIndent(serializedNode.indent); + node.setDirection(serializedNode.direction); + return node; + } + + override createDOM(config: EditorConfig): HTMLElement { + const tag = this.__tag; + const element = document.createElement(tag); + return this.addStylesHTMLElement(element, config.theme); + } + + override updateDOM(_prevNode: unknown, _dom: HTMLElement, _config: EditorConfig): boolean { + return false; + } +} + +export const $createTypographyNode = ( + value: TypographyValue, + key?: NodeKey +): TypographyElementNode => { + return new TypographyElementNode( + value.htmlTag, + value.styleObject, + value.themeTypographyName, + key + ); +}; + +export const $isTypographyElementNode = (node: ElementNode | LexicalNode | null): boolean => { + return node instanceof TypographyElementNode; +}; diff --git a/packages/lexical-editor/src/nodes/TypographyNode.ts b/packages/lexical-editor/src/nodes/TypographyNode.ts deleted file mode 100644 index 0760988622d..00000000000 --- a/packages/lexical-editor/src/nodes/TypographyNode.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - createCommand, - EditorConfig, - LexicalCommand, - LexicalEditor, - LexicalNode, - NodeKey, - RangeSelection, - SerializedTextNode, - Spread, - TextNode -} from "lexical"; -import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; -import { TypographyValue } from "~/context/TypographyActionContext"; -import { styleObjectToString } from "~/utils/styleObjectToString"; - -export const ADD_TYPOGRAPHY_COMMAND: LexicalCommand = - createCommand("ADD_TYPOGRAPHY_COMMAND"); -const TypographyNodeAttrName = "typography-theme"; - -export interface TypographyPayload { - value: TypographyValue; - caption?: LexicalEditor; - key?: NodeKey; -} - -type ThemeTypographyName = "normal" | string; - -export type SerializedTypographyNode = Spread< - { - themeTypographyName: ThemeTypographyName; - typographyStyles: Record; - type: "typography-node"; - version: 1; - }, - SerializedTextNode ->; - -/** - * Main responsibility of this node is to apply custom or Webiny theme typography to selected text. - * Extends the original TextNode node to add additional transformation and support for webiny theme typography. - */ -export class TypographyNode extends TextNode { - __themeTypographyName: ThemeTypographyName; - __typographyStyles: Record; - - constructor( - text: string, - typographyStyles: Record, - themeTypographyName?: ThemeTypographyName, - key?: NodeKey - ) { - super(text, key); - this.__themeTypographyName = themeTypographyName || "default"; - this.__typographyStyles = typographyStyles; - } - - static override getType(): string { - return "typography-node"; - } - - static override clone(node: TypographyNode): TypographyNode { - return new TypographyNode(node.__text, node.__color, node.__themeColor, node.__key); - } - - static override importJSON(serializedNode: SerializedTypographyNode): TextNode { - const node = new TypographyNode( - serializedNode.text, - serializedNode.typographyStyles, - serializedNode.themeTypographyName - ); - node.setTextContent(serializedNode.text); - node.setFormat(serializedNode.format); - node.setDetail(serializedNode.detail); - node.setMode(serializedNode.mode); - node.setStyle(serializedNode.style); - return node; - } - - override exportJSON(): SerializedTypographyNode { - return { - ...super.exportJSON(), - themeTypographyName: this.__themeTypographyName, - typographyStyles: this.__typographyStyles, - type: "typography-node", - version: 1 - }; - } - - addStylesHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { - // if theme is available get the latest typography value - if (theme?.styles?.typography) { - this.__typographyStyles = theme.styles.typography[this.__themeTypographyName]; - } - - element.setAttribute(TypographyNodeAttrName, this.__themeTypographyName); - element.style.cssText = styleObjectToString(this.__typographyStyles); - return element; - } - - override updateDOM(prevNode: TypographyNode, dom: HTMLElement, config: EditorConfig): boolean { - const isUpdated = super.updateDOM(prevNode, dom, config); - dom = this.addStylesHTMLElement(dom, config.theme); - return isUpdated; - } - - getTypographyValue(): { themeTypographyName: string; styleObject: Record } { - return { - themeTypographyName: this.__themeTypographyName, - styleObject: this.__typographyStyles - }; - } - - override createDOM(config: EditorConfig): HTMLElement { - const element = super.createDOM(config); - return this.addStylesHTMLElement(element, config.theme); - } -} - -export const $createTypographyNode = ( - text: string, - typographyStyles: Record, - themeTypographyName?: ThemeTypographyName, - key?: NodeKey -): TypographyNode => { - return new TypographyNode(text, typographyStyles, themeTypographyName, key); -}; - -export const $isTypographyNode = (node: LexicalNode): boolean => { - return node instanceof TypographyNode; -}; - -export function $applyStylesToNode(node: TypographyNode, nodeStyleProvider: RangeSelection) { - node.setFormat(nodeStyleProvider.format); - node.setStyle(nodeStyleProvider.style); -} diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/webinyNodes.ts index 1572693f5bd..0e835ecc899 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/webinyNodes.ts @@ -8,7 +8,7 @@ import { MarkNode } from "@lexical/mark"; import { OverflowNode } from "@lexical/overflow"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; import { FontColorNode } from "~/nodes/FontColorNode"; -import { TypographyNode } from "~/nodes/TypographyNode"; +import { TypographyElementNode } from "~/nodes/TypographyElementNode"; export const WebinyNodes: Array> = [ HeadingNode, @@ -23,5 +23,5 @@ export const WebinyNodes: Array> = [ OverflowNode, MarkNode, FontColorNode, - TypographyNode + TypographyElementNode ]; diff --git a/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx b/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx index 05487c2045b..efbda5199fa 100644 --- a/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx +++ b/packages/lexical-editor/src/plugins/TypographyPlugin/TypographyPlugin.tsx @@ -1,45 +1,24 @@ import React, { useEffect } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR } from "lexical"; import { - $createParagraphNode, - $getSelection, - $insertNodes, - $isRangeSelection, - $isRootOrShadowRoot, - COMMAND_PRIORITY_EDITOR -} from "lexical"; -import { $wrapNodeInElement } from "@lexical/utils"; -import { - $applyStylesToNode, $createTypographyNode, - ADD_TYPOGRAPHY_COMMAND, + ADD_TYPOGRAPHY_ELEMENT_COMMAND, TypographyPayload -} from "~/nodes/TypographyNode"; +} from "~/nodes/TypographyElementNode"; +import { $wrapNodes } from "@lexical/selection"; export const TypographyPlugin: React.FC = () => { const [editor] = useLexicalComposerContext(); useEffect(() => { return editor.registerCommand( - ADD_TYPOGRAPHY_COMMAND, + ADD_TYPOGRAPHY_ELEMENT_COMMAND, payload => { - editor.update(() => { - const { value } = payload; - const selection = $getSelection(); - - if ($isRangeSelection(selection)) { - const typographyNode = $createTypographyNode( - selection.getTextContent(), - value.styleObject, - value.themeTypographyName - ); - $applyStylesToNode(typographyNode, selection); - $insertNodes([typographyNode]); - if ($isRootOrShadowRoot(typographyNode.getParentOrThrow())) { - $wrapNodeInElement(typographyNode, $createParagraphNode).selectEnd(); - } - } - }); + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + $wrapNodes(selection, () => $createTypographyNode(payload.value)); + } return true; }, COMMAND_PRIORITY_EDITOR diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index 3b2592866b1..3e0b1271bcb 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -1,4 +1,12 @@ export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; -export { TypographyValue } from "~/context/TypographyActionContext"; +// Typography +export type TypographyHTMLTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p"; +export type TypographyValue = { + // CSSObject type + styleObject: Record; + // variable name defined in the theme + themeTypographyName: string; + htmlTag: TypographyHTMLTag; +}; diff --git a/packages/lexical-editor/src/utils/typography.ts b/packages/lexical-editor/src/utils/typography.ts new file mode 100644 index 00000000000..e5b3679d94a --- /dev/null +++ b/packages/lexical-editor/src/utils/typography.ts @@ -0,0 +1,28 @@ +import { TypographyHTMLTag } from "~/types"; + +type ThemeTypographyMetaData = { + // Name to be displayed on UI + displayName: string; + htmlTag: TypographyHTMLTag; +}; + +const TYPOGRAPHY_META_DATA: Record = { + normal: { displayName: "Normal", htmlTag: "p" }, + heading1: { displayName: "Heading 1", htmlTag: "h1" }, + heading2: { displayName: "Heading 2", htmlTag: "h2" }, + heading3: { displayName: "Heading 3", htmlTag: "h3" }, + heading4: { displayName: "Heading 4", htmlTag: "h4" }, + heading5: { displayName: "Heading 5", htmlTag: "h5" }, + heading6: { displayName: "Heading 6", htmlTag: "h6" }, + paragraph1: { displayName: "Paragraph 1", htmlTag: "p" }, + paragraph2: { displayName: "Paragraph 2", htmlTag: "p" } +}; + +/* + * @description Return metadata for the specific typography set in the theme. + * Note: As default will return metadata for 'normal' typography style. + */ +export const getTypographyMetaByName = (themeTypographyName: string): ThemeTypographyMetaData => { + const data = TYPOGRAPHY_META_DATA[themeTypographyName]; + return data ?? TYPOGRAPHY_META_DATA["normal"]; +}; From 296f333a07630b11c89eeee5805545a00746c5c5 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 15 Mar 2023 18:59:11 +0100 Subject: [PATCH 21/57] wip: new css object to string converter --- apps/theme/theme.ts | 14 +++++----- packages/lexical-editor/package.json | 3 ++- .../ToolbarActions/TypographyAction.tsx | 7 +---- .../src/nodes/TypographyElementNode.ts | 26 +------------------ .../src/utils/styleObjectToString.ts | 15 +++-------- yarn.lock | 8 ++++++ 6 files changed, 22 insertions(+), 51 deletions(-) diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index 5a31b2a985c..f7e57671226 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -44,13 +44,13 @@ const paragraphs = { }; export const typography = { - heading1: { ...headings, fontWeight: "bold", fontSize: "48px" }, - heading2: { ...headings, fontSize: "36px" }, - heading3: { ...headings, fontSize: "30px" }, - heading4: { ...headings, fontSize: "24px" }, - heading5: { ...headings, fontSize: "20px" }, - heading6: { ...headings, fontSize: "18px", lineHeight: "1.75rem" }, - paragraph1: { ...paragraphs, fontSize: "16.5px" }, + heading1: { ...headings, fontWeight: "bold", fontSize: 48 }, + heading2: { ...headings, fontSize: 36 }, + heading3: { ...headings, fontSize: 30 }, + heading4: { ...headings, fontSize: 24 }, + heading5: { ...headings, fontSize: 20 }, + heading6: { ...headings, fontSize: 18, lineHeight: "1.75rem" }, + paragraph1: { ...paragraphs, fontSize: 16.5 }, paragraph2: { ...paragraphs, fontSize: 12.5, diff --git a/packages/lexical-editor/package.json b/packages/lexical-editor/package.json index d9644fbb4fe..4d85d51852c 100644 --- a/packages/lexical-editor/package.json +++ b/packages/lexical-editor/package.json @@ -20,7 +20,8 @@ "@webiny/react-composition": "0.0.0", "lexical": "0.8.1", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "react-style-object-to-css": "^1.1.2" }, "devDependencies": { "@webiny/cli": "^5.33.1", diff --git a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx index 3ab7dfd772c..dabf83af15d 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { $getSelection, - $isNodeSelection, $isRangeSelection, $isRootOrShadowRoot, COMMAND_PRIORITY_CRITICAL, @@ -19,11 +18,8 @@ import { TypographyElementNode, TypographyPayload } from "~/nodes/TypographyElementNode"; -import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { $findMatchingParent, mergeRegister } from "@lexical/utils"; import { getSelectedNode } from "~/utils/getSelectedNode"; -import { ListNode } from "@lexical/list"; -import { useLexicalEditor } from "@lexical/react/DEPRECATED_useLexicalEditor"; -import { useRichTextEditor } from "~/hooks/useRichTextEditor"; /* * Base composable action component that is mounted on toolbar action as a placeholder for the custom toolbar action. @@ -89,7 +85,6 @@ export const TypographyAction: TypographyAction = () => { element = anchorNode.getTopLevelElementOrThrow(); } - // Update links const node = getSelectedNode(selection); const parent = node.getParent(); diff --git a/packages/lexical-editor/src/nodes/TypographyElementNode.ts b/packages/lexical-editor/src/nodes/TypographyElementNode.ts index b6f02c89534..6f71f382290 100644 --- a/packages/lexical-editor/src/nodes/TypographyElementNode.ts +++ b/packages/lexical-editor/src/nodes/TypographyElementNode.ts @@ -1,21 +1,17 @@ import { - $createParagraphNode, createCommand, - DOMConversionOutput, EditorConfig, ElementNode, LexicalCommand, LexicalEditor, LexicalNode, NodeKey, - RangeSelection, SerializedElementNode, Spread } from "lexical"; import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; import { styleObjectToString } from "~/utils/styleObjectToString"; import { TypographyHTMLTag, TypographyValue } from "~/types"; -import { $createHeadingNode } from "@lexical/rich-text"; // Command and payload export const ADD_TYPOGRAPHY_ELEMENT_COMMAND: LexicalCommand = createCommand( @@ -42,26 +38,6 @@ export type SerializedTypographyNode = Spread< SerializedElementNode >; -function convertElement(domNode: Node): DOMConversionOutput { - const nodeName = domNode.nodeName.toLowerCase(); - let node = null; - if ( - nodeName === "h1" || - nodeName === "h2" || - nodeName === "h3" || - nodeName === "h4" || - nodeName === "h5" || - nodeName === "h6" - ) { - node = $createHeadingNode(nodeName); - } - - if (nodeName === "p") { - node = $createParagraphNode(); - } - return { node }; -} - /** * Main responsibility of this node is to apply custom or Webiny theme typography to selected text. * Extends the original ElementNode node to add additional transformation and support for webiny theme typography. @@ -144,7 +120,7 @@ export class TypographyElementNode extends ElementNode { return this.addStylesHTMLElement(element, config.theme); } - override updateDOM(_prevNode: unknown, _dom: HTMLElement, _config: EditorConfig): boolean { + override updateDOM(): boolean { return false; } } diff --git a/packages/lexical-editor/src/utils/styleObjectToString.ts b/packages/lexical-editor/src/utils/styleObjectToString.ts index ba722c1f93f..797cebb6558 100644 --- a/packages/lexical-editor/src/utils/styleObjectToString.ts +++ b/packages/lexical-editor/src/utils/styleObjectToString.ts @@ -1,3 +1,5 @@ +const reactToCSS = require("react-style-object-to-css"); + /* * Converts CSS style objects to string * Example: @@ -7,16 +9,5 @@ export const styleObjectToString = (styleObject: Record): string => if (!styleObject) { return styleObject; } - return Object.keys(styleObject).reduce( - (acc, key) => - acc + - key - .split(/(?=[A-Z])/) - .join("-") - .toLowerCase() + - ":" + - styleObject[key] + - ";", - "" - ); + return reactToCSS(styleObject); }; diff --git a/yarn.lock b/yarn.lock index 3c1bb9f582c..377c093983f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15195,6 +15195,7 @@ __metadata: lexical: 0.8.1 react: ^17.0.2 react-dom: ^17.0.2 + react-style-object-to-css: ^1.1.2 languageName: unknown linkType: soft @@ -36865,6 +36866,13 @@ __metadata: languageName: node linkType: hard +"react-style-object-to-css@npm:^1.1.2": + version: 1.1.2 + resolution: "react-style-object-to-css@npm:1.1.2" + checksum: 1f854bf5c7fabcc0be3db3e35e4b346dd0d3cb2b09079f100380ab299d2db94ba75ab225254446958d820933c7be67b609219746610a14cda2c42e708711cc78 + languageName: node + linkType: hard + "react-syntax-highlighter@npm:^11.0.2": version: 11.0.3 resolution: "react-syntax-highlighter@npm:11.0.3" From 4485da029ea8198937296ad21fe621abb72a53e9 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 16 Mar 2023 11:15:41 +0100 Subject: [PATCH 22/57] refactor: use import instead of required for importing reactToCSS method --- packages/lexical-editor/src/utils/styleObjectToString.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lexical-editor/src/utils/styleObjectToString.ts b/packages/lexical-editor/src/utils/styleObjectToString.ts index 797cebb6558..134f89be6ce 100644 --- a/packages/lexical-editor/src/utils/styleObjectToString.ts +++ b/packages/lexical-editor/src/utils/styleObjectToString.ts @@ -1,4 +1,5 @@ -const reactToCSS = require("react-style-object-to-css"); +// @ts-ignore +import reactToCSS from "react-style-object-to-css"; /* * Converts CSS style objects to string From 58f91845adf50ffc1824fc19a9ad1f0c56d5d480 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Fri, 17 Mar 2023 20:41:41 +0100 Subject: [PATCH 23/57] feat: changes in theme typography structure and typography selection list by component --- apps/theme/theme.ts | 75 +++++++++++++++++++ packages/lexical-editor-actions/package.json | 1 + .../src/components/TypographyDropDown.tsx | 57 +++++--------- .../tsconfig.build.json | 3 +- packages/lexical-editor-actions/tsconfig.json | 9 ++- .../src/components/Toolbar/Toolbar.tsx | 9 ++- .../src/context/RichTextEditorContext.tsx | 8 +- packages/lexical-editor/src/index.tsx | 1 - .../src/nodes/TypographyElementNode.ts | 68 ++++++++--------- packages/lexical-editor/src/types.ts | 11 +-- .../lexical-editor/src/utils/typography.ts | 45 +++++------ packages/theme/src/types.ts | 18 +++++ yarn.lock | 1 + 13 files changed, 197 insertions(+), 109 deletions(-) diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index f7e57671226..3c96d676500 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -43,6 +43,7 @@ const paragraphs = { WebkitFontSmoothing: "antialiased" }; +// Legacy export const typography = { heading1: { ...headings, fontWeight: "bold", fontSize: 48 }, heading2: { ...headings, fontSize: 36 }, @@ -96,6 +97,80 @@ const theme = createTheme({ styles: { colors, typography, + typographyStyles: { + headings: [ + { + id: "heading1", + name: "Heading 1", + tag: "h1", + css: { ...headings, fontWeight: "bold", fontSize: 48 } + }, + { + id: "heading2", + name: "Heading 2", + tag: "h2", + css: { ...headings, fontSize: 36 } + }, + { + id: "heading3", + name: "Heading 3", + tag: "h3", + css: { ...headings, fontSize: 30 } + }, + { + id: "heading4", + name: "Heading 4", + tag: "h4", + css: { ...headings, fontSize: 24 } + }, + { + id: "heading5", + name: "Heading 5", + tag: "h5", + css: { ...headings, fontSize: 20 } + }, + { + id: "heading6", + name: "Heading 6", + tag: "h6", + css: { ...headings, fontSize: 18, lineHeight: "1.75rem" } + } + ], + paragraphs: [ + { + id: "paragraph1", + name: "Paragraph 1", + tag: "p", + css: { ...paragraphs, fontSize: 16.5 } + }, + { + id: "paragraph2", + name: "Paragraph 2", + tag: "p", + css: { + ...paragraphs, + fontSize: 12.5, + letterSpacing: "0.45px", + lineHeight: "19px" + } + } + ], + lists: [ + { id: "list1", name: "List 1", tag: "ul", css: { ...paragraphs, fontSize: 16.5 } } + ], + quotes: [ + { + id: "quote1", + name: "Quote1 1", + tag: "div", + css: { + ...paragraphs, + fontWeight: "bold", + fontSize: 22 + } + } + ] + }, elements: { document: { a: { color: colors.color1 }, diff --git a/packages/lexical-editor-actions/package.json b/packages/lexical-editor-actions/package.json index 360fbffaa8e..5727b12fad4 100644 --- a/packages/lexical-editor-actions/package.json +++ b/packages/lexical-editor-actions/package.json @@ -6,6 +6,7 @@ "@webiny/app-page-builder": "0.0.0", "@webiny/app-page-builder-elements": "0.0.0", "@webiny/lexical-editor": "0.0.0", + "@webiny/theme": "0.0.0", "classnames": "^2.3.1", "emotion": "^10.0.27", "react": "^17.0.2", diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx index f473d119fbb..4c7af31a3fe 100644 --- a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -2,44 +2,31 @@ import React from "react"; import { DropDown, DropDownItem, - getTypographyMetaByName, + useRichTextEditor, useTypographyAction } from "@webiny/lexical-editor"; import { usePageElements } from "@webiny/app-page-builder-elements"; -import { TypographyHTMLTag } from "@webiny/lexical-editor/types"; - -type TypographyDropDownItem = { - styleObject: Record; - themeTypographyName: string; - htmlTag: TypographyHTMLTag; - displayName: string; -}; +import { ThemeTypographyHTMLTag, TypographyStyle } from "@webiny/theme/types"; export const TypographyDropDown = () => { const { value, applyTypography } = useTypographyAction(); const { theme } = usePageElements(); const typographyStyles = theme.styles?.typography; + const { toolbarType } = useRichTextEditor(); const hasTypographyStyles = (): boolean => { return !!typographyStyles; }; - const typographyList = (): TypographyDropDownItem[] => { - const list: TypographyDropDownItem[] = []; - for (const key in typographyStyles) { - const styleObject = typographyStyles[key]; - const metadata = getTypographyMetaByName(key); - // filter only headings and paragraphs - if (key.includes("heading") || key.includes("paragraph")) { - list.push({ - styleObject, - themeTypographyName: key, - htmlTag: metadata.htmlTag, - displayName: metadata.displayName - }); - } + const getTypographyStyles = (): TypographyStyle[] => { + if (toolbarType === "heading") { + return theme.styles?.typographyStyles?.headings || []; + } + + if (toolbarType === "paragraph") { + return theme.styles?.typographyStyles?.paragraphs || []; } - return list; + return []; }; return ( @@ -48,30 +35,20 @@ export const TypographyDropDown = () => { - {typographyList().map(option => ( + {getTypographyStyles()?.map(option => ( - applyTypography({ - styleObject: option.styleObject, - themeTypographyName: option.themeTypographyName, - htmlTag: option.htmlTag - }) - } - key={option.themeTypographyName} + onClick={() => applyTypography(option)} + key={option.id} > - {option.displayName} + {option.name} ))} diff --git a/packages/lexical-editor-actions/tsconfig.build.json b/packages/lexical-editor-actions/tsconfig.build.json index 3e8742f1c5c..05793051ca6 100644 --- a/packages/lexical-editor-actions/tsconfig.build.json +++ b/packages/lexical-editor-actions/tsconfig.build.json @@ -4,7 +4,8 @@ "references": [ { "path": "../app-page-builder/tsconfig.build.json" }, { "path": "../app-page-builder-elements/tsconfig.build.json" }, - { "path": "../lexical-editor/tsconfig.build.json" } + { "path": "../lexical-editor/tsconfig.build.json" }, + { "path": "../theme/tsconfig.build.json" } ], "compilerOptions": { "rootDir": "./src", diff --git a/packages/lexical-editor-actions/tsconfig.json b/packages/lexical-editor-actions/tsconfig.json index b0f8095efea..f92ec02f3d5 100644 --- a/packages/lexical-editor-actions/tsconfig.json +++ b/packages/lexical-editor-actions/tsconfig.json @@ -1,10 +1,11 @@ { "extends": "../../tsconfig.json", - "include": ["src", "__tests__/**/*.ts"], + "include": ["src", "__tests__"], "references": [ { "path": "../app-page-builder" }, { "path": "../app-page-builder-elements" }, - { "path": "../lexical-editor" } + { "path": "../lexical-editor" }, + { "path": "../theme" } ], "compilerOptions": { "rootDirs": ["./src", "./__tests__"], @@ -18,7 +19,9 @@ "@webiny/app-page-builder-elements/*": ["../app-page-builder-elements/src/*"], "@webiny/app-page-builder-elements": ["../app-page-builder-elements/src"], "@webiny/lexical-editor/*": ["../lexical-editor/src/*"], - "@webiny/lexical-editor": ["../lexical-editor/src"] + "@webiny/lexical-editor": ["../lexical-editor/src"], + "@webiny/theme/*": ["../theme/src/*"], + "@webiny/theme": ["../theme/src"] }, "baseUrl": "." } diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx index 6180c11607b..e956dfaec68 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx @@ -27,8 +27,15 @@ interface FloatingToolbarProps { editor: LexicalEditor; } -const FloatingToolbar: FC = ({ children, anchorElem, editor }) => { +const FloatingToolbar: FC = ({ children, type, anchorElem, editor }) => { const popupCharStylesEditorRef = useRef(null); + const { toolbarType, setToolbarType } = useRichTextEditor(); + + useEffect(() => { + if (toolbarType !== type) { + setToolbarType(type); + } + }, [type]); const updateTextFormatFloatingToolbar = useCallback(() => { const selection = $getSelection(); diff --git a/packages/lexical-editor/src/context/RichTextEditorContext.tsx b/packages/lexical-editor/src/context/RichTextEditorContext.tsx index c222c87dc64..8fbc54d6e87 100644 --- a/packages/lexical-editor/src/context/RichTextEditorContext.tsx +++ b/packages/lexical-editor/src/context/RichTextEditorContext.tsx @@ -1,8 +1,11 @@ import React, { createContext, useState } from "react"; +import { ToolbarType } from "~/types"; export interface RichTextEditorContext { nodeIsText: boolean; setNodeIsText: (nodeIsText: boolean) => void; + toolbarType?: ToolbarType; + setToolbarType: (type: ToolbarType) => void; } export const RichTextEditorContext = createContext(undefined); @@ -13,6 +16,7 @@ interface RichTextEditorProviderProps { export const RichTextEditorProvider: React.FC = ({ children }) => { const [nodeIsText, setIsText] = useState(false); + const [toolbarType, setToolbarType] = useState(); const setNodeIsText = (nodeIsText: boolean) => { setIsText(nodeIsText); }; @@ -21,7 +25,9 @@ export const RichTextEditorProvider: React.FC = ({ {children} diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index ec83b8e3503..c9421b48480 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -45,7 +45,6 @@ export { AddRichTextEditorNodeType } from "~/components/AddRichTextEditorNodeTyp // utils export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; export { isValidLexicalData } from "~/utils/isValidLexicalData"; -export { getTypographyMetaByName } from "~/utils/typography"; // types export * as types from "./types"; // config diff --git a/packages/lexical-editor/src/nodes/TypographyElementNode.ts b/packages/lexical-editor/src/nodes/TypographyElementNode.ts index 6f71f382290..fde2b240cff 100644 --- a/packages/lexical-editor/src/nodes/TypographyElementNode.ts +++ b/packages/lexical-editor/src/nodes/TypographyElementNode.ts @@ -12,6 +12,7 @@ import { import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; import { styleObjectToString } from "~/utils/styleObjectToString"; import { TypographyHTMLTag, TypographyValue } from "~/types"; +import { findTypographyStyleById } from "~/utils/typography"; // Command and payload export const ADD_TYPOGRAPHY_ELEMENT_COMMAND: LexicalCommand = createCommand( @@ -26,11 +27,11 @@ export interface TypographyPayload { } // Node -type ThemeTypographyName = "normal" | string; export type SerializedTypographyNode = Spread< { tag: TypographyHTMLTag; - themeTypographyName: ThemeTypographyName; + styleId: string; + name: string; typographyStyles: Record; type: "typography-el-node"; version: 1; @@ -43,20 +44,17 @@ export type SerializedTypographyNode = Spread< * Extends the original ElementNode node to add additional transformation and support for webiny theme typography. */ export class TypographyElementNode extends ElementNode { + __styleId: string; __tag: TypographyHTMLTag; - __themeTypographyName: ThemeTypographyName; + __name: string; __typographyStyles: Record; - constructor( - tag: TypographyHTMLTag, - typographyStyles: Record, - themeTypographyName?: ThemeTypographyName, - key?: NodeKey - ) { + constructor(value: TypographyValue, key?: NodeKey) { super(key); - this.__tag = tag; - this.__themeTypographyName = themeTypographyName || "default"; - this.__typographyStyles = typographyStyles; + this.__tag = value.tag; + this.__styleId = value.id; + this.__name = value.name; + this.__typographyStyles = value.css; } static override getType(): string { @@ -65,28 +63,31 @@ export class TypographyElementNode extends ElementNode { static override clone(node: TypographyElementNode): TypographyElementNode { return new TypographyElementNode( - node.__tag, - node.__typographyStyles, - node.__themeTypographyName, + { + css: node.__typographyStyles, + id: node.__styleId, + name: node.__name, + tag: node.__tag + }, node.__key ); } getTypographyValue(): TypographyValue { return { - htmlTag: this.__tag, - styleObject: this.__typographyStyles, - themeTypographyName: this.__themeTypographyName + tag: this.__tag, + css: this.__typographyStyles, + id: this.__styleId, + name: this.__name }; } addStylesHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { - // if theme is available get the latest typography value - if (theme?.styles?.typography) { - this.__typographyStyles = theme.styles.typography[this.__themeTypographyName]; + const typographyStyleValue = findTypographyStyleById(theme, this.__styleId); + if (typographyStyleValue) { + this.__typographyStyles = typographyStyleValue.css; } - - element.setAttribute(TypographyNodeAttrName, this.__themeTypographyName); + element.setAttribute(TypographyNodeAttrName, this.__styleId); element.style.cssText = styleObjectToString(this.__typographyStyles); return element; } @@ -95,19 +96,21 @@ export class TypographyElementNode extends ElementNode { return { ...super.exportJSON(), tag: this.__tag, - themeTypographyName: this.__themeTypographyName, typographyStyles: this.__typographyStyles, + name: this.__name, + styleId: this.__styleId, type: "typography-el-node", version: 1 }; } static override importJSON(serializedNode: SerializedTypographyNode): TypographyElementNode { - const node = new TypographyElementNode( - serializedNode.tag, - serializedNode.typographyStyles, - serializedNode.themeTypographyName - ); + const node = new TypographyElementNode({ + id: serializedNode.styleId, + css: serializedNode.typographyStyles, + tag: serializedNode.tag, + name: serializedNode.name + }); node.setFormat(serializedNode.format); node.setIndent(serializedNode.indent); node.setDirection(serializedNode.direction); @@ -129,12 +132,7 @@ export const $createTypographyNode = ( value: TypographyValue, key?: NodeKey ): TypographyElementNode => { - return new TypographyElementNode( - value.htmlTag, - value.styleObject, - value.themeTypographyName, - key - ); + return new TypographyElementNode(value, key); }; export const $isTypographyElementNode = (node: ElementNode | LexicalNode | null): boolean => { diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index 3e0b1271bcb..92b1363498c 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -2,11 +2,12 @@ export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; // Typography -export type TypographyHTMLTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p"; +export type TypographyHTMLTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "ol" | "ul" | "div"; export type TypographyValue = { // CSSObject type - styleObject: Record; - // variable name defined in the theme - themeTypographyName: string; - htmlTag: TypographyHTMLTag; + css: Record; + id: string; + tag: TypographyHTMLTag; + // Display name + name: string; }; diff --git a/packages/lexical-editor/src/utils/typography.ts b/packages/lexical-editor/src/utils/typography.ts index e5b3679d94a..3858e4bc375 100644 --- a/packages/lexical-editor/src/utils/typography.ts +++ b/packages/lexical-editor/src/utils/typography.ts @@ -1,28 +1,29 @@ -import { TypographyHTMLTag } from "~/types"; +import { TypographyValue } from "~/types"; -type ThemeTypographyMetaData = { - // Name to be displayed on UI - displayName: string; - htmlTag: TypographyHTMLTag; +export const hasTypographyStyles = (theme: Record): boolean => { + return !!theme?.styles?.typographyStyles; }; -const TYPOGRAPHY_META_DATA: Record = { - normal: { displayName: "Normal", htmlTag: "p" }, - heading1: { displayName: "Heading 1", htmlTag: "h1" }, - heading2: { displayName: "Heading 2", htmlTag: "h2" }, - heading3: { displayName: "Heading 3", htmlTag: "h3" }, - heading4: { displayName: "Heading 4", htmlTag: "h4" }, - heading5: { displayName: "Heading 5", htmlTag: "h5" }, - heading6: { displayName: "Heading 6", htmlTag: "h6" }, - paragraph1: { displayName: "Paragraph 1", htmlTag: "p" }, - paragraph2: { displayName: "Paragraph 2", htmlTag: "p" } +export const getTypography = ( + typographyStyles: Record, + typographyStyleType: string +): TypographyValue[] | null => { + return typographyStyles[typographyStyleType] ?? null; }; -/* - * @description Return metadata for the specific typography set in the theme. - * Note: As default will return metadata for 'normal' typography style. - */ -export const getTypographyMetaByName = (themeTypographyName: string): ThemeTypographyMetaData => { - const data = TYPOGRAPHY_META_DATA[themeTypographyName]; - return data ?? TYPOGRAPHY_META_DATA["normal"]; +export const findTypographyStyleById = ( + theme: Record, + styleId: string +): TypographyValue | undefined => { + if (!hasTypographyStyles(theme)) { + return undefined; + } + const typographyStyles = theme?.styles?.typographyStyles; + for (const key in typographyStyles) { + const typographyTypeData = getTypography(typographyStyles, key); + if (typographyTypeData) { + return typographyTypeData.find(item => item.id === styleId); + } + } + return undefined; }; diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index e8a1ac30052..6f816e7f96c 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -11,9 +11,27 @@ export interface StylesObject { export type ThemeBreakpoints = Record; +export type HeadingHtmlTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; +export type ParagraphHtmlTag = "p"; +export type ListHtmlTag = "ul" | "ol"; +export type QuoteHtmlTag = "div"; +export type ThemeTypographyHTMLTag = HeadingHtmlTag | ParagraphHtmlTag | ListHtmlTag | QuoteHtmlTag; +export type TypographyType = "headings" | "paragraphs" | "quotes" | "lists"; +export type TypographyStyle = { + id: string; + name: string; + tag: T; + css: Record; +}; + +type Typography = { + [typeName in TypographyType]: TypographyStyle[]; +}; + export interface ThemeStyles { colors: Record; borderRadius?: number; + typographyStyles: Typography; typography: Record; elements: Record | StylesObject>; diff --git a/yarn.lock b/yarn.lock index 616d629b0c2..91e77878f0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14996,6 +14996,7 @@ __metadata: "@webiny/cli": 0.0.0 "@webiny/lexical-editor": 0.0.0 "@webiny/project-utils": 0.0.0 + "@webiny/theme": 0.0.0 classnames: ^2.3.1 emotion: ^10.0.27 react: ^17.0.2 From 4a9341d196a7ac1eed19f82fa41a69ea1b4da163 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Wed, 22 Mar 2023 19:53:22 +0100 Subject: [PATCH 24/57] wip: implement custom webiny list component that accept typography style --- apps/theme/theme.ts | 13 +- .../src/components/TypographyDropDown.tsx | 5 +- .../src/components/Editor/ParagraphEditor.tsx | 6 +- .../ToolbarActions/BulletListAction.tsx | 15 +- .../ToolbarActions/NumberedListAction.tsx | 22 +- .../src/context/RichTextEditorContext.tsx | 13 +- .../lexical-editor/src/hooks/useWebinyList.ts | 81 +++ packages/lexical-editor/src/index.tsx | 1 + .../lexical-editor/src/nodes/FontColorNode.ts | 2 +- .../src/nodes/TypographyElementNode.ts | 34 +- .../src/nodes/list-node/WebinyListItemNode.ts | 494 +++++++++++++++++ .../src/nodes/list-node/WebinyListNode.ts | 279 ++++++++++ .../src/nodes/list-node/commands.ts | 15 + .../src/nodes/list-node/formatList.ts | 501 ++++++++++++++++++ .../lexical-editor/src/nodes/webinyNodes.ts | 16 +- .../WebinyListPLugin/WebinyListPlugin.ts | 21 + packages/lexical-editor/src/types.ts | 57 +- .../src/utils/nodes/clearNodeFormating.ts | 32 ++ .../utils/nodes/getTextBlockSelectionData.ts | 88 +++ .../src/utils/nodes/list-node.ts | 158 ++++++ .../lexical-editor/src/utils/typography.ts | 14 +- packages/theme/src/types.ts | 2 +- 22 files changed, 1823 insertions(+), 46 deletions(-) create mode 100644 packages/lexical-editor/src/hooks/useWebinyList.ts create mode 100644 packages/lexical-editor/src/nodes/list-node/WebinyListItemNode.ts create mode 100644 packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts create mode 100644 packages/lexical-editor/src/nodes/list-node/commands.ts create mode 100644 packages/lexical-editor/src/nodes/list-node/formatList.ts create mode 100644 packages/lexical-editor/src/plugins/WebinyListPLugin/WebinyListPlugin.ts create mode 100644 packages/lexical-editor/src/utils/nodes/clearNodeFormating.ts create mode 100644 packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts create mode 100644 packages/lexical-editor/src/utils/nodes/list-node.ts diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index 3c96d676500..369cd8e76f0 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -156,13 +156,20 @@ const theme = createTheme({ } ], lists: [ - { id: "list1", name: "List 1", tag: "ul", css: { ...paragraphs, fontSize: 16.5 } } + { + id: "list", + name: "Default list", + tag: "ul", + css: { ...paragraphs, fontSize: 16.5 } + }, + { id: "list1", name: "List 1", tag: "ul", css: { ...paragraphs, fontSize: 18.5 } }, + { id: "list2", name: "List 2", tag: "ul", css: { ...paragraphs, fontSize: 21.5 } } ], quotes: [ { - id: "quote1", + id: "quote", name: "Quote1 1", - tag: "div", + tag: "quoteblock", css: { ...paragraphs, fontWeight: "bold", diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx index 4c7af31a3fe..f930f387565 100644 --- a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -7,11 +7,12 @@ import { } from "@webiny/lexical-editor"; import { usePageElements } from "@webiny/app-page-builder-elements"; import { ThemeTypographyHTMLTag, TypographyStyle } from "@webiny/theme/types"; +import { TypographyValue } from "@webiny/lexical-editor/types"; export const TypographyDropDown = () => { const { value, applyTypography } = useTypographyAction(); const { theme } = usePageElements(); - const typographyStyles = theme.styles?.typography; + const typographyStyles = theme.styles?.typographyStyles; const { toolbarType } = useRichTextEditor(); const hasTypographyStyles = (): boolean => { @@ -45,7 +46,7 @@ export const TypographyDropDown = () => { className={`item typography-item ${ value?.id === option.id ? "active dropdown-item-active" : "" }`} - onClick={() => applyTypography(option)} + onClick={() => applyTypography(option as TypographyValue)} key={option.id} > {option.name} diff --git a/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx b/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx index 3c5555deaeb..79535b8d466 100644 --- a/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx @@ -1,11 +1,11 @@ -import React from "react"; -import { ListPlugin } from "@lexical/react/LexicalListPlugin"; +import React from "react" import { CodeHighlightPlugin } from "~/plugins/CodeHighlightPlugin/CodeHighlightPlugin"; import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; import { FloatingLinkEditorPlugin } from "~/plugins/FloatingLinkEditorPlugin/FloatingLinkEditorPlugin"; import { ClickableLinkPlugin } from "~/plugins/ClickableLinkPlugin/ClickableLinkPlugin"; import { ParagraphToolbar } from "~/components/Toolbar/ParagraphToolbar"; import { RichTextEditor, RichTextEditorProps } from "~/components/Editor/RichTextEditor"; +import { WebinyListPlugin } from "~/plugins/WebinyListPLugin/WebinyListPlugin"; interface ParagraphLexicalEditorProps extends RichTextEditorProps { tag?: "p"; @@ -20,10 +20,10 @@ const ParagraphEditor: React.FC = ({ placeholder, t {...rest} > + - ); }; diff --git a/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx index be7afb49868..66531323ece 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx @@ -1,11 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { - $isListNode, - INSERT_UNORDERED_LIST_COMMAND, - ListNode, - REMOVE_LIST_COMMAND -} from "@lexical/list"; +import { $isListNode, ListNode } from "@lexical/list"; import { $getSelection, $isRangeSelection, @@ -14,6 +9,10 @@ import { SELECTION_CHANGE_COMMAND } from "lexical"; import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { + INSERT_UNORDERED_WEBINY_LIST_COMMAND, + REMOVE_WEBINY_LIST_COMMAND +} from "~/nodes/list-node/commands"; /** * Toolbar button action. On click will wrap the content in bullet list style. @@ -78,9 +77,9 @@ export const BulletListAction = () => { const formatBulletList = () => { if (!isActive) { // will update the active state in the useEffect - editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); + editor.dispatchCommand(INSERT_UNORDERED_WEBINY_LIST_COMMAND, { themeStyleId: "list1" }); } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); + editor.dispatchCommand(REMOVE_WEBINY_LIST_COMMAND, undefined); // removing will not update correctly the active state, so we need to set to false manually. setIsActive(false); } diff --git a/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx index 536aff0a38f..3df8547d301 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx @@ -1,11 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { - $isListNode, - INSERT_ORDERED_LIST_COMMAND, - ListNode, - REMOVE_LIST_COMMAND -} from "@lexical/list"; import { $getSelection, $isRangeSelection, @@ -14,6 +8,11 @@ import { SELECTION_CHANGE_COMMAND } from "lexical"; import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; +import { + INSERT_ORDERED_WEBINY_LIST_COMMAND, + REMOVE_WEBINY_LIST_COMMAND +} from "~/nodes/list-node/commands"; +import { $isWebinyListNode, WebinyListNode } from "~/nodes/list-node/WebinyListNode"; /** * Toolbar button action. On click will wrap the content in numbered list style. @@ -39,8 +38,11 @@ export const NumberedListAction = () => { element = anchorNode.getTopLevelElementOrThrow(); } - if ($isListNode(element)) { - const parentList = $getNearestNodeOfType(anchorNode, ListNode); + if ($isWebinyListNode(element)) { + const parentList = $getNearestNodeOfType( + anchorNode, + WebinyListNode + ); // get the type of the list that is selected with the cursor const type = parentList ? parentList.getListType() : element.getListType(); // set the button as active for numbered list @@ -78,9 +80,9 @@ export const NumberedListAction = () => { const formatNumberedList = () => { if (!isActive) { // will update the active state in the useEffect - editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); + editor.dispatchCommand(INSERT_ORDERED_WEBINY_LIST_COMMAND, { themeStyleId: "list" }); } else { - editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); + editor.dispatchCommand(REMOVE_WEBINY_LIST_COMMAND, undefined); // removing will not update correctly the active state, so we need to set to false manually. setIsActive(false); } diff --git a/packages/lexical-editor/src/context/RichTextEditorContext.tsx b/packages/lexical-editor/src/context/RichTextEditorContext.tsx index 8fbc54d6e87..2c9f528248c 100644 --- a/packages/lexical-editor/src/context/RichTextEditorContext.tsx +++ b/packages/lexical-editor/src/context/RichTextEditorContext.tsx @@ -1,11 +1,13 @@ import React, { createContext, useState } from "react"; -import { ToolbarType } from "~/types"; +import { TextBlockSelection, ToolbarType } from "~/types"; export interface RichTextEditorContext { nodeIsText: boolean; setNodeIsText: (nodeIsText: boolean) => void; toolbarType?: ToolbarType; setToolbarType: (type: ToolbarType) => void; + textBlockSelection: TextBlockSelection | null; + setTextBlockSelection: (textBlockSelection: TextBlockSelection) => void; } export const RichTextEditorContext = createContext(undefined); @@ -17,6 +19,11 @@ interface RichTextEditorProviderProps { export const RichTextEditorProvider: React.FC = ({ children }) => { const [nodeIsText, setIsText] = useState(false); const [toolbarType, setToolbarType] = useState(); + /* + * @desc Keeps data from current user text selection like range selection, nodes, node key... + */ + const [textBlockSelection, setTextBlockSelection] = useState(null); + const setNodeIsText = (nodeIsText: boolean) => { setIsText(nodeIsText); }; @@ -27,7 +34,9 @@ export const RichTextEditorProvider: React.FC = ({ nodeIsText, setNodeIsText, toolbarType, - setToolbarType + setToolbarType, + textBlockSelection, + setTextBlockSelection }} > {children} diff --git a/packages/lexical-editor/src/hooks/useWebinyList.ts b/packages/lexical-editor/src/hooks/useWebinyList.ts new file mode 100644 index 00000000000..6abdeae3a60 --- /dev/null +++ b/packages/lexical-editor/src/hooks/useWebinyList.ts @@ -0,0 +1,81 @@ +import type { LexicalEditor } from "lexical"; +import { mergeRegister } from "@lexical/utils"; +import { + COMMAND_PRIORITY_LOW, + INDENT_CONTENT_COMMAND, + INSERT_PARAGRAPH_COMMAND, + OUTDENT_CONTENT_COMMAND +} from "lexical"; +import { useEffect } from "react"; +import { + $handleListInsertParagraph, + indentList, + insertList, + outdentList, + removeList +} from "~/nodes/list-node/formatList"; +import { + INSERT_ORDERED_WEBINY_LIST_COMMAND, + INSERT_UNORDERED_WEBINY_LIST_COMMAND, + REMOVE_WEBINY_LIST_COMMAND +} from "~/nodes/list-node/commands"; + +export function useWebinyList(editor: LexicalEditor): void { + useEffect(() => { + return mergeRegister( + editor.registerCommand( + INDENT_CONTENT_COMMAND, + () => { + indentList(); + return false; + }, + COMMAND_PRIORITY_LOW + ), + editor.registerCommand( + OUTDENT_CONTENT_COMMAND, + () => { + outdentList(); + return false; + }, + COMMAND_PRIORITY_LOW + ), + editor.registerCommand( + INSERT_ORDERED_WEBINY_LIST_COMMAND, + ({ themeStyleId }) => { + insertList(editor, "number", themeStyleId); + return true; + }, + COMMAND_PRIORITY_LOW + ), + editor.registerCommand( + INSERT_UNORDERED_WEBINY_LIST_COMMAND, + ({ themeStyleId }) => { + insertList(editor, "bullet", themeStyleId); + return true; + }, + COMMAND_PRIORITY_LOW + ), + editor.registerCommand( + REMOVE_WEBINY_LIST_COMMAND, + () => { + removeList(editor); + return true; + }, + COMMAND_PRIORITY_LOW + ), + editor.registerCommand( + INSERT_PARAGRAPH_COMMAND, + () => { + const hasHandledInsertParagraph = $handleListInsertParagraph(); + + if (hasHandledInsertParagraph) { + return true; + } + + return false; + }, + COMMAND_PRIORITY_LOW + ) + ); + }, [editor]); +} diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index c9421b48480..2c33d36af84 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -45,6 +45,7 @@ export { AddRichTextEditorNodeType } from "~/components/AddRichTextEditorNodeTyp // utils export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; export { isValidLexicalData } from "~/utils/isValidLexicalData"; +export { clearNodeFormatting } from "~/utils/nodes/clearNodeFormating"; // types export * as types from "./types"; // config diff --git a/packages/lexical-editor/src/nodes/FontColorNode.ts b/packages/lexical-editor/src/nodes/FontColorNode.ts index 653300e9d72..0da8bb37c60 100644 --- a/packages/lexical-editor/src/nodes/FontColorNode.ts +++ b/packages/lexical-editor/src/nodes/FontColorNode.ts @@ -14,7 +14,7 @@ import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; export const ADD_FONT_COLOR_COMMAND: LexicalCommand = createCommand("ADD_FONT_COLOR_COMMAND"); -const FontColorNodeAttrName = "font-color-theme"; +const FontColorNodeAttrName = "data-theme-font-color-name"; export interface FontColorPayload { // This color can be hex string diff --git a/packages/lexical-editor/src/nodes/TypographyElementNode.ts b/packages/lexical-editor/src/nodes/TypographyElementNode.ts index fde2b240cff..9253c6baaaf 100644 --- a/packages/lexical-editor/src/nodes/TypographyElementNode.ts +++ b/packages/lexical-editor/src/nodes/TypographyElementNode.ts @@ -1,4 +1,5 @@ import { + $createParagraphNode, createCommand, EditorConfig, ElementNode, @@ -6,6 +7,7 @@ import { LexicalEditor, LexicalNode, NodeKey, + ParagraphNode, SerializedElementNode, Spread } from "lexical"; @@ -18,7 +20,7 @@ import { findTypographyStyleById } from "~/utils/typography"; export const ADD_TYPOGRAPHY_ELEMENT_COMMAND: LexicalCommand = createCommand( "ADD_TYPOGRAPHY_ELEMENT_COMMAND" ); -const TypographyNodeAttrName = "typography-el-theme"; +const TypographyNodeAttrName = "data-typography-style-id"; export interface TypographyPayload { value: TypographyValue; @@ -47,14 +49,14 @@ export class TypographyElementNode extends ElementNode { __styleId: string; __tag: TypographyHTMLTag; __name: string; - __typographyStyles: Record; + __css: Record; constructor(value: TypographyValue, key?: NodeKey) { super(key); this.__tag = value.tag; this.__styleId = value.id; this.__name = value.name; - this.__typographyStyles = value.css; + this.__css = value.css; } static override getType(): string { @@ -64,7 +66,7 @@ export class TypographyElementNode extends ElementNode { static override clone(node: TypographyElementNode): TypographyElementNode { return new TypographyElementNode( { - css: node.__typographyStyles, + css: node.__css, id: node.__styleId, name: node.__name, tag: node.__tag @@ -76,7 +78,7 @@ export class TypographyElementNode extends ElementNode { getTypographyValue(): TypographyValue { return { tag: this.__tag, - css: this.__typographyStyles, + css: this.__css, id: this.__styleId, name: this.__name }; @@ -85,10 +87,10 @@ export class TypographyElementNode extends ElementNode { addStylesHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { const typographyStyleValue = findTypographyStyleById(theme, this.__styleId); if (typographyStyleValue) { - this.__typographyStyles = typographyStyleValue.css; + this.__css = typographyStyleValue.css; } element.setAttribute(TypographyNodeAttrName, this.__styleId); - element.style.cssText = styleObjectToString(this.__typographyStyles); + element.style.cssText = styleObjectToString(this.__css); return element; } @@ -96,7 +98,7 @@ export class TypographyElementNode extends ElementNode { return { ...super.exportJSON(), tag: this.__tag, - typographyStyles: this.__typographyStyles, + typographyStyles: this.__css, name: this.__name, styleId: this.__styleId, type: "typography-el-node", @@ -126,6 +128,22 @@ export class TypographyElementNode extends ElementNode { override updateDOM(): boolean { return false; } + + override insertNewAfter(): ParagraphNode { + const newElement = $createParagraphNode(); + const direction = this.getDirection(); + newElement.setDirection(direction); + this.insertAfter(newElement); + return newElement; + } + + override collapseAtStart(): true { + const paragraph = $createParagraphNode(); + const children = this.getChildren(); + children.forEach(child => paragraph.append(child)); + this.replace(paragraph); + return true; + } } export const $createTypographyNode = ( diff --git a/packages/lexical-editor/src/nodes/list-node/WebinyListItemNode.ts b/packages/lexical-editor/src/nodes/list-node/WebinyListItemNode.ts new file mode 100644 index 00000000000..e98acfb6478 --- /dev/null +++ b/packages/lexical-editor/src/nodes/list-node/WebinyListItemNode.ts @@ -0,0 +1,494 @@ +import { + $createParagraphNode, + $isElementNode, + $isParagraphNode, + $isRangeSelection, + DOMConversionMap, + DOMConversionOutput, + EditorConfig, + EditorThemeClasses, + ElementNode, + GridSelection, + LexicalNode, + NodeKey, + NodeSelection, + ParagraphNode, + RangeSelection, + SerializedElementNode +} from "lexical"; +import { Spread } from "lexical"; +import { + $createWebinyListNode, + $isWebinyListNode, + WebinyListNode +} from "~/nodes/list-node/WebinyListNode"; +import { $createListNode } from "@lexical/list"; +import { addClassNamesToElement, removeClassNamesFromElement } from "@lexical/utils"; +import { + $handleIndent, + $handleOutdent, + updateChildrenListItemValue +} from "~/nodes/list-node/formatList"; + +export type SerializedWebinyListItemNode = Spread< + { + checked: boolean | undefined; + type: "webiny-listitem"; + value: number; + version: 1; + }, + SerializedElementNode +>; + +/** @noInheritDoc */ +export class WebinyListItemNode extends ElementNode { + /** @internal */ + __value: number; + /** @internal */ + __checked?: boolean; + + static override getType(): string { + return "webiny-listitem"; + } + + static override clone(node: WebinyListItemNode): WebinyListItemNode { + return new WebinyListItemNode(node.__value, node.__checked, node.__key); + } + + constructor(value?: number, checked?: boolean, key?: NodeKey) { + super(key); + this.__value = value === undefined ? 1 : value; + this.__checked = checked; + } + + override createDOM(config: EditorConfig): HTMLElement { + const element = document.createElement("li"); + const parent = this.getParent(); + + if ($isWebinyListNode(parent)) { + updateChildrenListItemValue(parent); + updateListItemChecked(element, this, null, parent); + } + element.value = this.__value; + $setListItemThemeClassNames(element, config.theme, this); + + return element; + } + + override updateDOM( + prevNode: WebinyListItemNode, + dom: HTMLElement, + config: EditorConfig + ): boolean { + const parent = this.getParent(); + + if ($isWebinyListNode(parent)) { + updateChildrenListItemValue(parent); + updateListItemChecked(dom, this, prevNode, parent); + } + // @ts-expect-error - this is always HTMLListItemElement + dom.value = this.__value; + + $setListItemThemeClassNames(dom, config.theme, this); + + return false; + } + + static importDOM(): DOMConversionMap | null { + return { + li: () => ({ + conversion: convertListItemElement, + priority: 0 + }) + }; + } + + static override importJSON(serializedNode: SerializedWebinyListItemNode): WebinyListItemNode { + const node = new WebinyListItemNode(serializedNode.value, serializedNode.checked); + node.setFormat(serializedNode.format); + node.setIndent(serializedNode.indent); + node.setDirection(serializedNode.direction); + return node; + } + + override exportJSON(): SerializedWebinyListItemNode { + return { + ...super.exportJSON(), + checked: this.getChecked(), + type: "webiny-listitem", + value: this.getValue(), + version: 1 + }; + } + + override append(...nodes: LexicalNode[]): this { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if ($isElementNode(node) && this.canMergeWith(node)) { + const children = node.getChildren(); + this.append(...children); + node.remove(); + } else { + super.append(node); + } + } + + return this; + } + + override replace(replaceWithNode: N): N { + if ($isWebinyListItemNode(replaceWithNode)) { + return super.replace(replaceWithNode); + } + + const list = this.getParentOrThrow(); + + if ($isWebinyListNode(list)) { + const childrenKeys = list.__children; + const childrenLength = childrenKeys.length; + const index = childrenKeys.indexOf(this.__key); + + if (index === 0) { + list.insertBefore(replaceWithNode); + } else if (index === childrenLength - 1) { + list.insertAfter(replaceWithNode); + } else { + // Split the list + const newList = $createWebinyListNode(list.getListType(), list.getStyleId()); + const children = list.getChildren(); + + for (let i = index + 1; i < childrenLength; i++) { + const child = children[i]; + newList.append(child); + } + list.insertAfter(replaceWithNode); + replaceWithNode.insertAfter(newList); + } + this.remove(); + + if (childrenLength === 1) { + list.remove(); + } + } + + return replaceWithNode; + } + + override insertAfter(node: LexicalNode): LexicalNode { + const listNode = this.getParentOrThrow(); + + if (!$isWebinyListNode(listNode)) { + console.log("insertAfter: webiny list node is not parent of list item node"); + return listNode; + } + + const siblings = this.getNextSiblings(); + + if ($isWebinyListItemNode(node)) { + const after = super.insertAfter(node); + const afterListNode = node.getParentOrThrow(); + + if ($isWebinyListNode(afterListNode)) { + afterListNode; + } + + return after; + } + + // Attempt to merge if the list is of the same type. + + if ($isWebinyListNode(node) && node.getListType() === listNode.getListType()) { + let child = node; + const children = node.getChildren(); + + for (let i = children.length - 1; i >= 0; i--) { + child = children[i]; + + this.insertAfter(child); + } + + return child; + } + + // Otherwise, split the list + // Split the lists and insert the node in between them + listNode.insertAfter(node); + + if (siblings.length !== 0) { + const newListNode = $createListNode(listNode.getListType()); + + siblings.forEach(sibling => newListNode.append(sibling)); + + node.insertAfter(newListNode); + } + + return node; + } + + override remove(preserveEmptyParent?: boolean): void { + const nextSibling = this.getNextSibling(); + super.remove(preserveEmptyParent); + + if (nextSibling !== null) { + const parent = nextSibling.getParent(); + + if ($isWebinyListNode(parent)) { + updateChildrenListItemValue(parent); + } + } + } + + override insertNewAfter(): WebinyListItemNode | ParagraphNode { + const newElement = $createWebinyListItemNode(this.__checked == null ? undefined : false); + this.insertAfter(newElement); + + return newElement; + } + + override collapseAtStart(selection: RangeSelection): true { + const paragraph = $createParagraphNode(); + const children = this.getChildren(); + children.forEach(child => paragraph.append(child)); + const listNode = this.getParentOrThrow(); + const listNodeParent = listNode.getParentOrThrow(); + const isIndented = $isWebinyListItemNode(listNodeParent); + + if (listNode.getChildrenSize() === 1) { + if (isIndented) { + // if the list node is nested, we just want to remove it, + // effectively unindenting it. + listNode.remove(); + listNodeParent.select(); + } else { + listNode.replace(paragraph); + // If we have selection on the list item, we'll need to move it + // to the paragraph + const anchor = selection.anchor; + const focus = selection.focus; + const key = paragraph.getKey(); + + if (anchor.type === "element" && anchor.getNode().is(this)) { + anchor.set(key, anchor.offset, "element"); + } + + if (focus.type === "element" && focus.getNode().is(this)) { + focus.set(key, focus.offset, "element"); + } + } + } else { + listNode.insertBefore(paragraph); + this.remove(); + } + + return true; + } + + getValue(): number { + const self = this.getLatest(); + + return self.__value; + } + + setValue(value: number): void { + const self = this.getWritable(); + self.__value = value; + } + + getChecked(): boolean | undefined { + const self = this.getLatest(); + + return self.__checked; + } + + setChecked(checked?: boolean): void { + const self = this.getWritable(); + self.__checked = checked; + } + + toggleChecked(): void { + this.setChecked(!this.__checked); + } + + override getIndent(): number { + // If we don't have a parent, we are likely serializing + const parent = this.getParent(); + if (parent === null) { + return this.getLatest().__indent; + } + // ListItemNode should always have a ListNode for a parent. + let listNodeParent = parent.getParentOrThrow(); + let indentLevel = 0; + while ($isWebinyListItemNode(listNodeParent)) { + listNodeParent = listNodeParent.getParentOrThrow().getParentOrThrow(); + indentLevel++; + } + + return indentLevel; + } + + override setIndent(indent: number): this { + let currentIndent = this.getIndent(); + while (currentIndent !== indent) { + if (currentIndent < indent) { + $handleIndent([this]); + currentIndent++; + } else { + $handleOutdent([this]); + currentIndent--; + } + } + + return this; + } + + override canIndent(): false { + // Indent/outdent is handled specifically in the RichText logic. + + return false; + } + + override insertBefore(nodeToInsert: LexicalNode): LexicalNode { + if ($isWebinyListItemNode(nodeToInsert)) { + const parent = this.getParentOrThrow(); + + if ($isWebinyListNode(parent)) { + const siblings = this.getNextSiblings(); + updateChildrenListItemValue(parent, siblings); + } + } + + return super.insertBefore(nodeToInsert); + } + + override canInsertAfter(node: LexicalNode): boolean { + return $isWebinyListNode(node); + } + + override canReplaceWith(replacement: LexicalNode): boolean { + return $isWebinyListItemNode(replacement); + } + + override canMergeWith(node: LexicalNode): boolean { + return $isParagraphNode(node) || $isWebinyListItemNode(node); + } + + override extractWithChild( + child: LexicalNode, + selection: RangeSelection | NodeSelection | GridSelection + ): boolean { + if (!$isRangeSelection(selection)) { + return false; + } + + const anchorNode = selection.anchor.getNode(); + const focusNode = selection.focus.getNode(); + + return ( + this.isParentOf(anchorNode) && + this.isParentOf(focusNode) && + this.getTextContent().length === selection.getTextContent().length + ); + } +} + +function $setListItemThemeClassNames( + dom: HTMLElement, + editorThemeClasses: EditorThemeClasses, + node: WebinyListItemNode +): void { + const classesToAdd = []; + const classesToRemove = []; + const listTheme = editorThemeClasses.list; + const listItemClassName = listTheme ? listTheme.listitem : undefined; + let nestedListItemClassName; + + if (listTheme && listTheme.nested) { + nestedListItemClassName = listTheme.nested.listitem; + } + + if (listItemClassName !== undefined) { + const listItemClasses = listItemClassName.split(" "); + classesToAdd.push(...listItemClasses); + } + + if (listTheme) { + const parentNode = node.getParent(); + const isCheckList = $isWebinyListNode(parentNode) && parentNode?.getListType() === "check"; + const checked = node.getChecked(); + + if (!isCheckList || checked) { + classesToRemove.push(listTheme.listitemUnchecked); + } + + if (!isCheckList || !checked) { + classesToRemove.push(listTheme.listitemChecked); + } + + if (isCheckList) { + classesToAdd.push(checked ? listTheme.listitemChecked : listTheme.listitemUnchecked); + } + } + + if (nestedListItemClassName !== undefined) { + const nestedListItemClasses = nestedListItemClassName.split(" "); + + if (node.getChildren().some(child => $isWebinyListNode(child))) { + classesToAdd.push(...nestedListItemClasses); + } else { + classesToRemove.push(...nestedListItemClasses); + } + } + + if (classesToRemove.length > 0) { + removeClassNamesFromElement(dom, ...classesToRemove); + } + + if (classesToAdd.length > 0) { + addClassNamesToElement(dom, ...classesToAdd); + } +} + +function updateListItemChecked( + dom: HTMLElement, + listItemNode: WebinyListItemNode, + prevListItemNode: WebinyListItemNode | null, + listNode: WebinyListNode +): void { + const isCheckList = listNode.getListType() === "check"; + + if (isCheckList) { + // Only add attributes for leaf list items + if ($isWebinyListNode(listItemNode.getFirstChild())) { + dom.removeAttribute("role"); + dom.removeAttribute("tabIndex"); + dom.removeAttribute("aria-checked"); + } else { + dom.setAttribute("role", "checkbox"); + dom.setAttribute("tabIndex", "-1"); + + if (!prevListItemNode || listItemNode.__checked !== prevListItemNode.__checked) { + dom.setAttribute("aria-checked", listItemNode.getChecked() ? "true" : "false"); + } + } + } else { + // Clean up checked state + if (listItemNode.getChecked() != null) { + listItemNode.setChecked(undefined); + } + } +} + +function convertListItemElement(): DOMConversionOutput { + return { node: $createWebinyListItemNode() }; +} + +export function $createWebinyListItemNode(checked?: boolean): WebinyListItemNode { + return new WebinyListItemNode(undefined, checked); +} + +export function $isWebinyListItemNode( + node: LexicalNode | null | undefined +): node is WebinyListItemNode { + return node instanceof WebinyListItemNode; +} diff --git a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts new file mode 100644 index 00000000000..286b9fac307 --- /dev/null +++ b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts @@ -0,0 +1,279 @@ +import { + DOMConversion, + DOMConversionMap, + DOMConversionOutput, + EditorConfig, + EditorThemeClasses, + ElementNode, + LexicalNode, + NodeKey, + SerializedElementNode, + Spread +} from "lexical"; +import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; +import { findTypographyStyleById } from "~/utils/typography"; +import { styleObjectToString } from "~/utils/styleObjectToString"; +import { addClassNamesToElement, removeClassNamesFromElement } from "@lexical/utils"; +import { ListNodeTagType } from "@lexical/list/LexicalListNode"; + +import { $getListDepth, wrapInListItem } from "~/utils/nodes/list-node"; +import { ListType } from "@lexical/list"; +import { $isWebinyListItemNode, WebinyListItemNode } from "~/nodes/list-node/WebinyListItemNode"; + +const TypographyStyleAttrName = "data-theme-list-style-id"; + +export type SerializedWebinyListNode = Spread< + { + themeStyleId: string; + listType: ListType; + start: number; + tag: ListNodeTagType; + type: "webiny-list"; + version: 1; + }, + SerializedElementNode +>; + +export class WebinyListNode extends ElementNode { + /** @internal */ + __tag: ListNodeTagType; + /** @internal */ + __start: number; + /** @internal */ + __listType: ListType; + + __themeStyleId: string; + + constructor(listType: ListType, themeStyleId: string, start: number, key?: NodeKey) { + super(key); + this.__themeStyleId = themeStyleId; + const _listType = TAG_TO_WEBINY_LIST_TYPE[listType] || listType; + this.__listType = _listType; + this.__tag = _listType === "number" ? "ol" : "ul"; + this.__start = start; + } + + static override getType() { + return "webiny-list"; + } + + addStylesHTMLElement(element: HTMLElement, theme: WebinyEditorTheme): HTMLElement { + let css = {}; + if (this.__themeStyleId) { + const typographyStyleValue = findTypographyStyleById(theme, this.__themeStyleId); + css = typographyStyleValue?.css ? typographyStyleValue.css : {}; + element.setAttribute(TypographyStyleAttrName, this.__themeStyleId); + } + element.style.cssText = styleObjectToString(css); + return element; + } + + override createDOM(config: EditorConfig): HTMLElement { + const tag = this.__tag; + const dom = document.createElement(tag); + + if (this.__start !== 1) { + dom.setAttribute("start", String(this.__start)); + } + + // @ts-expect-error Internal field. + dom.__lexicalListType = this.__listType; + setListThemeClassNames(dom, config.theme, this); + this.addStylesHTMLElement(dom, config.theme); + + return dom; + } + + static override clone(node: WebinyListNode): WebinyListNode { + return new WebinyListNode( + node.getListType(), + node.getStyleId(), + node.getStart(), + node.__key + ); + } + + getTag(): ListNodeTagType { + return this.__tag; + } + + getListType(): ListType { + return this.__listType; + } + + getStart(): number { + return this.__start; + } + + getStyleId(): string { + return this.__themeStyleId; + } + + static override importJSON(serializedNode: SerializedWebinyListNode): WebinyListNode { + const node = $createWebinyListNode( + serializedNode.listType, + serializedNode.themeStyleId, + serializedNode.start + ); + node.setFormat(serializedNode.format); + node.setIndent(serializedNode.indent); + node.setDirection(serializedNode.direction); + return node; + } + + // @ts-ignore + override exportJSON(): SerializedWebinyListNode { + return { + ...super.exportJSON(), + themeStyleId: this.__themeStyleId ?? "", + listType: this.getListType(), + start: this.getStart(), + tag: this.getTag(), + type: "webiny-list", + version: 1 + }; + } + + static importDomConversionMap(domNode: HTMLElement): DOMConversion | null { + if (!domNode.hasAttribute(TypographyStyleAttrName)) { + return null; + } + return { + conversion: convertWebinyListNode, + priority: 0 + } + } + + static importDOM(): DOMConversionMap | null { + return { + ol: (domNode: HTMLElement) => { + return this.importDomConversionMap(domNode); + }, + ul: (domNode: HTMLElement) => { + return this.importDomConversionMap(domNode) + }, + }; + } + + override updateDOM(prevNode: WebinyListNode, dom: HTMLElement, config: EditorConfig): boolean { + if (prevNode.__tag !== this.__tag) { + return true; + } + // update styles for different tag styles + setListThemeClassNames(dom, config.theme, this); + this.addStylesHTMLElement(dom, config.theme); + return false; + } +} + +function setListThemeClassNames( + dom: HTMLElement, + editorThemeClasses: EditorThemeClasses, + node: WebinyListNode +): void { + const classesToAdd = []; + const classesToRemove = []; + const listTheme = editorThemeClasses.list; + + if (listTheme !== undefined) { + const listLevelsClassNames = listTheme[`${node.__tag}Depth`] || []; + const listDepth = $getListDepth(node) - 1; + const normalizedListDepth = listDepth % listLevelsClassNames.length; + const listLevelClassName = listLevelsClassNames[normalizedListDepth]; + const listClassName = listTheme[node.__tag]; + let nestedListClassName; + const nestedListTheme = listTheme.nested; + + if (nestedListTheme !== undefined && nestedListTheme.list) { + nestedListClassName = nestedListTheme.list; + } + + if (listClassName !== undefined) { + classesToAdd.push(listClassName); + } + + if (listLevelClassName !== undefined) { + const listItemClasses = listLevelClassName.split(" "); + classesToAdd.push(...listItemClasses); + for (let i = 0; i < listLevelsClassNames.length; i++) { + if (i !== normalizedListDepth) { + classesToRemove.push(node.__tag + i); + } + } + } + + if (nestedListClassName !== undefined) { + const nestedListItemClasses = nestedListClassName.split(" "); + + if (listDepth > 1) { + classesToAdd.push(...nestedListItemClasses); + } else { + classesToRemove.push(...nestedListItemClasses); + } + } + } + + if (classesToRemove.length > 0) { + removeClassNamesFromElement(dom, ...classesToRemove); + } + + if (classesToAdd.length > 0) { + addClassNamesToElement(dom, ...classesToAdd); + } +} +/* + * This function normalizes the children of a ListNode after the conversion from HTML, + * ensuring that they are all ListItemNodes and contain either a single nested ListNode + * or some other inline content. + */ +function normalizeChildren(nodes: Array): Array { + const normalizedListItems: Array = []; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if ($isWebinyListItemNode(node)) { + normalizedListItems.push(node); + node.getChildren().forEach(child => { + if ($isWebinyListNode(child)) { + normalizedListItems.push(wrapInListItem(child)); + } + }); + } else { + normalizedListItems.push(wrapInListItem(node)); + } + } + return normalizedListItems; +} + +function convertWebinyListNode(domNode: Node): DOMConversionOutput { + const nodeName = domNode.nodeName.toLowerCase(); + let node = null; + + if (nodeName === "ol") { + node = $createWebinyListNode("number", ""); + } else if (nodeName === "ul") { + node = $createWebinyListNode("bullet", ""); + } + + return { + // @ts-ignore + after: normalizeChildren, + node + }; +} + +const TAG_TO_WEBINY_LIST_TYPE: Record = { + ol: "number", + ul: "bullet" +}; + +export function $createWebinyListNode( + listType: ListType, + themeStyleId: string, + start = 1 +): WebinyListNode { + return new WebinyListNode(listType, themeStyleId, start); +} + +export function $isWebinyListNode(node: LexicalNode | null | undefined): node is WebinyListNode { + return node instanceof WebinyListNode; +} diff --git a/packages/lexical-editor/src/nodes/list-node/commands.ts b/packages/lexical-editor/src/nodes/list-node/commands.ts new file mode 100644 index 00000000000..53f45eaacee --- /dev/null +++ b/packages/lexical-editor/src/nodes/list-node/commands.ts @@ -0,0 +1,15 @@ +import { createCommand, LexicalCommand } from "lexical"; + +export type WebinyListCommandPayload = { + themeStyleId: string; +}; + +export const INSERT_UNORDERED_WEBINY_LIST_COMMAND: LexicalCommand = + createCommand("INSERT_UNORDERED_WEBINY_LIST_COMMAND"); +export const INSERT_ORDERED_WEBINY_LIST_COMMAND: LexicalCommand = + createCommand("INSERT_ORDERED_WEBINY_LIST_COMMAND"); +export const INSERT_WEBINY_CHECK_LIST_COMMAND: LexicalCommand = + createCommand("INSERT_WEBINY_CHECK_LIST_COMMAND"); +export const REMOVE_WEBINY_LIST_COMMAND: LexicalCommand = createCommand( + "REMOVE_WEBINY_LIST_COMMAND" +); diff --git a/packages/lexical-editor/src/nodes/list-node/formatList.ts b/packages/lexical-editor/src/nodes/list-node/formatList.ts new file mode 100644 index 00000000000..354d8b4c20c --- /dev/null +++ b/packages/lexical-editor/src/nodes/list-node/formatList.ts @@ -0,0 +1,501 @@ +import { + $createParagraphNode, + $getSelection, + $isElementNode, + $isLeafNode, + $isParagraphNode, + $isRangeSelection, + $isRootOrShadowRoot, + DEPRECATED_$isGridSelection, + ElementNode, + LexicalEditor, + LexicalNode, + NodeKey, + ParagraphNode +} from "lexical"; +import { $createWebinyListNode, $isWebinyListNode, WebinyListNode } from "./WebinyListNode"; +import { + $getAllListItems, + $getTopListNode, + $removeHighestEmptyListParent, + findNearestWebinyListItemNode, + getUniqueWebinyListItemNodes, + isNestedListNode +} from "~/utils/nodes/list-node"; +import { $getNearestNodeOfType } from "@lexical/utils"; +import { + $createWebinyListItemNode, + $isWebinyListItemNode, + WebinyListItemNode +} from "~/nodes/list-node/WebinyListItemNode"; +import { ListType } from "@lexical/list"; + +const DEFAULT_LIST_START_NUMBER = 1; + +function $isSelectingEmptyListItem( + anchorNode: WebinyListItemNode | LexicalNode, + nodes: Array +): boolean { + return ( + $isWebinyListItemNode(anchorNode) && + (nodes.length === 0 || + (nodes.length === 1 && anchorNode.is(nodes[0]) && anchorNode.getChildrenSize() === 0)) + ); +} + +function $getListItemValue(listItem: WebinyListItemNode): number { + const list = listItem.getParent(); + + let value = 1; + + if (list !== null) { + if (!$isWebinyListNode(list)) { + console.log("$getListItemValue: webiny list node is not parent of webiny list item node"); + return DEFAULT_LIST_START_NUMBER; + } else { + value = list.getStart(); + } + } + + const siblings = listItem.getPreviousSiblings(); + for (let i = 0; i < siblings.length; i++) { + const sibling = siblings[i]; + + if ($isWebinyListItemNode(sibling) && !$isWebinyListNode(sibling.getFirstChild())) { + value++; + } + } + return value; +} + +export function insertList(editor: LexicalEditor, listType: ListType, styleId: string): void { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) { + const nodes = selection.getNodes(); + const anchor = selection.anchor; + const anchorNode = anchor.getNode(); + const anchorNodeParent = anchorNode.getParent(); + + if ($isSelectingEmptyListItem(anchorNode, nodes)) { + const list = $createWebinyListNode(listType, styleId); + + if ($isRootOrShadowRoot(anchorNodeParent)) { + anchorNode.replace(list); + const listItem = $createWebinyListItemNode(); + if ($isElementNode(anchorNode)) { + listItem.setFormat(anchorNode.getFormatType()); + listItem.setIndent(anchorNode.getIndent()); + } + list.append(listItem); + } else if ($isWebinyListItemNode(anchorNode)) { + const parent = anchorNode.getParentOrThrow(); + append(list, parent.getChildren()); + parent.replace(list); + } + + return; + } else { + const handled = new Set(); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if ($isElementNode(node) && node.isEmpty() && !handled.has(node.getKey())) { + createListOrMerge(node, listType, styleId); + continue; + } + + if ($isLeafNode(node)) { + let parent = node.getParent(); + while (parent != null) { + const parentKey = parent.getKey(); + + if ($isWebinyListNode(parent)) { + if (!handled.has(parentKey)) { + const newListNode = $createWebinyListNode(listType, styleId); + append(newListNode, parent.getChildren()); + parent.replace(newListNode); + updateChildrenListItemValue(newListNode); + handled.add(parentKey); + } + + break; + } else { + const nextParent = parent.getParent(); + + if ($isRootOrShadowRoot(nextParent) && !handled.has(parentKey)) { + handled.add(parentKey); + createListOrMerge(parent, listType, styleId); + break; + } + + parent = nextParent; + } + } + } + } + } + } + }); +} + +function append(node: ElementNode, nodesToAppend: Array) { + node.splice(node.getChildrenSize(), 0, nodesToAppend); +} + +function createListOrMerge(node: ElementNode, listType: ListType, styleId: string): WebinyListNode { + if ($isWebinyListNode(node)) { + return node; + } + + const previousSibling = node.getPreviousSibling(); + const nextSibling = node.getNextSibling(); + const listItem = $createWebinyListItemNode(); + listItem.setFormat(node.getFormatType()); + listItem.setIndent(node.getIndent()); + append(listItem, node.getChildren()); + + if ($isWebinyListNode(previousSibling) && listType === previousSibling.getListType()) { + previousSibling.append(listItem); + node.remove(); + // if the same type of list is on both sides, merge them. + + if ($isWebinyListNode(nextSibling) && listType === nextSibling.getListType()) { + append(previousSibling, nextSibling.getChildren()); + nextSibling.remove(); + } + return previousSibling; + } else if ($isWebinyListNode(nextSibling) && listType === nextSibling.getListType()) { + nextSibling.getFirstChildOrThrow().insertBefore(listItem); + node.remove(); + return nextSibling; + } else { + const list = $createWebinyListNode(listType, styleId); + list.append(listItem); + node.replace(list); + updateChildrenListItemValue(list); + return list; + } +} + +export function removeList(editor: LexicalEditor): void { + editor.update(() => { + const selection = $getSelection(); + + if ($isRangeSelection(selection)) { + const listNodes = new Set(); + const nodes = selection.getNodes(); + const anchorNode = selection.anchor.getNode(); + + if ($isSelectingEmptyListItem(anchorNode, nodes)) { + listNodes.add($getTopListNode(anchorNode)); + } else { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if ($isLeafNode(node)) { + const WebinyListItemNode = $getNearestNodeOfType(node, WebinyListNode); + + if (WebinyListItemNode != null) { + listNodes.add($getTopListNode(WebinyListItemNode)); + } + } + } + } + + for (const listNode of listNodes) { + let insertionPoint: WebinyListNode | ParagraphNode = listNode; + + const listItems = $getAllListItems(listNode); + + for (const WebinyListItemNode of listItems) { + const paragraph = $createParagraphNode(); + + append(paragraph, WebinyListItemNode.getChildren()); + + insertionPoint.insertAfter(paragraph); + insertionPoint = paragraph; + + // When the anchor and focus fall on the textNode + // we don't have to change the selection because the textNode will be appended to + // the newly generated paragraph. + // When selection is in empty nested list item, selection is actually on the WebinyListItemNode. + // When the corresponding WebinyListItemNode is deleted and replaced by the newly generated paragraph + // we should manually set the selection's focus and anchor to the newly generated paragraph. + if (WebinyListItemNode.__key === selection.anchor.key) { + selection.anchor.set(paragraph.getKey(), 0, "element"); + } + if (WebinyListItemNode.__key === selection.focus.key) { + selection.focus.set(paragraph.getKey(), 0, "element"); + } + + WebinyListItemNode.remove(); + } + listNode.remove(); + } + } + }); +} + +export function updateChildrenListItemValue( + list: WebinyListNode, + children?: Array +): void { + const childrenOrExisting = children || list.getChildren(); + if (childrenOrExisting !== undefined) { + for (let i = 0; i < childrenOrExisting.length; i++) { + const child = childrenOrExisting[i]; + if ($isWebinyListItemNode(child)) { + const prevValue = child.getValue(); + const nextValue = $getListItemValue(child); + + if (prevValue !== nextValue) { + child.setValue(nextValue); + } + } + } + } +} + +export function $handleIndent(WebinyListItemNodes: Array): void { + // go through each node and decide where to move it. + const removed = new Set(); + + WebinyListItemNodes.forEach((WebinyListItemNode: WebinyListItemNode) => { + if (isNestedListNode(WebinyListItemNode) || removed.has(WebinyListItemNode.getKey())) { + return; + } + + const parent = WebinyListItemNode.getParent(); + + // We can cast both of the below `isNestedListNode` only returns a boolean type instead of a user-defined type guards + const nextSibling = + WebinyListItemNode.getNextSibling() as WebinyListItemNode; + const previousSibling = + WebinyListItemNode.getPreviousSibling() as WebinyListItemNode; + // if there are nested lists on either side, merge them all together. + + if (isNestedListNode(nextSibling) && isNestedListNode(previousSibling)) { + const innerList = previousSibling.getFirstChild(); + + if ($isWebinyListNode(innerList)) { + innerList.append(WebinyListItemNode); + const nextInnerList = nextSibling.getFirstChild(); + + if ($isWebinyListNode(nextInnerList)) { + const children = nextInnerList.getChildren(); + append(innerList, children); + nextSibling.remove(); + removed.add(nextSibling.getKey()); + } + updateChildrenListItemValue(innerList); + } + } else if (isNestedListNode(nextSibling)) { + // if the WebinyListItemNode is next to a nested ListNode, merge them + const innerList = nextSibling.getFirstChild(); + + if ($isWebinyListNode(innerList)) { + const firstChild = innerList.getFirstChild(); + + if (firstChild !== null) { + firstChild.insertBefore(WebinyListItemNode); + } + updateChildrenListItemValue(innerList); + } + } else if (isNestedListNode(previousSibling)) { + const innerList = previousSibling.getFirstChild(); + + if ($isWebinyListNode(innerList)) { + innerList.append(WebinyListItemNode); + updateChildrenListItemValue(innerList); + } + } else { + // otherwise, we need to create a new nested ListNode + + if ($isWebinyListNode(parent)) { + const newListItem = $createWebinyListItemNode(); + const newList = $createWebinyListNode(parent.getListType(), parent.getStyleId()); + newListItem.append(newList); + newList.append(WebinyListItemNode); + + if (previousSibling) { + previousSibling.insertAfter(newListItem); + } else if (nextSibling) { + nextSibling.insertBefore(newListItem); + } else { + parent.append(newListItem); + } + } + } + + if ($isWebinyListNode(parent)) { + updateChildrenListItemValue(parent); + } + }); +} + +export function $handleOutdent(WebinyListItemNodes: Array): void { + // go through each node and decide where to move it. + + WebinyListItemNodes.forEach(WebinyListItemNode => { + if (isNestedListNode(WebinyListItemNode)) { + return; + } + const parentList = WebinyListItemNode.getParent(); + const grandparentListItem = parentList ? parentList.getParent() : undefined; + const greatGrandparentList = grandparentListItem + ? grandparentListItem.getParent() + : undefined; + // If it doesn't have these ancestors, it's not indented. + + if ( + $isWebinyListNode(greatGrandparentList) && + $isWebinyListItemNode(grandparentListItem) && + $isWebinyListNode(parentList) + ) { + // if it's the first child in it's parent list, insert it into the + // great grandparent list before the grandparent + const firstChild = parentList ? parentList.getFirstChild() : undefined; + const lastChild = parentList ? parentList.getLastChild() : undefined; + + if (WebinyListItemNode.is(firstChild)) { + grandparentListItem.insertBefore(WebinyListItemNode); + + if (parentList.isEmpty()) { + grandparentListItem.remove(); + } + // if it's the last child in it's parent list, insert it into the + // great grandparent list after the grandparent. + } else if (WebinyListItemNode.is(lastChild)) { + grandparentListItem.insertAfter(WebinyListItemNode); + + if (parentList.isEmpty()) { + grandparentListItem.remove(); + } + } else { + // otherwise, we need to split the siblings into two new nested lists + const listType = parentList.getListType(); + const themeStyleId = parentList.getStyleId(); + const previousSiblingsListItem = $createWebinyListItemNode(); + const previousSiblingsList = $createWebinyListNode(listType, themeStyleId); + previousSiblingsListItem.append(previousSiblingsList); + WebinyListItemNode.getPreviousSiblings().forEach(sibling => + previousSiblingsList.append(sibling) + ); + const nextSiblingsListItem = $createWebinyListItemNode(); + const nextSiblingsList = $createWebinyListNode(listType, themeStyleId); + nextSiblingsListItem.append(nextSiblingsList); + append(nextSiblingsList, WebinyListItemNode.getNextSiblings()); + // put the sibling nested lists on either side of the grandparent list item in the great grandparent. + grandparentListItem.insertBefore(previousSiblingsListItem); + grandparentListItem.insertAfter(nextSiblingsListItem); + // replace the grandparent list item (now between the siblings) with the outdented list item. + grandparentListItem.replace(WebinyListItemNode); + } + updateChildrenListItemValue(parentList); + updateChildrenListItemValue(greatGrandparentList); + } + }); +} + +function maybeIndentOrOutdent(direction: "indent" | "outdent"): void { + const selection = $getSelection(); + + if (!$isRangeSelection(selection)) { + return; + } + const selectedNodes = selection.getNodes(); + let WebinyListItemNodes: Array = []; + + if (selectedNodes.length === 0) { + selectedNodes.push(selection.anchor.getNode()); + } + + if (selectedNodes.length === 1) { + // Only 1 node selected. Selection may not contain the ListNodeItem so we traverse the tree to + // find whether this is part of a WebinyListItemNode + const nearestWebinyListItemNode = findNearestWebinyListItemNode(selectedNodes[0]); + + if (nearestWebinyListItemNode !== null) { + WebinyListItemNodes = [nearestWebinyListItemNode]; + } + } else { + WebinyListItemNodes = getUniqueWebinyListItemNodes(selectedNodes); + } + + if (WebinyListItemNodes.length > 0) { + if (direction === "indent") { + $handleIndent(WebinyListItemNodes); + } else { + $handleOutdent(WebinyListItemNodes); + } + } +} + +export function indentList(): void { + maybeIndentOrOutdent("indent"); +} + +export function outdentList(): void { + maybeIndentOrOutdent("outdent"); +} + +export function $handleListInsertParagraph(): boolean { + const selection = $getSelection(); + + if (!$isRangeSelection(selection) || !selection.isCollapsed()) { + return false; + } + // Only run this code on empty list items + const anchor = selection.anchor.getNode(); + + if (!$isWebinyListItemNode(anchor) || anchor.getTextContent() !== "") { + return false; + } + const topListNode = $getTopListNode(anchor); + const parent = anchor.getParent(); + + if($isWebinyListNode(parent)){ + console.log("A WebinyListItemNode must have a WebinyListNode for a parent.") + return false; + } + + const grandparent = parent?.getParent() || null; + + let replacementNode; + + if ($isRootOrShadowRoot(grandparent)) { + replacementNode = $createParagraphNode(); + topListNode.insertAfter(replacementNode); + } else if ($isWebinyListItemNode(grandparent)) { + replacementNode = $createWebinyListItemNode(); + grandparent.insertAfter(replacementNode); + } else { + return false; + } + replacementNode.select(); + + const nextSiblings = anchor.getNextSiblings(); + + if (nextSiblings.length > 0) { + const newList = $createWebinyListNode(parent?.getListType(), parent?.getStyleId()); + + if ($isParagraphNode(replacementNode)) { + replacementNode.insertAfter(newList); + } else { + const newListItem = $createWebinyListItemNode(); + newListItem.append(newList); + replacementNode.insertAfter(newListItem); + } + nextSiblings.forEach(sibling => { + sibling.remove(); + newList.append(sibling); + }); + } + + // Don't leave hanging nested empty lists + $removeHighestEmptyListParent(anchor); + + return true; +} diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/webinyNodes.ts index 0e835ecc899..9af73dc68a1 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/webinyNodes.ts @@ -3,17 +3,25 @@ import type { Klass, LexicalNode } from "lexical"; import { CodeHighlightNode, CodeNode } from "@lexical/code"; import { HashtagNode } from "@lexical/hashtag"; import { AutoLinkNode, LinkNode } from "@lexical/link"; -import { ListItemNode, ListNode } from "@lexical/list"; import { MarkNode } from "@lexical/mark"; import { OverflowNode } from "@lexical/overflow"; import { HeadingNode, QuoteNode } from "@lexical/rich-text"; import { FontColorNode } from "~/nodes/FontColorNode"; import { TypographyElementNode } from "~/nodes/TypographyElementNode"; +import { WebinyListNode } from "~/nodes/list-node/WebinyListNode"; +import { WebinyListItemNode } from "~/nodes/list-node/WebinyListItemNode"; -export const WebinyNodes: Array> = [ +export const WebinyNodes: ReadonlyArray< + | Klass + | { + replace: Klass; + with: (node: InstanceType) => LexicalNode; + } +> = [ HeadingNode, - ListNode, - ListItemNode, + // Don't forget to register your custom node separately! + WebinyListNode, + WebinyListItemNode, QuoteNode, CodeNode, HashtagNode, diff --git a/packages/lexical-editor/src/plugins/WebinyListPLugin/WebinyListPlugin.ts b/packages/lexical-editor/src/plugins/WebinyListPLugin/WebinyListPlugin.ts new file mode 100644 index 00000000000..54e39152f60 --- /dev/null +++ b/packages/lexical-editor/src/plugins/WebinyListPLugin/WebinyListPlugin.ts @@ -0,0 +1,21 @@ +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useEffect } from "react"; +import { WebinyListNode } from "~/nodes/list-node/WebinyListNode"; +import { WebinyListItemNode } from "~/nodes/list-node/WebinyListItemNode"; +import { useWebinyList } from "~/hooks/useWebinyList"; + +export function WebinyListPlugin(): null { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!editor.hasNodes([WebinyListNode, WebinyListItemNode])) { + throw new Error( + "WebinyListPlugin: WebinyListNode and/or WebinyListItemNode not registered on editor" + ); + } + }, [editor]); + + useWebinyList(editor); + + return null; +} diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index 92b1363498c..2f103f45a25 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -1,8 +1,62 @@ +import { ElementNode, LexicalNode, NodeSelection, RangeSelection, TextNode } from "lexical"; export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; +export type LexicalTextBlockType = "paragraph" | "heading" | "quoteblock" | "bullet" | "numbered"; + +export type NodeFormatting = { + textFormat: string; +}; + +export type FontColorFormatting = NodeFormatting & { + themeStyleId: string; + color: string; +}; + +export type TypographyFormatting = NodeFormatting & { + value: TypographyValue; +}; + +export type BlockSelectionTextFormat = { + bold: boolean; + underline: boolean; + italic: boolean; + // highlight: boolean #TODO implement with highlight action + code: boolean; +}; + +export type BlockSelectionStyleFormat = { + color: string; + fontSize: number | string; +}; + +/* + * @description Represent set of data from the current selection of the text and nodes selected by the user. + * You can access this object through the @see useRichTextEditor context. + * */ +export type TextBlockSelection = { + elementKey?: string; + blockType: LexicalTextBlockType; + selection: RangeSelection | NodeSelection | null; + element: LexicalNode; + parentElement: ElementNode | null; + node: ElementNode | TextNode; + textFormat: BlockSelectionTextFormat; + isElementDom: boolean; +}; + // Typography -export type TypographyHTMLTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "ol" | "ul" | "div"; +export type TypographyHTMLTag = + | "h1" + | "h2" + | "h3" + | "h4" + | "h5" + | "h6" + | "p" + | "ol" + | "ul" + | "quoteblock"; export type TypographyValue = { // CSSObject type css: Record; @@ -11,3 +65,4 @@ export type TypographyValue = { // Display name name: string; }; + diff --git a/packages/lexical-editor/src/utils/nodes/clearNodeFormating.ts b/packages/lexical-editor/src/utils/nodes/clearNodeFormating.ts new file mode 100644 index 00000000000..8ce674b803d --- /dev/null +++ b/packages/lexical-editor/src/utils/nodes/clearNodeFormating.ts @@ -0,0 +1,32 @@ +import { + $isRangeSelection, + $isTextNode, + GridSelection, + LexicalEditor, + NodeSelection, + RangeSelection +} from "lexical"; +import { $selectAll } from "@lexical/selection"; +import { $getNearestBlockElementAncestorOrThrow } from "@lexical/utils"; +import { $isDecoratorBlockNode } from "@lexical/react/LexicalDecoratorBlockNode"; + +export const clearNodeFormatting = ( + activeEditor: LexicalEditor, + selection: RangeSelection | NodeSelection | GridSelection | null +) => { + activeEditor.update(() => { + if ($isRangeSelection(selection)) { + $selectAll(selection); + selection.getNodes().forEach(node => { + if ($isTextNode(node)) { + node.setFormat(0); + node.setStyle(""); + $getNearestBlockElementAncestorOrThrow(node).setFormat(""); + } + if ($isDecoratorBlockNode(node)) { + node.setFormat(""); + } + }); + } + }); +}; diff --git a/packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts b/packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts new file mode 100644 index 00000000000..7db283cc3fd --- /dev/null +++ b/packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts @@ -0,0 +1,88 @@ +import { BlockSelectionTextFormat, TextBlockSelection } from "~/types"; +import { $isRangeSelection, $isRootOrShadowRoot, LexicalEditor, RangeSelection } from "lexical"; +import { $findMatchingParent } from "@lexical/utils"; +import { getSelectedNode } from "~/utils/getSelectedNode"; + +export const getTextFormatFromSelection = (selection: RangeSelection): BlockSelectionTextFormat => { + return !$isRangeSelection(selection) + ? { + italic: false, + bold: false, + underline: false, + code: false + } + : { + bold: selection.hasFormat("bold"), + italic: selection.hasFormat("italic"), + underline: selection.hasFormat("underline"), + code: selection.hasFormat("code") + }; +}; + +// TODO: set nodes selection +/*const NodesElection = { + link: { isSelected }, + fontColor: { isSelectied}, + typography: { isSelected: false } +}*/ + +export const getSelectionTextFormat = (selection: RangeSelection): BlockSelectionTextFormat => { + return !$isRangeSelection(selection) + ? { + italic: false, + bold: false, + underline: false, + code: false + } + : { + bold: selection.hasFormat("bold"), + italic: selection.hasFormat("italic"), + underline: selection.hasFormat("underline"), + code: selection.hasFormat("code") + }; +}; + +/* + * @desc Extract all data from the selection and provide + */ +const getTextBlockSelectionData = ( + activeEditor: LexicalEditor, + selection: RangeSelection +): TextBlockSelection | null => { + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + let element = + anchorNode.getKey() === "root" + ? anchorNode + : $findMatchingParent(anchorNode, e => { + const parent = e.getParent(); + return parent !== null && $isRootOrShadowRoot(parent); + }); + + if (element === null) { + element = anchorNode.getTopLevelElementOrThrow(); + } + + const elementKey = element.getKey(); + const elementDOM = activeEditor.getElementByKey(elementKey); + + // Update links + const node = getSelectedNode(selection); + const parent = node.getParent(); + const isElementDom = elementDOM !== null; + + return { + // node/element data from selection + elementKey, + element, + parentElement: parent, + node, + selection, + isElementDom, + // formatting and styles + textFormat: getSelectionTextFormat(selection), + blockType: "bullet" + }; + } + return null; +}; diff --git a/packages/lexical-editor/src/utils/nodes/list-node.ts b/packages/lexical-editor/src/utils/nodes/list-node.ts new file mode 100644 index 00000000000..8464b5eb4f8 --- /dev/null +++ b/packages/lexical-editor/src/utils/nodes/list-node.ts @@ -0,0 +1,158 @@ +import type { LexicalNode } from "lexical"; + +import { $isWebinyListNode, WebinyListNode } from "~/nodes/list-node/WebinyListNode"; +import { + $createWebinyListItemNode, + $isWebinyListItemNode, + WebinyListItemNode +} from "~/nodes/list-node/WebinyListItemNode"; + +export function $getListDepth(listNode: WebinyListNode): number { + let depth = 1; + let parent = listNode.getParent(); + + while (parent !== null) { + if ($isWebinyListNode(parent)) { + const parentList = parent.getParent(); + + if ($isWebinyListNode(parentList)) { + depth++; + parent = parentList?.getParent() || null; + continue; + } + // invariant(false, 'A ListItemNode must have a ListNode for a parent.'); + } + + return depth; + } + + return depth; +} + +export function $getTopListNode(listItem: LexicalNode): WebinyListNode { + let list = listItem.getParent(); + + if (!$isWebinyListNode(list)) { + console.log("A WebinyListItemNode must have a ListNode for a parent."); + return listItem as WebinyListNode; + } + + let parent: WebinyListNode | null = list; + + while (parent !== null) { + parent = parent.getParent(); + + if ($isWebinyListNode(parent)) { + list = parent; + } + } + + return list; +} + +export function $isLastItemInList(listItem: WebinyListItemNode): boolean { + let isLast = true; + const firstChild = listItem.getFirstChild() ?? undefined; + + if ($isWebinyListNode(firstChild)) { + return false; + } + let parent: WebinyListItemNode | null = listItem; + + while (parent !== null) { + if ($isWebinyListItemNode(parent)) { + if (parent.getNextSiblings().length > 0) { + isLast = false; + } + } + + parent = parent.getParent(); + } + + return isLast; +} + +// This should probably be $getAllChildrenOfType +export function $getAllListItems(node: WebinyListNode): Array { + let listItemNodes: Array = []; + const listChildren: Array = node + .getChildren() + .filter($isWebinyListItemNode); + + for (let i = 0; i < listChildren.length; i++) { + const listItemNode = listChildren[i]; + const firstChild = listItemNode?.getFirstChild(); + + if ($isWebinyListNode(firstChild)) { + listItemNodes = listItemNodes.concat($getAllListItems(firstChild)); + } else { + listItemNodes.push(listItemNode); + } + } + + return listItemNodes; +} + +export function isNestedListNode(node: LexicalNode | null | undefined): boolean { + return $isWebinyListItemNode(node) && $isWebinyListNode(node?.getFirstChild()); +} + +// TODO: rewrite with $findMatchingParent or *nodeOfType +export function findNearestWebinyListItemNode(node: LexicalNode): WebinyListItemNode | null { + let currentNode: LexicalNode | null = node; + + while (currentNode !== null) { + if ($isWebinyListItemNode(currentNode)) { + return currentNode; + } + currentNode = currentNode.getParent(); + } + + return null; +} + +export function getUniqueWebinyListItemNodes( + nodeList: Array +): Array { + const keys = new Set(); + + for (let i = 0; i < nodeList.length; i++) { + const node = nodeList[i]; + + if ($isWebinyListItemNode(node)) { + keys.add(node); + } + } + + return Array.from(keys); +} + +export function $removeHighestEmptyListParent(sublist: WebinyListItemNode | WebinyListNode) { + // Nodes may be repeatedly indented, to create deeply nested lists that each + // contain just one bullet. + // Our goal is to remove these (empty) deeply nested lists. The easiest + // way to do that is crawl back up the tree until we find a node that has siblings + // (e.g. is actually part of the list contents) and delete that, or delete + // the root of the list (if no list nodes have siblings.) + let emptyListPtr = sublist; + + while (emptyListPtr.getNextSibling() == null && emptyListPtr.getPreviousSibling() == null) { + const parent = emptyListPtr.getParent(); + + if ( + parent == null || + !($isWebinyListItemNode(emptyListPtr) || $isWebinyListNode(emptyListPtr)) + ) { + break; + } + + emptyListPtr = parent; + } + + emptyListPtr.remove(); +} + +export function wrapInListItem(node: LexicalNode): WebinyListItemNode { + const listItemWrapper = $createWebinyListItemNode(); + return listItemWrapper.append(node); +} diff --git a/packages/lexical-editor/src/utils/typography.ts b/packages/lexical-editor/src/utils/typography.ts index 3858e4bc375..19c8968344e 100644 --- a/packages/lexical-editor/src/utils/typography.ts +++ b/packages/lexical-editor/src/utils/typography.ts @@ -4,7 +4,12 @@ export const hasTypographyStyles = (theme: Record): boolean => { return !!theme?.styles?.typographyStyles; }; -export const getTypography = ( +/** + * @desc Take all styles from provided type, like headings, list, quotes... + * @param typographyStyles + * @param typographyStyleType + */ +export const getTypographyStylesByType = ( typographyStyles: Record, typographyStyleType: string ): TypographyValue[] | null => { @@ -20,9 +25,12 @@ export const findTypographyStyleById = ( } const typographyStyles = theme?.styles?.typographyStyles; for (const key in typographyStyles) { - const typographyTypeData = getTypography(typographyStyles, key); + const typographyTypeData = getTypographyStylesByType(typographyStyles, key); if (typographyTypeData) { - return typographyTypeData.find(item => item.id === styleId); + const style = typographyTypeData.find(item => item.id === styleId); + if (style) { + return style; + } } } return undefined; diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 6f816e7f96c..552c0835b64 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -14,7 +14,7 @@ export type ThemeBreakpoints = Record; export type HeadingHtmlTag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; export type ParagraphHtmlTag = "p"; export type ListHtmlTag = "ul" | "ol"; -export type QuoteHtmlTag = "div"; +export type QuoteHtmlTag = "quoteblock"; export type ThemeTypographyHTMLTag = HeadingHtmlTag | ParagraphHtmlTag | ListHtmlTag | QuoteHtmlTag; export type TypographyType = "headings" | "paragraphs" | "quotes" | "lists"; export type TypographyStyle = { From fe391f307672f7ce867624d1622124aa68a22d9e Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 23 Mar 2023 00:11:56 +0100 Subject: [PATCH 25/57] wip: typography styles listed by component --- apps/theme/theme.ts | 2 +- .../src/components/TypographyDropDown.tsx | 34 ++-- .../src/components/Toolbar/Toolbar.tsx | 14 +- .../ToolbarActions/TypographyAction.tsx | 60 ++----- .../src/context/RichTextEditorContext.tsx | 8 +- .../src/nodes/TypographyElementNode.ts | 1 - .../src/nodes/list-node/WebinyListNode.ts | 3 - packages/lexical-editor/src/types.ts | 47 +++--- .../src/utils/getLexicalTextSelectionState.ts | 158 ++++++++++++++++++ .../utils/nodes/getTextBlockSelectionData.ts | 88 ---------- 10 files changed, 232 insertions(+), 183 deletions(-) create mode 100644 packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts delete mode 100644 packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index 369cd8e76f0..556b110649a 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -162,7 +162,7 @@ const theme = createTheme({ tag: "ul", css: { ...paragraphs, fontSize: 16.5 } }, - { id: "list1", name: "List 1", tag: "ul", css: { ...paragraphs, fontSize: 18.5 } }, + { id: "list1", name: "List 1", tag: "ul", css: { ...paragraphs, fontSize: 18.5 , color: "#fa5723" } }, { id: "list2", name: "List 2", tag: "ul", css: { ...paragraphs, fontSize: 21.5 } } ], quotes: [ diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx index f930f387565..c676c5fda85 100644 --- a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useEffect, useState} from "react"; import { DropDown, DropDownItem, @@ -12,23 +12,33 @@ import { TypographyValue } from "@webiny/lexical-editor/types"; export const TypographyDropDown = () => { const { value, applyTypography } = useTypographyAction(); const { theme } = usePageElements(); + const [styles, setStyles] = useState[]>([]) const typographyStyles = theme.styles?.typographyStyles; - const { toolbarType } = useRichTextEditor(); + const { textBlockSelection } = useRichTextEditor(); + const textBLockType = textBlockSelection?.state?.textBlockType; const hasTypographyStyles = (): boolean => { return !!typographyStyles; }; - const getTypographyStyles = (): TypographyStyle[] => { - if (toolbarType === "heading") { - return theme.styles?.typographyStyles?.headings || []; + useEffect(() => { + if(textBLockType) { + switch (textBLockType) { + case "heading": + setStyles(theme.styles?.typographyStyles?.headings || []); + break; + case "paragraph": + setStyles(theme.styles?.typographyStyles?.paragraphs || []); + break; + case "bullet": + case "number": + setStyles(theme.styles?.typographyStyles?.lists || []); + break; + default: + setStyles([]); + } } - - if (toolbarType === "paragraph") { - return theme.styles?.typographyStyles?.paragraphs || []; - } - return []; - }; + }, [textBLockType]) return ( <> @@ -41,7 +51,7 @@ export const TypographyDropDown = () => { disabled={false} showScroll={false} > - {getTypographyStyles()?.map(option => ( + {styles?.map(option => ( = ({ children, type, anchorElem, editor }) => { const popupCharStylesEditorRef = useRef(null); - const { toolbarType, setToolbarType } = useRichTextEditor(); - + const { toolbarType, setToolbarType, setTextBlockSelection } = useRichTextEditor(); + const [activeEditor, setActiveEditor] = useState(editor); useEffect(() => { if (toolbarType !== type) { setToolbarType(type); @@ -49,6 +50,10 @@ const FloatingToolbar: FC = ({ children, type, anchorElem, let isLink = false; if ($isRangeSelection(selection)) { + const selectionState = getLexicalTextSelectionState(activeEditor, selection); + if(selectionState) { + setTextBlockSelection(selectionState); + } const node = getSelectedNode(selection); // Update links const parent = node.getParent(); @@ -107,8 +112,9 @@ const FloatingToolbar: FC = ({ children, type, anchorElem, editor.registerCommand( SELECTION_CHANGE_COMMAND, - () => { + (_payload, newEditor) => { updateTextFormatFloatingToolbar(); + setActiveEditor(newEditor); return false; }, COMMAND_PRIORITY_LOW diff --git a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx index dabf83af15d..9d7b343375e 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx @@ -20,6 +20,7 @@ import { } from "~/nodes/TypographyElementNode"; import { $findMatchingParent, mergeRegister } from "@lexical/utils"; import { getSelectedNode } from "~/utils/getSelectedNode"; +import {useRichTextEditor} from "~/hooks/useRichTextEditor"; /* * Base composable action component that is mounted on toolbar action as a placeholder for the custom toolbar action. @@ -51,9 +52,11 @@ export interface TypographyAction extends React.FC { export const TypographyAction: TypographyAction = () => { const [editor] = useLexicalComposerContext(); - const [activeEditor, setActiveEditor] = useState(editor); + // const [activeEditor, setActiveEditor] = useState(editor); const [typography, setTypography] = useState(); - + const { textBlockSelection } = useRichTextEditor(); + const isTypographySelected = textBlockSelection?.state?.typography.isSelected; + const textBLockType = textBlockSelection?.state?.textBlockType; const setTypographySelect = useCallback( (value: TypographyValue) => { setTypography(value); @@ -68,54 +71,13 @@ export const TypographyAction: TypographyAction = () => { }); }, []); - const updateToolbar = useCallback(() => { - const selection = $getSelection(); - - if ($isRangeSelection(selection)) { - const anchorNode = selection.anchor.getNode(); - let element = - anchorNode.getKey() === "root" - ? anchorNode - : $findMatchingParent(anchorNode, e => { - const parent = e.getParent(); - return parent !== null && $isRootOrShadowRoot(parent); - }); - - if (element === null) { - element = anchorNode.getTopLevelElementOrThrow(); - } - - const node = getSelectedNode(selection); - const parent = node.getParent(); - - if ($isTypographyElementNode(parent)) { - const el = element as TypographyElementNode; - setTypography(el.getTypographyValue()); - } - } - }, [activeEditor]); - - useEffect(() => { - return mergeRegister( - activeEditor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateToolbar(); - }); - }) - ); - }, [activeEditor, editor, updateToolbar]); - useEffect(() => { - return editor.registerCommand( - SELECTION_CHANGE_COMMAND, - (_payload, newEditor) => { - updateToolbar(); - setActiveEditor(newEditor); - return false; - }, - COMMAND_PRIORITY_CRITICAL - ); - }, [editor, updateToolbar]); + /* if ($isTypographyElementNode(parent)) { + const el = element as TypographyElementNode; + setTypography(el.getTypographyValue()); + }*/ + console.log("selected text block", textBlockSelection); + }, [isTypographySelected, textBLockType]) return ( void; toolbarType?: ToolbarType; setToolbarType: (type: ToolbarType) => void; - textBlockSelection: TextBlockSelection | null; - setTextBlockSelection: (textBlockSelection: TextBlockSelection) => void; + textBlockSelection: LexicalTextSelection | null; + setTextBlockSelection: (textBlockSelection: LexicalTextSelection) => void; } export const RichTextEditorContext = createContext(undefined); @@ -22,7 +22,7 @@ export const RichTextEditorProvider: React.FC = ({ /* * @desc Keeps data from current user text selection like range selection, nodes, node key... */ - const [textBlockSelection, setTextBlockSelection] = useState(null); + const [textBlockSelection, setTextBlockSelection] = useState(null); const setNodeIsText = (nodeIsText: boolean) => { setIsText(nodeIsText); diff --git a/packages/lexical-editor/src/nodes/TypographyElementNode.ts b/packages/lexical-editor/src/nodes/TypographyElementNode.ts index 9253c6baaaf..784f160ae72 100644 --- a/packages/lexical-editor/src/nodes/TypographyElementNode.ts +++ b/packages/lexical-editor/src/nodes/TypographyElementNode.ts @@ -74,7 +74,6 @@ export class TypographyElementNode extends ElementNode { node.__key ); } - getTypographyValue(): TypographyValue { return { tag: this.__tag, diff --git a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts index 286b9fac307..72e640423d8 100644 --- a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts +++ b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts @@ -135,9 +135,6 @@ export class WebinyListNode extends ElementNode { } static importDomConversionMap(domNode: HTMLElement): DOMConversion | null { - if (!domNode.hasAttribute(TypographyStyleAttrName)) { - return null; - } return { conversion: convertWebinyListNode, priority: 0 diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index 2f103f45a25..80ff057a259 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -1,48 +1,52 @@ import { ElementNode, LexicalNode, NodeSelection, RangeSelection, TextNode } from "lexical"; +import {ListType} from "@lexical/list"; export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; -export type LexicalTextBlockType = "paragraph" | "heading" | "quoteblock" | "bullet" | "numbered"; -export type NodeFormatting = { - textFormat: string; -}; +export type LexicalTextBlockType = ListType | "paragraph" | "heading" | "quoteblock" | "bullet" | "number" | "link" | undefined; -export type FontColorFormatting = NodeFormatting & { - themeStyleId: string; - color: string; +export type TextBlockSelectionFormat = { + bold: boolean; + underline: boolean; + italic: boolean; + // highlight: boolean #TODO implement with highlight action + code: boolean; }; -export type TypographyFormatting = NodeFormatting & { - value: TypographyValue; -}; +export type NodeState = { + isSelected: boolean; +} -export type BlockSelectionTextFormat = { +export type ToolbarState = { + // text format bold: boolean; underline: boolean; italic: boolean; // highlight: boolean #TODO implement with highlight action code: boolean; -}; - -export type BlockSelectionStyleFormat = { - color: string; - fontSize: number | string; -}; + // nodes selection state + link: NodeState, + typography: NodeState, + fontColor: NodeState, + list: NodeState, + quote: NodeState, + textBlockType: LexicalTextBlockType; +} /* * @description Represent set of data from the current selection of the text and nodes selected by the user. * You can access this object through the @see useRichTextEditor context. * */ -export type TextBlockSelection = { +export type LexicalTextSelection = { elementKey?: string; - blockType: LexicalTextBlockType; selection: RangeSelection | NodeSelection | null; element: LexicalNode; - parentElement: ElementNode | null; + parent: ElementNode | null; node: ElementNode | TextNode; - textFormat: BlockSelectionTextFormat; + anchorNode: ElementNode | TextNode; isElementDom: boolean; + state: ToolbarState | undefined; }; // Typography @@ -57,6 +61,7 @@ export type TypographyHTMLTag = | "ol" | "ul" | "quoteblock"; + export type TypographyValue = { // CSSObject type css: Record; diff --git a/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts b/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts new file mode 100644 index 00000000000..a626ebe966a --- /dev/null +++ b/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts @@ -0,0 +1,158 @@ +import { + TextBlockSelectionFormat, + LexicalTextSelection, + ToolbarState, TypographyValue +} from "~/types"; +import { + $isParagraphNode, + $isRangeSelection, + $isRootOrShadowRoot, + ElementNode, + LexicalEditor, + LexicalNode, + RangeSelection, + TextNode +} from "lexical"; +import {$findMatchingParent, $getNearestNodeOfType} from "@lexical/utils"; +import { getSelectedNode } from "~/utils/getSelectedNode"; +import {$isLinkNode} from "@lexical/link"; +import {$isWebinyListNode, WebinyListNode} from "~/nodes/list-node/WebinyListNode"; +import {$isHeadingNode, $isQuoteNode} from "@lexical/rich-text"; +import {$isTypographyElementNode} from "~/nodes/TypographyElementNode"; +import {$isFontColorNode} from "~/nodes/FontColorNode"; + +export const getSelectionTextFormat = (selection: RangeSelection | undefined): TextBlockSelectionFormat => { + return !$isRangeSelection(selection) + ? { + italic: false, + bold: false, + underline: false, + code: false + } + : { + bold: selection.hasFormat("bold"), + italic: selection.hasFormat("italic"), + underline: selection.hasFormat("underline"), + code: selection.hasFormat("code") + }; +} + +const getDefaultToolbarState = (): ToolbarState => { + return { + bold: false, + italic: false, + underline: false, + code: false, + link: { isSelected:false }, + list: { isSelected: false }, + typography: { isSelected: false }, + fontColor: { isSelected: false }, + quote: { isSelected: false }, + textBlockType: undefined + }; +} + +export const getToolbarState = (selection: RangeSelection, + node: LexicalNode, + parent: LexicalNode | null, + element: LexicalNode | null, + anchorNode: ElementNode | TextNode, + isDomElement: boolean): ToolbarState => { + + const textFormat = getSelectionTextFormat(selection); + let state: ToolbarState = getDefaultToolbarState(); + state = { + ...state, + bold: textFormat.bold, + italic: textFormat.italic, + underline: textFormat.underline, + code: textFormat.code, + }; + + // link + state.link.isSelected = ($isLinkNode(parent) || $isLinkNode(node)); + if(state.link.isSelected) { state.textBlockType = "link"; } + // font color + if($isFontColorNode(node)) { + state.fontColor.isSelected = true; + } + if ($isWebinyListNode(element)) { + const parentList = $getNearestNodeOfType( + anchorNode, + WebinyListNode, + ); + const type = parentList + ? parentList.getListType() + : element.getListType(); + state.textBlockType = type; + } + if ($isHeadingNode(node)) { + state.textBlockType = "heading"; + } + if ($isParagraphNode(element)) { + state.textBlockType = "paragraph"; + } + if ($isTypographyElementNode(element)) { + state.typography.isSelected = true; + const value = element?.getTypographyValue() as TypographyValue; + if(value.tag.includes("h")){ + state.textBlockType = "heading"; + } + if(value.tag.includes("p")){ + state.textBlockType = "paragraph"; + } + } + if ($isTypographyElementNode(element)) { + state.fontColor.isSelected = true; + } + if($isQuoteNode(element)) { + state.textBlockType = "quoteblock"; + state.quote.isSelected = true; + } + + return state +} + +/* + * @desc Extract all data from the selection and provide + */ +export const getLexicalTextSelectionState = ( + activeEditor: LexicalEditor, + selection: RangeSelection +): LexicalTextSelection | null => { + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + let element = + anchorNode.getKey() === "root" + ? anchorNode + : $findMatchingParent(anchorNode, e => { + const parent = e.getParent(); + return parent !== null && $isRootOrShadowRoot(parent); + }); + + if (element === null) { + element = anchorNode.getTopLevelElementOrThrow(); + } + + const elementKey = element.getKey(); + const elementDOM = activeEditor.getElementByKey(elementKey); + + // Update links + const node = getSelectedNode(selection); + const parent = node.getParent(); + const isElementDom = elementDOM !== null; + + return { + // node/element data from selection + elementKey, + element, + parent, + node, + anchorNode, + selection, + isElementDom, + state: getToolbarState(selection, node, parent, element, anchorNode, isElementDom) + }; + } + return null; +}; diff --git a/packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts b/packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts deleted file mode 100644 index 7db283cc3fd..00000000000 --- a/packages/lexical-editor/src/utils/nodes/getTextBlockSelectionData.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { BlockSelectionTextFormat, TextBlockSelection } from "~/types"; -import { $isRangeSelection, $isRootOrShadowRoot, LexicalEditor, RangeSelection } from "lexical"; -import { $findMatchingParent } from "@lexical/utils"; -import { getSelectedNode } from "~/utils/getSelectedNode"; - -export const getTextFormatFromSelection = (selection: RangeSelection): BlockSelectionTextFormat => { - return !$isRangeSelection(selection) - ? { - italic: false, - bold: false, - underline: false, - code: false - } - : { - bold: selection.hasFormat("bold"), - italic: selection.hasFormat("italic"), - underline: selection.hasFormat("underline"), - code: selection.hasFormat("code") - }; -}; - -// TODO: set nodes selection -/*const NodesElection = { - link: { isSelected }, - fontColor: { isSelectied}, - typography: { isSelected: false } -}*/ - -export const getSelectionTextFormat = (selection: RangeSelection): BlockSelectionTextFormat => { - return !$isRangeSelection(selection) - ? { - italic: false, - bold: false, - underline: false, - code: false - } - : { - bold: selection.hasFormat("bold"), - italic: selection.hasFormat("italic"), - underline: selection.hasFormat("underline"), - code: selection.hasFormat("code") - }; -}; - -/* - * @desc Extract all data from the selection and provide - */ -const getTextBlockSelectionData = ( - activeEditor: LexicalEditor, - selection: RangeSelection -): TextBlockSelection | null => { - if ($isRangeSelection(selection)) { - const anchorNode = selection.anchor.getNode(); - let element = - anchorNode.getKey() === "root" - ? anchorNode - : $findMatchingParent(anchorNode, e => { - const parent = e.getParent(); - return parent !== null && $isRootOrShadowRoot(parent); - }); - - if (element === null) { - element = anchorNode.getTopLevelElementOrThrow(); - } - - const elementKey = element.getKey(); - const elementDOM = activeEditor.getElementByKey(elementKey); - - // Update links - const node = getSelectedNode(selection); - const parent = node.getParent(); - const isElementDom = elementDOM !== null; - - return { - // node/element data from selection - elementKey, - element, - parentElement: parent, - node, - selection, - isElementDom, - // formatting and styles - textFormat: getSelectionTextFormat(selection), - blockType: "bullet" - }; - } - return null; -}; From 5b384859dfb61dcdf60fa2e98fbe9e5654155860 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 23 Mar 2023 11:50:15 +0100 Subject: [PATCH 26/57] wip: different styles for the list now can be applied from the menu --- apps/theme/theme.ts | 34 +++++- .../src/components/TypographyDropDown.tsx | 16 ++- .../src/components/Editor/ParagraphEditor.tsx | 2 +- .../src/components/Toolbar/Toolbar.tsx | 6 +- .../ToolbarActions/TypographyAction.tsx | 57 +++++---- packages/lexical-editor/src/index.tsx | 2 +- .../src/nodes/TypographyElementNode.ts | 2 +- .../src/nodes/list-node/WebinyListNode.ts | 17 ++- .../src/nodes/list-node/formatList.ts | 12 +- packages/lexical-editor/src/types.ts | 27 +++-- .../src/utils/getLexicalTextSelectionState.ts | 108 +++++++++--------- .../src/utils/{ => theme}/typography.ts | 0 12 files changed, 170 insertions(+), 113 deletions(-) rename packages/lexical-editor/src/utils/{ => theme}/typography.ts (100%) diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index 556b110649a..d474687cab9 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -158,12 +158,40 @@ const theme = createTheme({ lists: [ { id: "list", - name: "Default list", + name: "Default", tag: "ul", css: { ...paragraphs, fontSize: 16.5 } }, - { id: "list1", name: "List 1", tag: "ul", css: { ...paragraphs, fontSize: 18.5 , color: "#fa5723" } }, - { id: "list2", name: "List 2", tag: "ul", css: { ...paragraphs, fontSize: 21.5 } } + { + id: "list1", + name: "Bullet List 1", + tag: "ul", + css: { ...paragraphs, fontSize: 18.5, color: "#fa5723" } + }, + { + id: "list2", + name: "Bullet List 2", + tag: "ul", + css: { ...paragraphs, fontSize: 21.5 } + }, + { + id: "number-list", + name: "Default", + tag: "ol", + css: { ...paragraphs, fontSize: 16.5 } + }, + { + id: "number-list1", + name: "Numbered List 1", + tag: "ol", + css: { ...paragraphs, fontSize: 18.5, color: "#fa5723" } + }, + { + id: "number-list2", + name: "Numbered List 2", + tag: "ol", + css: { ...paragraphs, fontSize: 21.5 } + } ], quotes: [ { diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx index c676c5fda85..277a6e73950 100644 --- a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from "react"; +import React, { useEffect, useState } from "react"; import { DropDown, DropDownItem, @@ -12,7 +12,7 @@ import { TypographyValue } from "@webiny/lexical-editor/types"; export const TypographyDropDown = () => { const { value, applyTypography } = useTypographyAction(); const { theme } = usePageElements(); - const [styles, setStyles] = useState[]>([]) + const [styles, setStyles] = useState[]>([]); const typographyStyles = theme.styles?.typographyStyles; const { textBlockSelection } = useRichTextEditor(); const textBLockType = textBlockSelection?.state?.textBlockType; @@ -22,7 +22,7 @@ export const TypographyDropDown = () => { }; useEffect(() => { - if(textBLockType) { + if (textBLockType) { switch (textBLockType) { case "heading": setStyles(theme.styles?.typographyStyles?.headings || []); @@ -31,14 +31,20 @@ export const TypographyDropDown = () => { setStyles(theme.styles?.typographyStyles?.paragraphs || []); break; case "bullet": + setStyles( + theme.styles?.typographyStyles?.lists?.filter(x => x.tag === "ul") || [] + ); + break; case "number": - setStyles(theme.styles?.typographyStyles?.lists || []); + setStyles( + theme.styles?.typographyStyles?.lists?.filter(x => x.tag === "ol") || [] + ); break; default: setStyles([]); } } - }, [textBLockType]) + }, [textBLockType]); return ( <> diff --git a/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx b/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx index 79535b8d466..39c9aa0ffe7 100644 --- a/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/ParagraphEditor.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React from "react"; import { CodeHighlightPlugin } from "~/plugins/CodeHighlightPlugin/CodeHighlightPlugin"; import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"; import { FloatingLinkEditorPlugin } from "~/plugins/FloatingLinkEditorPlugin/FloatingLinkEditorPlugin"; diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx index 673abc842b5..e3445ce8a52 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx @@ -1,4 +1,4 @@ -import React, {FC, useCallback, useEffect, useRef, useState} from "react"; +import React, { FC, useCallback, useEffect, useRef, useState } from "react"; import { $getSelection, $isRangeSelection, @@ -19,7 +19,7 @@ import { getDOMRangeRect } from "~/utils/getDOMRangeRect"; import { setFloatingElemPosition } from "~/utils/setFloatingElemPosition"; import { getSelectedNode } from "~/utils/getSelectedNode"; import { useRichTextEditor } from "~/hooks/useRichTextEditor"; -import {getLexicalTextSelectionState} from "~/utils/getLexicalTextSelectionState"; +import { getLexicalTextSelectionState } from "~/utils/getLexicalTextSelectionState"; interface FloatingToolbarProps { type: ToolbarType; @@ -51,7 +51,7 @@ const FloatingToolbar: FC = ({ children, type, anchorElem, let isLink = false; if ($isRangeSelection(selection)) { const selectionState = getLexicalTextSelectionState(activeEditor, selection); - if(selectionState) { + if (selectionState) { setTextBlockSelection(selectionState); } const node = getSelectedNode(selection); diff --git a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx index 9d7b343375e..653b1df1b02 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/TypographyAction.tsx @@ -1,26 +1,17 @@ import React, { useCallback, useEffect, useState } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { - $getSelection, - $isRangeSelection, - $isRootOrShadowRoot, - COMMAND_PRIORITY_CRITICAL, - LexicalCommand, - SELECTION_CHANGE_COMMAND -} from "lexical"; +import { LexicalCommand } from "lexical"; import { Compose, makeComposable } from "@webiny/react-composition"; import { TypographyActionContext } from "~/context/TypographyActionContext"; import { TypographyValue } from "~/types"; +import { ADD_TYPOGRAPHY_ELEMENT_COMMAND, TypographyPayload } from "~/nodes/TypographyElementNode"; +import { useRichTextEditor } from "~/hooks/useRichTextEditor"; import { - $isTypographyElementNode, - ADD_TYPOGRAPHY_ELEMENT_COMMAND, - TypographyElementNode, - TypographyPayload -} from "~/nodes/TypographyElementNode"; -import { $findMatchingParent, mergeRegister } from "@lexical/utils"; -import { getSelectedNode } from "~/utils/getSelectedNode"; -import {useRichTextEditor} from "~/hooks/useRichTextEditor"; + INSERT_ORDERED_WEBINY_LIST_COMMAND, + INSERT_UNORDERED_WEBINY_LIST_COMMAND, + WebinyListCommandPayload +} from "~/nodes/list-node/commands"; /* * Base composable action component that is mounted on toolbar action as a placeholder for the custom toolbar action. @@ -66,18 +57,42 @@ export const TypographyAction: TypographyAction = () => { const onTypographySelect = useCallback((value: TypographyValue) => { setTypographySelect(value); - editor.dispatchCommand>(ADD_TYPOGRAPHY_ELEMENT_COMMAND, { - value - }); + + if (value.tag.includes("h") || value.tag.includes("p")) { + editor.dispatchCommand>( + ADD_TYPOGRAPHY_ELEMENT_COMMAND, + { + value + } + ); + } + + if (value.tag === "ol") { + editor.dispatchCommand>( + INSERT_ORDERED_WEBINY_LIST_COMMAND, + { + themeStyleId: value.id + } + ); + } + + if (value.tag === "ul") { + editor.dispatchCommand>( + INSERT_UNORDERED_WEBINY_LIST_COMMAND, + { + themeStyleId: value.id + } + ); + } }, []); useEffect(() => { - /* if ($isTypographyElementNode(parent)) { + /* if ($isTypographyElementNode(parent)) { const el = element as TypographyElementNode; setTypography(el.getTypographyValue()); }*/ console.log("selected text block", textBlockSelection); - }, [isTypographySelected, textBLockType]) + }, [isTypographySelected, textBLockType]); return ( = createCommand( diff --git a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts index 72e640423d8..6208cdb7e93 100644 --- a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts +++ b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts @@ -11,11 +11,10 @@ import { Spread } from "lexical"; import { WebinyEditorTheme } from "~/themes/webinyLexicalTheme"; -import { findTypographyStyleById } from "~/utils/typography"; +import { findTypographyStyleById } from "~/utils/theme/typography"; import { styleObjectToString } from "~/utils/styleObjectToString"; import { addClassNamesToElement, removeClassNamesFromElement } from "@lexical/utils"; import { ListNodeTagType } from "@lexical/list/LexicalListNode"; - import { $getListDepth, wrapInListItem } from "~/utils/nodes/list-node"; import { ListType } from "@lexical/list"; import { $isWebinyListItemNode, WebinyListItemNode } from "~/nodes/list-node/WebinyListItemNode"; @@ -134,21 +133,21 @@ export class WebinyListNode extends ElementNode { }; } - static importDomConversionMap(domNode: HTMLElement): DOMConversion | null { + static importDomConversionMap(): DOMConversion | null { return { conversion: convertWebinyListNode, priority: 0 - } + }; } static importDOM(): DOMConversionMap | null { return { - ol: (domNode: HTMLElement) => { - return this.importDomConversionMap(domNode); - }, - ul: (domNode: HTMLElement) => { - return this.importDomConversionMap(domNode) + ol: () => { + return this.importDomConversionMap(); }, + ul: () => { + return this.importDomConversionMap(); + } }; } diff --git a/packages/lexical-editor/src/nodes/list-node/formatList.ts b/packages/lexical-editor/src/nodes/list-node/formatList.ts index 354d8b4c20c..fcac9e0ffa7 100644 --- a/packages/lexical-editor/src/nodes/list-node/formatList.ts +++ b/packages/lexical-editor/src/nodes/list-node/formatList.ts @@ -50,7 +50,9 @@ function $getListItemValue(listItem: WebinyListItemNode): number { if (list !== null) { if (!$isWebinyListNode(list)) { - console.log("$getListItemValue: webiny list node is not parent of webiny list item node"); + console.log( + "$getListItemValue: webiny list node is not parent of webiny list item node" + ); return DEFAULT_LIST_START_NUMBER; } else { value = list.getStart(); @@ -456,10 +458,10 @@ export function $handleListInsertParagraph(): boolean { const topListNode = $getTopListNode(anchor); const parent = anchor.getParent(); - if($isWebinyListNode(parent)){ - console.log("A WebinyListItemNode must have a WebinyListNode for a parent.") - return false; - } + if ($isWebinyListNode(parent)) { + console.log("A WebinyListItemNode must have a WebinyListNode for a parent."); + return false; + } const grandparent = parent?.getParent() || null; diff --git a/packages/lexical-editor/src/types.ts b/packages/lexical-editor/src/types.ts index 80ff057a259..c156af8aab0 100644 --- a/packages/lexical-editor/src/types.ts +++ b/packages/lexical-editor/src/types.ts @@ -1,10 +1,18 @@ import { ElementNode, LexicalNode, NodeSelection, RangeSelection, TextNode } from "lexical"; -import {ListType} from "@lexical/list"; +import { ListType } from "@lexical/list"; export type ToolbarType = "heading" | "paragraph" | string; export type LexicalValue = string; export { FontColorPicker } from "~/components/ToolbarActions/FontColorAction"; -export type LexicalTextBlockType = ListType | "paragraph" | "heading" | "quoteblock" | "bullet" | "number" | "link" | undefined; +export type LexicalTextBlockType = + | ListType + | "paragraph" + | "heading" + | "quoteblock" + | "bullet" + | "number" + | "link" + | undefined; export type TextBlockSelectionFormat = { bold: boolean; @@ -16,7 +24,7 @@ export type TextBlockSelectionFormat = { export type NodeState = { isSelected: boolean; -} +}; export type ToolbarState = { // text format @@ -26,13 +34,13 @@ export type ToolbarState = { // highlight: boolean #TODO implement with highlight action code: boolean; // nodes selection state - link: NodeState, - typography: NodeState, - fontColor: NodeState, - list: NodeState, - quote: NodeState, + link: NodeState; + typography: NodeState; + fontColor: NodeState; + list: NodeState; + quote: NodeState; textBlockType: LexicalTextBlockType; -} +}; /* * @description Represent set of data from the current selection of the text and nodes selected by the user. @@ -70,4 +78,3 @@ export type TypographyValue = { // Display name name: string; }; - diff --git a/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts b/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts index a626ebe966a..a60cb956c95 100644 --- a/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts +++ b/packages/lexical-editor/src/utils/getLexicalTextSelectionState.ts @@ -1,7 +1,8 @@ import { TextBlockSelectionFormat, LexicalTextSelection, - ToolbarState, TypographyValue + ToolbarState, + TypographyValue } from "~/types"; import { $isParagraphNode, @@ -13,15 +14,17 @@ import { RangeSelection, TextNode } from "lexical"; -import {$findMatchingParent, $getNearestNodeOfType} from "@lexical/utils"; +import { $findMatchingParent, $getNearestNodeOfType } from "@lexical/utils"; import { getSelectedNode } from "~/utils/getSelectedNode"; -import {$isLinkNode} from "@lexical/link"; -import {$isWebinyListNode, WebinyListNode} from "~/nodes/list-node/WebinyListNode"; -import {$isHeadingNode, $isQuoteNode} from "@lexical/rich-text"; -import {$isTypographyElementNode} from "~/nodes/TypographyElementNode"; -import {$isFontColorNode} from "~/nodes/FontColorNode"; +import { $isLinkNode } from "@lexical/link"; +import { $isWebinyListNode, WebinyListNode } from "~/nodes/list-node/WebinyListNode"; +import { $isHeadingNode, $isQuoteNode } from "@lexical/rich-text"; +import { $isTypographyElementNode } from "~/nodes/TypographyElementNode"; +import { $isFontColorNode } from "~/nodes/FontColorNode"; -export const getSelectionTextFormat = (selection: RangeSelection | undefined): TextBlockSelectionFormat => { +export const getSelectionTextFormat = ( + selection: RangeSelection | undefined +): TextBlockSelectionFormat => { return !$isRangeSelection(selection) ? { italic: false, @@ -35,7 +38,7 @@ export const getSelectionTextFormat = (selection: RangeSelection | undefined): T underline: selection.hasFormat("underline"), code: selection.hasFormat("code") }; -} +}; const getDefaultToolbarState = (): ToolbarState => { return { @@ -43,22 +46,22 @@ const getDefaultToolbarState = (): ToolbarState => { italic: false, underline: false, code: false, - link: { isSelected:false }, + link: { isSelected: false }, list: { isSelected: false }, typography: { isSelected: false }, fontColor: { isSelected: false }, quote: { isSelected: false }, textBlockType: undefined }; -} - -export const getToolbarState = (selection: RangeSelection, - node: LexicalNode, - parent: LexicalNode | null, - element: LexicalNode | null, - anchorNode: ElementNode | TextNode, - isDomElement: boolean): ToolbarState => { +}; +export const getToolbarState = ( + selection: RangeSelection, + node: LexicalNode, + parent: LexicalNode | null, + element: LexicalNode | null, + anchorNode: ElementNode | TextNode +): ToolbarState => { const textFormat = getSelectionTextFormat(selection); let state: ToolbarState = getDefaultToolbarState(); state = { @@ -66,52 +69,49 @@ export const getToolbarState = (selection: RangeSelection, bold: textFormat.bold, italic: textFormat.italic, underline: textFormat.underline, - code: textFormat.code, + code: textFormat.code }; // link - state.link.isSelected = ($isLinkNode(parent) || $isLinkNode(node)); - if(state.link.isSelected) { state.textBlockType = "link"; } + state.link.isSelected = $isLinkNode(parent) || $isLinkNode(node); + if (state.link.isSelected) { + state.textBlockType = "link"; + } // font color - if($isFontColorNode(node)) { + if ($isFontColorNode(node)) { state.fontColor.isSelected = true; } - if ($isWebinyListNode(element)) { - const parentList = $getNearestNodeOfType( - anchorNode, - WebinyListNode, - ); - const type = parentList - ? parentList.getListType() - : element.getListType(); - state.textBlockType = type; - } - if ($isHeadingNode(node)) { + if ($isWebinyListNode(element)) { + const parentList = $getNearestNodeOfType(anchorNode, WebinyListNode); + const type = parentList ? parentList.getListType() : element.getListType(); + state.textBlockType = type; + } + if ($isHeadingNode(node)) { + state.textBlockType = "heading"; + } + if ($isParagraphNode(element)) { + state.textBlockType = "paragraph"; + } + if ($isTypographyElementNode(element)) { + state.typography.isSelected = true; + const value = element?.getTypographyValue() as TypographyValue; + if (value.tag.includes("h")) { state.textBlockType = "heading"; } - if ($isParagraphNode(element)) { + if (value.tag.includes("p")) { state.textBlockType = "paragraph"; } - if ($isTypographyElementNode(element)) { - state.typography.isSelected = true; - const value = element?.getTypographyValue() as TypographyValue; - if(value.tag.includes("h")){ - state.textBlockType = "heading"; - } - if(value.tag.includes("p")){ - state.textBlockType = "paragraph"; - } - } - if ($isTypographyElementNode(element)) { - state.fontColor.isSelected = true; - } - if($isQuoteNode(element)) { - state.textBlockType = "quoteblock"; - state.quote.isSelected = true; - } + } + if ($isTypographyElementNode(element)) { + state.fontColor.isSelected = true; + } + if ($isQuoteNode(element)) { + state.textBlockType = "quoteblock"; + state.quote.isSelected = true; + } - return state -} + return state; +}; /* * @desc Extract all data from the selection and provide @@ -151,7 +151,7 @@ export const getLexicalTextSelectionState = ( anchorNode, selection, isElementDom, - state: getToolbarState(selection, node, parent, element, anchorNode, isElementDom) + state: getToolbarState(selection, node, parent, element, anchorNode) }; } return null; diff --git a/packages/lexical-editor/src/utils/typography.ts b/packages/lexical-editor/src/utils/theme/typography.ts similarity index 100% rename from packages/lexical-editor/src/utils/typography.ts rename to packages/lexical-editor/src/utils/theme/typography.ts From ed3063a3a82882270d25a9408f089517cc96e0a0 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 23 Mar 2023 19:21:40 +0100 Subject: [PATCH 27/57] wip: list component finished --- .../src/components/Toolbar/Toolbar.tsx | 1 - .../src/nodes/list-node/WebinyListNode.ts | 5 ++++ .../src/nodes/list-node/formatList.ts | 15 ++++++----- .../src/utils/nodes/list-node.ts | 27 +++---------------- 4 files changed, 16 insertions(+), 32 deletions(-) diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx index e3445ce8a52..5cc3102c923 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx @@ -109,7 +109,6 @@ const FloatingToolbar: FC = ({ children, type, anchorElem, updateTextFormatFloatingToolbar(); }); }), - editor.registerCommand( SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { diff --git a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts index 6208cdb7e93..a89cb6a5870 100644 --- a/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts +++ b/packages/lexical-editor/src/nodes/list-node/WebinyListNode.ts @@ -160,6 +160,10 @@ export class WebinyListNode extends ElementNode { this.addStylesHTMLElement(dom, config.theme); return false; } + + override extractWithChild(child: LexicalNode): boolean { + return $isWebinyListItemNode(child); + } } function setListThemeClassNames( @@ -217,6 +221,7 @@ function setListThemeClassNames( addClassNamesToElement(dom, ...classesToAdd); } } + /* * This function normalizes the children of a ListNode after the conversion from HTML, * ensuring that they are all ListItemNodes and contain either a single nested ListNode diff --git a/packages/lexical-editor/src/nodes/list-node/formatList.ts b/packages/lexical-editor/src/nodes/list-node/formatList.ts index fcac9e0ffa7..b348e5653fb 100644 --- a/packages/lexical-editor/src/nodes/list-node/formatList.ts +++ b/packages/lexical-editor/src/nodes/list-node/formatList.ts @@ -408,7 +408,7 @@ function maybeIndentOrOutdent(direction: "indent" | "outdent"): void { return; } const selectedNodes = selection.getNodes(); - let WebinyListItemNodes: Array = []; + let webinyListItemNodes: Array = []; if (selectedNodes.length === 0) { selectedNodes.push(selection.anchor.getNode()); @@ -420,17 +420,17 @@ function maybeIndentOrOutdent(direction: "indent" | "outdent"): void { const nearestWebinyListItemNode = findNearestWebinyListItemNode(selectedNodes[0]); if (nearestWebinyListItemNode !== null) { - WebinyListItemNodes = [nearestWebinyListItemNode]; + webinyListItemNodes = [nearestWebinyListItemNode]; } } else { - WebinyListItemNodes = getUniqueWebinyListItemNodes(selectedNodes); + webinyListItemNodes = getUniqueWebinyListItemNodes(selectedNodes); } - if (WebinyListItemNodes.length > 0) { + if (webinyListItemNodes.length > 0) { if (direction === "indent") { - $handleIndent(WebinyListItemNodes); + $handleIndent(webinyListItemNodes); } else { - $handleOutdent(WebinyListItemNodes); + $handleOutdent(webinyListItemNodes); } } } @@ -449,6 +449,7 @@ export function $handleListInsertParagraph(): boolean { if (!$isRangeSelection(selection) || !selection.isCollapsed()) { return false; } + // Only run this code on empty list items const anchor = selection.anchor.getNode(); @@ -458,7 +459,7 @@ export function $handleListInsertParagraph(): boolean { const topListNode = $getTopListNode(anchor); const parent = anchor.getParent(); - if ($isWebinyListNode(parent)) { + if (!$isWebinyListNode(parent)) { console.log("A WebinyListItemNode must have a WebinyListNode for a parent."); return false; } diff --git a/packages/lexical-editor/src/utils/nodes/list-node.ts b/packages/lexical-editor/src/utils/nodes/list-node.ts index 8464b5eb4f8..e9405c58e1d 100644 --- a/packages/lexical-editor/src/utils/nodes/list-node.ts +++ b/packages/lexical-editor/src/utils/nodes/list-node.ts @@ -12,7 +12,7 @@ export function $getListDepth(listNode: WebinyListNode): number { let parent = listNode.getParent(); while (parent !== null) { - if ($isWebinyListNode(parent)) { + if ($isWebinyListItemNode(parent)) { const parentList = parent.getParent(); if ($isWebinyListNode(parentList)) { @@ -20,7 +20,7 @@ export function $getListDepth(listNode: WebinyListNode): number { parent = parentList?.getParent() || null; continue; } - // invariant(false, 'A ListItemNode must have a ListNode for a parent.'); + console.log("A WebinyListItemNode must have a WebinyListNode for a parent."); } return depth; @@ -34,6 +34,7 @@ export function $getTopListNode(listItem: LexicalNode): WebinyListNode { if (!$isWebinyListNode(list)) { console.log("A WebinyListItemNode must have a ListNode for a parent."); + debugger; return listItem as WebinyListNode; } @@ -50,28 +51,6 @@ export function $getTopListNode(listItem: LexicalNode): WebinyListNode { return list; } -export function $isLastItemInList(listItem: WebinyListItemNode): boolean { - let isLast = true; - const firstChild = listItem.getFirstChild() ?? undefined; - - if ($isWebinyListNode(firstChild)) { - return false; - } - let parent: WebinyListItemNode | null = listItem; - - while (parent !== null) { - if ($isWebinyListItemNode(parent)) { - if (parent.getNextSiblings().length > 0) { - isLast = false; - } - } - - parent = parent.getParent(); - } - - return isLast; -} - // This should probably be $getAllChildrenOfType export function $getAllListItems(node: WebinyListNode): Array { let listItemNodes: Array = []; From 47a44c53de5fb1004c903d5fcd7d1d8b4f7ee42c Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 23 Mar 2023 21:58:45 +0100 Subject: [PATCH 28/57] wip: implemented custom webiny quote component --- apps/theme/theme.ts | 24 ++- .../src/components/TypographyDropDown.tsx | 5 + .../commands.ts => commands/webiny-list.ts} | 8 +- .../src/commands/webiny-quote.ts | 8 + .../src/components/Editor/HeadingEditor.tsx | 2 + .../src/components/Editor/RichTextEditor.tsx | 2 + .../ToolbarActions/BulletListAction.tsx | 2 +- .../ToolbarActions/NumberedListAction.tsx | 2 +- .../components/ToolbarActions/QuoteAction.tsx | 47 ++---- .../ToolbarActions/TypographyAction.tsx | 14 +- .../lexical-editor/src/hooks/useWebinyList.ts | 2 +- .../src/hooks/useWebinyQuote.ts | 24 +++ .../src/nodes/WebinyQuoteNode.ts | 151 ++++++++++++++++++ .../lexical-editor/src/nodes/webinyNodes.ts | 5 +- .../WebinyQuoteNodePlugin.ts | 20 +++ packages/lexical-editor/src/types.ts | 28 +++- .../src/utils/getLexicalTextSelectionState.ts | 3 +- .../src/utils/nodes/addClassNamesToElement.ts | 11 ++ .../src/utils/nodes/formatToParagraph.ts | 18 +++ .../src/utils/nodes/formatToQuote.ts | 12 ++ .../nodes/removeClassNamesFromElement.ts | 10 ++ 21 files changed, 341 insertions(+), 57 deletions(-) rename packages/lexical-editor/src/{nodes/list-node/commands.ts => commands/webiny-list.ts} (62%) create mode 100644 packages/lexical-editor/src/commands/webiny-quote.ts create mode 100644 packages/lexical-editor/src/hooks/useWebinyQuote.ts create mode 100644 packages/lexical-editor/src/nodes/WebinyQuoteNode.ts create mode 100644 packages/lexical-editor/src/plugins/WebinyQuoteNodePlugin/WebinyQuoteNodePlugin.ts create mode 100644 packages/lexical-editor/src/utils/nodes/addClassNamesToElement.ts create mode 100644 packages/lexical-editor/src/utils/nodes/formatToParagraph.ts create mode 100644 packages/lexical-editor/src/utils/nodes/formatToQuote.ts create mode 100644 packages/lexical-editor/src/utils/nodes/removeClassNamesFromElement.ts diff --git a/apps/theme/theme.ts b/apps/theme/theme.ts index d474687cab9..9fde382a026 100644 --- a/apps/theme/theme.ts +++ b/apps/theme/theme.ts @@ -196,13 +196,35 @@ const theme = createTheme({ quotes: [ { id: "quote", - name: "Quote1 1", + name: "Quote 1", tag: "quoteblock", css: { ...paragraphs, fontWeight: "bold", fontSize: 22 } + }, + { + id: "quote1", + name: "Quote 2", + tag: "quoteblock", + css: { + ...paragraphs, + fontWeight: "bold", + fontSize: 26, + color: "#2a9d8f" + } + }, + { + id: "quote2", + name: "Quote 3", + tag: "quoteblock", + css: { + ...paragraphs, + fontWeight: "bold", + fontSize: 31, + color: "#023e8a" + } } ] }, diff --git a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx index 277a6e73950..934b2e59674 100644 --- a/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-actions/src/components/TypographyDropDown.tsx @@ -40,6 +40,11 @@ export const TypographyDropDown = () => { theme.styles?.typographyStyles?.lists?.filter(x => x.tag === "ol") || [] ); break; + case "quoteblock": + setStyles( + theme.styles?.typographyStyles?.quotes || [] + ); + break; default: setStyles([]); } diff --git a/packages/lexical-editor/src/nodes/list-node/commands.ts b/packages/lexical-editor/src/commands/webiny-list.ts similarity index 62% rename from packages/lexical-editor/src/nodes/list-node/commands.ts rename to packages/lexical-editor/src/commands/webiny-list.ts index 53f45eaacee..00e70c9d4fd 100644 --- a/packages/lexical-editor/src/nodes/list-node/commands.ts +++ b/packages/lexical-editor/src/commands/webiny-list.ts @@ -8,8 +8,6 @@ export const INSERT_UNORDERED_WEBINY_LIST_COMMAND: LexicalCommand = createCommand("INSERT_ORDERED_WEBINY_LIST_COMMAND"); -export const INSERT_WEBINY_CHECK_LIST_COMMAND: LexicalCommand = - createCommand("INSERT_WEBINY_CHECK_LIST_COMMAND"); -export const REMOVE_WEBINY_LIST_COMMAND: LexicalCommand = createCommand( - "REMOVE_WEBINY_LIST_COMMAND" -); + +export const REMOVE_WEBINY_LIST_COMMAND: LexicalCommand = + createCommand("REMOVE_WEBINY_LIST_COMMAND"); diff --git a/packages/lexical-editor/src/commands/webiny-quote.ts b/packages/lexical-editor/src/commands/webiny-quote.ts new file mode 100644 index 00000000000..019d12d77c0 --- /dev/null +++ b/packages/lexical-editor/src/commands/webiny-quote.ts @@ -0,0 +1,8 @@ +import { createCommand, LexicalCommand } from "lexical"; + +export type WebinyQuoteCommandPayload = { + themeStyleId: string; +}; + +export const INSERT_WEBINY_QUOTE_COMMAND: LexicalCommand = + createCommand("INSERT_WEBINY_QUOTE_COMMAND"); diff --git a/packages/lexical-editor/src/components/Editor/HeadingEditor.tsx b/packages/lexical-editor/src/components/Editor/HeadingEditor.tsx index cb5835d1903..a3e58d72a51 100644 --- a/packages/lexical-editor/src/components/Editor/HeadingEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/HeadingEditor.tsx @@ -4,6 +4,7 @@ import { ClickableLinkPlugin } from "~/plugins/ClickableLinkPlugin/ClickableLink import { FloatingLinkEditorPlugin } from "~/plugins/FloatingLinkEditorPlugin/FloatingLinkEditorPlugin"; import { HeadingToolbar } from "~/components/Toolbar/HeadingToolbar"; import { RichTextEditor, RichTextEditorProps } from "~/components/Editor/RichTextEditor"; +import {WebinyQuotePlugin} from "~/plugins/WebinyQuoteNodePlugin/WebinyQuoteNodePlugin"; interface HeadingEditorProps extends RichTextEditorProps { tag?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; @@ -18,6 +19,7 @@ export const HeadingEditor: React.FC = ({ tag, placeholder, {...rest} > + diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index 10ee8f9fa72..ce927ecc6f4 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -20,6 +20,7 @@ import { FontColorPlugin } from "~/plugins/FontColorPlugin/FontColorPlugin"; import { webinyEditorTheme, WebinyTheme } from "~/themes/webinyLexicalTheme"; import { WebinyNodes } from "~/nodes/webinyNodes"; import { TypographyPlugin } from "~/plugins/TypographyPlugin/TypographyPlugin"; +import { WebinyQuotePlugin} from "~/plugins/WebinyQuoteNodePlugin/WebinyQuoteNodePlugin"; export interface RichTextEditorProps { toolbar?: React.ReactNode; @@ -101,6 +102,7 @@ const BaseRichTextEditor: React.FC = ({ + {/* Events */} {onBlur && } {focus && } diff --git a/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx index 66531323ece..eb6a71ad59f 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/BulletListAction.tsx @@ -12,7 +12,7 @@ import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexi import { INSERT_UNORDERED_WEBINY_LIST_COMMAND, REMOVE_WEBINY_LIST_COMMAND -} from "~/nodes/list-node/commands"; +} from "~/commands/webiny-list"; /** * Toolbar button action. On click will wrap the content in bullet list style. diff --git a/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx index 3df8547d301..366efe80b06 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/NumberedListAction.tsx @@ -11,7 +11,7 @@ import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexi import { INSERT_ORDERED_WEBINY_LIST_COMMAND, REMOVE_WEBINY_LIST_COMMAND -} from "~/nodes/list-node/commands"; +} from "~/commands/webiny-list"; import { $isWebinyListNode, WebinyListNode } from "~/nodes/list-node/WebinyListNode"; /** diff --git a/packages/lexical-editor/src/components/ToolbarActions/QuoteAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/QuoteAction.tsx index 7203e23d9c3..7108ad5a1e1 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/QuoteAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/QuoteAction.tsx @@ -1,51 +1,28 @@ -import React, { useState } from "react"; -import { $wrapNodes } from "@lexical/selection"; -import { - $createParagraphNode, - $getSelection, - $isRangeSelection, - DEPRECATED_$isGridSelection -} from "lexical"; +import React, {useEffect, useState} from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; -import { QuoteNode } from "@lexical/rich-text"; - -export function $createQuoteNode(): QuoteNode { - return new QuoteNode(); -} - +import {formatToQuote} from "~/utils/nodes/formatToQuote"; +import {formatToParagraph} from "~/utils/nodes/formatToParagraph"; +import {useRichTextEditor} from "~/hooks/useRichTextEditor"; export const QuoteAction = () => { const [editor] = useLexicalComposerContext(); const [isActive, setIsActive] = useState(false); - - const formatToParagraph = () => { - editor.update(() => { - const selection = $getSelection(); - - if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) { - $wrapNodes(selection, () => $createParagraphNode()); - } - }); - }; - - const formatToQuote = () => { - editor.update(() => { - const selection = $getSelection(); - if ($isRangeSelection(selection) || DEPRECATED_$isGridSelection(selection)) { - $wrapNodes(selection, () => $createQuoteNode()); - } - }); - }; + const { textBlockSelection } = useRichTextEditor(); + const isQuoteSelected = !!textBlockSelection?.state?.quote.isSelected; const formatText = () => { if (!isActive) { - formatToQuote(); + formatToQuote(editor); setIsActive(true); return; } - formatToParagraph(); + formatToParagraph(editor); setIsActive(false); }; + useEffect(() => { + setIsActive(isQuoteSelected); + }, [isQuoteSelected]) + return (