Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/pluggableWidgets/rich-text-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 3 additions & 5 deletions packages/pluggableWidgets/rich-text-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/rich-text-web/src/RichText.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@
<enumerationValue key="characterCountHtml">Character count (including HTML)</enumerationValue>
</enumerationValues>
</property>
<property key="styleDataFormat" type="enumeration" defaultValue="inline">
<caption>Style data format</caption>
<description>Choose how to render styling attribute in HTML</description>
<enumerationValues>
<enumerationValue key="inline">inline</enumerationValue>
<enumerationValue key="class">class</enumerationValue>
</enumerationValues>
</property>
</propertyGroup>
</propertyGroup>
<propertyGroup caption="Custom toolbar">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ describe("Rich Text", () => {
customFonts: [],
enableDefaultUpload: true,
formOrientation: "vertical",
linkValidation: true
linkValidation: true,
styleDataFormat: "inline"
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -68,7 +69,7 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul
customViewCodeHandler,
customImageUploadHandler
} = useEmbedModal(ref, props);
const customIndentHandler = getIndentHandler(ref);
const customIndentHandler = mxOptions.styleDataFormat === "inline" ? getIndentHandler(ref) : undefined;

// quill instance is not changing, thus, the function reference has to stays.
useLayoutEffect(() => {
Expand Down Expand Up @@ -133,7 +134,8 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul

const quill = new MxQuill(editorContainer, options);
ref.current = quill;
quill.registerCustomModules(mxOptions);
quill.setStyleDataFormat(mxOptions.styleDataFormat);
registerCustomFormats(mxOptions);

const delta = quill.clipboard.convert({ html: defaultValue ?? "" });
quill.updateContents(delta, Quill.sources.SILENT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef
import { RichTextContainerProps } from "typings/RichTextProps";
import { EditorContext, EditorProvider } from "../store/EditorProvider";
import { useActionEvents } from "../store/useActionEvents";
import { type MxQuillModulesOptions } from "../utils/formats";
import MendixTheme from "../utils/themes/mxTheme";
import { MxQuillModulesOptions } from "../utils/MxQuill";
import { createPreset } from "./CustomToolbars/presets";
import Editor from "./Editor";
import { StickySentinel } from "./StickySentinel";
Expand Down Expand Up @@ -193,6 +193,7 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
key={`${toolbarId}_${stringAttribute.readOnly}`}
options={
{
styleDataFormat: props.styleDataFormat,
fonts: props.customFonts,
links: {
validate: props.linkValidation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,59 @@
width: fit-content;
min-width: 50vw;

.code-input {
.code-editor-container {
width: 100%;
}

.code-editor {
min-height: 50vh;
margin: 10px;
padding: 10px;
border: 0;
width: calc(100% - 32px);
font-family: monospace;
line-height: var(--form-input-line-height, 1.2em);
font-size: var(--font-size-default, 14px);
overflow: auto;
white-space: pre;
&-container {
max-height: 70vh;
overflow: hidden;
}
}

.cm-editor {
width: 100%;
.code-input,
.code-preview {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.code-input {
z-index: 1;
color: transparent;
background: transparent;
caret-color: var(--brand-primary, #264ae5);
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-all;
resize: none;
}
.code-preview {
z-index: 0;
pointer-events: none;

code {
padding: unset;
overflow: unset;
background-color: transparent;
}
}
.code-buffer {
opacity: 0;
background-color: transparent;
}
}

&.image-dialog {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ReactElement, useCallback, useState } from "react";
import { type viewCodeConfigType } from "../../utils/formats";
import { DialogBody, DialogContent, DialogFooter, DialogHeader, FormControl } from "./DialogContent";
import CodeMirror, { ViewUpdate } from "@uiw/react-codemirror";
import { html } from "@codemirror/lang-html";
import { githubLight } from "@uiw/codemirror-theme-github";
import { EditorView } from "codemirror";
import hljs from "highlight.js";
import beautify from "js-beautify";
import { ReactElement, useCallback, useEffect, useRef, useState } from "react";
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
import "highlight.js/styles/github.css";
import { DialogBody, DialogContent, DialogFooter, DialogHeader, FormControl } from "./DialogContent";
import { type viewCodeConfigType } from "../../utils/formats";

export interface ViewCodeDialogProps {
currentCode?: string;
Expand All @@ -28,14 +27,28 @@ const BEAUTIFY_OPTIONS: beautify.HTMLBeautifyOptions = {

export default function ViewCodeDialog(props: ViewCodeDialogProps): ReactElement {
const { onSubmit, onClose, currentCode, formOrientation } = props;
const codeRef = useRef<HTMLElement>(null);
const [formState, setFormState] = useState({
// temporarily change tab characters to em space to avoid beautify removing them
src:
beautify.html(currentCode?.replace(/\t/g, "&emsp;") ?? "", BEAUTIFY_OPTIONS)?.replace(/&emsp;/g, "\t") || ""
});
const onCodeChange = useCallback((value: string, _viewUpdate: ViewUpdate) => {
setFormState({ ...formState, src: value });
}, []);

useEffect(() => {
const codeElement = codeRef.current;
hljs.highlightAll();
return () => {
if (codeElement) {
delete codeElement.dataset.highlighted;
}
};
}, [formState]);
const onCodeChange = useCallback(
(value: string) => {
setFormState({ ...formState, src: value });
},
[formState]
);

return (
<DialogContent className={"view-code-dialog"} formOrientation={formOrientation}>
Expand All @@ -45,15 +58,35 @@ export default function ViewCodeDialog(props: ViewCodeDialogProps): ReactElement
<label>Source Code</label>
</div>
<FormControl label="Code input" formOrientation={props.formOrientation} inputId="rich-text-code-input">
<CodeMirror
className="form-control mx-textarea-input mx-textarea-noresize code-input"
value={formState.src}
extensions={[EditorView.lineWrapping, html()]}
onChange={onCodeChange}
basicSetup
theme={githubLight}
maxHeight="70vh"
/>
<ScrollSync>
<div className="code-editor-container">
<ScrollSyncPane>
<textarea
spellCheck={false}
value={formState.src}
id="rich-text-code-input-buffer"
disabled
className="code-editor code-buffer"
/>
</ScrollSyncPane>
<ScrollSyncPane>
<textarea
spellCheck={false}
value={formState.src}
onChange={e => onCodeChange(e.target.value)}
className="code-input code-editor"
id="rich-text-code-input"
/>
</ScrollSyncPane>
<ScrollSyncPane>
<pre className="code-preview code-editor" aria-hidden="true">
<code ref={codeRef} className="language-html">
{formState.src}
</code>
</pre>
</ScrollSyncPane>
</div>
</ScrollSync>
</FormControl>
<DialogFooter onSubmit={() => onSubmit(formState)} onClose={onClose}></DialogFooter>
</DialogBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use "RichTextIcons";
@use "RichTextFormatStyle";

$rte-border-color-default: #ced0d3;
$rte-gray-ligher: #f8f8f8;
Expand Down Expand Up @@ -170,4 +171,9 @@ $rte-brand-primary: #264ae5;
.flexcontainer.flex-column {
overflow: visible;
}

VIDEO,
IFRAME {
pointer-events: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@import "variables";

$color: #3d1466;
.widget-rich-text {
@each $name, $font in $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;
}
}
}
72 changes: 72 additions & 0 deletions packages/pluggableWidgets/rich-text-web/src/ui/variables.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
$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;

$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;
}

$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
);
Loading
Loading