Skip to content
Merged
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
8 changes: 8 additions & 0 deletions packages/super-editor/src/core/Editor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,13 @@ export declare class Editor {
*/
can(): CanObject;

/**
* Get the maximum content size based on page dimensions and margins.
* When the cursor is inside a table cell, the max width is constrained to that
* cell's width so that newly inserted images are never wider than their containing cell.
* Returns empty object in web layout mode or when no page size is available.
*/
getMaxContentSize(): { width?: number; height?: number };

[key: string]: any;
}
37 changes: 35 additions & 2 deletions packages/super-editor/src/core/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ const PIXELS_PER_INCH = 96;
const MAX_HEIGHT_BUFFER_PX = 50;
const MAX_WIDTH_BUFFER_PX = 20;

/**
Comment thread
caio-pizzol marked this conversation as resolved.
* Given a table cell node, returns the total cell content width in pixels.
* Sums all colwidth values and subtracts left/right cell margins (padding).
*/
function getCellContentWidthPx(cellNode: PmNode): number {
const colwidth: number[] = cellNode.attrs?.colwidth ?? [];
const totalWidth = colwidth.reduce((sum: number, w: number) => sum + (w || 0), 0);
const margins = cellNode.attrs?.cellMargins;
const leftMargin = margins?.left ?? 0;
const rightMargin = margins?.right ?? 0;
return Math.max(totalWidth - leftMargin - rightMargin, 0);
}

/**
* Image storage structure used by the image extension
*/
Expand Down Expand Up @@ -2246,8 +2259,13 @@ export class Editor extends EventEmitter<EditorEventMap> {
}

/**
* Get the maximum content size based on page dimensions and margins
* @returns Size object with width and height in pixels, or empty object if no page size
* Get the maximum content size based on page dimensions and margins.
*
* When the cursor is inside a table cell, the max width is constrained to that
* cell's width (derived from `colwidth` minus cell margins) so that newly inserted
* images are never wider than their containing cell.
*
* @returns Size object with width and height in pixels, or empty object if no page size.
* @note In web layout mode, returns empty object to skip content constraints.
* CSS max-width: 100% handles responsive display while preserving full resolution.
*/
Expand Down Expand Up @@ -2278,6 +2296,21 @@ export class Editor extends EventEmitter<EditorEventMap> {
// All sizes are in inches so we multiply by PIXELS_PER_INCH to get pixels
const maxHeight = height * PIXELS_PER_INCH - topPx - bottomPx - MAX_HEIGHT_BUFFER_PX;
const maxWidth = width * PIXELS_PER_INCH - leftPx - rightPx - MAX_WIDTH_BUFFER_PX;

// When the cursor is inside a table cell, constrain width to the cell's content
// width so images inserted into a cell are never wider than that cell.
Comment thread
caio-pizzol marked this conversation as resolved.
const { $head } = this.state.selection;
for (let d = $head.depth; d > 0; d--) {
const node = $head.node(d);
if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
const cellWidth = getCellContentWidthPx(node);
if (cellWidth > 0) {
return { width: cellWidth, height: maxHeight };
}
break;
}
}

return {
width: maxWidth,
height: maxHeight,
Expand Down
149 changes: 148 additions & 1 deletion packages/super-editor/src/core/Editor.webLayout.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { Editor } from './Editor.js';
import { loadTestDataForEditorTests } from '@tests/helpers/helpers.js';
import { getStarterExtensions } from '@extensions/index.js';
Expand Down Expand Up @@ -177,4 +177,151 @@ describe('Editor Web Layout Mode', () => {
});
Comment thread
caio-pizzol marked this conversation as resolved.
});
});
describe('table cell context', () => {
/**
* Builds a minimal fake editor whose state.selection.$head walks up through
* ancestor nodes at the given depths. Each entry in `ancestors` becomes the
* node returned by $head.node(d) for d = ancestors.length down to 1.
*
* pageSize is in inches (matching the real converter shape).
*/
function makeEditor({
ancestors,
pageSize = { width: 8.5, height: 11 },
pageMargins = { top: 1, bottom: 1, left: 1, right: 1 },
}: {
ancestors: Array<{ type: { name: string }; attrs: Record<string, unknown> }>;
pageSize?: { width: number; height: number };
pageMargins?: { top: number; bottom: number; left: number; right: number };
}) {
const $head = {
depth: ancestors.length,
node: (d: number) => ancestors[d - 1],
};

return {
converter: { pageStyles: { pageSize, pageMargins } },
options: { viewOptions: { layout: 'print' } },
state: { selection: { $head } },
isWebLayout() {
return (this as any).options.viewOptions?.layout === 'web';
},
};
}

it('constrains width to cell colwidth when cursor is inside a tableCell', () => {
const editor = makeEditor({
ancestors: [
{ type: { name: 'tableRow' }, attrs: {} },
{ type: { name: 'tableCell' }, attrs: { colwidth: [200], cellMargins: null } },
{ type: { name: 'paragraph' }, attrs: {} },
],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(200);
// Height is still derived from the page dimensions
expect(size.height).toBeGreaterThan(0);
});

it('subtracts left and right cellMargins from the cell width', () => {
const editor = makeEditor({
ancestors: [
{ type: { name: 'tableRow' }, attrs: {} },
{
type: { name: 'tableCell' },
attrs: { colwidth: [300], cellMargins: { left: 20, right: 15 } },
},
{ type: { name: 'paragraph' }, attrs: {} },
],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(265); // 300 - 20 - 15
});

it('sums multiple colwidth values for spanned cells', () => {
const editor = makeEditor({
ancestors: [
{ type: { name: 'tableRow' }, attrs: {} },
{
type: { name: 'tableCell' },
attrs: { colwidth: [150, 150], cellMargins: null },
},
{ type: { name: 'paragraph' }, attrs: {} },
],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(300);
});

it('constrains width when cursor is inside a tableHeader', () => {
const editor = makeEditor({
ancestors: [
{ type: { name: 'tableRow' }, attrs: {} },
{ type: { name: 'tableHeader' }, attrs: { colwidth: [180], cellMargins: null } },
{ type: { name: 'paragraph' }, attrs: {} },
],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(180);
});

it('falls back to page content width when not inside a table cell', () => {
// Standard Letter page (8.5 × 11 in) with 1 in margins on each side
const PIXELS_PER_INCH = 96;
const MAX_WIDTH_BUFFER_PX = 20;
const expectedWidth = (8.5 - 1 - 1) * PIXELS_PER_INCH - MAX_WIDTH_BUFFER_PX; // 6.5 in content

const editor = makeEditor({
ancestors: [{ type: { name: 'paragraph' }, attrs: {} }],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(expectedWidth);
});

it('falls back to page content width when colwidth is empty', () => {
const PIXELS_PER_INCH = 96;
const MAX_WIDTH_BUFFER_PX = 20;
const expectedWidth = (8.5 - 1 - 1) * PIXELS_PER_INCH - MAX_WIDTH_BUFFER_PX;

const editor = makeEditor({
ancestors: [
{ type: { name: 'tableRow' }, attrs: {} },
{ type: { name: 'tableCell' }, attrs: { colwidth: [], cellMargins: null } },
{ type: { name: 'paragraph' }, attrs: {} },
],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(expectedWidth);
});

it('falls back to page content width when colwidth is missing', () => {
const PIXELS_PER_INCH = 96;
const MAX_WIDTH_BUFFER_PX = 20;
const expectedWidth = (8.5 - 1 - 1) * PIXELS_PER_INCH - MAX_WIDTH_BUFFER_PX;

const editor = makeEditor({
ancestors: [
{ type: { name: 'tableRow' }, attrs: {} },
{ type: { name: 'tableCell' }, attrs: { colwidth: null, cellMargins: null } },
{ type: { name: 'paragraph' }, attrs: {} },
],
});

const size = Editor.prototype.getMaxContentSize.call(editor);

expect(size.width).toBe(expectedWidth);
});
});
});
Loading
Loading