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 782a344759..66531a6b68 100644
--- a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js
+++ b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js
@@ -6,6 +6,7 @@ test.afterEach("Cleanup session", async ({ page }) => {
});
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
}) => {
@@ -133,6 +134,37 @@ test.describe("RichText", () => {
});
});
+ 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 page.waitForLoadState("networkidle");
diff --git a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js-snapshots/viewCodeDialog-chromium-linux.png b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js-snapshots/viewCodeDialog-chromium-linux.png
index b4ca13e1a7..1d733a5a99 100644
Binary files a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js-snapshots/viewCodeDialog-chromium-linux.png and b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js-snapshots/viewCodeDialog-chromium-linux.png differ
diff --git a/packages/pluggableWidgets/rich-text-web/package.json b/packages/pluggableWidgets/rich-text-web/package.json
index 5fb882b303..f1f07abf43 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,19 @@
"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",
"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"
+ "quill-resize-module": "^2.0.4",
+ "react-scroll-sync": "^1.0.2"
},
"devDependencies": {
"@mendix/automation-utils": "workspace:*",
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/components/Editor.tsx b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx
index a13fdd1730..0bb367e654 100644
--- a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx
+++ b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx
@@ -13,14 +13,15 @@ import {
import { RichTextContainerProps } from "../../typings/RichTextProps";
import { EditorDispatchContext } from "../store/EditorProvider";
import { SET_FULLSCREEN_ACTION } from "../store/store";
-import "../utils/customPluginRegisters";
+import { registerCustomFormats } from "../utils/customPluginRegisters";
import "../utils/formats/quill-table-better/assets/css/quill-table-better.scss";
+import { MxQuillModulesOptions } from "../utils/formats";
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 MxQuill from "../utils/MxQuill";
import { useEmbedModal } from "./CustomToolbars/useEmbedModal";
import Dialog from "./ModalDialog/Dialog";
@@ -68,7 +69,9 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject {
@@ -133,7 +136,8 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject(null);
const [formState, setFormState] = useState({
// temporarily change tab characters to em space to avoid beautify removing them
src:
beautify.html(currentCode?.replace(/\t/g, " ") ?? "", BEAUTIFY_OPTIONS)?.replace(/ /g, "\t") || ""
});
- const onCodeChange = useCallback((value: string, _viewUpdate: ViewUpdate) => {
- setFormState({ ...formState, src: value });
+
+ useEffect(() => {
+ const codeElement = codeRef.current;
+ if (codeElement) {
+ hljs.highlightElement(codeElement);
+ }
+ return () => {
+ if (codeElement) {
+ delete codeElement.dataset.highlighted;
+ }
+ };
+ }, [formState]);
+
+ const onCodeChange = useCallback((value: string) => {
+ setFormState({ src: value });
}, []);
return (
@@ -45,15 +58,35 @@ export default function ViewCodeDialog(props: ViewCodeDialogProps): ReactElement
-
+
+
+
+
+
+
+
+
+
+
+ {formState.src}
+
+
+
+
+
onSubmit(formState)} onClose={onClose}>
diff --git a/packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss b/packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss
index a54e9ebf74..74e5ea1deb 100644
--- a/packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss
+++ b/packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss
@@ -1,4 +1,5 @@
@use "RichTextIcons";
+@use "RichTextFormatStyle";
$rte-border-color-default: #ced0d3;
$rte-gray-ligher: #f8f8f8;
@@ -170,4 +171,9 @@ $rte-brand-primary: #264ae5;
.flexcontainer.flex-column {
overflow: visible;
}
+
+ VIDEO,
+ IFRAME {
+ pointer-events: none;
+ }
}
diff --git a/packages/pluggableWidgets/rich-text-web/src/ui/RichTextFormatStyle.scss b/packages/pluggableWidgets/rich-text-web/src/ui/RichTextFormatStyle.scss
new file mode 100644
index 0000000000..3c921fdd54
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/src/ui/RichTextFormatStyle.scss
@@ -0,0 +1,29 @@
+@import "variables";
+
+$color: #3d1466;
+.widget-rich-text {
+ @each $name, $font in $fonts {
+ .font-family-#{$name} {
+ @include font($font);
+ }
+ }
+ @each $name, $font in $quill-fonts {
+ .font-family-#{$name} {
+ @include font($font);
+ }
+ }
+ @each $size in $font-sizes {
+ .ql-size-#{$size} {
+ font-size: $size;
+ }
+ }
+
+ @each $color in $colors {
+ .ql-color-#{"\\" + $color} {
+ color: $color;
+ }
+ .ql-bg-#{"\\" + $color} {
+ background-color: $color;
+ }
+ }
+}
diff --git a/packages/pluggableWidgets/rich-text-web/src/ui/variables.scss b/packages/pluggableWidgets/rich-text-web/src/ui/variables.scss
new file mode 100644
index 0000000000..44fe1b4d61
--- /dev/null
+++ b/packages/pluggableWidgets/rich-text-web/src/ui/variables.scss
@@ -0,0 +1,77 @@
+$font-andale-mono: "andale mono", monospace;
+$font-arial: arial, helvetica, sans-serif;
+$font-arial-black: "arial black", sans-serif;
+$font-book-antiqua: "book antiqua", palatino, serif;
+$font-comic-sans: "comic sans ms", sans-serif;
+$font-courier-new: "courier new", courier, monospace;
+$font-helvetica: helvetica, arial, sans-serif;
+$font-impact: impact, sans-serif;
+$font-symbol: symbol;
+$font-terminal: terminal, monaco, monospace;
+$font-times-new-roman: "times new roman", times, serif;
+$font-trebuchet: "trebuchet ms", geneva, sans-serif;
+$font-serif: Georgia, "times New roman", serif;
+
+$fonts: (
+ andale-mono: $font-andale-mono,
+ arial: $font-arial,
+ arial-black: $font-arial-black,
+ book-antiqua: $font-book-antiqua,
+ comic-sans: $font-comic-sans,
+ courier-new: $font-courier-new,
+ helvetica: $font-helvetica,
+ impact: $font-impact,
+ symbol: $font-symbol,
+ terminal: $font-terminal,
+ times-new-roman: $font-times-new-roman,
+ trebuchet: $font-trebuchet
+);
+
+$quill-fonts: (
+ serif: $font-serif
+);
+
+@mixin font($font) {
+ font-family: $font;
+}
+
+$font-sizes: (8px, 9px, 10px, 12px, 14px, 16px, 20px, 24px, 32px, 42px, 54px, 68px, 84px, 98px);
+
+// https://github.com/slab/quill/blob/main/packages/quill/src/themes/base.ts
+$colors: (
+ #000000,
+ #e60000,
+ #ff9900,
+ #ffff00,
+ #008a00,
+ #0066cc,
+ #9933ff,
+ #ffffff,
+ #facccc,
+ #ffebcc,
+ #ffffcc,
+ #cce8cc,
+ #cce0f5,
+ #ebd6ff,
+ #bbbbbb,
+ #f06666,
+ #ffc266,
+ #ffff66,
+ #66b966,
+ #66a3e0,
+ #c285ff,
+ #888888,
+ #a10000,
+ #b26b00,
+ #b2b200,
+ #006100,
+ #0047b2,
+ #6b24b2,
+ #444444,
+ #5c0000,
+ #663d00,
+ #666600,
+ #003700,
+ #002966,
+ #3d1466
+);
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts b/packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts
index 1d3710bd66..0548cd28be 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts
@@ -42,9 +42,6 @@ import TextBlot, { escapeText } from "quill/blots/text";
import { Delta, Op } from "quill/core";
import Editor from "quill/core/editor";
import { STANDARD_LIST_TYPES } from "./formats/customList";
-import { FontStyleAttributor, formatCustomFonts } from "./formats/fonts";
-import CustomLink, { CustomLinkNoValidation } from "./formats/link";
-import { CustomFontsType } from "../../typings/RichTextProps";
interface ListItem {
child: Blot;
@@ -80,17 +77,11 @@ class MxEditor extends Editor {
}
}
-export interface MxQuillModulesOptions {
- fonts: CustomFontsType[];
- links: {
- validate: boolean;
- };
-}
-
/**
* Extension's of quill to allow us to replace the editor instance.
*/
export default class MxQuill extends Quill {
+ private styleDataFormat: "inline" | "class" = "inline";
constructor(container: HTMLElement | string, options: QuillOptions = {}) {
super(container, options);
this.editor = new MxEditor(this.scroll);
@@ -101,16 +92,12 @@ export default class MxQuill extends Quill {
return this.updateContents(this.getContents().transform(dlta as Delta, false), source);
}
- registerCustomModules(props: MxQuillModulesOptions): void {
- const { fonts, links } = props;
- const customFonts = formatCustomFonts(fonts);
- const FontStyle = new FontStyleAttributor(customFonts);
- Quill.register(FontStyle, true);
- if (links.validate) {
- Quill.register(CustomLink, true);
- } else {
- Quill.register(CustomLinkNoValidation, true);
- }
+ setStyleDataFormat(format: "inline" | "class"): void {
+ this.styleDataFormat = format;
+ }
+
+ getStyleDataFormat(): "inline" | "class" {
+ return this.styleDataFormat;
}
}
@@ -170,9 +157,15 @@ function convertListHTML(items: ListItem[], lastIndent: number, types: string[])
if (indent > lastIndent) {
// modified by web-content: get proper list-style-type
const expectedType = getExpectedType(type, indent);
+
+ let listStyleTypeAttribute = `style="list-style-type: ${expectedType}"`;
+ if ((child.domNode as HTMLElement)?.dataset?.styleFormat === "class") {
+ const listStyleClass = `ql-list-style-${expectedType}`;
+ listStyleTypeAttribute = `class="${listStyleClass}"`;
+ }
types.push(type);
if (indent === lastIndent + 1) {
- return `<${tag} style="list-style-type: ${expectedType}">${convertHTML(
+ return `<${tag} ${listStyleTypeAttribute}>${convertHTML(
child,
offset,
length
@@ -197,6 +190,7 @@ function convertHTML(blot: Blot, index: number, length: number, isRoot = false):
if ("html" in blot && typeof blot.html === "function") {
return blot.html(index, length);
}
+
if (blot instanceof TextBlot) {
const escapedText = escapeText(blot.value().slice(index, index + length));
return escapedText;
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts b/packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts
index c954bb99c5..d44137813f 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts
@@ -1,24 +1,28 @@
-import { Attributor } from "parchment";
import Quill from "quill";
-import QuillResize from "quill-resize-module";
+import { AlignClass, AlignStyle } from "quill/formats/align";
+import { BackgroundClass, BackgroundStyle } from "quill/formats/background";
+import { ColorClass, ColorStyle } from "quill/formats/color";
+import { DirectionStyle, DirectionClass } from "quill/formats/direction";
+import IndentClass from "quill/formats/indent";
+import { SizeClass, SizeStyle } from "quill/formats/size";
+import { MxQuillModulesOptions } from "./formats";
import MxBlock from "./formats/block";
import Button from "./formats/button";
-import CustomListItem from "./formats/customList";
-import "./formats/fonts";
-import "./formats/fontsize";
+import { CustomListItem, CustomListItemClass } from "./formats/customList";
+import { FontClassAttributor, FontStyleAttributor, formatCustomFonts } from "./formats/fonts";
+import { FONT_SIZE_LIST } from "./formats/fontsize";
import Formula from "./formats/formula";
import CustomImage from "./formats/image";
import { IndentLeftStyle, IndentRightStyle } from "./formats/indent";
+import CustomLink, { CustomLinkNoValidation } from "./formats/link";
import QuillTableBetter from "./formats/quill-table-better/quill-table-better";
import SoftBreak from "./formats/softBreak";
import CustomVideo from "./formats/video";
import { WhiteSpaceStyle } from "./formats/whiteSpace";
+import { MxResizeModule } from "./modules/resize";
+import MxScroll from "./modules/scroll";
import MxUploader from "./modules/uploader";
import MendixTheme from "./themes/mxTheme";
-import MxScroll from "./modules/scroll";
-const direction = Quill.import("attributors/style/direction") as Attributor;
-const alignment = Quill.import("attributors/style/align") as Attributor;
-
class Empty {
doSomething(): string {
return "";
@@ -29,21 +33,58 @@ class Empty {
*/
Quill.debug("error");
Quill.register({ "themes/snow": MendixTheme }, true);
-Quill.register(CustomListItem, true);
Quill.register(WhiteSpaceStyle, true);
Quill.register(CustomVideo, true);
Quill.register(CustomImage, true);
Quill.register({ "formats/softbreak": SoftBreak }, true);
-Quill.register(direction, true);
-Quill.register(alignment, true);
-Quill.register(IndentLeftStyle, true);
-Quill.register(IndentRightStyle, true);
Quill.register(Formula, true);
Quill.register(Button, true);
Quill.register(MxBlock, true);
Quill.register({ "modules/uploader": MxUploader }, true);
Quill.register({ "blots/scroll": MxScroll }, true);
-Quill.register("modules/resize", QuillResize, true);
+Quill.register("modules/resize", MxResizeModule, true);
// add empty handler for view code, this format is handled by toolbar's custom config via ViewCodeDialog
Quill.register({ "ui/view-code": Empty });
Quill.register({ "modules/table-better": QuillTableBetter }, true);
+
+export function registerCustomFormats(props: MxQuillModulesOptions): void {
+ const { fonts, links, styleDataFormat } = props;
+
+ // register formats based on styleDataFormat option
+ const customFonts = formatCustomFonts(fonts);
+ if (styleDataFormat === "inline") {
+ const FontStyle = new FontStyleAttributor(customFonts);
+ Quill.register(FontStyle, true);
+
+ SizeStyle.whitelist = FONT_SIZE_LIST;
+ Quill.register(SizeStyle, true);
+
+ Quill.register(CustomListItem, true);
+ Quill.register(DirectionStyle, true);
+ Quill.register(AlignStyle, true);
+ Quill.register(IndentLeftStyle, true);
+ Quill.register(IndentRightStyle, true);
+ Quill.register(ColorStyle, true);
+ Quill.register(BackgroundStyle, true);
+ } else {
+ IndentClass.whitelist = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
+ const FontClass = new FontClassAttributor(customFonts);
+ Quill.register(FontClass, true);
+
+ SizeClass.whitelist = FONT_SIZE_LIST;
+ Quill.register(SizeClass, true);
+ Quill.register(CustomListItemClass, true);
+ Quill.register(DirectionClass, true);
+ Quill.register(AlignClass, true);
+ Quill.register(IndentClass, true);
+ Quill.register(ColorClass, true);
+ Quill.register(BackgroundClass, true);
+ }
+
+ // register link format based on validation requirement
+ if (links.validate) {
+ Quill.register(CustomLink, true);
+ } else {
+ Quill.register(CustomLinkNoValidation, true);
+ }
+}
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats.d.ts b/packages/pluggableWidgets/rich-text-web/src/utils/formats.d.ts
index 6058445ab1..28c292bda7 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats.d.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats.d.ts
@@ -28,3 +28,10 @@ export type imageConfigType = {
entityGuid?: string;
keepAspectRatio?: boolean;
};
+export interface MxQuillModulesOptions {
+ styleDataFormat: "inline" | "class";
+ fonts: CustomFontsType[];
+ links: {
+ validate: boolean;
+ };
+}
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.ts b/packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.ts
index 19ea919c67..00bdc507a1 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.ts
@@ -1,12 +1,12 @@
import ListItem from "quill/formats/list";
import "./customList.scss";
+import Scroll from "quill/blots/scroll";
/**
* adding custom list item, alowing extra list style
*/
export const STANDARD_LIST_TYPES = ["ordered", "checked", "unchecked", "bullet"];
-
-export default class CustomListItem extends ListItem {
+export class CustomListItem extends ListItem {
format(name: string, value: string): void {
if (name === this.statics.blotName && value) {
if (!STANDARD_LIST_TYPES.find(x => x === value)) {
@@ -38,3 +38,10 @@ export default class CustomListItem extends ListItem {
return domNode.dataset.customList || domNode.dataset.list || undefined;
}
}
+
+export class CustomListItemClass extends CustomListItem {
+ constructor(scroll: Scroll, domNode: HTMLElement) {
+ super(scroll, domNode);
+ domNode.dataset.styleFormat = "class";
+ }
+}
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.scss b/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.scss
index 258576b2dc..4485cdb364 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.scss
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.scss
@@ -1,34 +1,5 @@
-$font-andale-mono: "andale mono", monospace;
-$font-arial: arial, helvetica, sans-serif;
-$font-arial-black: "arial black", sans-serif;
-$font-book-antiqua: "book antiqua", palatino, serif;
-$font-comic-sans: "comic sans ms", sans-serif;
-$font-courier-new: "courier new", courier, monospace;
-$font-helvetica: helvetica, arial, sans-serif;
-$font-impact: impact, sans-serif;
-$font-symbol: symbol;
-$font-terminal: terminal, monaco, monospace;
-$font-times-new-roman: "times new roman", times, serif;
-$font-trebuchet: "trebuchet ms", geneva, sans-serif;
+@import "../../ui/variables";
-$fonts: (
- andale-mono: $font-andale-mono,
- arial: $font-arial,
- arial-black: $font-arial-black,
- book-antiqua: $font-book-antiqua,
- comic-sans: $font-comic-sans,
- courier-new: $font-courier-new,
- helvetica: $font-helvetica,
- impact: $font-impact,
- symbol: $font-symbol,
- terminal: $font-terminal,
- times-new-roman: $font-times-new-roman,
- trebuchet: $font-trebuchet
-);
-
-@mixin font($font) {
- font-family: $font;
-}
.widget-rich-text {
.ql-toolbar {
.ql-picker {
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.ts b/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.ts
index 6b3e445bc0..7e35202b8b 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.ts
@@ -1,4 +1,4 @@
-import { Scope, StyleAttributor } from "parchment";
+import { ClassAttributor, Scope, StyleAttributor } from "parchment";
import { CustomFontsType } from "../../../typings/RichTextProps";
import "./fonts.scss";
@@ -54,6 +54,38 @@ export class FontStyleAttributor extends StyleAttributor {
}
}
+export class FontClassAttributor extends ClassAttributor {
+ private fontList: typeof FONT_LIST = [];
+
+ constructor(fontList: typeof FONT_LIST) {
+ super("font", "font-family", config);
+ this.fontList = fontList;
+ }
+
+ add(node: HTMLElement, value: any): boolean {
+ if (!this.canAdd(node, value)) {
+ return false;
+ }
+ node.dataset.value = value;
+ const allFonts = [...FONT_LIST, ...this.fontList];
+ const style = allFonts.find(x => x.value === value)?.value;
+ if (style) {
+ super.add(node, style);
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ value(node: HTMLElement): any {
+ const value = node.dataset.value;
+ if (this.canAdd(node, value) && value) {
+ return value;
+ }
+ return "";
+ }
+}
+
export function formatCustomFonts(fonts: CustomFontsType[] = []): typeof FONT_LIST {
return fonts.map(font => ({
value: font.fontName?.toLowerCase().split(" ").join("-") ?? "",
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats/fontsize.ts b/packages/pluggableWidgets/rich-text-web/src/utils/formats/fontsize.ts
index 29335852a3..9eadd57d0b 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats/fontsize.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats/fontsize.ts
@@ -1,7 +1,3 @@
-import { Attributor } from "parchment";
-import Quill from "quill";
-const Size = Quill.import("attributors/style/size") as Attributor;
-
import "./fonts.scss";
export const FONT_SIZE_LIST = [
@@ -20,6 +16,3 @@ export const FONT_SIZE_LIST = [
"84px",
"98px"
];
-
-Size.whitelist = FONT_SIZE_LIST;
-Quill.register(Size, true);
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats/indent.ts b/packages/pluggableWidgets/rich-text-web/src/utils/formats/indent.ts
index d00f8a1ddc..7bd1285bce 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats/indent.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats/indent.ts
@@ -1,7 +1,7 @@
import { Scope, StyleAttributor } from "parchment";
+import { INDENT_MAGIC_NUMBER } from "../helpers";
import "./fonts.scss";
-const INDENT_MAGIC_NUMBER = 3;
const indentLists = ["3em", "6em", "9em", "12em", "15em", "18em", "21em", "24em", "27em"];
/**
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts b/packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts
index c6a8bf24f7..753f53467e 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts
@@ -1,8 +1,8 @@
import Quill from "quill";
import QuillResize from "quill-resize-module";
import { ACTION_DISPATCHER } from "../helpers";
-import MxResizeToolbar from "../modules/resizeToolbar";
import MxResize from "../modules/resize";
+import MxResizeToolbar from "../modules/resizeToolbar";
export const RESIZE_MODULE_CONFIG = {
modules: ["DisplaySize", MxResizeToolbar, MxResize, "Keyboard"],
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/helpers.ts b/packages/pluggableWidgets/rich-text-web/src/utils/helpers.ts
index 7f60084c39..c7ed30f02d 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/helpers.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/helpers.ts
@@ -1,6 +1,7 @@
import { CSSProperties } from "react";
import { RichTextContainerProps } from "typings/RichTextProps";
+export const INDENT_MAGIC_NUMBER = 3;
export const ACTION_DISPATCHER = "ACTION_DISPATCHER";
function getHeightScale(height: number, heightUnit: "pixels" | "percentageOfParent" | "percentageOfView"): string {
@@ -32,3 +33,44 @@ export function constructWrapperStyle(props: RichTextContainerProps): CSSPropert
return wrapperStyle;
}
+
+export function normalizeStyleAndClassAttribute(doc: Document, styleDataFormat: "inline" | "class"): void {
+ if (styleDataFormat === "class") {
+ const allIndentLeftElements = doc.querySelectorAll("[style*=padding-left]");
+ const allIndentRightElements = doc.querySelectorAll("[style*=padding-right]");
+ allIndentLeftElements.forEach(element => {
+ const paddingLeft = (element as HTMLElement).style.paddingLeft || "0em";
+ const indentValue = parseInt(paddingLeft.replace("px", "").replace("em", ""), 10);
+ if (indentValue) {
+ const indentClassValue = Math.round(indentValue / INDENT_MAGIC_NUMBER);
+ element.classList.add(`ql-indent-${indentClassValue}`);
+ (element as HTMLElement).style.removeProperty("padding-left");
+ }
+ });
+ allIndentRightElements.forEach(element => {
+ const paddingRight = (element as HTMLElement).style.paddingRight || "0em";
+ const indentValue = parseInt(paddingRight.replace("px", "").replace("em", ""), 10);
+ if (indentValue) {
+ const indentClassValue = Math.round(indentValue / INDENT_MAGIC_NUMBER);
+ element.classList.add(`ql-indent-${indentClassValue}`);
+ (element as HTMLElement).style.removeProperty("padding-right");
+ }
+ });
+ } else if (styleDataFormat === "inline") {
+ const allIndentsElements = doc.querySelectorAll("[class*=ql-indent-]");
+ allIndentsElements.forEach(element => {
+ const indentClass = Array.from(element.classList).find(className => className.startsWith("ql-indent-"));
+ if (indentClass) {
+ const indentValue = parseInt(indentClass.replace("ql-indent-", ""), 10);
+ if (indentValue) {
+ if (element.classList.contains("ql-direction-rtl")) {
+ (element as HTMLElement).style.paddingRight = `${indentValue * INDENT_MAGIC_NUMBER}em`;
+ } else {
+ (element as HTMLElement).style.paddingLeft = `${indentValue * INDENT_MAGIC_NUMBER}em`;
+ }
+ }
+ element.classList.remove(indentClass);
+ }
+ });
+ }
+}
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/modules/clipboard.ts b/packages/pluggableWidgets/rich-text-web/src/utils/modules/clipboard.ts
index 0b98faa349..645e722567 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/modules/clipboard.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/modules/clipboard.ts
@@ -7,6 +7,8 @@
import { EmbedBlot, type ScrollBlot } from "parchment";
import Quill, { Delta } from "quill";
import Clipboard, { matchNewline } from "quill/modules/clipboard";
+import { normalizeStyleAndClassAttribute } from "../helpers";
+import MxQuill from "../MxQuill";
export default class CustomClipboard extends Clipboard {
constructor(quill: Quill, options: any) {
@@ -24,6 +26,11 @@ export default class CustomClipboard extends Clipboard {
this.addMatcher("ol, ul", matchList);
this.addMatcher("a", matchLink);
}
+
+ protected normalizeHTML(doc: Document): void {
+ super.normalizeHTML(doc);
+ normalizeStyleAndClassAttribute(doc, (this.quill as MxQuill)?.getStyleDataFormat() || "inline");
+ }
}
function isLine(node: Node, scroll: ScrollBlot): any {
diff --git a/packages/pluggableWidgets/rich-text-web/src/utils/modules/resize.ts b/packages/pluggableWidgets/rich-text-web/src/utils/modules/resize.ts
index 8c88b4ee8d..1cf17bef30 100644
--- a/packages/pluggableWidgets/rich-text-web/src/utils/modules/resize.ts
+++ b/packages/pluggableWidgets/rich-text-web/src/utils/modules/resize.ts
@@ -23,6 +23,12 @@ type LimitConfig = {
type CalculateSizeEvent = { clientX: number; clientY: number };
+export class MxResizeModule extends QuillResize {
+ initializeEmbed(): void {
+ // override parents, do nothing
+ }
+}
+
export default class MxResize extends QuillResize.Modules.Resize {
// modified from https://github.com/mudoo/quill-resize-module/blob/master/src/modules/Resize.js
calcSize(evt: CalculateSizeEvent, limit: LimitConfig = {}): ImageSizes {
diff --git a/packages/pluggableWidgets/rich-text-web/typings/RichTextProps.d.ts b/packages/pluggableWidgets/rich-text-web/typings/RichTextProps.d.ts
index 685894966f..c32e097386 100644
--- a/packages/pluggableWidgets/rich-text-web/typings/RichTextProps.d.ts
+++ b/packages/pluggableWidgets/rich-text-web/typings/RichTextProps.d.ts
@@ -33,6 +33,8 @@ export interface CustomFontsType {
export type StatusBarContentEnum = "wordCount" | "characterCount" | "characterCountHtml";
+export type StyleDataFormatEnum = "inline" | "class";
+
export type ToolbarConfigEnum = "basic" | "advanced";
export type CtItemTypeEnum = "separator" | "undo" | "redo" | "bold" | "italic" | "underline" | "strike" | "superScript" | "subScript" | "orderedList" | "bulletList" | "lowerAlphaList" | "checkList" | "minIndent" | "plusIndent" | "direction" | "link" | "image" | "video" | "formula" | "blockquote" | "code" | "codeBlock" | "viewCode" | "align" | "centerAlign" | "rightAlign" | "font" | "size" | "color" | "background" | "header" | "fullscreen" | "clean" | "tableBetter";
@@ -83,6 +85,7 @@ export interface RichTextContainerProps {
imageSourceContent?: ReactNode;
enableDefaultUpload: boolean;
statusBarContent: StatusBarContentEnum;
+ styleDataFormat: StyleDataFormatEnum;
toolbarConfig: ToolbarConfigEnum;
history: boolean;
fontStyle: boolean;
@@ -133,6 +136,7 @@ export interface RichTextPreviewProps {
imageSourceContent: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> };
enableDefaultUpload: boolean;
statusBarContent: StatusBarContentEnum;
+ styleDataFormat: StyleDataFormatEnum;
toolbarConfig: ToolbarConfigEnum;
history: boolean;
fontStyle: boolean;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cb1ef83ce6..21333cb61c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2159,12 +2159,6 @@ importers:
packages/pluggableWidgets/rich-text-web:
dependencies:
- '@codemirror/lang-html':
- specifier: ^6.4.9
- version: 6.4.11
- '@codemirror/state':
- specifier: ^6.5.2
- version: 6.5.2
'@floating-ui/dom':
specifier: ^1.7.4
version: 1.7.4
@@ -2174,15 +2168,12 @@ importers:
'@melloware/coloris':
specifier: ^0.25.0
version: 0.25.0
- '@uiw/codemirror-theme-github':
- specifier: ^4.23.13
- version: 4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
- '@uiw/react-codemirror':
- specifier: ^4.23.13
- version: 4.25.2(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.6)(codemirror@6.0.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
classnames:
specifier: ^2.5.1
version: 2.5.1
+ highlight.js:
+ specifier: ^11.11.1
+ version: 11.11.1
js-beautify:
specifier: ^1.15.4
version: 1.15.4
@@ -2204,6 +2195,9 @@ importers:
quill-resize-module:
specifier: ^2.0.4
version: 2.0.8
+ react-scroll-sync:
+ specifier: ^1.0.2
+ version: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@mendix/automation-utils':
specifier: workspace:*
@@ -3862,14 +3856,8 @@ packages:
'@codemirror/commands@6.9.0':
resolution: {integrity: sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w==}
- '@codemirror/lang-css@6.3.1':
- resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
-
- '@codemirror/lang-html@6.4.11':
- resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==}
-
- '@codemirror/lang-javascript@6.2.4':
- resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==}
+ '@codemirror/lang-json@6.0.2':
+ resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/language@6.11.3':
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
@@ -3981,10 +3969,6 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
- '@eslint-community/regexpp@4.12.1':
- resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
- engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
-
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
@@ -4227,20 +4211,14 @@ packages:
'@lezer/common@1.5.1':
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
- '@lezer/css@1.3.0':
- resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
-
'@lezer/highlight@1.2.1':
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
'@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
- '@lezer/html@1.3.12':
- resolution: {integrity: sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==}
-
- '@lezer/javascript@1.5.4':
- resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
+ '@lezer/json@1.0.3':
+ resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.2':
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
@@ -7388,6 +7366,10 @@ packages:
hermes-parser@0.33.3:
resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==}
+ highlight.js@11.11.1:
+ resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
+ engines: {node: '>=12.0.0'}
+
hoist-non-react-statics@2.5.5:
resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==}
@@ -9524,6 +9506,12 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-scroll-sync@1.0.2:
+ resolution: {integrity: sha512-uQTyayj/DbkejXFwsQI7o+5+pNC2uXJVsYpiGVOMKTF6NzIoE6cvEsw4NjTEKAX21iSgTJexWIE2WWyD3oBtsA==}
+ peerDependencies:
+ react: '>=18.0.0 <19.0.0'
+ react-dom: '>=18.0.0 <19.0.0'
+
react-test-renderer@19.2.4:
resolution: {integrity: sha512-Ttl5D7Rnmi6JGMUpri4UjB4BAN0FPs4yRDnu2XSsigCWOLm11o8GwRlVsh27ER+4WFqsGtrBuuv5zumUaRCmKw==}
peerDependencies:
@@ -11925,9 +11913,9 @@ snapshots:
'@codemirror/autocomplete@6.19.0':
dependencies:
'@codemirror/language': 6.11.3
- '@codemirror/state': 6.5.2
+ '@codemirror/state': 6.5.4
'@codemirror/view': 6.38.6
- '@lezer/common': 1.2.3
+ '@lezer/common': 1.5.1
'@codemirror/commands@6.10.2':
dependencies:
@@ -11943,35 +11931,10 @@ snapshots:
'@codemirror/view': 6.38.6
'@lezer/common': 1.2.3
- '@codemirror/lang-css@6.3.1':
+ '@codemirror/lang-json@6.0.2':
dependencies:
- '@codemirror/autocomplete': 6.19.0
'@codemirror/language': 6.11.3
- '@codemirror/state': 6.5.2
- '@lezer/common': 1.2.3
- '@lezer/css': 1.3.0
-
- '@codemirror/lang-html@6.4.11':
- dependencies:
- '@codemirror/autocomplete': 6.19.0
- '@codemirror/lang-css': 6.3.1
- '@codemirror/lang-javascript': 6.2.4
- '@codemirror/language': 6.11.3
- '@codemirror/state': 6.5.2
- '@codemirror/view': 6.38.6
- '@lezer/common': 1.2.3
- '@lezer/css': 1.3.0
- '@lezer/html': 1.3.12
-
- '@codemirror/lang-javascript@6.2.4':
- dependencies:
- '@codemirror/autocomplete': 6.19.0
- '@codemirror/language': 6.11.3
- '@codemirror/lint': 6.9.0
- '@codemirror/state': 6.5.2
- '@codemirror/view': 6.38.6
- '@lezer/common': 1.2.3
- '@lezer/javascript': 1.5.4
+ '@lezer/json': 1.0.3
'@codemirror/language@6.11.3':
dependencies:
@@ -11984,7 +11947,7 @@ snapshots:
'@codemirror/lint@6.9.0':
dependencies:
- '@codemirror/state': 6.5.2
+ '@codemirror/state': 6.5.4
'@codemirror/view': 6.38.6
crelt: 1.0.6
@@ -12142,8 +12105,6 @@ snapshots:
eslint: 9.39.3(jiti@2.6.1)
eslint-visitor-keys: 3.4.3
- '@eslint-community/regexpp@4.12.1': {}
-
'@eslint-community/regexpp@4.12.2': {}
'@eslint/config-array@0.21.1':
@@ -12499,12 +12460,6 @@ snapshots:
'@lezer/common@1.5.1': {}
- '@lezer/css@1.3.0':
- dependencies:
- '@lezer/common': 1.2.3
- '@lezer/highlight': 1.2.1
- '@lezer/lr': 1.4.2
-
'@lezer/highlight@1.2.1':
dependencies:
'@lezer/common': 1.2.3
@@ -12513,13 +12468,7 @@ snapshots:
dependencies:
'@lezer/common': 1.5.1
- '@lezer/html@1.3.12':
- dependencies:
- '@lezer/common': 1.2.3
- '@lezer/highlight': 1.2.1
- '@lezer/lr': 1.4.2
-
- '@lezer/javascript@1.5.4':
+ '@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
@@ -13680,38 +13629,38 @@ snapshots:
'@typescript-eslint/types': 8.57.0
eslint-visitor-keys: 5.0.1
- '@uiw/codemirror-extensions-basic-setup@4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
+ '@uiw/codemirror-extensions-basic-setup@4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.4)(@codemirror/view@6.38.6)':
dependencies:
'@codemirror/autocomplete': 6.19.0
'@codemirror/commands': 6.9.0
'@codemirror/language': 6.11.3
'@codemirror/lint': 6.9.0
'@codemirror/search': 6.5.11
- '@codemirror/state': 6.5.2
+ '@codemirror/state': 6.5.4
'@codemirror/view': 6.38.6
- '@uiw/codemirror-theme-github@4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
+ '@uiw/codemirror-theme-github@4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.4)(@codemirror/view@6.38.6)':
dependencies:
- '@uiw/codemirror-themes': 4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
+ '@uiw/codemirror-themes': 4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.4)(@codemirror/view@6.38.6)
transitivePeerDependencies:
- '@codemirror/language'
- '@codemirror/state'
- '@codemirror/view'
- '@uiw/codemirror-themes@4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)':
+ '@uiw/codemirror-themes@4.25.2(@codemirror/language@6.11.3)(@codemirror/state@6.5.4)(@codemirror/view@6.38.6)':
dependencies:
'@codemirror/language': 6.11.3
- '@codemirror/state': 6.5.2
+ '@codemirror/state': 6.5.4
'@codemirror/view': 6.38.6
- '@uiw/react-codemirror@4.25.2(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.6)(codemirror@6.0.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ '@uiw/react-codemirror@4.25.2(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.19.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.38.6)(codemirror@6.0.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.28.6
'@codemirror/commands': 6.9.0
- '@codemirror/state': 6.5.2
+ '@codemirror/state': 6.5.4
'@codemirror/theme-one-dark': 6.1.3
'@codemirror/view': 6.38.6
- '@uiw/codemirror-extensions-basic-setup': 4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)
+ '@uiw/codemirror-extensions-basic-setup': 4.25.2(@codemirror/autocomplete@6.19.0)(@codemirror/commands@6.9.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.0)(@codemirror/search@6.5.11)(@codemirror/state@6.5.4)(@codemirror/view@6.38.6)
codemirror: 6.0.2
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -13865,10 +13814,6 @@ snapshots:
dependencies:
acorn: 8.15.0
- acorn-jsx@5.3.2(acorn@8.15.0):
- dependencies:
- acorn: 8.15.0
-
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@@ -15052,11 +14997,11 @@ snapshots:
dom-helpers@3.4.0:
dependencies:
- '@babel/runtime': 7.28.4
+ '@babel/runtime': 7.28.6
dom-helpers@5.2.1:
dependencies:
- '@babel/runtime': 7.28.4
+ '@babel/runtime': 7.28.6
csstype: 3.1.3
dom-serializer@1.4.1:
@@ -15530,8 +15475,8 @@ snapshots:
eslint@9.39.3(jiti@2.6.1):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.3(jiti@2.6.1))
- '@eslint-community/regexpp': 4.12.1
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3(jiti@2.6.1))
+ '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
@@ -15578,8 +15523,8 @@ snapshots:
espree@10.4.0:
dependencies:
- acorn: 8.15.0
- acorn-jsx: 5.3.2(acorn@8.15.0)
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
eslint-visitor-keys: 4.2.1
espree@9.6.1:
@@ -16249,6 +16194,8 @@ snapshots:
dependencies:
hermes-estree: 0.33.3
+ highlight.js@11.11.1: {}
+
hoist-non-react-statics@2.5.5: {}
hono@4.12.4: {}
@@ -18854,6 +18801,11 @@ snapshots:
react-refresh@0.14.2: {}
+ react-scroll-sync@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
react-test-renderer@19.2.4(react@18.3.1):
dependencies:
react: 18.3.1
@@ -20062,7 +20014,7 @@ snapshots:
uncontrollable@7.2.1(react@18.3.1):
dependencies:
- '@babel/runtime': 7.28.4
+ '@babel/runtime': 7.28.6
'@types/react': 19.2.2
invariant: 2.2.4
react: 18.3.1