Skip to content

Commit f7d16ca

Browse files
author
gjulivan
committed
chore: updated version of rich text with tiptap
1 parent 34eccd5 commit f7d16ca

211 files changed

Lines changed: 17314 additions & 12473 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/pluggableWidgets/rich-text-web/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1010

1111
- We fixed a security vulnerability (CVE-2026-13149).
1212

13+
### Added
14+
15+
- We added new configuration to allow users to use class names instead of inline styling in generated HTML to support strict CSP.
16+
17+
### Fixed
18+
19+
- We fixed Tiptap duplicate extension warnings for `link`, `textStyle`, `underline`, and `textDirection` by properly configuring StarterKit, removing redundant extension registrations in TextColorClass, memoizing the extensions array to prevent re-registration on component re-renders, and disabling the core TextDirection extension that conflicts with our custom implementation.
20+
21+
- We fixed an issue where empty editor content was saving as `<p></p>` instead of an empty string, which incorrectly passed required field validation. Empty content now correctly saves as `""`, ensuring proper validation behavior. Note: This is a breaking change for forms that were relying on the incorrect behavior - required RichText fields will now correctly reject empty content.
22+
23+
- We fixed an issue where the editor pasting back the whole sentence instead of the single copied word
24+
25+
### Changed
26+
27+
- 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.
28+
1329
## [4.12.0] - 2026-04-22
1430

1531
### Added

packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect, test } from "@mendix/run-e2e/fixtures";
22
import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers";
33

44
test.describe("RichText", () => {
5+
test.describe.configure({ mode: "serial" });
56
test("compares with a screenshot baseline and checks if inline basic mode are rendered as expected", async ({
67
page
78
}) => {
@@ -115,6 +116,37 @@ test.describe("RichText", () => {
115116
await expect(page.locator(".mx-name-richText6")).toHaveScreenshot(`readOnlyModeReadPanel.png`);
116117
});
117118

119+
test("compares with a screenshot baseline and checks if class mode editor is rendered as expected", async ({
120+
page
121+
}) => {
122+
await page.goto("/p/classmode");
123+
await waitForMendixApp(page);
124+
await expect(page.locator(".mx-name-richText1")).toBeVisible();
125+
await expect(page.locator(".mx-name-richText1")).toHaveScreenshot(`classModeEditor.png`, { threshold: 0.4 });
126+
});
127+
128+
test("checks that class mode editor output uses CSS classes instead of inline styles", async ({ page }) => {
129+
await page.goto("/p/classmode");
130+
await waitForMendixApp(page);
131+
const html = await page.locator(".mx-name-richText1 .ql-editor").innerHTML();
132+
expect(html).toMatch(/class="ql-color-/);
133+
expect(html).toMatch(/class="ql-bg-/);
134+
expect(html).toMatch(/class="ql-indent-/);
135+
expect(html).toMatch(/data-style-format="class"/);
136+
expect(html).not.toMatch(/style="color:/);
137+
expect(html).not.toMatch(/style="background-color:/);
138+
expect(html).not.toMatch(/style="padding-left:/);
139+
});
140+
141+
test("compares with a screenshot baseline of the View/Edit Code dialog in class mode", async ({ page }) => {
142+
await page.goto("/p/classmode");
143+
await waitForMendixApp(page);
144+
await page.click(".mx-name-richText1 .ql-toolbar button.ql-view-code");
145+
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
146+
`classModeViewCodeDialog.png`
147+
);
148+
});
149+
118150
test("compares with a screenshot for rich text inside modal popup layout", async ({ page }) => {
119151
await page.goto("/");
120152
await waitForMendixApp(page);
@@ -134,4 +166,40 @@ test.describe("RichText", () => {
134166
`richTextDialogInsidePopupEdit.png`
135167
);
136168
});
169+
170+
test("empty content persists as empty string, not <p></p>", async ({ page }) => {
171+
await page.goto("/");
172+
await waitForMendixApp(page);
173+
await page.click("text=Generate Data");
174+
await page.goto("/p/basic");
175+
await waitForMendixApp(page);
176+
177+
// Find the first editable rich text editor
178+
const editor = page.locator(".mx-name-richText1 .tiptap");
179+
await editor.scrollIntoViewIfNeeded();
180+
await expect(editor).toBeVisible();
181+
182+
// Click into the editor and clear all content
183+
await editor.click();
184+
await page.keyboard.press("Control+A");
185+
await page.keyboard.press("Backspace");
186+
187+
// Blur the editor to trigger save
188+
await page.keyboard.press("Tab");
189+
await page.waitForTimeout(500);
190+
191+
// The editor should now be empty (visual check)
192+
const textContent = await editor.textContent();
193+
expect(textContent?.trim() || "").toBe("");
194+
195+
// Reload the page to verify persistence
196+
await page.reload();
197+
await waitForMendixApp(page);
198+
199+
// The editor should still be empty after reload
200+
const reloadedEditor = page.locator(".mx-name-richText1 .tiptap");
201+
await expect(reloadedEditor).toBeVisible();
202+
const reloadedContent = await reloadedEditor.textContent();
203+
expect(reloadedContent?.trim() || "").toBe("");
204+
});
137205
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-06-05
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
## Context
2+
3+
The rich text editor uses TipTap, which already supports column widths via the `colwidth` attribute on table cells. TipTap's base `TableCell` extension includes this attribute, and the `TableBackgroundColor` extension reads it to generate `<colgroup>` elements for column sizing.
4+
5+
Currently:
6+
7+
- `colwidth` is an array of pixel values (e.g., `[150]` for single column, `[100, 150, 200]` for colspan)
8+
- TipTap has a `resizable: true` option that should enable drag-to-resize, but the custom `TableBackgroundColorNodeView` doesn't implement resize handles
9+
- Users can click column borders, which sets a width, but cannot drag to adjust
10+
- No UI control exists to set exact pixel widths
11+
12+
The existing cell configuration dropdown already has sections for background color, border color/style/width using a `ConfigurationSection` pattern.
13+
14+
## Goals / Non-Goals
15+
16+
**Goals:**
17+
18+
- Add a number input control to the cell configuration dropdown for setting column width
19+
- Support exact pixel values (25-1000px range)
20+
- Provide clear button to reset to auto width
21+
- Handle colspan cells by setting only the first column width
22+
- Follow existing configuration dropdown patterns and styling
23+
- Validate input and clamp to acceptable ranges
24+
25+
**Non-Goals:**
26+
27+
- Implementing drag-to-resize handles (would require significant NodeView work)
28+
- Supporting percentage or other CSS units (only pixels for now)
29+
- Setting widths for all columns in a colspan cell (only first column)
30+
- Syncing widths across all rows (only first row widths apply per TipTap's design)
31+
- Adding column width presets dropdown (keep it simple with number input only)
32+
33+
## Decisions
34+
35+
### Decision 1: Use number input type (not dropdown with presets)
36+
37+
**Rationale:** User requested "exact pixel values" in requirements. A number input provides precision without limiting users to preset sizes.
38+
39+
**Alternatives considered:**
40+
41+
- Dropdown with presets (Small/Medium/Large): Too restrictive, doesn't meet "exact values" requirement
42+
- Hybrid (input + preset buttons): Over-engineered for initial implementation
43+
- Text input: Number input provides better UX with built-in increment/decrement and validation
44+
45+
**Implementation:** Add new `"numberInput"` type to `ConfigurationSection` interface alongside existing `"colorPicker"` and `"dropdown"` types.
46+
47+
---
48+
49+
### Decision 2: Set only first column width for colspan cells
50+
51+
**Rationale:**
52+
53+
- User explicitly requested "set only first column" in requirements
54+
- Simplifies implementation and UX (no need for multi-column width editor)
55+
- Matches TipTap's array-based `colwidth` structure where `colwidth[0]` is the first column
56+
57+
**Alternatives considered:**
58+
59+
- Set all spanned columns to same width: Could surprise users if they had different widths set
60+
- Show per-column width array editor: Complex UI for edge case (most cells don't have colspan)
61+
- Disable control for colspan cells: Too restrictive
62+
63+
**Implementation:** `onChange` handler creates `colwidth` array with single value: `[width]`
64+
65+
---
66+
67+
### Decision 3: Use setCellAttribute command (not custom command)
68+
69+
**Rationale:**
70+
71+
- `setCellAttribute("colwidth", value)` should work because `colwidth` is in TipTap's base TableCell
72+
- Other cell properties (backgroundColor, borderColor, etc.) use `setCellAttribute` successfully
73+
- No need to define custom commands in extensions
74+
75+
**Alternatives considered:**
76+
77+
- Custom `setCellWidth` command in `TableCellBackgroundColor`: Unnecessary abstraction
78+
- Modify `TableCellBackgroundColor.addAttributes()` to re-declare colwidth: Already inherits via `...this.parent?.()`
79+
80+
**Implementation:** Direct call to `editor.chain().focus().setCellAttribute("colwidth", [width]).run()`
81+
82+
---
83+
84+
### Decision 4: Render clear button conditionally (when value exists)
85+
86+
**Rationale:**
87+
88+
- Provides explicit way to reset to auto width (null)
89+
- Visual indicator that a custom width is set
90+
- Follows common UI pattern (seen in search inputs, etc.)
91+
92+
**Alternatives considered:**
93+
94+
- Always show clear button: Clutters UI when empty
95+
- Use empty string to mean auto: Less explicit, could be confusing
96+
- Add separate "Auto" button: Redundant with clear functionality
97+
98+
**Implementation:** Render `×` button only when `currentValue !== null`
99+
100+
---
101+
102+
### Decision 5: Store null for auto width (not empty array or zero)
103+
104+
**Rationale:**
105+
106+
- TipTap treats `colwidth: null` as auto-sizing behavior
107+
- Consistent with how TipTap's base extensions work
108+
- `colwidth: []` or `colwidth: [0]` could cause layout issues
109+
110+
**Implementation:** When user clears input, call `setCellAttribute("colwidth", null)`
111+
112+
## Risks / Trade-offs
113+
114+
### Risk: Parent attribute inheritance might not work
115+
116+
If `TableCellBackgroundColor` doesn't properly inherit `colwidth` from base `TableCell`, `setCellAttribute` calls will fail silently.
117+
118+
**Mitigation:** The extension already uses `...this.parent?.()` in `addAttributes()`, which should preserve `colwidth`. If issues arise, explicitly re-declare `colwidth` in the extension's attributes.
119+
120+
---
121+
122+
### Risk: First-row-only behavior might confuse users
123+
124+
TipTap only uses `colwidth` from the first row of the table to generate `<colgroup>`. Setting widths on other rows has no effect.
125+
126+
**Mitigation:** Could add tooltip or helper text: "Note: Only first row column widths apply." For initial implementation, leave as-is since it matches TipTap's inherent behavior.
127+
128+
---
129+
130+
### Risk: Custom NodeView might interfere with colwidth updates
131+
132+
The `TableBackgroundColorNodeView` manually generates `<colgroup>` in `updateColgroup()`. If it's not reactive to `colwidth` changes, widths might not update visually.
133+
134+
**Mitigation:** The NodeView's `update()` method already calls `updateColgroup()` on node changes, so it should react to `colwidth` updates. Test thoroughly during implementation.
135+
136+
---
137+
138+
### Trade-off: No drag-to-resize (only manual input)
139+
140+
Users lose the convenience of dragging column borders to resize.
141+
142+
**Mitigation:** The number input provides precision that drag handles lack. If drag-to-resize is needed later, it would require integrating ProseMirror's `columnResizing` plugin into the custom NodeView (~200 lines of code). For now, manual input meets the stated requirements.
143+
144+
---
145+
146+
### Trade-off: Pixel-only units (no percentages)
147+
148+
Users cannot set responsive column widths using percentages.
149+
150+
**Mitigation:** Percentage support would require custom rendering logic (TipTap's `colwidth` is pixel-only). Tables in rich text editors typically use fixed layouts. Can be added later if users request it.
151+
152+
## Migration Plan
153+
154+
No migration needed. This is a purely additive feature:
155+
156+
- Existing tables without `colwidth` continue to work (auto width)
157+
- Existing tables with `colwidth` set (via TipTap's base functionality) display correctly
158+
- No data model changes or breaking API changes
159+
160+
Deployment:
161+
162+
1. Merge code changes
163+
2. Run build
164+
3. Deploy widget to Mendix project
165+
4. Feature is immediately available in cell configuration dropdown
166+
167+
Rollback:
168+
169+
- If issues arise, remove the new configuration section from `createCellConfigurationSections()`
170+
- No data corruption risk (colwidth attribute is standard TipTap)
171+
172+
## Open Questions
173+
174+
1. **Should we show the actual rendered column width vs. the set value?**
175+
- The set `colwidth` might differ from actual rendered width due to table layout constraints
176+
- For initial implementation: Show the set value only (simpler)
177+
- Can add "measured width" tooltip later if needed
178+
179+
2. **Should we prevent setting widths on non-first-row cells?**
180+
- Pro: Avoids confusion about why widths don't apply
181+
- Con: Restricts user control, might not match mental model
182+
- Decision: Allow setting on any cell (simpler), rely on TipTap's first-row behavior
183+
184+
3. **Should we add keyboard shortcuts for common widths?**
185+
- Out of scope for initial implementation
186+
- Can be added later if users request it
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## Why
2+
3+
Users need precise control over table column widths in the rich text editor. Currently, while TipTap supports column widths via the `colwidth` attribute, there's no UI control to set them. Users can only click column borders which sets widths without drag-to-resize capability, making it difficult to achieve desired layouts.
4+
5+
## What Changes
6+
7+
- Add a "Column Width" number input control to the cell configuration dropdown
8+
- Support setting exact pixel widths (25-1000px range)
9+
- Provide "Clear" button to reset to auto width
10+
- Handle colspan cells by setting only the first column width
11+
- Integrate with existing `setCellAttribute` command for `colwidth` attribute
12+
13+
## Capabilities
14+
15+
### New Capabilities
16+
17+
- `table-column-width-control`: UI control in cell configuration dropdown allowing users to set precise column widths in pixels, with validation, auto width support, and colspan handling
18+
19+
### Modified Capabilities
20+
21+
<!-- No existing spec requirements are changing - this is a new UI feature using existing TipTap colwidth infrastructure -->
22+
23+
## Impact
24+
25+
**Affected Files:**
26+
27+
- `src/components/toolbars/components/ConfigurationDropdown.tsx` - Add number input type support
28+
- `src/components/toolbars/components/ConfigurationDropdown.scss` - Add number input styling
29+
- `src/components/toolbars/ToolbarConfig.ts` - Update ConfigurationSection type definition
30+
- `src/components/toolbars/helpers/configurationHelpers.ts` - Add column width section
31+
32+
**User Impact:**
33+
34+
- Positive: Users gain precise control over table column widths via familiar number input UI
35+
- No breaking changes: Feature is additive, existing tables continue to work
36+
37+
**Technical Impact:**
38+
39+
- Uses existing TipTap `colwidth` attribute mechanism (no changes to data model)
40+
- No impact on existing table resize behavior (custom NodeView remains unchanged)
41+
- Follows established configuration dropdown patterns

0 commit comments

Comments
 (0)