Skip to content

Commit 868d0fb

Browse files
committed
chore: updated version of rich text with tiptap
1 parent 7b57e49 commit 868d0fb

25 files changed

Lines changed: 1684 additions & 217 deletions

packages/pluggableWidgets/rich-text-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mendix/rich-text-web",
33
"widgetName": "RichText",
4-
"version": "4.12.0",
4+
"version": "4.13.0",
55
"description": "Rich inline or toolbar text editing",
66
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
77
"license": "Apache-2.0",

packages/pluggableWidgets/rich-text-web/src/RichText.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
</propertyGroup>
1818
<propertyGroup caption="General">
1919
<systemProperty key="Label" />
20-
<property key="enableStatusBar" type="boolean" defaultValue="true">
20+
<property key="enableStatusBar" type="boolean" defaultValue="false">
2121
<caption>Enable status bar</caption>
22-
<description />
22+
<description>Show a status bar at the bottom of the editor displaying document metrics</description>
2323
</property>
2424
</propertyGroup>
2525
<propertyGroup caption="Toolbar">

packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { TextColorClass } from "../extensions/TextColorClass";
3131
import { TextDirection } from "../extensions/TextDirection";
3232
import { TextHighlightClass } from "../extensions/TextHighlightClass";
3333
import { ConfirmDialog } from "./toolbars/components/ConfirmDialog";
34+
import { ToolbarGroupsConfig } from "./toolbars/ToolbarConfig";
35+
import { PresetEnum, ToolbarConfigEnum, AdvancedConfigType } from "../../typings/RichTextProps";
3436

3537
export interface EditorProps {
3638
defaultValue?: string;
@@ -40,6 +42,10 @@ export interface EditorProps {
4042
showToolbar?: boolean;
4143
styleDataFormat?: "inline" | "class";
4244
imageSourceContent?: ReactNode;
45+
preset?: PresetEnum;
46+
toolbarConfig?: ToolbarConfigEnum;
47+
toolbarGroups?: ToolbarGroupsConfig;
48+
advancedConfig?: AdvancedConfigType[];
4349
}
4450

4551
export interface EditorHandle {
@@ -55,9 +61,22 @@ interface EditorInnerProps {
5561
readOnly: boolean;
5662
className?: string;
5763
imageSourceContent?: ReactNode;
64+
preset?: PresetEnum;
65+
toolbarConfig?: ToolbarConfigEnum;
66+
toolbarGroups?: ToolbarGroupsConfig;
67+
advancedConfig?: AdvancedConfigType[];
5868
}
5969

60-
function EditorInner({ showToolbar, readOnly, className, imageSourceContent }: EditorInnerProps): ReactElement {
70+
function EditorInner({
71+
showToolbar,
72+
readOnly,
73+
className,
74+
imageSourceContent,
75+
preset,
76+
toolbarConfig,
77+
toolbarGroups,
78+
advancedConfig
79+
}: EditorInnerProps): ReactElement {
6180
const { editor, codeViewState, codeViewDispatch } = useCurrentEditor();
6281
const textareaRef = useRef<HTMLTextAreaElement>(null);
6382

@@ -80,7 +99,15 @@ function EditorInner({ showToolbar, readOnly, className, imageSourceContent }: E
8099
return (
81100
<>
82101
<div className="tiptap-wrapper">
83-
{showToolbar && !readOnly && <Toolbar imageSourceContent={imageSourceContent} />}
102+
{showToolbar && !readOnly && (
103+
<Toolbar
104+
imageSourceContent={imageSourceContent}
105+
preset={preset}
106+
toolbarConfig={toolbarConfig}
107+
toolbarGroups={toolbarGroups}
108+
advancedConfig={advancedConfig}
109+
/>
110+
)}
84111
{codeViewState.isCodeView ? (
85112
<textarea
86113
ref={textareaRef}
@@ -117,7 +144,11 @@ const Editor = forwardRef<EditorHandle, EditorProps>((props, ref) => {
117144
className,
118145
showToolbar = true,
119146
styleDataFormat = "inline",
120-
imageSourceContent
147+
imageSourceContent,
148+
preset,
149+
toolbarConfig,
150+
toolbarGroups,
151+
advancedConfig
121152
} = props;
122153

123154
const extensions = [
@@ -212,6 +243,10 @@ const Editor = forwardRef<EditorHandle, EditorProps>((props, ref) => {
212243
readOnly={!!readOnly}
213244
className={className}
214245
imageSourceContent={imageSourceContent}
246+
preset={preset}
247+
toolbarConfig={toolbarConfig}
248+
toolbarGroups={toolbarGroups}
249+
advancedConfig={advancedConfig}
215250
/>
216251
</EditorContextProvider>
217252
);

packages/pluggableWidgets/rich-text-web/src/components/EditorWrapper.tsx

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,40 @@
1-
import { ReactElement, useRef } from "react";
1+
import { ReactElement, useRef, useState, useEffect } from "react";
22
import { RichTextContainerProps } from "typings/RichTextProps";
33
import { useDebounceWithStatus } from "@mendix/widget-plugin-hooks/useDebounceWithStatus";
44
import Editor, { EditorHandle } from "./Editor";
5+
import { StatusBar, StatusBarMetricType } from "./StatusBar";
56

67
export interface EditorWrapperProps extends RichTextContainerProps {
78
className?: string;
89
}
910

1011
function EditorWrapper(props: EditorWrapperProps): ReactElement {
11-
const { stringAttribute, className, styleDataFormat, imageSourceContent } = props;
12+
const {
13+
stringAttribute,
14+
className,
15+
styleDataFormat,
16+
imageSourceContent,
17+
preset,
18+
toolbarConfig,
19+
history,
20+
fontStyle,
21+
fontScript,
22+
list,
23+
indent,
24+
embed,
25+
align,
26+
code,
27+
fontColor,
28+
header,
29+
view,
30+
remove,
31+
tableBetter,
32+
advancedConfig,
33+
enableStatusBar,
34+
statusBarContent
35+
} = props;
1236
const editorRef = useRef<EditorHandle>(null);
37+
const [editorText, setEditorText] = useState<string>("");
1338

1439
const [setAttributeValueDebounce] = useDebounceWithStatus(
1540
(html?: string) => {
@@ -27,18 +52,67 @@ function EditorWrapper(props: EditorWrapperProps): ReactElement {
2752
}
2853
};
2954

55+
// Update editor text for status bar when editor content changes
56+
useEffect(() => {
57+
if (editorRef.current && enableStatusBar) {
58+
const text = editorRef.current.getText();
59+
setEditorText(text);
60+
}
61+
}, [stringAttribute.value, enableStatusBar]);
62+
63+
// Determine status bar content based on metric type
64+
const statusBarContentValue = (() => {
65+
if (!enableStatusBar) return "";
66+
67+
switch (statusBarContent) {
68+
case "wordCount":
69+
case "characterCount":
70+
return editorText;
71+
case "characterCountHtml":
72+
return stringAttribute.value || "";
73+
default:
74+
return editorText;
75+
}
76+
})();
77+
3078
return (
3179
<div className={className}>
3280
{stringAttribute.status === "available" && (
33-
<Editor
34-
ref={editorRef}
35-
defaultValue={stringAttribute.value}
36-
onUpdate={handleUpdate}
37-
readOnly={stringAttribute.readOnly}
38-
className="tiptap-editor"
39-
styleDataFormat={styleDataFormat}
40-
imageSourceContent={imageSourceContent}
41-
/>
81+
<>
82+
<Editor
83+
ref={editorRef}
84+
defaultValue={stringAttribute.value}
85+
onUpdate={handleUpdate}
86+
readOnly={stringAttribute.readOnly}
87+
className="tiptap-editor"
88+
styleDataFormat={styleDataFormat}
89+
imageSourceContent={imageSourceContent}
90+
preset={preset}
91+
toolbarConfig={toolbarConfig}
92+
toolbarGroups={{
93+
history,
94+
fontStyle,
95+
fontScript,
96+
list,
97+
indent,
98+
embed,
99+
align,
100+
code,
101+
fontColor,
102+
header,
103+
view,
104+
remove,
105+
tableBetter
106+
}}
107+
advancedConfig={advancedConfig}
108+
/>
109+
{enableStatusBar && (
110+
<StatusBar
111+
content={statusBarContentValue}
112+
metricType={statusBarContent as StatusBarMetricType}
113+
/>
114+
)}
115+
</>
42116
)}
43117
</div>
44118
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { ReactElement, useMemo } from "react";
2+
3+
export type StatusBarMetricType = "wordCount" | "characterCount" | "characterCountHtml";
4+
5+
export interface StatusBarProps {
6+
content: string;
7+
metricType: StatusBarMetricType;
8+
}
9+
10+
function getWordCount(text: string): number {
11+
if (!text || text.trim().length === 0) {
12+
return 0;
13+
}
14+
// Split on whitespace and filter out empty strings
15+
return text
16+
.trim()
17+
.split(/\s+/)
18+
.filter(word => word.length > 0).length;
19+
}
20+
21+
function getCharCount(text: string): number {
22+
if (!text) {
23+
return 0;
24+
}
25+
return text.length;
26+
}
27+
28+
function getHtmlCharCount(html: string): number {
29+
if (!html) {
30+
return 0;
31+
}
32+
return html.length;
33+
}
34+
35+
export function StatusBar({ content, metricType }: StatusBarProps): ReactElement {
36+
// Debounced metric calculation using useMemo
37+
// The metric updates only when content changes, providing built-in debouncing
38+
const metricValue = useMemo(() => {
39+
switch (metricType) {
40+
case "wordCount":
41+
return getWordCount(content);
42+
case "characterCount":
43+
return getCharCount(content);
44+
case "characterCountHtml":
45+
return getHtmlCharCount(content);
46+
default:
47+
return 0;
48+
}
49+
}, [content, metricType]);
50+
51+
const displayText = useMemo(() => {
52+
switch (metricType) {
53+
case "wordCount":
54+
return `Words: ${metricValue}`;
55+
case "characterCount":
56+
return `Characters: ${metricValue}`;
57+
case "characterCountHtml":
58+
return `Characters (HTML): ${metricValue}`;
59+
default:
60+
return "";
61+
}
62+
}, [metricType, metricValue]);
63+
64+
return (
65+
<div className="rich-text-status-bar">
66+
<span className="status-bar-text">{displayText}</span>
67+
</div>
68+
);
69+
}

packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.scss

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
.tiptap-toolbar {
22
display: flex;
33
flex-direction: column;
4-
border-bottom: 1px solid #ced0d3;
5-
background-color: #f8f8f8;
6-
border-radius: 4px 4px 0 0;
7-
84
border: unset;
95
box-shadow: var(--shadow-small, 0 2px 4px 0) var(--shadow-color-border, #00000014);
106

@@ -13,6 +9,13 @@
139
flex-wrap: wrap;
1410
gap: 4px;
1511
padding: 8px;
12+
box-shadow: var(--shadow-small, 0 2px 4px 0) var(--shadow-color-border, #00000014);
13+
14+
&:empty {
15+
height: 0;
16+
padding: 0;
17+
box-shadow: none;
18+
}
1619

1720
&.table-operations-row {
1821
max-height: 0;
@@ -110,15 +113,12 @@
110113
}
111114

112115
button {
113-
background: white;
114-
border: 1px solid #d0d0d0;
115-
border-radius: 3px;
116-
padding: 6px 10px;
116+
background: none;
117+
border: none;
117118
cursor: pointer;
118-
font-size: 14px;
119-
font-weight: 500;
120-
color: #333;
121-
min-width: 32px;
119+
height: 24px;
120+
padding: 3px 5px;
121+
width: 28px;
122122
transition: all 0.2s;
123123

124124
&:hover:not(:disabled) {

0 commit comments

Comments
 (0)