Skip to content

Commit 9f25ab8

Browse files
fix(super-editor): constrain images to table cell width in web layout (SD-2416) (#2680)
* fix(super-editor): constrain images to table cell width in web layout mode getMaxContentSize() returned an empty object in web layout mode, bypassing the table cell width constraint added in SD-1156. Move the cell check before the web layout early return so images inserted into a cell are resized to fit. * test: add web layout and Infinity height coverage for image cell constraint - Add tableHeader web layout test to Editor.webLayout.test.ts - Add getAllowedImageDimensions tests for height: Infinity (the web layout + table cell code path) - Add test for image fitting within constraints with Infinity height --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com>
1 parent 25a2705 commit 9f25ab8

3 files changed

Lines changed: 120 additions & 17 deletions

File tree

packages/super-editor/src/editors/v1/core/Editor.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,15 +2492,33 @@ export class Editor extends EventEmitter<EditorEventMap> {
24922492
* images are never wider than their containing cell.
24932493
*
24942494
* @returns Size object with width and height in pixels, or empty object if no page size.
2495-
* @note In web layout mode, returns empty object to skip content constraints.
2496-
* CSS max-width: 100% handles responsive display while preserving full resolution.
2495+
* @note In web layout mode, returns empty object to skip content constraints unless
2496+
* the cursor is inside a table cell, in which case the cell width is returned.
24972497
*/
24982498
getMaxContentSize(): { width?: number; height?: number } {
24992499
if (!this.converter) return {};
25002500

2501-
// In web layout mode: skip constraints, let CSS handle responsive sizing
2502-
// This preserves full image resolution while CSS max-width: 100% handles display
2501+
// When the cursor is inside a table cell, constrain width to the cell's content
2502+
// width so images inserted into a cell are never wider than that cell.
2503+
// This applies to both print and web layout modes.
2504+
let cellConstraintWidth = 0;
2505+
const { $head } = this.state.selection;
2506+
for (let d = $head.depth; d > 0; d--) {
2507+
const node = $head.node(d);
2508+
if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
2509+
cellConstraintWidth = getCellContentWidthPx(node);
2510+
break;
2511+
}
2512+
}
2513+
2514+
// In web layout mode there are no page dimensions. Return cell width constraint
2515+
// if inside a table cell, otherwise skip constraints entirely (CSS handles display).
25032516
if (this.isWebLayout()) {
2517+
if (cellConstraintWidth > 0) {
2518+
// Infinity lets getAllowedImageDimensions() skip the height check while
2519+
// still applying the width constraint (web layout has no page height).
2520+
return { width: cellConstraintWidth, height: Infinity };
2521+
}
25042522
return {};
25052523
}
25062524

@@ -2523,18 +2541,8 @@ export class Editor extends EventEmitter<EditorEventMap> {
25232541
const maxHeight = height * PIXELS_PER_INCH - topPx - bottomPx - MAX_HEIGHT_BUFFER_PX;
25242542
const maxWidth = width * PIXELS_PER_INCH - leftPx - rightPx - MAX_WIDTH_BUFFER_PX;
25252543

2526-
// When the cursor is inside a table cell, constrain width to the cell's content
2527-
// width so images inserted into a cell are never wider than that cell.
2528-
const { $head } = this.state.selection;
2529-
for (let d = $head.depth; d > 0; d--) {
2530-
const node = $head.node(d);
2531-
if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
2532-
const cellWidth = getCellContentWidthPx(node);
2533-
if (cellWidth > 0) {
2534-
return { width: cellWidth, height: maxHeight };
2535-
}
2536-
break;
2537-
}
2544+
if (cellConstraintWidth > 0) {
2545+
return { width: cellConstraintWidth, height: maxHeight };
25382546
}
25392547

25402548
return {

packages/super-editor/src/editors/v1/core/Editor.webLayout.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,12 @@ describe('Editor Web Layout Mode', () => {
187187
*/
188188
function makeEditor({
189189
ancestors,
190+
layout = 'print' as 'print' | 'web',
190191
pageSize = { width: 8.5, height: 11 },
191192
pageMargins = { top: 1, bottom: 1, left: 1, right: 1 },
192193
}: {
193194
ancestors: Array<{ type: { name: string }; attrs: Record<string, unknown> }>;
195+
layout?: 'print' | 'web';
194196
pageSize?: { width: number; height: number };
195197
pageMargins?: { top: number; bottom: number; left: number; right: number };
196198
}) {
@@ -201,7 +203,7 @@ describe('Editor Web Layout Mode', () => {
201203

202204
return {
203205
converter: { pageStyles: { pageSize, pageMargins } },
204-
options: { viewOptions: { layout: 'print' } },
206+
options: { viewOptions: { layout } },
205207
state: { selection: { $head } },
206208
isWebLayout() {
207209
return (this as any).options.viewOptions?.layout === 'web';
@@ -323,5 +325,86 @@ describe('Editor Web Layout Mode', () => {
323325

324326
expect(size.width).toBe(expectedWidth);
325327
});
328+
329+
describe('web layout mode', () => {
330+
it('constrains width to cell colwidth in web layout mode', () => {
331+
const editor = makeEditor({
332+
layout: 'web',
333+
ancestors: [
334+
{ type: { name: 'tableRow' }, attrs: {} },
335+
{ type: { name: 'tableCell' }, attrs: { colwidth: [200], cellMargins: null } },
336+
{ type: { name: 'paragraph' }, attrs: {} },
337+
],
338+
});
339+
340+
const size = Editor.prototype.getMaxContentSize.call(editor);
341+
342+
expect(size.width).toBe(200);
343+
// Web layout has no page height, so Infinity is used to skip the height check
344+
// while still letting getAllowedImageDimensions apply the width constraint.
345+
expect(size.height).toBe(Infinity);
346+
});
347+
348+
it('subtracts cell margins in web layout mode', () => {
349+
const editor = makeEditor({
350+
layout: 'web',
351+
ancestors: [
352+
{ type: { name: 'tableRow' }, attrs: {} },
353+
{
354+
type: { name: 'tableCell' },
355+
attrs: { colwidth: [300], cellMargins: { left: 20, right: 15 } },
356+
},
357+
{ type: { name: 'paragraph' }, attrs: {} },
358+
],
359+
});
360+
361+
const size = Editor.prototype.getMaxContentSize.call(editor);
362+
363+
expect(size.width).toBe(265); // 300 - 20 - 15
364+
expect(size.height).toBe(Infinity);
365+
});
366+
367+
it('returns empty object in web layout mode when not inside a table cell', () => {
368+
const editor = makeEditor({
369+
layout: 'web',
370+
ancestors: [{ type: { name: 'paragraph' }, attrs: {} }],
371+
});
372+
373+
const size = Editor.prototype.getMaxContentSize.call(editor);
374+
375+
expect(size).toEqual({});
376+
});
377+
378+
it('returns empty object in web layout mode when colwidth is empty', () => {
379+
const editor = makeEditor({
380+
layout: 'web',
381+
ancestors: [
382+
{ type: { name: 'tableRow' }, attrs: {} },
383+
{ type: { name: 'tableCell' }, attrs: { colwidth: [], cellMargins: null } },
384+
{ type: { name: 'paragraph' }, attrs: {} },
385+
],
386+
});
387+
388+
const size = Editor.prototype.getMaxContentSize.call(editor);
389+
390+
expect(size).toEqual({});
391+
});
392+
393+
it('constrains width for tableHeader in web layout mode', () => {
394+
const editor = makeEditor({
395+
layout: 'web',
396+
ancestors: [
397+
{ type: { name: 'tableRow' }, attrs: {} },
398+
{ type: { name: 'tableHeader' }, attrs: { colwidth: [250], cellMargins: { left: 10, right: 10 } } },
399+
{ type: { name: 'paragraph' }, attrs: {} },
400+
],
401+
});
402+
403+
const size = Editor.prototype.getMaxContentSize.call(editor);
404+
405+
expect(size.width).toBe(230); // 250 - 10 - 10
406+
expect(size.height).toBe(Infinity);
407+
});
408+
});
326409
});
327410
});

packages/super-editor/src/editors/v1/extensions/image/imageHelpers/basicHelpers.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ describe('image helper utilities', () => {
7171
expect(height).toBeLessThanOrEqual(300);
7272
});
7373

74+
it('constrains width while skipping height clamp when maxHeight is Infinity', () => {
75+
const { width, height } = getAllowedImageDimensions(1000, 800, () => ({ width: 200, height: Infinity }));
76+
expect(width).toBe(200);
77+
expect(height).toBe(160); // aspect ratio preserved: 1000/800 = 1.25, 200/1.25 = 160
78+
});
79+
80+
it('returns original dimensions when maxHeight is Infinity and image fits', () => {
81+
const { width, height } = getAllowedImageDimensions(100, 80, () => ({ width: 200, height: Infinity }));
82+
expect(width).toBe(100);
83+
expect(height).toBe(80);
84+
});
85+
7486
it('processing image returns resized base64 when no resize needed', async () => {
7587
const originalImage = globalThis.Image;
7688
class MockImage {

0 commit comments

Comments
 (0)