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
14 changes: 14 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,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where onblur and onchange when user leave editor events not firing correctly if a focusable element is clicked as change focus user action.

### Changed

- We changed Tab keyboard behavior to add indentation instead of exiting focus from editor.
- We changed `&nbsp;` mark for empty line in favor for `<br />` break tag instead.

### Added

- We added alt+F11 keyboard shortcut to do focus next, and alt+F10 to focus on toolbar.
- We added shift+enter keyboard shortcut to add `<br />` break tag.

## [4.10.0] - 2025-10-02

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/rich-text-web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mendix/rich-text-web",
"widgetName": "RichText",
"version": "4.10.0",
"version": "4.11.0",
"description": "Rich inline or toolbar text editing",
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
"license": "Apache-2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,14 @@ import { SET_FULLSCREEN_ACTION } from "../store/store";
import "../utils/customPluginRegisters";
import { FontStyleAttributor, formatCustomFonts } from "../utils/formats/fonts";
import "../utils/formats/quill-table-better/assets/css/quill-table-better.scss";
import QuillTableBetter from "../utils/formats/quill-table-better/quill-table-better";
import { RESIZE_MODULE_CONFIG } from "../utils/formats/resizeModuleConfig";
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 from "../utils/MxQuill";
import {
enterKeyKeyboardHandler,
exitFullscreenKeyboardHandler,
getIndentHandler,
gotoStatusBarKeyboardHandler,
gotoToolbarKeyboardHandler
} from "./CustomToolbars/toolbarHandlers";
import { useEmbedModal } from "./CustomToolbars/useEmbedModal";
import Dialog from "./ModalDialog/Dialog";
import MxUploader from "../utils/modules/uploader";

export interface EditorProps
extends Pick<RichTextContainerProps, "imageSource" | "imageSourceContent" | "enableDefaultUpload"> {
Expand Down Expand Up @@ -115,41 +109,20 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul
theme,
modules: {
keyboard: {
bindings: {
enter: {
key: "Enter",
handler: enterKeyKeyboardHandler
},
focusTab: {
key: "F10",
altKey: true,
handler: gotoToolbarKeyboardHandler
},
tab: {
key: "Tab",
handler: gotoStatusBarKeyboardHandler
},
escape: {
key: "Escape",
handler: exitFullscreenKeyboardHandler
},
...QuillTableBetter.keyboardBindings
}
bindings: getKeyboardBindings()
},
table: false,
"table-better": {
language: "en_US",
menus: ["column", "row", "merge", "table", "cell", "wrap", "copy", "delete", "grid"],
toolbarTable: !readOnly
},
toolbar
toolbar,
...getResizeModuleConfig(readOnly)
},
readOnly
};

if (!readOnly && options.modules) {
options.modules.resize = RESIZE_MODULE_CONFIG;
}
const quill = new MxQuill(editorContainer, options);
ref.current = quill;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { If } from "@mendix/widget-plugin-component-kit/If";
import { useDebounceWithStatus } from "@mendix/widget-plugin-hooks/useDebounceWithStatus";
import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
import classNames from "classnames";
import Quill, { Range } from "quill";
import Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import { CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState } from "react";
import { RichTextContainerProps } from "typings/RichTextProps";
import { EditorContext, EditorProvider } from "../store/EditorProvider";
import { useActionEvents } from "../store/useActionEvents";
import { updateLegacyQuillFormats } from "../utils/helpers";
import MendixTheme from "../utils/themes/mxTheme";
import { createPreset } from "./CustomToolbars/presets";
Expand Down Expand Up @@ -50,11 +51,9 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
} = props;

const globalState = useContext(EditorContext);

const isFirstLoad = useRef<boolean>(false);
const quillRef = useRef<Quill>(null);
const [isFocus, setIsFocus] = useState(false);
const editorValueRef = useRef<string>("");
const actionEvents = useActionEvents({ onBlur, onFocus, onChange, onChangeType, quill: quillRef?.current });
const toolbarRef = useRef<HTMLDivElement>(null);
const [wordCount, setWordCount] = useState(0);

Expand Down Expand Up @@ -128,34 +127,6 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [quillRef.current, stringAttribute, calculateCounts, onChange?.isExecuting]);

const onSelectionChange = useCallback(
(range: Range) => {
if (range) {
// User cursor is selecting
if (!isFocus) {
setIsFocus(true);
executeAction(onFocus);
editorValueRef.current = quillRef.current?.getText() || "";
}
} else {
// Cursor not in the editor
if (isFocus) {
setIsFocus(false);
executeAction(onBlur);

if (onChangeType === "onLeave") {
if (editorValueRef.current !== quillRef.current?.getText()) {
executeAction(onChange);
}
}
}
}
(quillRef.current?.theme as MendixTheme).updatePicker(range);
},

[isFocus, onFocus, onBlur, onChange, onChangeType]
);

const toolbarId = `widget_${id.replaceAll(".", "_")}_toolbar`;
const shouldHideToolbar = (stringAttribute.readOnly && readOnlyStyle !== "text") || toolbarLocation === "hide";
const toolbarPreset = shouldHideToolbar ? [] : createPreset(props);
Expand All @@ -182,7 +153,8 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
}
}}
spellCheck={props.spellCheck}
tabIndex={tabIndex}
tabIndex={tabIndex ?? -1}
{...actionEvents}
>
<If condition={toolbarLocation === "auto"}>
<StickySentinel />
Expand Down Expand Up @@ -220,7 +192,6 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
}
toolbarId={shouldHideToolbar ? undefined : toolbarOptions ? toolbarOptions : toolbarId}
onTextChange={onTextChange}
onSelectionChange={onSelectionChange}
className={"widget-rich-text-container"}
readOnly={stringAttribute.readOnly}
key={`${toolbarId}_${stringAttribute.readOnly}`}
Expand All @@ -231,15 +202,19 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
formOrientation={formOrientation}
/>
</div>
<If condition={enableStatusBar}>
<div className="widget-rich-text-footer" tabIndex={-1}>

<div
className={classNames("widget-rich-text-footer", { "hide-status-bar": !enableStatusBar })}
tabIndex={-1}
>
<If condition={enableStatusBar}>
<span>
<span>{wordCount}</span>
<span>{` ${statusBarContent === "wordCount" ? "word" : "character"}`}</span>
<span>{wordCount === 1 ? "" : "s"}</span>
</span>
</div>
</If>
</If>
</div>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggableWidgets/rich-text-web/src/package.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://www.mendix.com/package/1.0/">
<clientModule name="RichText" version="4.10.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<clientModule name="RichText" version="4.11.0" xmlns="http://www.mendix.com/clientModule/1.0/">
<widgetFiles>
<widgetFile path="RichText.xml" />
</widgetFiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { executeAction } from "@mendix/widget-plugin-platform/framework/execute-action";
import Quill from "quill";
import { FocusEvent, useMemo, useRef } from "react";
import { RichTextContainerProps } from "typings/RichTextProps";

type UseActionEventsReturnValue = {
onFocus: (e: FocusEvent) => void;
onBlur: (e: FocusEvent) => void;
};

interface useActionEventsProps
extends Pick<RichTextContainerProps, "onFocus" | "onBlur" | "onChange" | "onChangeType"> {
quill?: Quill | null;
}

function isInternalTarget(
currentTarget: EventTarget & Element,
relatedTarget: (EventTarget & Element) | null
): boolean | undefined {
return (
currentTarget?.contains(relatedTarget) ||
currentTarget?.ownerDocument.querySelector(".widget-rich-text-modal-body")?.contains(relatedTarget)
);
}

export function useActionEvents(props: useActionEventsProps): UseActionEventsReturnValue {
const editorValueRef = useRef<string>("");
return useMemo(() => {
return {
onFocus: (e: FocusEvent): void => {
const { relatedTarget, currentTarget } = e;
if (!isInternalTarget(currentTarget, relatedTarget)) {
executeAction(props.onFocus);
editorValueRef.current = props.quill?.getText() || "";
}
},
onBlur: (e: FocusEvent): void => {
const { relatedTarget, currentTarget } = e;
if (!isInternalTarget(currentTarget, relatedTarget)) {
executeAction(props.onBlur);
if (props.onChangeType === "onLeave") {
if (props.quill) {
// validate if the text really changed
const currentText = props.quill.getText();
if (currentText !== editorValueRef.current) {
executeAction(props.onChange);
editorValueRef.current = currentText;
}
} else {
executeAction(props.onChange);
}
}
}
}
};
}, [props.onFocus, props.quill, props.onBlur, props.onChangeType, props.onChange]);
}
11 changes: 9 additions & 2 deletions packages/pluggableWidgets/rich-text-web/src/ui/RichText.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ $rte-brand-primary: #264ae5;
display: none;
}

.widget-rich-text-footer {
&-footer {
align-items: center;
border-top: 1px solid var(--border-color-default, $rte-border-color-default);
color: var(--font-color-detail);
display: flex;
flex: 0 0 auto;
Expand All @@ -101,6 +100,14 @@ $rte-brand-primary: #264ae5;
position: relative;
text-transform: none;
justify-content: end;

&:not(.hide-status-bar) {
border-top: 1px solid var(--border-color-default, $rte-border-color-default);
}

&:focus-within {
border-top: 1px solid var(--form-input-border-focus-color, var(--brand-primary, $rte-brand-primary));
}
}

&.editor-readPanel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function convertHTML(blot: Blot, index: number, length: number, isRoot = false):
}
if (blot instanceof TextBlot) {
const escapedText = escapeText(blot.value().slice(index, index + length));
return escapedText.replaceAll(" ", "&nbsp;");
return escapedText;
}
if (blot instanceof ParentBlot) {
// TODO fix API
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CustomListItem from "./formats/customList";
import CustomLink from "./formats/link";
import CustomVideo from "./formats/video";
import CustomImage from "./formats/image";
import SoftBreak from "./formats/softBreak";
import Button from "./formats/button";
import { Attributor } from "parchment";
const direction = Quill.import("attributors/style/direction") as Attributor;
Expand All @@ -18,6 +19,7 @@ import MxUploader from "./modules/uploader";
import MxBlock from "./formats/block";
import CustomClipboard from "./modules/clipboard";
import { WhiteSpaceStyle } from "./formats/whiteSpace";

class Empty {
doSomething(): string {
return "";
Expand All @@ -33,6 +35,7 @@ Quill.register(WhiteSpaceStyle, true);
Quill.register(CustomLink, 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ class MxBlock extends Block {
// quill return empty paragraph when there is no content (just empty line)
// to preserve the line breaks, we add empty space
if (this.domNode.childElementCount === 1 && this.domNode.children[0] instanceof HTMLBRElement) {
return this.domNode.outerHTML.replace(/<br>/g, "&nbsp;");
return this.domNode.outerHTML.replace(/<br>/g, "<br />");
} else if (this.domNode.childElementCount === 0 && this.domNode.textContent?.trim() === "") {
this.domNode.innerHTML = "&nbsp;";
this.domNode.innerHTML = "<br />";
return this.domNode.outerHTML;
} else {
return this.domNode.outerHTML;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ export const RESIZE_MODULE_CONFIG = {
}
}
};

export function getResizeModuleConfig(isReadOnly?: boolean): Record<string, unknown> | undefined {
if (isReadOnly) {
return {};
}
return { resize: RESIZE_MODULE_CONFIG };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// import { BlockEmbed } from "quill/blots/block";
import { EmbedBlot } from "parchment";
/**
* custom video link handler, allowing width and height config
*/
class SoftBreak extends EmbedBlot {
static create(_value: unknown): Element {
const node = super.create() as HTMLElement;
return node;
}
}

// SoftBreak.scope = Scope.INLINE_BLOT;
SoftBreak.blotName = "softbreak";
SoftBreak.tagName = "BR";

export default SoftBreak;
Loading
Loading