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
1 change: 1 addition & 0 deletions packages/roosterjs-content-model-plugins/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { TableEditPlugin } from './tableEdit/TableEditPlugin';
export { OnTableEditorCreatedCallback } from './tableEdit/OnTableEditorCreatedCallback';
export { TableEditFeatureName } from './tableEdit/editors/features/TableEditFeatureName';
export { TableWithRoot } from './tableEdit/TableWithRoot';
export { PastePlugin } from './paste/PastePlugin';
export { DefaultSanitizers } from './paste/DefaultSanitizers';
export { EditPlugin, EditOptions } from './edit/EditPlugin';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { isNodeOfType, normalizeRect } from 'roosterjs-content-model-dom';
import { TableEditor } from './editors/TableEditor';
import type { TableWithRoot } from './TableWithRoot';
import type { TableEditFeatureName } from './editors/features/TableEditFeatureName';
import type { OnTableEditorCreatedCallback } from './OnTableEditorCreatedCallback';
import type { EditorPlugin, IEditor, PluginEvent, Rect } from 'roosterjs-content-model-types';
import type {
DOMHelper,
EditorPlugin,
IEditor,
PluginEvent,
Rect,
} from 'roosterjs-content-model-types';

const TABLE_RESIZER_LENGTH = 12;

Expand All @@ -12,7 +19,7 @@ const TABLE_RESIZER_LENGTH = 12;
export class TableEditPlugin implements EditorPlugin {
private editor: IEditor | null = null;
private onMouseMoveDisposer: (() => void) | null = null;
private tableRectMap: { table: HTMLTableElement; rect: Rect }[] | null = null;
private tableRectMap: (TableWithRoot & { rect: Rect })[] | null = null;
private tableEditor: TableEditor | null = null;

/**
Expand All @@ -22,11 +29,13 @@ export class TableEditPlugin implements EditorPlugin {
* If not specified, the plugin will be inserted in document.body
* @param onTableEditorCreated An optional callback to customize the Table Editors elements when created.
* @param disableFeatures An optional array of TableEditFeatures to disable
* @param tableSelector A function to select the tables to be edited. By default, it selects all contentEditable tables.
*/
constructor(
private anchorContainerSelector?: string,
private onTableEditorCreated?: OnTableEditorCreatedCallback,
private disableFeatures?: TableEditFeatureName[]
private disableFeatures?: TableEditFeatureName[],
private tableSelector: (domHelper: DOMHelper) => TableWithRoot[] = defaultTableSelector
) {}

/**
Expand Down Expand Up @@ -105,20 +114,21 @@ export class TableEditPlugin implements EditorPlugin {
const editorWindow = this.editor.getDocument().defaultView || window;
const x = e.pageX - editorWindow.scrollX;
const y = e.pageY - editorWindow.scrollY;
let currentTable: HTMLTableElement | null = null;
let currentTable: TableWithRoot | null = null;

//Find table in range of mouse
if (this.tableRectMap) {
for (let i = this.tableRectMap.length - 1; i >= 0; i--) {
const { table, rect } = this.tableRectMap[i];
const entry = this.tableRectMap[i];
const { rect } = entry;

if (
x >= rect.left - TABLE_RESIZER_LENGTH &&
x <= rect.right + TABLE_RESIZER_LENGTH &&
y >= rect.top - TABLE_RESIZER_LENGTH &&
y <= rect.bottom + TABLE_RESIZER_LENGTH
) {
currentTable = table;
currentTable = entry;
break;
}
}
Expand All @@ -130,23 +140,28 @@ export class TableEditPlugin implements EditorPlugin {

/**
* @internal Public only for unit test
* @param table Table to use when setting the Editors
* @param entry Table to use when setting the Editors
* @param event (Optional) Mouse event
*/
public setTableEditor(table: HTMLTableElement | null, event?: MouseEvent) {
if (this.tableEditor && !this.tableEditor.isEditing() && table != this.tableEditor.table) {
public setTableEditor(entry: TableWithRoot | null, event?: MouseEvent) {
if (
this.tableEditor &&
!this.tableEditor.isEditing() &&
entry?.table != this.tableEditor.table
) {
this.disposeTableEditor();
}

if (!this.tableEditor && table && this.editor && table.rows.length > 0) {
if (!this.tableEditor && entry && this.editor && entry.table.rows.length > 0) {
// anchorContainerSelector is used to specify the container to host the plugin, which can be outside of the editor's div
const container = this.anchorContainerSelector
? this.editor.getDocument().querySelector(this.anchorContainerSelector)
: undefined;

this.tableEditor = new TableEditor(
this.editor,
table,
entry.table,
entry.logicalRoot,
this.invalidateTableRects,
isNodeOfType(container, 'ELEMENT_NODE') ? container : undefined,
event?.currentTarget,
Expand All @@ -169,18 +184,27 @@ export class TableEditPlugin implements EditorPlugin {
if (!this.tableRectMap && this.editor) {
this.tableRectMap = [];

const tables = this.editor.getDOMHelper().queryElements('table');
const tables = this.tableSelector(this.editor.getDOMHelper());
tables.forEach(table => {
if (table.isContentEditable) {
const rect = normalizeRect(table.getBoundingClientRect());
if (rect && this.tableRectMap) {
this.tableRectMap.push({
table,
rect,
});
}
const rect = normalizeRect(table.table.getBoundingClientRect());

if (rect && this.tableRectMap) {
this.tableRectMap.push({
...table,
Copy link

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the spread operator here (...table) may unintentionally include extra properties; prefer an explicit object literal ({ table: table.table, logicalRoot: table.logicalRoot, rect }).

Suggested change
...table,
table: table.table,
logicalRoot: table.logicalRoot,

Copilot uses AI. Check for mistakes.
rect,
});
}
});
}
}
}

function defaultTableSelector(domHelper: DOMHelper): TableWithRoot[] {
return domHelper
.queryElements('table')
.filter(table => table.isContentEditable)
.map(table => ({
table,
logicalRoot: null,
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Represents a table and its container (logical root)
*/
export interface TableWithRoot {
/**
* The table element
*/
table: HTMLTableElement;
/**
* The logical root element of the table
* This is the element that contains the table and all its ancestors
* It is used to determine the logical root of the table
*/
logicalRoot: HTMLDivElement | null;
Copy link

Copilot AI May 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restricting logicalRoot to HTMLDivElement could prevent usage with other container types; consider using HTMLElement | null for broader compatibility.

Suggested change
logicalRoot: HTMLDivElement | null;
logicalRoot: HTMLElement | null;

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class TableEditor {
constructor(
private editor: IEditor,
public readonly table: HTMLTableElement,
public readonly logicalRoot: HTMLDivElement | null,
private onChanged: () => void,
private anchorContainer?: HTMLElement,
private contentDiv?: EventTarget | null,
Expand Down Expand Up @@ -287,7 +288,8 @@ export class TableEditor {
this.table,
this.isRTL,
!!isHorizontal,
this.onInserted,
this.onBeforeEditTable,
this.onAfterInsert,
this.anchorContainer,
this.onEditorCreated
);
Expand Down Expand Up @@ -362,13 +364,15 @@ export class TableEditor {
};

private onStartTableMove = () => {
this.onBeforeEditTable();
this.isCurrentlyEditing = true;
this.disposeTableResizer();
this.disposeTableInserter();
this.disposeCellResizers();
};

private onStartResize() {
this.onBeforeEditTable();
this.isCurrentlyEditing = true;
const range = this.editor.getDOMSelection();

Expand All @@ -386,7 +390,11 @@ export class TableEditor {
return this.onFinishEditing();
};

private onInserted = () => {
private onBeforeEditTable = () => {
this.editor.setLogicalRoot(this.logicalRoot);
};

private onAfterInsert = () => {
this.disposeTableResizer();
this.onFinishEditing();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export function createTableInserter(
table: HTMLTableElement,
isRTL: boolean,
isHorizontal: boolean,
onInsert: () => void,
onBeforeInsert: () => void,
onAfterInserted: () => void,
anchorContainer?: HTMLElement,
onTableEditorCreated?: OnTableEditorCreatedCallback
): TableEditFeature | null {
Expand Down Expand Up @@ -82,7 +83,8 @@ export function createTableInserter(
table,
isHorizontal,
editor,
onInsert,
onBeforeInsert,
onAfterInserted,
onTableEditorCreated
);

Expand All @@ -104,7 +106,8 @@ export class TableInsertHandler implements Disposable {
private table: HTMLTableElement,
private isHorizontal: boolean,
private editor: IEditor,
private onInsert: () => void,
private onBeforeInsert: () => void,
private onAfterInsert: () => void,
onTableEditorCreated?: OnTableEditorCreatedCallback
) {
this.div.addEventListener('click', this.insertTd);
Expand Down Expand Up @@ -133,6 +136,8 @@ export class TableInsertHandler implements Disposable {
return;
}

this.onBeforeInsert();

// Insert row or column
formatTableWithContentModel(
this.editor,
Expand All @@ -152,7 +157,7 @@ export class TableInsertHandler implements Disposable {
}
);

this.onInsert();
this.onAfterInsert();
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('TableEditPlugin', () => {

spyOn(plugin, 'setTableEditor').and.callThrough();

plugin.setTableEditor(table);
plugin.setTableEditor({ table, logicalRoot: null });

if (mouseOutListener) {
const boundedListener = mouseOutListener.bind(ele);
Expand Down Expand Up @@ -131,7 +131,7 @@ describe('TableEditPlugin', () => {

spyOn(plugin, 'setTableEditor').and.callThrough();

plugin.setTableEditor(table);
plugin.setTableEditor({ table, logicalRoot: null });

if (mouseOutListener) {
const boundedListener = mouseOutListener.bind(ele);
Expand Down Expand Up @@ -199,7 +199,7 @@ describe('TableEditPlugin', () => {

spyOn(plugin, 'setTableEditor').and.callThrough();

plugin.setTableEditor(table);
plugin.setTableEditor({ table, logicalRoot: null });

if (mouseOutListener) {
const boundedListener = mouseOutListener.bind(ele);
Expand Down Expand Up @@ -235,7 +235,7 @@ describe('TableEditPlugin', () => {

spyOn(plugin, 'setTableEditor').and.callThrough();

plugin.setTableEditor(table);
plugin.setTableEditor({ table, logicalRoot: null });

if (mouseOutListener) {
const boundedListener = mouseOutListener.bind(ele);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('TableEditor', () => {
tEditor = new TableEditor(
editor,
table,
null,
() => {},
anchorContainer,
contentDiv,
Expand Down Expand Up @@ -166,7 +167,7 @@ describe('TableEditor', () => {
const anchor = editor
.getDocument()
.getElementsByClassName(ANCHOR_CLASS)[0] as HTMLElement;
tEditor = new TableEditor(editor, table, () => {}, anchor, contentDiv, undefined);
tEditor = new TableEditor(editor, table, null, () => {}, anchor, contentDiv, undefined);

// Move mouse to the first cell
const cellRect = getCellRect(editor, 0, 0);
Expand All @@ -188,7 +189,7 @@ describe('TableEditor', () => {
const anchor = editor
.getDocument()
.getElementsByClassName(ANCHOR_CLASS)[0] as HTMLElement;
tEditor = new TableEditor(editor, table, () => {}, anchor, contentDiv, undefined);
tEditor = new TableEditor(editor, table, null, () => {}, anchor, contentDiv, undefined);

// Move mouse to the first cell
const cellRect = getCellRect(editor, 0, 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import * as getIntersectedRect from '../../lib/pluginUtils/Rect/getIntersectedRect';
import { ContentModelTable, EditorOptions, IEditor } from 'roosterjs-content-model-types';
import { Editor } from 'roosterjs-content-model-core';
import { getCurrentTable, getTableColumns, getTableRows } from './TableEditTestHelper';
import { getModelTable } from './tableData';
import {
HORIZONTAL_INSERTER_ID,
TableInsertHandler,
VERTICAL_INSERTER_ID,
createTableInserter,
} from '../../lib/tableEdit/editors/features/TableInserter';
import { ContentModelTable, EditorOptions, IEditor } from 'roosterjs-content-model-types';

import { getCurrentTable, getTableColumns, getTableRows } from './TableEditTestHelper';

describe('Table Inserter tests', () => {
let editor: IEditor;
Expand Down Expand Up @@ -78,6 +77,7 @@ describe('Table Inserter tests', () => {
target as HTMLTableElement,
inserterType == HORIZONTAL_INSERTER_ID,
editor,
() => {},
onInsertSpy
);

Expand Down Expand Up @@ -141,6 +141,7 @@ describe('Table Inserter tests', () => {
false,
false,
() => {},
() => {},
undefined,
(editorType, element) => {
if (element && editorType == 'VerticalTableInserter') {
Expand Down Expand Up @@ -196,6 +197,7 @@ describe('Table Inserter tests', () => {
false,
false,
() => {},
() => {},
undefined,
(editorType, element) => {
if (element && editorType == 'TableMover') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ describe('Table Mover Tests', () => {
it('On click event', () => {
const table = document.getElementById(targetId) as HTMLTableElement;

const tableEditor = new TableEditor(editor, table, () => true);
const tableEditor = new TableEditor(editor, table, null, () => true);

tableEditor.onSelect(table);

Expand Down
Loading