From 78c4bbd0dd2dbb809660f9bb01c485c28347c05a Mon Sep 17 00:00:00 2001 From: gjulivan Date: Fri, 22 May 2026 16:05:36 +0200 Subject: [PATCH 1/5] chore: initial version of rich text with tiptap --- .../rich-text-web/CHANGELOG.md | 12 + .../rich-text-web/e2e/RichText.spec.js | 32 + .../rich-text-web/package.json | 41 +- .../src/RichText.editorConfig.ts | 19 +- .../src/RichText.editorPreview.tsx | 21 +- .../rich-text-web/src/RichText.tsx | 43 +- .../rich-text-web/src/RichText.xml | 8 + .../src/__tests__/RichText.spec.tsx | 3 +- .../src/__tests__/customList.spec.ts | 59 + .../rich-text-web/src/__tests__/fonts.spec.ts | 148 ++ .../src/__tests__/helpers.spec.ts | 143 + .../rich-text-web/src/assets/.gitkeep | 0 .../rich-text-web/src/assets/Icons.tsx | 101 - .../components/CustomToolbars/Fullscreen.tsx | 23 - .../CustomToolbars/ToolbarWrapper.tsx | 84 - .../components/CustomToolbars/UndoRedo.tsx | 19 - .../components/CustomToolbars/constants.ts | 249 -- .../CustomToolbars/customToolbars.d.ts | 21 - .../src/components/CustomToolbars/presets.ts | 44 - .../CustomToolbars/useEmbedModal.ts | 252 -- .../src/components/DynamicTableStyles.tsx | 77 + .../src/components/DynamicTextColorStyles.tsx | 81 + .../rich-text-web/src/components/Editor.tsx | 389 +-- .../src/components/EditorContext.tsx | 90 + .../src/components/EditorWrapper.tsx | 228 +- .../src/components/ImageResize.tsx | 95 + .../src/components/ModalDialog/Dialog.scss | 169 -- .../src/components/ModalDialog/Dialog.tsx | 116 - .../components/ModalDialog/DialogContent.tsx | 121 - .../components/ModalDialog/ImageDialog.tsx | 275 -- .../src/components/ModalDialog/LinkDialog.tsx | 79 - .../components/ModalDialog/VideoDialog.tsx | 166 -- .../components/ModalDialog/ViewCodeDialog.tsx | 62 - .../src/components/StickySentinel.tsx | 49 - .../rich-text-web/src/components/Toolbar.tsx | 106 - .../src/components/toolbars/Toolbar.scss | 218 ++ .../src/components/toolbars/Toolbar.tsx | 123 + .../src/components/toolbars/ToolbarConfig.ts | 562 ++++ .../toolbars/components/CodeView.tsx | 30 + .../toolbars/components/ColorPicker.scss | 9 + .../toolbars/components/ColorPicker.tsx | 98 + .../toolbars/components/ConfirmDialog.tsx | 42 + .../toolbars/components/Dialog.scss | 331 +++ .../components/toolbars/components/Dialog.tsx | 47 + .../toolbars/components/ImageDialog.tsx | 344 +++ .../toolbars/components/LinkDialog.tsx | 180 ++ .../toolbars/components/TableGrid.tsx | 29 + .../components/TableGridSelector.scss | 51 + .../toolbars/components/TableGridSelector.tsx | 81 + .../toolbars/components/ToolbarButton.tsx | 58 + .../toolbars/components/ToolbarDropdown.tsx | 74 + .../toolbars/components/VideoDialog.tsx | 110 + .../toolbars/helpers/colorPickerHelpers.ts | 47 + .../toolbars/helpers/toolbarTypes.ts | 120 + .../src/components/toolbars/index.ts | 19 + .../src/extensions/FontFamilyClass.ts | 83 + .../rich-text-web/src/extensions/FontSize.ts | 88 + .../src/extensions/ImageResize.ts | 81 + .../rich-text-web/src/extensions/Indent.ts | 159 ++ .../src/extensions/TableBackgroundColor.ts | 50 + .../extensions/TableCellBackgroundColor.ts | 48 + .../src/extensions/TextColorClass.ts | 114 + .../src/extensions/TextDirection.ts | 76 + .../src/extensions/TextHighlightClass.ts | 109 + .../src/store/EditorProvider.tsx | 15 - .../rich-text-web/src/store/store.ts | 25 - .../src/store/useActionEvents.ts | 59 - .../rich-text-web/src/ui/RichText.scss | 494 +++- .../src/ui/RichTextFormatStyle.scss | 44 + .../rich-text-web/src/ui/RichTextIcons.scss | 71 - .../rich-text-web/src/ui/TableStyle.scss | 56 + .../formats/fonts.scss => ui/variables.scss} | 75 +- .../rich-text-web/src/utils/MxQuill.ts | 236 -- .../src/utils/customPluginRegisters.ts | 49 - .../rich-text-web/src/utils/formats.d.ts | 7 + .../rich-text-web/src/utils/formats/block.ts | 35 - .../rich-text-web/src/utils/formats/button.ts | 55 - .../src/utils/formats/customList.scss | 7 - .../src/utils/formats/customList.ts | 40 - .../rich-text-web/src/utils/formats/fonts.ts | 63 - .../src/utils/formats/fontsize.ts | 25 - .../src/utils/formats/formula.ts | 31 - .../src/utils/formats/header.scss | 12 - .../rich-text-web/src/utils/formats/image.ts | 38 - .../rich-text-web/src/utils/formats/indent.ts | 52 - .../rich-text-web/src/utils/formats/link.ts | 61 - .../assets/css/quill-table-better.scss | 684 ----- .../quill-table-better/assets/icon/check.png | Bin 209 -> 0 bytes .../quill-table-better/config/index.ts | 448 ---- .../quill-table-better/formats/header.ts | 82 - .../quill-table-better/formats/list.ts | 161 -- .../quill-table-better/formats/table.ts | 785 ------ .../quill-table-better/language/de_DE.ts | 60 - .../quill-table-better/language/en_US.ts | 60 - .../quill-table-better/language/fr_FR.ts | 60 - .../quill-table-better/language/index.ts | 63 - .../quill-table-better/language/pl_PL.ts | 60 - .../quill-table-better/language/ru_RU.ts | 60 - .../quill-table-better/language/tr_TR.ts | 60 - .../quill-table-better/language/zh_CN.ts | 60 - .../quill-table-better/modules/clipboard.ts | 58 - .../quill-table-better/modules/toolbar.ts | 247 -- .../quill-table-better/quill-table-better.ts | 369 --- .../utils/formats/quill-table-better/types.ts | 85 - .../quill-table-better/ui/cell-selection.ts | 718 ----- .../quill-table-better/ui/operate-line.ts | 446 ---- .../quill-table-better/ui/table-menus.ts | 854 ------ .../ui/table-properties-form.ts | 606 ----- .../quill-table-better/ui/toolbar-table.ts | 123 - .../utils/clipboard-matchers.ts | 90 - .../formats/quill-table-better/utils/index.ts | 383 --- .../src/utils/formats/resizeModuleConfig.ts | 71 - .../src/utils/formats/softBreak.ts | 17 - .../src/utils/formats/video.scss | 5 - .../rich-text-web/src/utils/formats/video.ts | 35 - .../src/utils/formats/whiteSpace.ts | 22 - .../rich-text-web/src/utils/helpers.ts | 42 + .../src/utils/modules/clipboard.ts | 162 -- .../src/utils/modules/keyboard.ts | 67 - .../rich-text-web/src/utils/modules/resize.ts | 103 - .../src/utils/modules/resizeToolbar.ts | 38 - .../rich-text-web/src/utils/modules/scroll.ts | 16 - .../src/utils/modules/toolbarHandlers.ts | 183 -- .../src/utils/modules/uploader.ts | 35 - .../rich-text-web/src/utils/themes/mxTheme.ts | 143 - .../src/utils/themes/mxTooltip.ts | 103 - .../rich-text-web/typings/RichTextProps.d.ts | 4 + .../rich-text-web/typings/modules.d.ts | 63 - pnpm-lock.yaml | 2313 +++++++++++++---- 129 files changed, 6786 insertions(+), 11614 deletions(-) create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/customList.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/fonts.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/helpers.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/assets/.gitkeep delete mode 100644 packages/pluggableWidgets/rich-text-web/src/assets/Icons.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/Fullscreen.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/ToolbarWrapper.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/UndoRedo.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/constants.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/customToolbars.d.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/presets.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/useEmbedModal.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/DynamicTableStyles.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/DynamicTextColorStyles.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/EditorContext.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ImageResize.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/Dialog.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/Dialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/DialogContent.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/ImageDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/LinkDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/VideoDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/ViewCodeDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/StickySentinel.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/Toolbar.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/CodeView.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ColorPicker.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ColorPicker.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ConfirmDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/Dialog.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/Dialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ImageDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/LinkDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/TableGrid.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/TableGridSelector.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/TableGridSelector.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ToolbarButton.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ToolbarDropdown.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/VideoDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/colorPickerHelpers.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/toolbarTypes.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/index.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/FontFamilyClass.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/FontSize.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/ImageResize.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/Indent.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TableBackgroundColor.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TableCellBackgroundColor.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextColorClass.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextDirection.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextHighlightClass.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/store/EditorProvider.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/store/store.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/store/useActionEvents.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/ui/RichTextFormatStyle.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/ui/TableStyle.scss rename packages/pluggableWidgets/rich-text-web/src/{utils/formats/fonts.scss => ui/variables.scss} (57%) delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/block.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/button.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/fontsize.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/formula.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/header.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/image.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/indent.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/link.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/assets/css/quill-table-better.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/assets/icon/check.png delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/config/index.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/formats/header.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/formats/list.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/formats/table.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/de_DE.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/en_US.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/fr_FR.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/index.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/pl_PL.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/ru_RU.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/tr_TR.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/zh_CN.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/modules/clipboard.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/modules/toolbar.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/quill-table-better.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/types.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/cell-selection.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/operate-line.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/table-menus.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/table-properties-form.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/toolbar-table.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/utils/clipboard-matchers.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/utils/index.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/softBreak.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/video.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/video.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/whiteSpace.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/clipboard.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/keyboard.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/resize.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/resizeToolbar.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/scroll.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/toolbarHandlers.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/uploader.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/themes/mxTheme.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/themes/mxTooltip.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/typings/modules.d.ts diff --git a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md index 4289f15ca5..38deab1766 100644 --- a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md +++ b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- We added new configuration to allow users to use class names instead of inline styling in generated HTML to support strict CSP. + +### Fixed + +- We fixed an issue where the editor pasting back the whole sentence instead of the single copied word + +### Changed + +- We removed codemirror from code dialog viewer due to unsupported strict CSP policy. A simple internally built code editor using highlightjs is now replacing it. + ## [4.12.0] - 2026-04-22 ### Added diff --git a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js index c05860d869..e203f40cee 100644 --- a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js +++ b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js @@ -2,6 +2,7 @@ import { expect, test } from "@mendix/run-e2e/fixtures"; import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; test.describe("RichText", () => { + test.describe.configure({ mode: "serial" }); test("compares with a screenshot baseline and checks if inline basic mode are rendered as expected", async ({ page }) => { @@ -115,6 +116,37 @@ test.describe("RichText", () => { await expect(page.locator(".mx-name-richText6")).toHaveScreenshot(`readOnlyModeReadPanel.png`); }); + test("compares with a screenshot baseline and checks if class mode editor is rendered as expected", async ({ + page + }) => { + await page.goto("/p/classmode"); + await page.waitForLoadState("networkidle"); + await expect(page.locator(".mx-name-richText1")).toBeVisible(); + await expect(page.locator(".mx-name-richText1")).toHaveScreenshot(`classModeEditor.png`, { threshold: 0.4 }); + }); + + test("checks that class mode editor output uses CSS classes instead of inline styles", async ({ page }) => { + await page.goto("/p/classmode"); + await page.waitForLoadState("networkidle"); + const html = await page.locator(".mx-name-richText1 .ql-editor").innerHTML(); + expect(html).toMatch(/class="ql-color-/); + expect(html).toMatch(/class="ql-bg-/); + expect(html).toMatch(/class="ql-indent-/); + expect(html).toMatch(/data-style-format="class"/); + expect(html).not.toMatch(/style="color:/); + expect(html).not.toMatch(/style="background-color:/); + expect(html).not.toMatch(/style="padding-left:/); + }); + + test("compares with a screenshot baseline of the View/Edit Code dialog in class mode", async ({ page }) => { + await page.goto("/p/classmode"); + await page.waitForLoadState("networkidle"); + await page.click(".mx-name-richText1 .ql-toolbar button.ql-view-code"); + await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot( + `classModeViewCodeDialog.png` + ); + }); + test("compares with a screenshot for rich text inside modal popup layout", async ({ page }) => { await page.goto("/"); await waitForMendixApp(page); diff --git a/packages/pluggableWidgets/rich-text-web/package.json b/packages/pluggableWidgets/rich-text-web/package.json index 5fb882b303..c9fadb888f 100644 --- a/packages/pluggableWidgets/rich-text-web/package.json +++ b/packages/pluggableWidgets/rich-text-web/package.json @@ -24,15 +24,15 @@ }, "testProject": { "githubUrl": "https://github.com/mendix/testProjects", - "branchName": "rich-text-v4-web" + "branchName": "rich-text-v4-web-v2" }, "scripts": { "build": "cross-env MPKOUTPUT=RichText.mpk pluggable-widgets-tools build:web", "create-gh-release": "rui-create-gh-release", "create-translation": "rui-create-translation", "dev": "cross-env MPKOUTPUT=RichText.mpk pluggable-widgets-tools start:web", - "e2e": "run-e2e ci", - "e2edev": "run-e2e dev --with-preps", + "e2e": "MENDIX_VERSION=11.9.1 run-e2e ci", + "e2edev": "MENDIX_VERSION=11.9.1 run-e2e dev --with-preps", "format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .", "lint": "eslint src/ package.json", "publish-marketplace": "rui-publish-marketplace", @@ -43,21 +43,42 @@ "verify": "rui-verify-package-format" }, "dependencies": { - "@codemirror/lang-html": "^6.4.9", - "@codemirror/state": "^6.5.2", "@floating-ui/dom": "^1.7.4", "@floating-ui/react": "^0.26.27", "@melloware/coloris": "^0.25.0", - "@uiw/codemirror-theme-github": "^4.23.13", - "@uiw/react-codemirror": "^4.23.13", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@tiptap/core": "^3.23.4", + "@tiptap/extension-color": "^3.23.4", + "@tiptap/extension-font-family": "^3.23.4", + "@tiptap/extension-highlight": "^3.23.5", + "@tiptap/extension-image": "^3.23.4", + "@tiptap/extension-link": "^3.23.4", + "@tiptap/extension-list": "^3.23.5", + "@tiptap/extension-list-item": "^3.23.5", + "@tiptap/extension-subscript": "^3.23.4", + "@tiptap/extension-superscript": "^3.23.4", + "@tiptap/extension-table": "^3.23.4", + "@tiptap/extension-table-cell": "^3.23.4", + "@tiptap/extension-table-header": "^3.23.4", + "@tiptap/extension-table-row": "^3.23.4", + "@tiptap/extension-task-item": "^3.23.5", + "@tiptap/extension-task-list": "^3.23.5", + "@tiptap/extension-text-align": "^3.23.4", + "@tiptap/extension-text-style": "^3.23.4", + "@tiptap/extension-underline": "^3.23.4", + "@tiptap/extension-youtube": "^3.23.4", + "@tiptap/pm": "^3.23.4", + "@tiptap/react": "^3.23.4", + "@tiptap/starter-kit": "^3.23.4", + "@uiw/react-color-compact": "^2.10.1", "classnames": "^2.5.1", + "highlight.js": "^11.11.1", "js-beautify": "^1.15.4", "katex": "^0.16.22", "linkifyjs": "^4.3.2", "lodash.merge": "^4.6.2", - "parchment": "^3.0.0", - "quill": "^2.0.3", - "quill-resize-module": "^2.0.4" + "react-dropzone": "^14.3.8", + "react-scroll-sync": "^1.0.2" }, "devDependencies": { "@mendix/automation-utils": "workspace:*", diff --git a/packages/pluggableWidgets/rich-text-web/src/RichText.editorConfig.ts b/packages/pluggableWidgets/rich-text-web/src/RichText.editorConfig.ts index 24a8972d85..abdc183d28 100644 --- a/packages/pluggableWidgets/rich-text-web/src/RichText.editorConfig.ts +++ b/packages/pluggableWidgets/rich-text-web/src/RichText.editorConfig.ts @@ -6,8 +6,6 @@ import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; import { RichTextPreviewProps } from "typings/RichTextProps"; -import RichTextPreviewSVGDark from "./assets/rich-text-preview-dark.svg"; -import RichTextPreviewSVGLight from "./assets/rich-text-preview-light.svg"; const toolbarGroupKeys: Array = [ "history", @@ -74,17 +72,16 @@ export function getProperties(values: RichTextPreviewProps, defaultProperties: P return defaultProperties; } -export function getPreview(props: RichTextPreviewProps, isDarkMode: boolean): StructurePreviewProps { - const variant = isDarkMode ? RichTextPreviewSVGDark : RichTextPreviewSVGLight; - const doc = decodeURIComponent(variant.replace("data:image/svg+xml,", "")); - +export function getPreview(props: RichTextPreviewProps, _isDarkMode: boolean): StructurePreviewProps { const richTextPreview = container()( rowLayout({ columnSize: "grow", borders: false })({ - type: "Image", - document: props.stringAttribute - ? doc.replace("[No attribute selected]", `[${props.stringAttribute}]`) - : doc, - height: 150 + type: "Container", + children: [ + { + type: "Text", + content: props.stringAttribute ? `Rich Text: ${props.stringAttribute}` : "Rich Text Editor" + } + ] }) ); diff --git a/packages/pluggableWidgets/rich-text-web/src/RichText.editorPreview.tsx b/packages/pluggableWidgets/rich-text-web/src/RichText.editorPreview.tsx index 8b7f935fa8..1cb5211223 100644 --- a/packages/pluggableWidgets/rich-text-web/src/RichText.editorPreview.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/RichText.editorPreview.tsx @@ -1,19 +1,10 @@ -import { ReactElement } from "react"; -import RichTextPreviewSVG from "./assets/rich-text-preview-light.svg"; +import { ReactElement, createElement } from "react"; import { RichTextPreviewProps } from "../typings/RichTextProps"; -export function preview(props: RichTextPreviewProps): ReactElement { - let doc = decodeURI(RichTextPreviewSVG); - doc = props.stringAttribute ? doc.replace("[No attribute selected]", `[${props.stringAttribute}]`) : doc; +export function preview(_props: RichTextPreviewProps): ReactElement { + return createElement("div", { className: "widget-rich-text-preview" }, "Rich Text Editor"); +} - return ( -
- - {props.imageSource && ( - -
- - )} -
- ); +export function getPreviewCss(): string { + return require("./ui/RichText.scss"); } diff --git a/packages/pluggableWidgets/rich-text-web/src/RichText.tsx b/packages/pluggableWidgets/rich-text-web/src/RichText.tsx index 283829650c..9eb127cb07 100644 --- a/packages/pluggableWidgets/rich-text-web/src/RichText.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/RichText.tsx @@ -1,59 +1,24 @@ import { ValidationAlert } from "@mendix/widget-plugin-component-kit/Alert"; import classNames from "classnames"; -import { Fragment, ReactElement, useEffect, useState } from "react"; +import { Fragment, ReactElement } from "react"; import { RichTextContainerProps } from "../typings/RichTextProps"; import EditorWrapper from "./components/EditorWrapper"; import "./ui/RichText.scss"; -import { constructWrapperStyle } from "./utils/helpers"; export default function RichText(props: RichTextContainerProps): ReactElement { - const { stringAttribute, readOnlyStyle } = props; - - const wrapperStyle = constructWrapperStyle(props); - const [isIncubator, setIsIncubator] = useState(true); - - useEffect(() => { - // this is a fix for dojo runtime rendering - // in dojo runtime, DOM is rendered inside
at the inital stage - // and moved to content once it fully loads, which cause rich text editor looses reference to it's iframe - // this fix waits for it to be fully out of incubator div, then only fully renders rich text afterwards. - const observedIncubator = document.querySelector(`.mx-incubator.mx-offscreen`); - const observer = new MutationObserver((_mutationList, _observer) => { - if (!observedIncubator?.childElementCount || observedIncubator?.childElementCount <= 0) { - setIsIncubator(false); - } - }); - - if (observedIncubator && observedIncubator.childElementCount) { - observer.observe(observedIncubator, { - childList: true - }); - } else { - // eslint-disable-next-line react-hooks/set-state-in-effect - setIsIncubator(false); - } - - return () => { - observer.disconnect(); - }; - }, []); + const { stringAttribute } = props; return ( - {stringAttribute.status === "loading" || isIncubator ? ( + {stringAttribute.status === "loading" ? (
) : ( )} {stringAttribute.validation} diff --git a/packages/pluggableWidgets/rich-text-web/src/RichText.xml b/packages/pluggableWidgets/rich-text-web/src/RichText.xml index 8fb841b775..0ce72a407b 100644 --- a/packages/pluggableWidgets/rich-text-web/src/RichText.xml +++ b/packages/pluggableWidgets/rich-text-web/src/RichText.xml @@ -220,6 +220,14 @@ Character count (including HTML) + + Style data format + Choose how to render styling attribute in HTML + + inline + class + + diff --git a/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.spec.tsx b/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.spec.tsx index 1ab4c89bad..870d49894d 100644 --- a/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.spec.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.spec.tsx @@ -47,7 +47,8 @@ describe("Rich Text", () => { customFonts: [], enableDefaultUpload: true, formOrientation: "vertical", - linkValidation: true + linkValidation: true, + styleDataFormat: "inline" }; }); diff --git a/packages/pluggableWidgets/rich-text-web/src/__tests__/customList.spec.ts b/packages/pluggableWidgets/rich-text-web/src/__tests__/customList.spec.ts new file mode 100644 index 0000000000..0fec09d663 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/src/__tests__/customList.spec.ts @@ -0,0 +1,59 @@ +import { CustomListItem, CustomListItemClass, STANDARD_LIST_TYPES } from "../utils/formats/customList"; + +// CustomListItem and CustomListItemClass extend Quill's ListItem blot. +// We test only the static helpers and the constructor-level DOM mutation, +// which do not require a live Quill / Scroll instance. + +function makeListNode(listType = "ordered"): HTMLElement { + const li = document.createElement("li"); + li.dataset.list = listType; + return li; +} + +describe("STANDARD_LIST_TYPES", () => { + it("contains exactly the four standard types", () => { + expect(STANDARD_LIST_TYPES).toEqual(["ordered", "checked", "unchecked", "bullet"]); + }); +}); + +describe("CustomListItem.formats", () => { + it("returns data-list value for standard list types", () => { + const node = makeListNode("ordered"); + expect(CustomListItem.formats(node)).toBe("ordered"); + }); + + it("prefers data-custom-list over data-list when both are present", () => { + const node = makeListNode("ordered"); + node.dataset.customList = "lower-alpha"; + expect(CustomListItem.formats(node)).toBe("lower-alpha"); + }); + + it("returns undefined when neither attribute is present", () => { + const node = document.createElement("li"); + expect(CustomListItem.formats(node)).toBeUndefined(); + }); +}); + +describe("CustomListItemClass — styleFormat marker contract", () => { + // CustomListItemClass constructor assigns domNode.dataset.styleFormat = "class". + // Instantiating it requires a live Quill Scroll instance (a Quill integration concern), + // so here we verify the contract at the class-definition level and the DOM-mutation logic + // in isolation. + + it("is a subclass of CustomListItem", () => { + expect(Object.getPrototypeOf(CustomListItemClass)).toBe(CustomListItem); + }); + + it("the styleFormat marker 'class' round-trips correctly on a DOM node (logic under test)", () => { + // This mirrors exactly what the constructor body does: + // domNode.dataset.styleFormat = "class"; + const node = makeListNode("ordered"); + node.dataset.styleFormat = "class"; + expect(node.dataset.styleFormat).toBe("class"); + }); + + it("inline-mode list nodes do NOT have a styleFormat marker by default", () => { + const node = makeListNode("ordered"); + expect(node.dataset.styleFormat).toBeUndefined(); + }); +}); diff --git a/packages/pluggableWidgets/rich-text-web/src/__tests__/fonts.spec.ts b/packages/pluggableWidgets/rich-text-web/src/__tests__/fonts.spec.ts new file mode 100644 index 0000000000..780fc6dd16 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/src/__tests__/fonts.spec.ts @@ -0,0 +1,148 @@ +import { FONT_LIST, FontClassAttributor, FontStyleAttributor, formatCustomFonts } from "../utils/formats/fonts"; + +// parchment ClassAttributor and StyleAttributor operate directly on HTMLElement nodes — +// no Quill instance is needed for unit-level attribute tests. + +function makeSpan(): HTMLElement { + return document.createElement("span"); +} + +// FontStyleAttributor -------------------------------------------------------- + +describe("FontStyleAttributor", () => { + let attr: FontStyleAttributor; + + beforeEach(() => { + attr = new FontStyleAttributor([]); + }); + + it("adds font-family style for a known font value", () => { + const node = makeSpan(); + const result = attr.add(node, "arial"); + expect(result).toBe(true); + expect(node.style.fontFamily).toMatch(/arial/i); + expect(node.dataset.value).toBe("arial"); + }); + + it("returns false for an unknown font value", () => { + const node = makeSpan(); + const result = attr.add(node, "not-a-real-font"); + expect(result).toBe(false); + expect(node.style.fontFamily).toBe(""); + }); + + it("reads back the value via dataset.value", () => { + const node = makeSpan(); + attr.add(node, "courier-new"); + expect(attr.value(node)).toBe("courier-new"); + }); + + it("returns empty string for a node with no dataset.value", () => { + const node = makeSpan(); + expect(attr.value(node)).toBe(""); + }); + + it("applies custom fonts passed to the constructor", () => { + const custom = new FontStyleAttributor([ + { value: "my-font", description: "My Font", style: "MyFont, sans-serif" } + ]); + const node = makeSpan(); + expect(custom.add(node, "my-font")).toBe(true); + expect(node.style.fontFamily).toMatch(/MyFont/i); + }); + + it("FONT_LIST contains all 13 fonts including serif", () => { + const values = FONT_LIST.map(f => f.value); + expect(values).toContain("serif"); + expect(values).toHaveLength(13); + }); +}); + +// FontClassAttributor -------------------------------------------------------- + +describe("FontClassAttributor", () => { + let attr: FontClassAttributor; + + beforeEach(() => { + attr = new FontClassAttributor([]); + }); + + it("adds font-family- class for a known font value", () => { + const node = makeSpan(); + const result = attr.add(node, "arial"); + expect(result).toBe(true); + expect(node.classList.contains("font-family-arial")).toBe(true); + expect(node.dataset.value).toBe("arial"); + }); + + it("returns false for an unknown font value and adds no class", () => { + const node = makeSpan(); + const result = attr.add(node, "not-a-real-font"); + expect(result).toBe(false); + const hasClass = Array.from(node.classList).some(c => c.startsWith("font-family-")); + expect(hasClass).toBe(false); + }); + + it("reads back the value via dataset.value", () => { + const node = makeSpan(); + attr.add(node, "impact"); + expect(attr.value(node)).toBe("impact"); + }); + + it("returns empty string for a node with no dataset.value", () => { + const node = makeSpan(); + expect(attr.value(node)).toBe(""); + }); + + it("adds font-family-serif class for the serif font (Critical #3 regression guard)", () => { + const node = makeSpan(); + const result = attr.add(node, "serif"); + expect(result).toBe(true); + expect(node.classList.contains("font-family-serif")).toBe(true); + }); + + it("applies custom fonts passed to the constructor", () => { + const custom = new FontClassAttributor([ + { value: "my-font", description: "My Font", style: "MyFont, sans-serif" } + ]); + const node = makeSpan(); + expect(custom.add(node, "my-font")).toBe(true); + expect(node.classList.contains("font-family-my-font")).toBe(true); + }); + + it("emits class-based name, not inline style", () => { + const node = makeSpan(); + attr.add(node, "helvetica"); + expect(node.style.fontFamily).toBe(""); + expect(node.classList.contains("font-family-helvetica")).toBe(true); + }); +}); + +// formatCustomFonts ---------------------------------------------------------- + +describe("formatCustomFonts", () => { + it("maps custom font objects to FONT_LIST shape", () => { + const result = formatCustomFonts([{ fontName: "My Brand Font", fontStyle: "MyBrandFont, sans-serif" }]); + expect(result).toEqual([ + { value: "my-brand-font", description: "My Brand Font", style: "MyBrandFont, sans-serif" } + ]); + }); + + it("lowercases and hyphenates multi-word font names", () => { + const result = formatCustomFonts([{ fontName: "Open Sans", fontStyle: "Open Sans, sans-serif" }]); + expect(result[0].value).toBe("open-sans"); + }); + + it("returns an empty array when called with no arguments", () => { + expect(formatCustomFonts()).toEqual([]); + }); + + it("returns an empty array for an empty input", () => { + expect(formatCustomFonts([])).toEqual([]); + }); + + it("handles undefined fontName gracefully", () => { + const result = formatCustomFonts([{ fontName: undefined as any, fontStyle: "serif" }]); + expect(result[0].value).toBe(""); + }); +}); diff --git a/packages/pluggableWidgets/rich-text-web/src/__tests__/helpers.spec.ts b/packages/pluggableWidgets/rich-text-web/src/__tests__/helpers.spec.ts new file mode 100644 index 0000000000..85f9649a37 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/src/__tests__/helpers.spec.ts @@ -0,0 +1,143 @@ +import { INDENT_MAGIC_NUMBER, normalizeStyleAndClassAttribute } from "../utils/helpers"; + +function makeDoc(html: string): Document { + const doc = document.implementation.createHTMLDocument(); + doc.body.innerHTML = html; + return doc; +} + +describe("INDENT_MAGIC_NUMBER", () => { + it("equals 3", () => { + expect(INDENT_MAGIC_NUMBER).toBe(3); + }); +}); + +describe("normalizeStyleAndClassAttribute — class mode (inline → class)", () => { + it("converts padding-left:3em to ql-indent-1 and removes the style", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + const p = doc.querySelector("p")!; + expect(p.classList.contains("ql-indent-1")).toBe(true); + expect(p.style.paddingLeft).toBe(""); + }); + + it("converts padding-left:6em to ql-indent-2", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + expect(doc.querySelector("p")!.classList.contains("ql-indent-2")).toBe(true); + }); + + it("converts padding-left:9em to ql-indent-3", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + expect(doc.querySelector("p")!.classList.contains("ql-indent-3")).toBe(true); + }); + + it("rounds non-multiples of 3 using Math.round (5em → ql-indent-2)", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + expect(doc.querySelector("p")!.classList.contains("ql-indent-2")).toBe(true); + }); + + it("ignores elements with padding-left:0em (zero is falsy — no class added)", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + const p = doc.querySelector("p")!; + const hasIndentClass = Array.from(p.classList).some(c => c.startsWith("ql-indent-")); + expect(hasIndentClass).toBe(false); + }); + + it("converts RTL padding-right:3em to ql-indent-1 and removes the style", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + const p = doc.querySelector("p")!; + expect(p.classList.contains("ql-indent-1")).toBe(true); + expect(p.style.paddingRight).toBe(""); + }); + + it("converts multiple elements independently", () => { + const doc = makeDoc(` +

a

+

b

+ `); + normalizeStyleAndClassAttribute(doc, "class"); + const [a, b] = Array.from(doc.querySelectorAll("p")); + expect(a.classList.contains("ql-indent-1")).toBe(true); + expect(b.classList.contains("ql-indent-2")).toBe(true); + }); + + it("leaves elements without padding-left unchanged", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "class"); + const p = doc.querySelector("p")!; + const hasIndentClass = Array.from(p.classList).some(c => c.startsWith("ql-indent-")); + expect(hasIndentClass).toBe(false); + }); +}); + +describe("normalizeStyleAndClassAttribute — inline mode (class → inline)", () => { + it("converts ql-indent-1 to padding-left:3em and removes the class", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + const p = doc.querySelector("p")!; + expect(p.style.paddingLeft).toBe("3em"); + expect(p.classList.contains("ql-indent-1")).toBe(false); + }); + + it("converts ql-indent-2 to padding-left:6em", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + expect(doc.querySelector("p")!.style.paddingLeft).toBe("6em"); + }); + + it("converts ql-indent-3 to padding-left:9em", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + expect(doc.querySelector("p")!.style.paddingLeft).toBe("9em"); + }); + + it("uses padding-right for RTL elements (ql-direction-rtl)", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + const p = doc.querySelector("p")!; + expect(p.style.paddingRight).toBe("3em"); + expect(p.style.paddingLeft).toBe(""); + }); + + it("removes ql-indent-* class after conversion", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + expect(doc.querySelector("p")!.classList.contains("ql-indent-2")).toBe(false); + }); + + it("skips ql-indent-0 (zero — no padding added, class still removed)", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + const p = doc.querySelector("p")!; + expect(p.style.paddingLeft).toBe(""); + expect(p.classList.contains("ql-indent-0")).toBe(false); + }); + + it("preserves other classes on the element", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + expect(doc.querySelector("p")!.classList.contains("some-other-class")).toBe(true); + }); + + it("converts multiple elements independently", () => { + const doc = makeDoc(` +

a

+

b

+ `); + normalizeStyleAndClassAttribute(doc, "inline"); + const [a, b] = Array.from(doc.querySelectorAll("p")); + expect(a.style.paddingLeft).toBe("3em"); + expect(b.style.paddingLeft).toBe("9em"); + }); + + it("leaves elements without ql-indent-* unchanged", () => { + const doc = makeDoc(`

text

`); + normalizeStyleAndClassAttribute(doc, "inline"); + expect(doc.querySelector("p")!.style.paddingLeft).toBe(""); + }); +}); diff --git a/packages/pluggableWidgets/rich-text-web/src/assets/.gitkeep b/packages/pluggableWidgets/rich-text-web/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/pluggableWidgets/rich-text-web/src/assets/Icons.tsx b/packages/pluggableWidgets/rich-text-web/src/assets/Icons.tsx deleted file mode 100644 index e6f84d8528..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/assets/Icons.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { ReactElement } from "react"; - -export function IconLowerAlpha(): ReactElement { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/Fullscreen.tsx b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/Fullscreen.tsx deleted file mode 100644 index bc464f3264..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/Fullscreen.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ReactElement, useCallback, useContext } from "react"; -import { ToolbarButton } from "./ToolbarWrapper"; -import { CustomToolbarProps } from "./customToolbars"; -import { ACTION_DISPATCHER } from "../../utils/helpers"; -import { SET_FULLSCREEN_ACTION } from "../../store/store"; -import { EditorContext } from "../../store/EditorProvider"; -import classNames from "classnames"; - -export function FullscreenButton({ quill }: CustomToolbarProps): ReactElement { - const handleClick = useCallback(() => { - quill?.emitter.emit(ACTION_DISPATCHER, { type: SET_FULLSCREEN_ACTION }); - }, [quill]); - - const { isFullscreen } = useContext(EditorContext); - - return ( - - ); -} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/ToolbarWrapper.tsx b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/ToolbarWrapper.tsx deleted file mode 100644 index a1d08a66ff..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/ToolbarWrapper.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { If } from "@mendix/widget-plugin-component-kit/If"; -import { createContext, PropsWithChildren, ReactElement, useContext } from "react"; -import { PresetEnum } from "typings/RichTextProps"; -import type { ToolbarButtonProps, ToolbarConsumerContext, ToolbarContextType } from "./customToolbars"; - -export function presetToNumberConverter(preset: PresetEnum): number { - switch (preset) { - case "basic": - return 1; - case "standard": - return 2; - case "full": - return 3; - case "custom": - return 4; - default: - return 1; - } -} - -export const ToolbarContext = createContext({ - presetValue: 0 -}); - -export function FormatsContainer({ presetValue, children }: ToolbarConsumerContext & PropsWithChildren): ReactElement { - const toolbarContextValue = useContext(ToolbarContext); - return ( - = presetValue}> - - {children} - - - ); -} - -export function ToolbarButton({ - presetValue, - children, - className, - value, - onClick, - title -}: ToolbarButtonProps): ReactElement { - const toolbarContextValue = useContext(ToolbarContext); - return ( - = presetValue}> - - - ); -} - -export function ToolbarDropdown({ presetValue, className, value, title }: ToolbarButtonProps): ReactElement { - const toolbarContextValue = useContext(ToolbarContext); - return ( - = presetValue}> - - - ); -} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/UndoRedo.tsx b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/UndoRedo.tsx deleted file mode 100644 index 6f84717003..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/UndoRedo.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ReactElement, useCallback } from "react"; -import { ToolbarButton } from "./ToolbarWrapper"; -import type { CustomToolbarProps } from "./customToolbars"; - -export function UndoToolbar({ quill }: CustomToolbarProps): ReactElement { - const onUndo = useCallback(() => { - quill?.history.undo(); - }, [quill]); - - return ; -} - -export function RedoToolbar({ quill }: CustomToolbarProps): ReactElement { - const onRedo = useCallback(() => { - quill?.history.redo(); - }, [quill]); - - return ; -} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/constants.ts b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/constants.ts deleted file mode 100644 index dfcda2a3c4..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/constants.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type Quill from "quill"; -import { FunctionComponent } from "react"; -import { IconLowerAlpha } from "../../assets/Icons"; -import { FONT_LIST } from "../../utils/formats/fonts"; -import { FONT_SIZE_LIST } from "../../utils/formats/fontsize"; -import { ToolbarButton, ToolbarDropdown } from "./ToolbarWrapper"; -import { RedoToolbar, UndoToolbar } from "./UndoRedo"; -import { FullscreenButton } from "./Fullscreen"; -type DefaultComponentProps = { - className?: string; - value?: string | any[]; - presetValue?: number; - title: string; - children?: FunctionComponent; -}; - -type CustomComponentProps = - | { - quill?: Quill | null; - } - | DefaultComponentProps; - -type toolbarMappingType = { - [k: string]: { - component: FunctionComponent; - custom?: boolean; - } & DefaultComponentProps; -}; - -export const TOOLBAR_MAPPING: toolbarMappingType = { - undo: { component: UndoToolbar, custom: true, title: "Undo" }, - redo: { component: RedoToolbar, custom: true, title: "Redo" }, - bold: { component: ToolbarButton, className: "ql-bold icons icon-Text-bold", title: "Bold" }, - italic: { component: ToolbarButton, className: "ql-italic icons icon-Text-italic", title: "Italic" }, - underline: { - component: ToolbarButton, - className: "ql-underline icons icon-Text-underline", - presetValue: 2, - title: "Underline" - }, - strike: { - component: ToolbarButton, - className: "ql-strike icons icon-Text-strikethrough", - presetValue: 3, - title: "Strike" - }, - superScript: { - component: ToolbarButton, - className: "ql-script icons icon-Text-superscript", - value: "super", - title: "Superscript" - }, - subScript: { - component: ToolbarButton, - className: "ql-script icons icon-Text-subscript", - value: "sub", - title: "Subscript" - }, - size: { component: ToolbarDropdown, className: "size ql-size", value: FONT_SIZE_LIST, title: "Font size" }, - orderedList: { - component: ToolbarButton, - className: "ql-list icons icon-List-numbers", - value: "ordered", - title: "Default list" - }, - bulletList: { - component: ToolbarButton, - className: "ql-list icons icon-List-bullets", - value: "bullet", - title: "Bullet list" - }, - lowerAlphaList: { - component: ToolbarButton, - className: "ql-list icons icon-List-lower-alpha", - value: "lower-alpha", - presetValue: 2, - title: "Lower alpha list", - children: IconLowerAlpha - }, - checkList: { - component: ToolbarButton, - className: "ql-list icons icon-List-checklist", - value: "check", - presetValue: 3, - title: "Check list" - }, - minIndent: { - component: ToolbarButton, - className: "ql-indent icons icon-Text-indent-right", - value: "-1", - title: "Decrease indent" - }, - plusIndent: { - component: ToolbarButton, - className: "ql-indent icons icon-Text-indent-left", - value: "+1", - title: "Increase indent" - }, - direction: { - component: ToolbarButton, - className: "ql-direction direction icons", - value: "rtl", - presetValue: 2, - title: "Text direction" - }, - link: { component: ToolbarButton, className: "ql-link icons icon-Hyperlink", title: "Insert/edit link" }, - image: { - component: ToolbarButton, - className: "ql-image icons icon-Image", - presetValue: 2, - title: "Insert/edit image" - }, - video: { - component: ToolbarButton, - className: "ql-video icons icon-Film", - presetValue: 3, - title: "Insert/edit video" - }, - formula: { - component: ToolbarButton, - className: "ql-formula icons icon-Insert-edit-math", - presetValue: 3, - title: "Insert/edit formula" - }, - blockquote: { component: ToolbarButton, className: "ql-blockquote icons icon-Blockquote", title: "Blockquote" }, - codeBlock: { component: ToolbarButton, className: "ql-code-block icons icon-Code-block", title: "Code block" }, - code: { component: ToolbarButton, className: "ql-code icons icon-Inline-code", title: "Code" }, - viewCode: { - component: ToolbarButton, - className: "ql-view-code icons icon-View-edit-code", - title: "View/edit Code" - }, - align: { component: ToolbarButton, className: "ql-align icons icon-Text-align-left", title: "Left align" }, - centerAlign: { - component: ToolbarDropdown, - className: "ql-align icons", - value: ["center", "justify"], - title: "Center align" - }, - rightAlign: { - component: ToolbarButton, - className: "ql-align icons icon-Text-align-right", - value: "right", - title: "Right align" - }, - font: { component: ToolbarDropdown, className: "ql-font font", value: FONT_LIST, title: "Font type" }, - color: { component: ToolbarDropdown, className: "ql-color icons", value: [], title: "Font color" }, - background: { component: ToolbarDropdown, className: "ql-background icons", value: [], title: "Font background" }, - header: { - component: ToolbarDropdown, - className: "ql-header", - value: ["1", "2", "3", "4", "5", "6", false], - title: "Font header" - }, - clean: { component: ToolbarButton, className: "ql-clean icons icon-Clear-formating", title: "Clear formatting" }, - fullscreen: { - component: FullscreenButton, - title: "Fullscreen", - custom: true - }, - tableBetter: { - component: ToolbarButton, - className: "ql-table-better icons icon-Table", - title: "Create Table", - presetValue: 2 - } -}; - -type ToolbarGroupType = { - [k: string]: string[]; -}; - -export const TOOLBAR_GROUP: ToolbarGroupType = { - history: ["undo", "redo"], - fontStyle: ["bold", "italic", "underline", "strike"], - fontScript: ["superScript", "subScript"], - list: ["orderedList", "bulletList", "lowerAlphaList", "checkList"], - indent: ["minIndent", "plusIndent", "direction"], - align: ["align", "centerAlign", "rightAlign"], - fontColor: ["font", "size", "color", "background"], - embed: ["link", "image", "video", "formula"], - header: ["header"], - code: ["blockquote", "code", "codeBlock", "viewCode"], - remove: ["clean"], - view: ["fullscreen"], - tableBetter: ["tableBetter"] -}; - -export type toolbarContentType = { - presetValue?: number; - children: Array; -}; - -export const DEFAULT_TOOLBAR: toolbarContentType[] = [ - { - presetValue: 2, - children: TOOLBAR_GROUP.history - }, - { - presetValue: 1, - children: TOOLBAR_GROUP.fontStyle - }, - { - presetValue: 3, - children: TOOLBAR_GROUP.fontScript - }, - { - presetValue: 1, - children: TOOLBAR_GROUP.list - }, - { - presetValue: 1, - children: TOOLBAR_GROUP.indent - }, - { - presetValue: 2, - children: TOOLBAR_GROUP.align - }, - { - presetValue: 2, - children: TOOLBAR_GROUP.fontColor - }, - { - presetValue: 1, - children: TOOLBAR_GROUP.embed - }, - { - presetValue: 3, - children: TOOLBAR_GROUP.header - }, - { - presetValue: 2, - children: TOOLBAR_GROUP.code - }, - { - presetValue: 1, - children: TOOLBAR_GROUP.remove - }, - { - presetValue: 2, - children: TOOLBAR_GROUP.view - }, - { - presetValue: 2, - children: TOOLBAR_GROUP.tableBetter - } -]; - -export const IMG_MIME_TYPES = ["image/png", "image/jpeg"]; diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/customToolbars.d.ts b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/customToolbars.d.ts deleted file mode 100644 index ab02d0d63a..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/customToolbars.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Quill from "quill"; - -export type ToolbarContextType = { - presetValue: number; -}; - -export type ToolbarConsumerContext = { - presetValue?: number; -}; - -export type ToolbarButtonProps = { - className?: string; - onClick?: () => void; - value?: any; - title: string; -} & ToolbarConsumerContext & - PropsWithChildren; - -export type CustomToolbarProps = { - quill: Quill; -}; diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/presets.ts b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/presets.ts deleted file mode 100644 index f4534048b8..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/presets.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { RichTextContainerProps } from "typings/RichTextProps"; -import { DEFAULT_TOOLBAR, TOOLBAR_GROUP, type toolbarContentType } from "./constants"; - -export function createPreset(config?: Partial): toolbarContentType[] { - return config?.preset !== "custom" - ? DEFAULT_TOOLBAR - : config?.toolbarConfig === "basic" - ? defineBasicGroups(config) - : defineAdvancedGroups(config!); -} - -function defineBasicGroups(widgetProps: Partial): toolbarContentType[] { - const enabledGroups: Array = Object.entries(widgetProps).map(([prop, enabled]) => { - if (Object.hasOwn(TOOLBAR_GROUP, prop) && enabled) { - return { - children: TOOLBAR_GROUP[prop] - }; - } else { - return undefined; - } - }); - return enabledGroups.filter(x => x !== undefined) as toolbarContentType[]; -} - -function defineAdvancedGroups(widgetProps: Partial): toolbarContentType[] { - const { advancedConfig: items } = widgetProps; - - const result: toolbarContentType[] = [ - { - children: [] - } - ]; - items?.forEach(item => { - if (item.ctItemType === "separator") { - result.push({ - children: [] - }); - } else { - result[result.length - 1].children.push(item.ctItemType); - } - }); - - return result; -} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/useEmbedModal.ts b/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/useEmbedModal.ts deleted file mode 100644 index 34aee064a9..0000000000 --- a/packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/useEmbedModal.ts +++ /dev/null @@ -1,252 +0,0 @@ -import Quill, { Range } from "quill"; -import { Delta } from "quill/core"; -import Emitter from "quill/core/emitter"; -import { Dispatch, MutableRefObject, SetStateAction, useState } from "react"; -import { RichTextContainerProps } from "typings/RichTextProps"; -import { IMG_MIME_TYPES } from "./constants"; -import { - imageConfigType, - type linkConfigType, - type videoConfigType, - type videoEmbedConfigType, - viewCodeConfigType -} from "../../utils/formats"; -import { type ChildDialogProps } from "../ModalDialog/Dialog"; -import { type VideoFormType } from "../ModalDialog/VideoDialog"; - -type ModalReturnType = { - showDialog: boolean; - setShowDialog: Dispatch>; - dialogConfig: ChildDialogProps; - customLinkHandler(value: any): void; - customVideoHandler(value: any): void; - customViewCodeHandler(value: any): void; - customImageUploadHandler(value: any): void; -}; - -export function useEmbedModal( - ref: MutableRefObject, - props: Pick -): ModalReturnType { - const [showDialog, setShowDialog] = useState(false); - const [dialogConfig, setDialogConfig] = useState({}); - const openDialog = (): void => { - setShowDialog(true); - }; - const closeDialog = (): void => { - setShowDialog(false); - setTimeout(() => ref.current?.focus(), 50); - }; - const customLinkHandler = (value: any): void => { - const selection = ref.current?.getSelection(); - const text = selection ? ref.current?.getText(selection.index, selection.length) : ""; - if (value) { - setDialogConfig({ - dialogType: "link", - config: { - onSubmit: (value: linkConfigType) => { - const index = selection?.index ?? 0; - const length = selection?.length ?? 0; - const textToDisplay = value.text ?? value.href; - const linkDelta = new Delta().retain(index).delete(length).insert(textToDisplay); - ref.current?.updateContents(linkDelta, Emitter.sources.SILENT); - ref.current?.setSelection(index, textToDisplay.length); - ref.current?.format("link", value); - closeDialog(); - }, - onClose: closeDialog, - defaultValue: { ...value, text }, - formOrientation: props.formOrientation - } - }); - openDialog(); - } else { - ref.current?.format("link", false); - closeDialog(); - } - }; - - const customVideoHandler = (value: any): void => { - const selection = ref.current?.getSelection(); - if (value === true || value.type === "video") { - setDialogConfig({ - dialogType: "video", - config: { - onSubmit: (submittedValue: VideoFormType) => { - if ( - Object.hasOwn(submittedValue, "src") && - (submittedValue as videoConfigType).src !== undefined - ) { - const currentValue = submittedValue as videoConfigType; - if (value.type === "video") { - const index = selection?.index ?? 0; - const length = selection?.length ?? 1; - const videoConfig = { - width: currentValue.width, - height: currentValue.height - }; - // update existing video value - const delta = new Delta().retain(index).retain(length, videoConfig); - ref.current?.updateContents(delta, Emitter.sources.USER); - } else { - // insert new video - const delta = new Delta() - .retain(selection?.index ?? 0) - .delete(selection?.length ?? 0) - .insert( - { video: currentValue }, - { width: currentValue.width, height: currentValue.height } - ); - ref.current?.updateContents(delta, Emitter.sources.USER); - } - } else { - const currentValue = submittedValue as videoEmbedConfigType; - const res = ref.current?.clipboard.convert({ - html: currentValue.embedcode - }); - if (res) { - // insert video via embed code; - const delta = new Delta() - .retain(selection?.index ?? 0) - .delete(selection?.length ?? 0) - .concat(res); - ref.current?.updateContents(delta, Emitter.sources.USER); - } - } - - closeDialog(); - }, - onClose: closeDialog, - selection: ref.current?.getSelection(), - defaultValue: { ...value }, - formOrientation: props.formOrientation - } - }); - openDialog(); - } else { - ref.current?.format("link", false); - closeDialog(); - } - }; - - const customViewCodeHandler = (value: any): void => { - if (value === true) { - setDialogConfig({ - dialogType: "view-code", - config: { - currentCode: ref.current?.getSemanticHTML(), - onSubmit: (value: viewCodeConfigType) => { - const newDelta = ref.current?.clipboard.convert({ html: value.src }); - if (newDelta) { - ref.current?.setContents(newDelta, Emitter.sources.USER); - } - closeDialog(); - }, - onClose: closeDialog, - formOrientation: props.formOrientation - } - }); - openDialog(); - } else { - ref.current?.format("link", false); - closeDialog(); - } - }; - - const customImageUploadHandler = (value: any): void => { - const selection = ref.current?.getSelection(true); - setDialogConfig({ - dialogType: "image", - config: { - onSubmit: (value: imageConfigType) => { - const defaultImageConfig = { - alt: value.alt, - width: value.width, - height: value.keepAspectRatio ? undefined : value.height - }; - - if (value.src) { - const index = selection?.index ?? 0; - const length = 1; - const imageConfig = defaultImageConfig; - // update existing image attribute - const imageUpdateDelta = new Delta().retain(index).retain(length, imageConfig); - ref.current?.updateContents(imageUpdateDelta, Emitter.sources.USER); - } else { - // upload new image - if (selection) { - if (value.files) { - uploadImage(ref, selection, value); - } else if (value.entityGuid) { - const imageConfig = { - ...defaultImageConfig, - "data-src": value.entityGuid - }; - const delta = new Delta() - .retain(selection.index) - .delete(selection.length) - .insert({ image: value.entityGuid }, imageConfig); - ref.current?.updateContents(delta, Emitter.sources.USER); - } - } - } - closeDialog(); - }, - onClose: closeDialog, - defaultValue: { ...value }, - formOrientation: props.formOrientation - } - }); - openDialog(); - }; - - return { - showDialog, - setShowDialog, - dialogConfig, - customLinkHandler, - customVideoHandler, - customViewCodeHandler, - customImageUploadHandler - }; -} - -function uploadImage(ref: MutableRefObject, range: Range, options: imageConfigType): void { - const uploads: File[] = []; - const { files } = options; - if (files) { - Array.from(files).forEach(file => { - if (file && IMG_MIME_TYPES.includes(file.type)) { - uploads.push(file); - } - }); - if (uploads.length > 0) { - if (!ref.current?.scroll.query("image")) { - return; - } - const promises = uploads.map>(file => { - return new Promise(resolve => { - const reader = new FileReader(); - reader.onload = () => { - resolve(reader.result as string); - }; - reader.readAsDataURL(file); - }); - }); - Promise.all(promises).then(images => { - const update = images.reduce((delta: Delta, image) => { - return delta.insert( - { image }, - { - alt: options.alt, - width: options.width, - height: options.height - } - ); - }, new Delta().retain(range.index).delete(range.length)) as Delta; - ref.current?.updateContents(update, Emitter.sources.USER); - ref.current?.setSelection(range.index + images.length, Emitter.sources.SILENT); - }); - } - } -} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/DynamicTableStyles.tsx b/packages/pluggableWidgets/rich-text-web/src/components/DynamicTableStyles.tsx new file mode 100644 index 0000000000..98a4830d2e --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/src/components/DynamicTableStyles.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef } from "react"; +import { Editor } from "@tiptap/react"; + +export interface DynamicTableStylesProps { + editor: Editor | null; +} + +export function DynamicTableStyles({ editor }: DynamicTableStylesProps): null { + const styleElementRef = useRef(null); + + useEffect(() => { + if (!editor) return; + + // Create style element if it doesn't exist + if (!styleElementRef.current) { + styleElementRef.current = document.createElement("style"); + styleElementRef.current.setAttribute("data-tiptap-table-styles", ""); + document.head.appendChild(styleElementRef.current); + } + + const updateStyles = (): void => { + if (!styleElementRef.current) return; + + // Find all elements with data-background-color attribute + const elements = document.querySelectorAll("[data-background-color]"); + const colorsInUse = new Set(); + + elements.forEach(element => { + const color = element.getAttribute("data-background-color"); + if (color) { + colorsInUse.add(color); + // Add color-specific class to element + const sanitizedColor = color.replace(/[^a-zA-Z0-9]/g, ""); + const colorClass = `bg-color-${sanitizedColor}`; + + if (!element.classList.contains(colorClass)) { + element.classList.add(colorClass); + } + } + }); + + // Generate CSS rules only for colors that are actually in use + const cssRules: string[] = []; + colorsInUse.forEach(color => { + const sanitizedColor = color.replace(/[^a-zA-Z0-9]/g, ""); + cssRules.push(`.bg-color-${sanitizedColor} { background-color: ${color} !important; }`); + }); + + // Update style element + styleElementRef.current.textContent = cssRules.join("\n"); + }; + + // Update styles on editor update + editor.on("update", updateStyles); + editor.on("selectionUpdate", updateStyles); + + // Initial update + updateStyles(); + + // Cleanup + return () => { + editor.off("update", updateStyles); + editor.off("selectionUpdate", updateStyles); + }; + }, [editor]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (styleElementRef.current && styleElementRef.current.parentNode) { + styleElementRef.current.parentNode.removeChild(styleElementRef.current); + } + }; + }, []); + + return null; +} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/DynamicTextColorStyles.tsx b/packages/pluggableWidgets/rich-text-web/src/components/DynamicTextColorStyles.tsx new file mode 100644 index 0000000000..e286ff5816 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/src/components/DynamicTextColorStyles.tsx @@ -0,0 +1,81 @@ +import { useEffect, useRef } from "react"; +import { Editor } from "@tiptap/react"; + +export interface DynamicTextColorStylesProps { + editor: Editor | null; +} + +export function DynamicTextColorStyles({ editor }: DynamicTextColorStylesProps): null { + const styleElementRef = useRef(null); + const processedColorsRef = useRef>(new Set()); + + useEffect(() => { + if (!editor) return; + + // Create style element if it doesn't exist + if (!styleElementRef.current) { + styleElementRef.current = document.createElement("style"); + styleElementRef.current.setAttribute("data-tiptap-text-color-styles", ""); + document.head.appendChild(styleElementRef.current); + } + + const updateStyles = (): void => { + if (!styleElementRef.current) return; + + // Find all elements with data-text-color attribute + const elements = document.querySelectorAll("[data-text-color]"); + const colorsInUse = new Set(); + + elements.forEach(element => { + const color = element.getAttribute("data-text-color"); + if (color) { + colorsInUse.add(color); + // Add color-specific class to element + const sanitizedColor = color.replace(/[^a-zA-Z0-9]/g, ""); + const colorClass = `text-color-${sanitizedColor}`; + + if (!element.classList.contains(colorClass)) { + element.classList.add(colorClass); + } + } + }); + + // Generate CSS rules only for colors that are actually in use + const cssRules: string[] = []; + colorsInUse.forEach(color => { + const sanitizedColor = color.replace(/[^a-zA-Z0-9]/g, ""); + cssRules.push(`.text-color-${sanitizedColor} { color: ${color} !important; }`); + }); + + // Update style element + styleElementRef.current.textContent = cssRules.join("\n"); + + // Update processed colors reference + processedColorsRef.current = colorsInUse; + }; + + // Update styles on editor update + editor.on("update", updateStyles); + editor.on("selectionUpdate", updateStyles); + + // Initial update + updateStyles(); + + // Cleanup + return () => { + editor.off("update", updateStyles); + editor.off("selectionUpdate", updateStyles); + }; + }, [editor]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (styleElementRef.current && styleElementRef.current.parentNode) { + styleElementRef.current.parentNode.removeChild(styleElementRef.current); + } + }; + }, []); + + return null; +} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx index a13fdd1730..7748da66f5 100644 --- a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx @@ -1,206 +1,219 @@ -import Quill, { EmitterSource, QuillOptions, Range } from "quill"; -import Delta from "quill-delta"; -import { - CSSProperties, - forwardRef, - Fragment, - MutableRefObject, - useContext, - useEffect, - useLayoutEffect, - useRef -} from "react"; -import { RichTextContainerProps } from "../../typings/RichTextProps"; -import { EditorDispatchContext } from "../store/EditorProvider"; -import { SET_FULLSCREEN_ACTION } from "../store/store"; -import "../utils/customPluginRegisters"; -import "../utils/formats/quill-table-better/assets/css/quill-table-better.scss"; -import { getResizeModuleConfig } from "../utils/formats/resizeModuleConfig"; -import { ACTION_DISPATCHER } from "../utils/helpers"; -import { getKeyboardBindings } from "../utils/modules/keyboard"; -import { getIndentHandler } from "../utils/modules/toolbarHandlers"; -import MxUploader from "../utils/modules/uploader"; -import MxQuill, { MxQuillModulesOptions } from "../utils/MxQuill"; -import { useEmbedModal } from "./CustomToolbars/useEmbedModal"; -import Dialog from "./ModalDialog/Dialog"; - -export interface EditorProps extends Pick< - RichTextContainerProps, - "imageSource" | "imageSourceContent" | "enableDefaultUpload" -> { - options: MxQuillModulesOptions; +import type { Editor as TipTapEditor } from "@tiptap/core"; +// import { Color } from "@tiptap/extension-color"; +// import { Highlight } from "@tiptap/extension-highlight"; +// import { ListItem } from "@tiptap/extension-list-item"; +import { Link } from "@tiptap/extension-link"; +import { Subscript } from "@tiptap/extension-subscript"; +import { Superscript } from "@tiptap/extension-superscript"; +import { TableHeader } from "@tiptap/extension-table-header"; +import { TableRow } from "@tiptap/extension-table-row"; +import { TaskItem } from "@tiptap/extension-task-item"; +import { TaskList } from "@tiptap/extension-task-list"; +import { TextAlign } from "@tiptap/extension-text-align"; +import { TextStyle } from "@tiptap/extension-text-style"; +import { Underline } from "@tiptap/extension-underline"; +import { Youtube } from "@tiptap/extension-youtube"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { StarterKit } from "@tiptap/starter-kit"; + +import { ChangeEvent, forwardRef, ReactElement, ReactNode, useImperativeHandle, useRef } from "react"; +import { EditorContextProvider, useCurrentEditor } from "./EditorContext"; +// import { DynamicTableStyles } from "./DynamicTableStyles"; +// import { DynamicTextColorStyles } from "./DynamicTextColorStyles"; +import { Toolbar } from "./toolbars"; +import { FontFamilyClass } from "../extensions/FontFamilyClass"; +import { FontSize } from "../extensions/FontSize"; +import { ImageResize } from "../extensions/ImageResize"; +import { Indent } from "../extensions/Indent"; +import { TableBackgroundColor } from "../extensions/TableBackgroundColor"; +import { TableCellBackgroundColor } from "../extensions/TableCellBackgroundColor"; +import { TextColorClass } from "../extensions/TextColorClass"; +import { TextDirection } from "../extensions/TextDirection"; +import { TextHighlightClass } from "../extensions/TextHighlightClass"; +import { ConfirmDialog } from "./toolbars/components/ConfirmDialog"; + +export interface EditorProps { defaultValue?: string; - onTextChange?: (...args: [delta: Delta, oldContent: Delta, source: EmitterSource]) => void; - onSelectionChange?: (...args: [range: Range, oldRange: Range, source: EmitterSource]) => void; - formOrientation: "horizontal" | "vertical"; - theme: string; - style?: CSSProperties; - className?: string; - toolbarId?: string | Array; + onUpdate?: (html: string) => void; readOnly?: boolean; + className?: string; + showToolbar?: boolean; + styleDataFormat?: "inline" | "class"; + imageSourceContent?: ReactNode; +} + +export interface EditorHandle { + getHTML: () => string; + getText: () => string; + focus: () => void; + blur: () => void; + getEditor: () => TipTapEditor | null; +} + +interface EditorInnerProps { + showToolbar: boolean; + readOnly: boolean; + className?: string; + imageSourceContent?: ReactNode; +} + +function EditorInner({ showToolbar, readOnly, className, imageSourceContent }: EditorInnerProps): ReactElement { + const { editor, codeViewState, codeViewDispatch } = useCurrentEditor(); + const textareaRef = useRef(null); + + const handleSaveCode = (): void => { + if (!editor) return; + + // Update editor content with modified HTML + editor.commands.setContent(codeViewState.htmlCode); + codeViewDispatch({ type: "SAVE_CODE_CHANGES" }); + }; + + const handleCancelCode = (): void => { + codeViewDispatch({ type: "CANCEL_CODE_CHANGES" }); + }; + + const handleHtmlChange = (e: ChangeEvent): void => { + codeViewDispatch({ type: "UPDATE_HTML_CODE", html: e.target.value }); + }; + + return ( + <> +
+ {showToolbar && !readOnly && } + {codeViewState.isCodeView ? ( +