Skip to content

Commit

Permalink
Port Table edit plugin (#2358)
Browse files Browse the repository at this point in the history
* Standalone Editor step 2

* Standalone Editor step 3

* improve

* Standalone Editor step 4

* Standalone Editor: Remove compatible enums from standalone editor

* improve

* Standalone Editor: Create new event types

* Port to new event system

* Revert "Port to new event system"

This reverts commit 60cf041.

* Port to new event system

* Improve

* fix build

* fix demo

* Fix buttons

* fix build

* fix build

* fix build

* plugin added to cm demo site

* pluginUtils imported

* IStandaloneEditor changes

* editTable can skip undo snapshot

* TableEditPlugin port start

* move D&D to folder

* add plugin to Standalone demo and remove old plugin

* demo fix

* rename D&D and fix import

* demo site changes

* rename selector to mover

* new plugin utils, organisation

* isMobileOrTablet

* cleanup

* normalise width too

* remove onShowHelperElement, fix containment, instancing, others

* implement cell resizer

* fix type import

* fix selection

* fix imports

* fix instanceof, use formatTableWithContentModel, getDOMHelper

* fix merge

* small fix

* fix table resize functionality

* fix merge

* fix import

* fix dependency

* fix exports

* merge fix

* export MIN_WIDTH

* fix resizers

* add check

* fix color

* fix instanceof

* change IStandaloneEditor to IEditor

* fix export const

* rename const

* fix cell resizer, add ids

* add tests

* fix build issues

---------

Co-authored-by: Jiuqing Song <jisong@microsoft.com>
  • Loading branch information
Andres-CT98 and JiuqingSong authored Feb 29, 2024
1 parent 78b498a commit c950e5b
Show file tree
Hide file tree
Showing 27 changed files with 3,475 additions and 14 deletions.
1 change: 0 additions & 1 deletion demo/scripts/controls/BuildInPluginState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export interface BuildInPluginList {
imageEdit: boolean;
cutPasteListChain: boolean;
tableCellSelection: boolean;
tableResize: boolean;
customReplace: boolean;
listEditMenu: boolean;
imageEditMenu: boolean;
Expand Down
10 changes: 9 additions & 1 deletion demo/scripts/controls/ContentModelEditorMainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { alignJustifyButton } from './ribbonButtons/contentModel/alignJustifyBut
import { alignLeftButton } from './ribbonButtons/contentModel/alignLeftButton';
import { alignRightButton } from './ribbonButtons/contentModel/alignRightButton';
import { arrayPush } from 'roosterjs-editor-dom';
import { AutoFormatPlugin, EditPlugin, PastePlugin } from 'roosterjs-content-model-plugins';
import { backgroundColorButton } from './ribbonButtons/contentModel/backgroundColorButton';
import { blockQuoteButton } from './ribbonButtons/contentModel/blockQuoteButton';
import { boldButton } from './ribbonButtons/contentModel/boldButton';
Expand Down Expand Up @@ -81,6 +80,12 @@ import { underlineButton } from './ribbonButtons/contentModel/underlineButton';
import { undoButton } from './ribbonButtons/contentModel/undoButton';
import { zoom } from './ribbonButtons/contentModel/zoom';
import { ContentModelSegmentFormat, IEditor, Snapshots } from 'roosterjs-content-model-types';
import {
AutoFormatPlugin,
EditPlugin,
PastePlugin,
TableEditPlugin,
} from 'roosterjs-content-model-plugins';
import {
spaceAfterButton,
spaceBeforeButton,
Expand Down Expand Up @@ -170,6 +175,7 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
private formatPainterPlugin: ContentModelFormatPainterPlugin;
private pastePlugin: PastePlugin;
private sampleEntityPlugin: SampleEntityPlugin;
private tableEditPlugin: TableEditPlugin;
private snapshots: Snapshots;
private buttons: ContentModelRibbonButton<any>[] = [
formatPainterButton,
Expand Down Expand Up @@ -259,6 +265,7 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
this.pasteOptionPlugin = createPasteOptionPlugin();
this.emojiPlugin = createEmojiPlugin();
this.formatPainterPlugin = new ContentModelFormatPainterPlugin();
this.tableEditPlugin = new TableEditPlugin();
this.pastePlugin = new PastePlugin();
this.sampleEntityPlugin = new SampleEntityPlugin();
this.state = {
Expand Down Expand Up @@ -378,6 +385,7 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
this.contentModelRibbonPlugin,
this.formatPainterPlugin,
this.pastePlugin,
this.tableEditPlugin,
this.contentModelAutoFormatPlugin,
this.contentModelEditPlugin,
this.contentModelPanePlugin.getInnerRibbonPlugin(),
Expand Down
5 changes: 4 additions & 1 deletion demo/scripts/controls/StandaloneEditorMainPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { alignCenterButton } from './ribbonButtons/contentModel/alignCenterButto
import { alignJustifyButton } from './ribbonButtons/contentModel/alignJustifyButton';
import { alignLeftButton } from './ribbonButtons/contentModel/alignLeftButton';
import { alignRightButton } from './ribbonButtons/contentModel/alignRightButton';
import { AutoFormatPlugin, EditPlugin } from 'roosterjs-content-model-plugins';
import { AutoFormatPlugin, EditPlugin, TableEditPlugin } from 'roosterjs-content-model-plugins';
import { backgroundColorButton } from './ribbonButtons/contentModel/backgroundColorButton';
import { blockQuoteButton } from './ribbonButtons/contentModel/blockQuoteButton';
import { boldButton } from './ribbonButtons/contentModel/boldButton';
Expand Down Expand Up @@ -166,6 +166,7 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
private contentAutoFormatPlugin: AutoFormatPlugin;
private snapshotPlugin: ContentModelSnapshotPlugin;
private formatPainterPlugin: ContentModelFormatPainterPlugin;
private tableEditPlugin: TableEditPlugin;
private snapshots: Snapshots<Snapshot>;
private buttons: ContentModelRibbonButton<any>[] = [
formatPainterButton,
Expand Down Expand Up @@ -253,6 +254,7 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
this.contentAutoFormatPlugin = new AutoFormatPlugin();
this.contentModelRibbonPlugin = new ContentModelRibbonPlugin();
this.formatPainterPlugin = new ContentModelFormatPainterPlugin();
this.tableEditPlugin = new TableEditPlugin();
this.state = {
showSidePane: window.location.hash != '',
popoutWindow: null,
Expand Down Expand Up @@ -345,6 +347,7 @@ class ContentModelEditorMainPane extends MainPaneBase<ContentModelMainPaneState>
plugins={[
this.contentModelRibbonPlugin,
this.formatPainterPlugin,
this.tableEditPlugin,
this.contentModelEditPlugin,
this.contentAutoFormatPlugin,
]}
Expand Down
4 changes: 0 additions & 4 deletions demo/scripts/controls/getToggleablePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { HyperLink } from 'roosterjs-editor-plugins/lib/HyperLink';
import { ImageEdit } from 'roosterjs-editor-plugins/lib/ImageEdit';
import { Paste } from 'roosterjs-editor-plugins/lib/Paste';
import { TableCellSelection } from 'roosterjs-editor-plugins/lib/TableCellSelection';
import { TableResize } from 'roosterjs-editor-plugins/lib/TableResize';
import { Watermark } from 'roosterjs-editor-plugins/lib/Watermark';
import {
createContextMenuPlugin,
Expand Down Expand Up @@ -43,9 +42,6 @@ export default function getToggleablePlugins(initState: BuildInPluginState) {
imageEdit,
cutPasteListChain: pluginList.cutPasteListChain ? new CutPasteListChain() : null,
tableCellSelection: pluginList.tableCellSelection ? new TableCellSelection() : null,
tableResize: pluginList.tableResize
? new TableResize(undefined, initState.tableFeaturesContainerSelector)
: null,
customReplace: pluginList.customReplace ? new CustomReplacePlugin() : null,
autoFormat: pluginList.autoFormat ? new AutoFormat() : null,
listEditMenu:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const initialState: BuildInPluginState = {
imageEdit: true,
cutPasteListChain: false,
tableCellSelection: true,
tableResize: true,
customReplace: true,
listEditMenu: true,
imageEditMenu: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export default class ContentModelPlugins extends React.Component<PluginsProps, {
(state, value) => (state.applyChangesOnMouseUp = value)
)
)}
{this.renderPluginItem('tableResize', 'Table Resize Plugin')}
{this.renderPluginItem('customReplace', 'Custom Replace Plugin (autocomplete)')}
{this.renderPluginItem(
'contextMenu',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const initialState: BuildInPluginState = {
imageEdit: true,
cutPasteListChain: true,
tableCellSelection: true,
tableResize: true,
customReplace: true,
listEditMenu: true,
imageEditMenu: true,
Expand Down
1 change: 0 additions & 1 deletion demo/scripts/controls/sidePane/editorOptions/Plugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export default class Plugins extends React.Component<PluginsProps, {}> {
)
)}
{this.renderPluginItem('cutPasteListChain', 'CutPasteListChainPlugin')}
{this.renderPluginItem('tableResize', 'Table Resize Plugin')}
{this.renderPluginItem('customReplace', 'Custom Replace Plugin (autocomplete)')}
{this.renderPluginItem(
'contextMenu',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
CutPasteListChainCode,
ImageEditCode,
ContentModelPasteCode,
TableResizeCode,
} from './SimplePluginCode';

export default class PluginsCode extends CodeElement {
Expand All @@ -26,7 +25,6 @@ export default class PluginsCode extends CodeElement {
pluginList.watermark && new WatermarkCode(this.state.watermarkText),
pluginList.imageEdit && new ImageEditCode(),
pluginList.cutPasteListChain && new CutPasteListChainCode(),
pluginList.tableResize && new TableResizeCode(),
pluginList.customReplace && new CustomReplaceCode(),
pluginList.tableCellSelection && new TableCellSelectionCode(),
].filter(plugin => !!plugin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export {
export { setSelection } from './publicApi/selection/setSelection';

export { applyTableFormat } from './publicApi/table/applyTableFormat';
export { normalizeTable } from './publicApi/table/normalizeTable';
export { normalizeTable, MIN_ALLOWED_TABLE_CELL_WIDTH } from './publicApi/table/normalizeTable';
export { setTableCellBackgroundColor } from './publicApi/table/setTableCellBackgroundColor';
export { getSelectedCells } from './publicApi/table/getSelectedCells';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type {
ContentModelTableCell,
} from 'roosterjs-content-model-types';

/**
* Minimum width for a table cell
*/
export const MIN_ALLOWED_TABLE_CELL_WIDTH: number = 30;
const MIN_HEIGHT = 22;

/**
Expand Down Expand Up @@ -75,6 +79,8 @@ export function normalizeTable(
for (let i = 0; i < columns; i++) {
if (table.widths[i] === undefined) {
table.widths[i] = getTableCellWidth(columns);
} else if (table.widths[i] < MIN_ALLOWED_TABLE_CELL_WIDTH) {
table.widths[i] = MIN_ALLOWED_TABLE_CELL_WIDTH;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { TableEditPlugin } from './tableEdit/TableEditPlugin';
export { PastePlugin } from './paste/PastePlugin';
export { EditPlugin } from './edit/EditPlugin';
export { AutoFormatPlugin, AutoFormatOptions } from './autoFormat/AutoFormatPlugin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import normalizeRect from '../pluginUtils/Rect/normalizeRect';
import TableEditor from './editors/TableEditor';
import { isNodeOfType } from 'roosterjs-content-model-dom';
import type { EditorPlugin, IEditor, PluginEvent, Rect } from 'roosterjs-content-model-types';

const TABLE_RESIZER_LENGTH = 12;

/**
* TableEdit plugin, provides the ability to resize a table by drag-and-drop
*/
export class TableEditPlugin implements EditorPlugin {
private editor: IEditor | null = null;
private onMouseMoveDisposer: (() => void) | null = null;
private tableRectMap: { table: HTMLTableElement; rect: Rect }[] | null = null;
private tableEditor: TableEditor | null = null;

/**
* Construct a new instance of TableResize plugin
* @param anchorContainerSelector An optional selector string to specify the container to host the plugin.
* The container must not be affected by transform: scale(), otherwise the position calculation will be wrong.
* If not specified, the plugin will be inserted in document.body
*/
constructor(private anchorContainerSelector?: string) {}

/**
* Get a friendly name of this plugin
*/
getName() {
return 'TableEdit';
}

/**
* Initialize this plugin. This should only be called from Editor
* @param editor Editor instance
*/
initialize(editor: IEditor) {
this.editor = editor;
this.onMouseMoveDisposer = this.editor.attachDomEvent({
mousemove: { beforeDispatch: this.onMouseMove },
});
const scrollContainer = this.editor.getScrollContainer();
scrollContainer.addEventListener('mouseout', this.onMouseOut);
}

private onMouseOut = ({ relatedTarget, currentTarget }: MouseEvent) => {
const relatedTargetNode = relatedTarget as Node;
const currentTargetNode = currentTarget as Node;
if (
isNodeOfType(relatedTargetNode, 'ELEMENT_NODE') &&
isNodeOfType(currentTargetNode, 'ELEMENT_NODE') &&
this.tableEditor &&
!this.tableEditor.isOwnedElement(relatedTargetNode) &&
!currentTargetNode.contains(relatedTargetNode)
) {
this.setTableEditor(null);
}
};

/**
* Dispose this plugin
*/
dispose() {
const scrollContainer = this.editor?.getScrollContainer();
scrollContainer?.removeEventListener('mouseout', this.onMouseOut);
this.onMouseMoveDisposer?.();
this.invalidateTableRects();
this.disposeTableEditor();
this.editor = null;
this.onMouseMoveDisposer = null;
}

/**
* Handle events triggered from editor
* @param event PluginEvent object
*/
onPluginEvent(e: PluginEvent) {
switch (e.eventType) {
case 'input':
case 'contentChanged':
case 'scroll':
case 'zoomChanged':
this.setTableEditor(null);
this.invalidateTableRects();
break;
}
}

private onMouseMove = (event: Event) => {
const e = event as MouseEvent;

if (e.buttons > 0 || !this.editor) {
return;
}

this.ensureTableRects();

const editorWindow = this.editor.getDocument().defaultView || window;
const x = e.pageX - editorWindow.scrollX;
const y = e.pageY - editorWindow.scrollY;
let currentTable: HTMLTableElement | 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];

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;
break;
}
}
}

this.setTableEditor(currentTable, e);
this.tableEditor?.onMouseMove(x, y);
};

/**
* @internal Public only for unit test
* @param table 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) {
this.disposeTableEditor();
}

if (!this.tableEditor && table && this.editor && table.rows.length > 0) {
const container = this.anchorContainerSelector
? this.editor.getDOMHelper().queryElements(this.anchorContainerSelector)[0]
: undefined;

this.tableEditor = new TableEditor(
this.editor,
table,
this.invalidateTableRects,
isNodeOfType(container as Node, 'ELEMENT_NODE') ? container : undefined,
event?.currentTarget
);
}
}

private invalidateTableRects = () => {
this.tableRectMap = null;
};

private disposeTableEditor() {
this.tableEditor?.dispose();
this.tableEditor = null;
}

private ensureTableRects() {
if (!this.tableRectMap && this.editor) {
this.tableRectMap = [];

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

0 comments on commit c950e5b

Please sign in to comment.