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
16 changes: 16 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,22 @@ 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 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.

- 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.

- 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
68 changes: 68 additions & 0 deletions packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect, test } from "@mendix/run-e2e/fixtures";
import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers";

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
}) => {
Expand Down Expand Up @@ -115,6 +116,37 @@ test.describe("RichText", () => {
await expect(page.locator(".mx-name-richText6")).toHaveScreenshot(`readOnlyModeReadPanel.png`);
});

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 waitForMendixApp(page);
Expand All @@ -134,4 +166,40 @@ test.describe("RichText", () => {
`richTextDialogInsidePopupEdit.png`
);
});

test("empty content persists as empty string, not <p></p>", async ({ page }) => {
await page.goto("/");
await waitForMendixApp(page);
await page.click("text=Generate Data");
await page.goto("/p/basic");
await waitForMendixApp(page);

// Find the first editable rich text editor
const editor = page.locator(".mx-name-richText1 .tiptap");
await editor.scrollIntoViewIfNeeded();
await expect(editor).toBeVisible();

// Click into the editor and clear all content
await editor.click();
await page.keyboard.press("Control+A");
await page.keyboard.press("Backspace");

// Blur the editor to trigger save
await page.keyboard.press("Tab");
await page.waitForTimeout(500);

// The editor should now be empty (visual check)
const textContent = await editor.textContent();
expect(textContent?.trim() || "").toBe("");

// Reload the page to verify persistence
await page.reload();
await waitForMendixApp(page);

// The editor should still be empty after reload
const reloadedEditor = page.locator(".mx-name-richText1 .tiptap");
await expect(reloadedEditor).toBeVisible();
const reloadedContent = await reloadedEditor.textContent();
expect(reloadedContent?.trim() || "").toBe("");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-05
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
## Context

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.

Currently:

- `colwidth` is an array of pixel values (e.g., `[150]` for single column, `[100, 150, 200]` for colspan)
- TipTap has a `resizable: true` option that should enable drag-to-resize, but the custom `TableBackgroundColorNodeView` doesn't implement resize handles
- Users can click column borders, which sets a width, but cannot drag to adjust
- No UI control exists to set exact pixel widths

The existing cell configuration dropdown already has sections for background color, border color/style/width using a `ConfigurationSection` pattern.

## Goals / Non-Goals

**Goals:**

- Add a number input control to the cell configuration dropdown for setting column width
- Support exact pixel values (25-1000px range)
- Provide clear button to reset to auto width
- Handle colspan cells by setting only the first column width
- Follow existing configuration dropdown patterns and styling
- Validate input and clamp to acceptable ranges

**Non-Goals:**

- Implementing drag-to-resize handles (would require significant NodeView work)
- Supporting percentage or other CSS units (only pixels for now)
- Setting widths for all columns in a colspan cell (only first column)
- Syncing widths across all rows (only first row widths apply per TipTap's design)
- Adding column width presets dropdown (keep it simple with number input only)

## Decisions

### Decision 1: Use number input type (not dropdown with presets)

**Rationale:** User requested "exact pixel values" in requirements. A number input provides precision without limiting users to preset sizes.

**Alternatives considered:**

- Dropdown with presets (Small/Medium/Large): Too restrictive, doesn't meet "exact values" requirement
- Hybrid (input + preset buttons): Over-engineered for initial implementation
- Text input: Number input provides better UX with built-in increment/decrement and validation

**Implementation:** Add new `"numberInput"` type to `ConfigurationSection` interface alongside existing `"colorPicker"` and `"dropdown"` types.

---

### Decision 2: Set only first column width for colspan cells

**Rationale:**

- User explicitly requested "set only first column" in requirements
- Simplifies implementation and UX (no need for multi-column width editor)
- Matches TipTap's array-based `colwidth` structure where `colwidth[0]` is the first column

**Alternatives considered:**

- Set all spanned columns to same width: Could surprise users if they had different widths set
- Show per-column width array editor: Complex UI for edge case (most cells don't have colspan)
- Disable control for colspan cells: Too restrictive

**Implementation:** `onChange` handler creates `colwidth` array with single value: `[width]`

---

### Decision 3: Use setCellAttribute command (not custom command)

**Rationale:**

- `setCellAttribute("colwidth", value)` should work because `colwidth` is in TipTap's base TableCell
- Other cell properties (backgroundColor, borderColor, etc.) use `setCellAttribute` successfully
- No need to define custom commands in extensions

**Alternatives considered:**

- Custom `setCellWidth` command in `TableCellBackgroundColor`: Unnecessary abstraction
- Modify `TableCellBackgroundColor.addAttributes()` to re-declare colwidth: Already inherits via `...this.parent?.()`

**Implementation:** Direct call to `editor.chain().focus().setCellAttribute("colwidth", [width]).run()`

---

### Decision 4: Render clear button conditionally (when value exists)

**Rationale:**

- Provides explicit way to reset to auto width (null)
- Visual indicator that a custom width is set
- Follows common UI pattern (seen in search inputs, etc.)

**Alternatives considered:**

- Always show clear button: Clutters UI when empty
- Use empty string to mean auto: Less explicit, could be confusing
- Add separate "Auto" button: Redundant with clear functionality

**Implementation:** Render `×` button only when `currentValue !== null`

---

### Decision 5: Store null for auto width (not empty array or zero)

**Rationale:**

- TipTap treats `colwidth: null` as auto-sizing behavior
- Consistent with how TipTap's base extensions work
- `colwidth: []` or `colwidth: [0]` could cause layout issues

**Implementation:** When user clears input, call `setCellAttribute("colwidth", null)`

## Risks / Trade-offs

### Risk: Parent attribute inheritance might not work

If `TableCellBackgroundColor` doesn't properly inherit `colwidth` from base `TableCell`, `setCellAttribute` calls will fail silently.

**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.

---

### Risk: First-row-only behavior might confuse users

TipTap only uses `colwidth` from the first row of the table to generate `<colgroup>`. Setting widths on other rows has no effect.

**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.

---

### Risk: Custom NodeView might interfere with colwidth updates

The `TableBackgroundColorNodeView` manually generates `<colgroup>` in `updateColgroup()`. If it's not reactive to `colwidth` changes, widths might not update visually.

**Mitigation:** The NodeView's `update()` method already calls `updateColgroup()` on node changes, so it should react to `colwidth` updates. Test thoroughly during implementation.

---

### Trade-off: No drag-to-resize (only manual input)

Users lose the convenience of dragging column borders to resize.

**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.

---

### Trade-off: Pixel-only units (no percentages)

Users cannot set responsive column widths using percentages.

**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.

## Migration Plan

No migration needed. This is a purely additive feature:

- Existing tables without `colwidth` continue to work (auto width)
- Existing tables with `colwidth` set (via TipTap's base functionality) display correctly
- No data model changes or breaking API changes

Deployment:

1. Merge code changes
2. Run build
3. Deploy widget to Mendix project
4. Feature is immediately available in cell configuration dropdown

Rollback:

- If issues arise, remove the new configuration section from `createCellConfigurationSections()`
- No data corruption risk (colwidth attribute is standard TipTap)

## Open Questions

1. **Should we show the actual rendered column width vs. the set value?**
- The set `colwidth` might differ from actual rendered width due to table layout constraints
- For initial implementation: Show the set value only (simpler)
- Can add "measured width" tooltip later if needed

2. **Should we prevent setting widths on non-first-row cells?**
- Pro: Avoids confusion about why widths don't apply
- Con: Restricts user control, might not match mental model
- Decision: Allow setting on any cell (simpler), rely on TipTap's first-row behavior

3. **Should we add keyboard shortcuts for common widths?**
- Out of scope for initial implementation
- Can be added later if users request it
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Why

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.

## What Changes

- Add a "Column Width" number input control to the cell configuration dropdown
- Support setting exact pixel widths (25-1000px range)
- Provide "Clear" button to reset to auto width
- Handle colspan cells by setting only the first column width
- Integrate with existing `setCellAttribute` command for `colwidth` attribute

## Capabilities

### New Capabilities

- `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

### Modified Capabilities

<!-- No existing spec requirements are changing - this is a new UI feature using existing TipTap colwidth infrastructure -->

## Impact

**Affected Files:**

- `src/components/toolbars/components/ConfigurationDropdown.tsx` - Add number input type support
- `src/components/toolbars/components/ConfigurationDropdown.scss` - Add number input styling
- `src/components/toolbars/ToolbarConfig.ts` - Update ConfigurationSection type definition
- `src/components/toolbars/helpers/configurationHelpers.ts` - Add column width section

**User Impact:**

- Positive: Users gain precise control over table column widths via familiar number input UI
- No breaking changes: Feature is additive, existing tables continue to work

**Technical Impact:**

- Uses existing TipTap `colwidth` attribute mechanism (no changes to data model)
- No impact on existing table resize behavior (custom NodeView remains unchanged)
- Follows established configuration dropdown patterns
Loading
Loading