diff --git a/demo/scripts/controlsV2/roosterjsReact/contextMenu/menus/createImageEditMenuProvider.tsx b/demo/scripts/controlsV2/roosterjsReact/contextMenu/menus/createImageEditMenuProvider.tsx index 389df94883a..ac418c79dd4 100644 --- a/demo/scripts/controlsV2/roosterjsReact/contextMenu/menus/createImageEditMenuProvider.tsx +++ b/demo/scripts/controlsV2/roosterjsReact/contextMenu/menus/createImageEditMenuProvider.tsx @@ -1,7 +1,7 @@ import { createContextMenuProvider } from '../utils/createContextMenuProvider'; import { EditorPlugin, IEditor, ImageEditor } from 'roosterjs-content-model-types'; import { formatImageWithContentModel } from 'roosterjs-content-model-api'; -import { iterateSelections, updateImageMetadata } from 'roosterjs-content-model-dom'; +import { iterateSelections, mutateBlock, updateImageMetadata } from 'roosterjs-content-model-dom'; import { setImageAltText } from 'roosterjs-content-model-api'; import { showInputDialog } from '../../inputDialog/utils/showInputDialog'; import type { ContextMenuItem } from '../types/ContextMenuItem'; @@ -208,7 +208,7 @@ function removeImage(editor: IEditor) { const index = block.segments.indexOf(segment); if (index >= 0) { - block.segments.splice(index, 1); + mutateBlock(block).segments.splice(index, 1); changed = true; } } diff --git a/demo/scripts/controlsV2/roosterjsReact/emoji/plugin/createEmojiPlugin.ts b/demo/scripts/controlsV2/roosterjsReact/emoji/plugin/createEmojiPlugin.ts index 7c6c0a7b004..5713bc3d07e 100644 --- a/demo/scripts/controlsV2/roosterjsReact/emoji/plugin/createEmojiPlugin.ts +++ b/demo/scripts/controlsV2/roosterjsReact/emoji/plugin/createEmojiPlugin.ts @@ -1,5 +1,10 @@ import * as React from 'react'; -import { isModifierKey, isNodeOfType, iterateSelections } from 'roosterjs-content-model-dom'; +import { + isModifierKey, + isNodeOfType, + iterateSelections, + mutateSegment, +} from 'roosterjs-content-model-dom'; import { KeyCodes } from '@fluentui/react/lib/Utilities'; import { MoreEmoji } from '../utils/emojiList'; import { showEmojiCallout } from '../components/showEmojiCallout'; @@ -264,11 +269,13 @@ class EmojiPlugin implements ReactEditorPlugin { previousSegment?.segmentType == 'Text' && previousSegment.text.endsWith(wordBeforeCursor) ) { - previousSegment.text = - previousSegment.text.substring( - 0, - previousSegment.text.length - wordBeforeCursor.length - ) + emoji.codePoint; + mutateSegment(block, previousSegment, segment => { + segment.text = + segment.text.substring( + 0, + segment.text.length - wordBeforeCursor.length + ) + emoji.codePoint; + }); } } diff --git a/demo/scripts/controlsV2/sidePane/contentModel/buttons/exportButton.ts b/demo/scripts/controlsV2/sidePane/contentModel/buttons/exportButton.ts index 026da4fecf8..fd9ef800578 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/buttons/exportButton.ts +++ b/demo/scripts/controlsV2/sidePane/contentModel/buttons/exportButton.ts @@ -1,4 +1,5 @@ import { getCurrentContentModel } from '../currentModel'; +import { mutateBlock } from 'roosterjs-content-model-dom'; import { RibbonButton } from '../../../roosterjsReact/ribbon'; export const exportButton: RibbonButton<'buttonNameExport'> = { @@ -10,7 +11,7 @@ export const exportButton: RibbonButton<'buttonNameExport'> = { if (model) { editor.formatContentModel(currentModel => { - currentModel.blocks = model.blocks; + mutateBlock(currentModel).blocks = model.blocks; return true; }); diff --git a/demo/scripts/controlsV2/sidePane/contentModel/buttons/importModelButton.ts b/demo/scripts/controlsV2/sidePane/contentModel/buttons/importModelButton.ts index 92a94a77f98..ab153a4975d 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/buttons/importModelButton.ts +++ b/demo/scripts/controlsV2/sidePane/contentModel/buttons/importModelButton.ts @@ -32,8 +32,11 @@ export const importModelButton: RibbonButton<'buttonNameImportModel'> = { const importedModel = JSON.parse(values.model); if (isBlockGroupOfType(importedModel, 'Document')) { editor.formatContentModel(model => { - model.blocks = importedModel.blocks; - model.format = importedModel.format; + const mutableModel = model; + + mutableModel.blocks = importedModel.blocks; + mutableModel.format = importedModel.format; + return true; }); } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts index 2ff9d86662f..217a0d5f641 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -112,7 +112,7 @@ export function setModelIndentation( break; } else if (currentParent.blockGroupType == 'FormatContainer' && index >= 0) { - delete currentParent.cachedElement; + mutateBlock(currentParent); currentBlock = currentParent; currentParent = path[index + 1]; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts index 4ec4c6f9772..fd7dee7ced8 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts @@ -9,6 +9,4 @@ import type { export function alignTable(table: ShallowMutableContentModelTable, operation: TableAlignOperation) { table.format.marginLeft = operation == 'alignLeft' ? '' : 'auto'; table.format.marginRight = operation == 'alignRight' ? '' : 'auto'; - - delete table.cachedElement; } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts index ebe4480f0af..a54c6ec1748 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts @@ -71,8 +71,6 @@ function alignTableCellInternal( const format = cell?.format; if (format) { - delete cell.cachedElement; - callback(mutateBlock(cell)); cell.blocks.forEach(block => { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTable.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTable.ts index 63d85496fdb..411d3452929 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTable.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTable.ts @@ -5,5 +5,4 @@ import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-ty */ export function deleteTable(table: ShallowMutableContentModelTable) { table.rows = []; - delete table.cachedElement; } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableCells.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableCells.ts index 9f12c22fdf0..a732c226253 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableCells.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableCells.ts @@ -23,8 +23,6 @@ export function mergeTableCells(table: ShallowMutableContentModelTable) { mutableCell.spanAbove = rowIndex > sel.firstRow; } } - - delete table.rows[rowIndex].cachedElement; } } } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableColumn.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableColumn.ts index 5a0dddc85aa..c904d96ff6a 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableColumn.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableColumn.ts @@ -46,8 +46,6 @@ export function mergeTableColumn( mutateBlock(newCell).isSelected = true; } } - - delete table.rows[rowIndex].cachedElement; } } } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableRow.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableRow.ts index bc382738982..6f7d298bd6f 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableRow.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableRow.ts @@ -37,8 +37,7 @@ export function mergeTableRow( let newSelectedRow = mergingRowIndex; while (table.rows[newSelectedRow]?.cells[colIndex]?.spanAbove) { - delete table.rows[newSelectedRow].cells[colIndex].cachedElement; - delete table.rows[newSelectedRow].cachedElement; + mutateBlock(table.rows[newSelectedRow].cells[colIndex]); newSelectedRow--; } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellHorizontally.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellHorizontally.ts index 937efffaae1..8bfc7a37dc9 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellHorizontally.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellHorizontally.ts @@ -46,7 +46,7 @@ export function splitTableCellHorizontally(table: ShallowMutableContentModelTabl } row.cells.splice(colIndex + 1, 0, newCell); - delete row.cells[colIndex].cachedElement; + mutateBlock(row.cells[colIndex]); } }); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellVertically.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellVertically.ts index 4cfce0e8a6c..7762c2a7162 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellVertically.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellVertically.ts @@ -17,11 +17,7 @@ export function splitTableCellVertically(table: ShallowMutableContentModelTable) const row = table.rows[rowIndex]; const belowRow = table.rows[rowIndex + 1]; - row.cells.forEach(cell => { - delete cell.cachedElement; - }); - - delete row.cachedElement; + row.cells.forEach(mutateBlock); if ( belowRow?.cells.every( @@ -36,8 +32,6 @@ export function splitTableCellVertically(table: ShallowMutableContentModelTable) mutateBlock(belowCell).spanAbove = false; } }); - - delete belowRow.cachedElement; } else { const newHeight = Math.max((row.height /= 2), MIN_HEIGHT); const newRow: ContentModelTableRow = { diff --git a/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts b/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts index 57465e84f6e..0bf8f635329 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts @@ -4,7 +4,6 @@ import { getSelectedCells, mutateBlock, getTableMetadata, - hasMetadata, parseValueWithUnit, setFirstColumnFormatBorders, updateTableCellMetadata, @@ -370,10 +369,10 @@ export function applyTableBorderFormat( modifyPerimeter(tableModel, sel, borderFormat, perimeter, isRtl); } - const tableMeta = hasMetadata(tableModel) ? getTableMetadata(tableModel) : {}; + const tableMeta = getTableMetadata(tableModel); if (tableMeta) { // Enforce first column format if necessary - setFirstColumnFormatBorders(tableModel.rows, tableMeta); + setFirstColumnFormatBorders(mutateBlock(tableModel).rows, tableMeta); } return true; diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts index 0a7aa3c1183..2f9780cb1eb 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts @@ -3,6 +3,7 @@ import { addTextSegment, buildSelectionMarker, getRegularSelectionOffsets, + mutateBlock, processChildNode, } from 'roosterjs-content-model-dom'; import type { @@ -13,8 +14,8 @@ import type { InsertPoint, DomToModelContext, ContentModelBlockGroup, - ContentModelDocument, FormatContentModelContext, + ShallowMutableContentModelDocument, } from 'roosterjs-content-model-types'; /** @@ -28,7 +29,7 @@ export function formatInsertPointWithContentModel( editor: IEditor, insertPoint: DOMInsertPoint, callback: ( - model: ContentModelDocument, + model: ShallowMutableContentModelDocument, context: FormatContentModelContext, insertPoint?: InsertPoint ) => void, @@ -47,7 +48,7 @@ export function formatInsertPointWithContentModel( const index = paragraph.segments.indexOf(marker); if (index >= 0) { - paragraph.segments.splice(index, 1); + mutateBlock(paragraph).segments.splice(index, 1); } } return true; diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts index e903080f745..1607a04c695 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts @@ -1,5 +1,5 @@ import { getSelectedParagraphs } from 'roosterjs-content-model-dom'; -import type { ContentModelParagraph, IEditor } from 'roosterjs-content-model-types'; +import type { IEditor, ShallowMutableContentModelParagraph } from 'roosterjs-content-model-types'; /** * Invoke a callback to format the selected paragraph using Content Model @@ -10,11 +10,11 @@ import type { ContentModelParagraph, IEditor } from 'roosterjs-content-model-typ export function formatParagraphWithContentModel( editor: IEditor, apiName: string, - setStyleCallback: (paragraph: ContentModelParagraph) => void + setStyleCallback: (paragraph: ShallowMutableContentModelParagraph) => void ) { editor.formatContentModel( (model, context) => { - const paragraphs = getSelectedParagraphs(model); + const paragraphs = getSelectedParagraphs(model, true /*mutate*/); paragraphs.forEach(setStyleCallback); context.newPendingFormat = 'preserve'; diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTableWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTableWithContentModel.ts index cad3a7e074c..004208a688c 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTableWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTableWithContentModel.ts @@ -8,8 +8,13 @@ import { getFirstSelectedTable, normalizeTable, setSelection, + mutateBlock, } from 'roosterjs-content-model-dom'; -import type { ContentModelTable, IEditor, TableSelection } from 'roosterjs-content-model-types'; +import type { + IEditor, + ShallowMutableContentModelTable, + TableSelection, +} from 'roosterjs-content-model-types'; /** * Invoke a callback to format the selected table using Content Model @@ -21,14 +26,16 @@ import type { ContentModelTable, IEditor, TableSelection } from 'roosterjs-conte export function formatTableWithContentModel( editor: IEditor, apiName: string, - callback: (tableModel: ContentModelTable) => void, + callback: (tableModel: ShallowMutableContentModelTable) => void, selectionOverride?: TableSelection ) { editor.formatContentModel( model => { - const [tableModel, path] = getFirstSelectedTable(model); + const [readonlyTableModel, path] = getFirstSelectedTable(model); + + if (readonlyTableModel) { + const tableModel = mutateBlock(readonlyTableModel); - if (tableModel) { callback(tableModel); if (!hasSelectionInBlock(tableModel)) { diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTextSegmentBeforeSelectionMarker.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTextSegmentBeforeSelectionMarker.ts index aaed95fe722..cca5bceb881 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTextSegmentBeforeSelectionMarker.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatTextSegmentBeforeSelectionMarker.ts @@ -1,12 +1,12 @@ -import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import { getSelectedSegmentsAndParagraphs, mutateSegment } from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, - ContentModelParagraph, ContentModelSegmentFormat, ContentModelText, FormatContentModelContext, FormatContentModelOptions, IEditor, + ShallowMutableContentModelDocument, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -18,9 +18,9 @@ import type { export function formatTextSegmentBeforeSelectionMarker( editor: IEditor, callback: ( - model: ContentModelDocument, + model: ShallowMutableContentModelDocument, previousSegment: ContentModelText, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, markerFormat: ContentModelSegmentFormat, context: FormatContentModelContext ) => boolean, @@ -33,22 +33,34 @@ export function formatTextSegmentBeforeSelectionMarker( model, false /*includeFormatHolder*/ ); + let rewrite = false; - if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) { - const marker = selectedSegmentsAndParagraphs[0][0]; - const paragraph = selectedSegmentsAndParagraphs[0][1]; - const markerIndex = paragraph.segments.indexOf(marker); - if (marker.segmentType === 'SelectionMarker' && markerIndex > 0) { - const previousSegment = paragraph.segments[markerIndex - 1]; - if (previousSegment && previousSegment.segmentType === 'Text') { - result = true; + if ( + selectedSegmentsAndParagraphs.length > 0 && + selectedSegmentsAndParagraphs[0][0].segmentType == 'SelectionMarker' && + selectedSegmentsAndParagraphs[0][1] + ) { + mutateSegment( + selectedSegmentsAndParagraphs[0][1], + selectedSegmentsAndParagraphs[0][0], + (marker, paragraph, markerIndex) => { + const previousSegment = paragraph.segments[markerIndex - 1]; - return callback(model, previousSegment, paragraph, marker.format, context); + if (previousSegment && previousSegment.segmentType === 'Text') { + result = true; + rewrite = callback( + model, + previousSegment, + paragraph, + marker.format, + context + ); + } } - } + ); } - return false; + return rewrite; }, options); return result; diff --git a/packages/roosterjs-content-model-api/test/modelApi/table/alignTableTest.ts b/packages/roosterjs-content-model-api/test/modelApi/table/alignTableTest.ts index e7c729ad421..3dfcd13b626 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/table/alignTableTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/table/alignTableTest.ts @@ -53,14 +53,11 @@ describe('alignTable', () => { }); it('Align table to left, check cached table and align is cleared', () => { - const tableNode = document.createElement('table'); const table = createTable(1); table.format.textAlign = 'start'; table.format.htmlAlign = 'end'; - table.cachedElement = tableNode; - alignTable(table, 'alignRight'); expect(table.format).toEqual({ @@ -69,6 +66,5 @@ describe('alignTable', () => { textAlign: 'start', htmlAlign: 'end', }); - expect(table.cachedElement).toBeUndefined(); }); }); diff --git a/packages/roosterjs-content-model-api/test/modelApi/table/deleteTableTest.ts b/packages/roosterjs-content-model-api/test/modelApi/table/deleteTableTest.ts index 2985740f74c..5375d7fb36a 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/table/deleteTableTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/table/deleteTableTest.ts @@ -5,11 +5,8 @@ describe('deleteTable', () => { it('deleteTable', () => { const table = createTable(2); - table.cachedElement = {} as any; - deleteTable(table); expect(table.rows).toEqual([]); - expect(table.cachedElement).toBeUndefined(); }); }); diff --git a/packages/roosterjs-content-model-api/test/publicApi/utils/formatTableWithContentModelTest.ts b/packages/roosterjs-content-model-api/test/publicApi/utils/formatTableWithContentModelTest.ts index df6b7a61ef3..27e3442a29b 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/utils/formatTableWithContentModelTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/utils/formatTableWithContentModelTest.ts @@ -6,7 +6,7 @@ import { ContentModelDocument, IEditor } from 'roosterjs-content-model-types'; import { formatTableWithContentModel } from '../../../lib/publicApi/utils/formatTableWithContentModel'; import { createContentModelDocument, - createTable, + createTable as originalCreateTable, createTableCell, } from 'roosterjs-content-model-dom'; @@ -16,6 +16,14 @@ describe('formatTableWithContentModel', () => { let model: ContentModelDocument; let formatResult: boolean | undefined; + function createTable(rowCount: number) { + const table = originalCreateTable(rowCount); + + table.cachedElement = {} as any; + + return table; + } + beforeEach(() => { formatResult = undefined; formatContentModelSpy = jasmine @@ -60,6 +68,7 @@ describe('formatTableWithContentModel', () => { selectionOverride: undefined, }); expect(formatResult).toBeFalse(); + expect(table.cachedElement).toBeDefined(); }); it('Model with selected table, has selection in block, no metadata', () => { @@ -92,6 +101,7 @@ describe('formatTableWithContentModel', () => { expect(normalizeTable.normalizeTable).toHaveBeenCalledWith(table, undefined); expect(applyTableFormat.applyTableFormat).not.toHaveBeenCalled(); expect(formatResult).toBeTrue(); + expect(table.cachedElement).toBeUndefined(); }); it('Model with selected table, no selection in block, no metadata', () => { @@ -152,6 +162,7 @@ describe('formatTableWithContentModel', () => { isHeader: false, dataset: {}, }); + expect(table.cachedElement).toBeUndefined(); }); it('Model with selected table, no selection in block, has metadata', () => { @@ -213,6 +224,7 @@ describe('formatTableWithContentModel', () => { isHeader: false, dataset: {}, }); + expect(table.cachedElement).toBeUndefined(); }); it('With default format and additional parameters', () => { @@ -280,5 +292,6 @@ describe('formatTableWithContentModel', () => { isHeader: false, dataset: {}, }); + expect(table.cachedElement).toBeUndefined(); }); }); diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts index cdc177336a1..79965ccdcb2 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts @@ -29,8 +29,8 @@ import type { PluginWithState, ContentModelDocument, ContentModelParagraph, - TableSelectionContext, ContentModelSegment, + ReadonlyTableSelectionContext, } from 'roosterjs-content-model-types'; /** @@ -246,7 +246,7 @@ class CopyPastePlugin implements PluginWithState { export function adjustSelectionForCopyCut(pasteModel: ContentModelDocument) { let selectionMarker: ContentModelSegment | undefined; let firstBlock: ContentModelParagraph | undefined; - let tableContext: TableSelectionContext | undefined; + let tableContext: ReadonlyTableSelectionContext | undefined; iterateSelections(pasteModel, (_, tableCtxt, block, segments) => { if (selectionMarker) { diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/entity/adjustSelectionAroundEntity.ts b/packages/roosterjs-content-model-core/lib/corePlugin/entity/adjustSelectionAroundEntity.ts index b7c1975da3f..0c5f82d2efb 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/entity/adjustSelectionAroundEntity.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/entity/adjustSelectionAroundEntity.ts @@ -5,11 +5,11 @@ import { isNodeOfType, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlockGroup, ContentModelEntity, - ContentModelParagraph, - ContentModelSegment, IEditor, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, } from 'roosterjs-content-model-types'; /** @@ -122,9 +122,9 @@ function getNewRange( } function findPairedDelimiter( - entitySegment: ContentModelSegment, - path: ContentModelBlockGroup[], - paragraph: ContentModelParagraph, + entitySegment: ReadonlyContentModelSegment, + path: ReadonlyContentModelBlockGroup[], + paragraph: ReadonlyContentModelParagraph, movingBefore: boolean ) { let entity: ContentModelEntity | null = null; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/format/applyPendingFormat.ts b/packages/roosterjs-content-model-core/lib/corePlugin/format/applyPendingFormat.ts index 06b69640752..d5086b2b99c 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/format/applyPendingFormat.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/format/applyPendingFormat.ts @@ -1,6 +1,7 @@ import { createText, iterateSelections, + mutateSegment, normalizeContentModel, setParagraphNotImplicit, } from 'roosterjs-content-model-dom'; @@ -40,19 +41,25 @@ export function applyPendingFormat( // For space, there can be (space) or   ( ), we treat them as the same if (subStr == data || (data == ANSI_SPACE && subStr == NON_BREAK_SPACE)) { - marker.format = { ...format }; - previousSegment.text = text.substring(0, text.length - data.length); + mutateSegment(block, previousSegment, previousSegment => { + previousSegment.text = text.substring(0, text.length - data.length); + }); - const newText = createText( - data == ANSI_SPACE ? NON_BREAK_SPACE : data, - { - ...previousSegment.format, - ...format, - } - ); + mutateSegment(block, marker, (marker, block) => { + marker.format = { ...format }; + + const newText = createText( + data == ANSI_SPACE ? NON_BREAK_SPACE : data, + { + ...previousSegment.format, + ...format, + } + ); + + block.segments.splice(index, 0, newText); + setParagraphNotImplicit(block); + }); - block.segments.splice(index, 0, newText); - setParagraphNotImplicit(block); isChanged = true; } } diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index 2446e2ee8c4..7a0f0cbe3dc 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -136,7 +136,7 @@ describe('paste with content model & paste plugin', () => { paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 4); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); }); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts index 1760a8c7d87..1fd69175cf6 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts @@ -66,7 +66,6 @@ export function normalizeTable( cell.spanAbove = false; } else if (rowIndex > 0 && colIndex > 0 && cell.isHeader) { cell.isHeader = false; - delete cell.cachedElement; } if (colIndex == 0) { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/setTableCellBackgroundColor.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/setTableCellBackgroundColor.ts index 258e3cbb24f..4711ade630b 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/setTableCellBackgroundColor.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/setTableCellBackgroundColor.ts @@ -55,8 +55,6 @@ export function setTableCellBackgroundColor( removeAdaptiveCellColor(cell); } } - - delete cell.cachedElement; } function removeAdaptiveCellColor(cell: ShallowMutableContentModelTableCell) { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/setTableCellBackgroundColorTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/setTableCellBackgroundColorTest.ts index ca884d9774a..d49ff3a3a52 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/setTableCellBackgroundColorTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/setTableCellBackgroundColorTest.ts @@ -1,22 +1,5 @@ -import { createTableCell as originalCreateTableCell } from 'roosterjs-content-model-dom'; +import { createTableCell } from 'roosterjs-content-model-dom'; import { setTableCellBackgroundColor } from '../../../lib/modelApi/editing/setTableCellBackgroundColor'; -import { - ContentModelTableCellFormat, - ShallowMutableContentModelTableCell, -} from 'roosterjs-content-model-types'; - -function createTableCell( - spanLeftOrColSpan?: boolean | number, - spanAboveOrRowSpan?: boolean | number, - isHeader?: boolean, - format?: ContentModelTableCellFormat -): ShallowMutableContentModelTableCell { - const cell = originalCreateTableCell(spanLeftOrColSpan, spanAboveOrRowSpan, isHeader, format); - - cell.cachedElement = {} as any; - - return cell; -} describe('setTableCellBackgroundColor', () => { it('Set to null', () => { diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts index 95a865d734e..faf8a0bc51e 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/hyphen/transformHyphen.ts @@ -1,8 +1,8 @@ import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; import type { - ContentModelParagraph, ContentModelText, FormatContentModelContext, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -10,7 +10,7 @@ import type { */ export function transformHyphen( previousSegment: ContentModelText, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext ): boolean { const segments = previousSegment.text.split(' '); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index ca39668f0b5..91d293953ac 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -1,10 +1,10 @@ import { matchLink } from 'roosterjs-content-model-api'; import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; import type { - ContentModelParagraph, ContentModelText, FormatContentModelContext, LinkData, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -12,7 +12,7 @@ import type { */ export function createLinkAfterSpace( previousSegment: ContentModelText, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext ) { const link = previousSegment.text.split(' ').pop(); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts index ef85ec409e8..14ee7c25f28 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/list/keyboardListTrigger.ts @@ -5,17 +5,17 @@ import { setModelListStyle, } from 'roosterjs-content-model-api'; import type { - ContentModelDocument, - ContentModelParagraph, FormatContentModelContext, + ReadonlyContentModelDocument, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** * @internal */ export function keyboardListTrigger( - model: ContentModelDocument, - paragraph: ContentModelParagraph, + model: ReadonlyContentModelDocument, + paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext, shouldSearchForBullet: boolean = true, shouldSearchForNumbering: boolean = true @@ -33,7 +33,7 @@ export function keyboardListTrigger( } const triggerList = ( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, listType: 'OL' | 'UL', styleType: number, index?: number diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformFraction.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformFraction.ts index 1c42add3ea7..905412bd217 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformFraction.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformFraction.ts @@ -1,8 +1,8 @@ import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; import type { - ContentModelParagraph, ContentModelText, FormatContentModelContext, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const FRACTIONS: Record = { @@ -16,7 +16,7 @@ const FRACTIONS: Record = { */ export function transformFraction( previousSegment: ContentModelText, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext ): boolean { const fraction = previousSegment.text.split(' ').pop()?.trim(); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformOrdinals.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformOrdinals.ts index 235a9424551..2bef05ac209 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformOrdinals.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/numbers/transformOrdinals.ts @@ -1,8 +1,8 @@ import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; import type { - ContentModelParagraph, ContentModelText, FormatContentModelContext, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const getOrdinal = (value: number) => { @@ -19,7 +19,7 @@ const getOrdinal = (value: number) => { */ export function transformOrdinals( previousSegment: ContentModelText, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext ): boolean { const value = previousSegment.text.split(' ').pop()?.trim(); diff --git a/packages/roosterjs-content-model-plugins/lib/customReplace/CustomReplacePlugin.ts b/packages/roosterjs-content-model-plugins/lib/customReplace/CustomReplacePlugin.ts index bedeefd966d..0ee0f9b1708 100644 --- a/packages/roosterjs-content-model-plugins/lib/customReplace/CustomReplacePlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/customReplace/CustomReplacePlugin.ts @@ -1,11 +1,11 @@ import { formatTextSegmentBeforeSelectionMarker } from 'roosterjs-content-model-api'; import type { - ContentModelParagraph, ContentModelText, EditorInputEvent, EditorPlugin, IEditor, PluginEvent, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -34,7 +34,7 @@ export interface CustomReplace { previousSegment: ContentModelText, stringToReplace: string, replacementString: string, - paragraph?: ContentModelParagraph + paragraph?: ShallowMutableContentModelParagraph ) => boolean; } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts index de316a958ab..b467d1eef05 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts @@ -1,4 +1,4 @@ -import { deleteSegment } from 'roosterjs-content-model-dom'; +import { deleteSegment, mutateBlock } from 'roosterjs-content-model-dom'; import type { DeleteSelectionStep } from 'roosterjs-content-model-types'; /** @@ -11,9 +11,10 @@ export const deleteAllSegmentBefore: DeleteSelectionStep = context => { const { paragraph, marker } = context.insertPoint; const index = paragraph.segments.indexOf(marker); + const mutableParagraph = mutateBlock(paragraph); for (let i = index - 1; i >= 0; i--) { - const segment = paragraph.segments[i]; + const segment = mutableParagraph.segments[i]; segment.isSelected = true; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts index 8148d5f9a66..86406c0e402 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts @@ -80,7 +80,7 @@ const insertNewLine = ( index: number ) => { const quoteLength = quote.blocks.length; - quote.blocks.splice(quoteLength - 1, 1); + mutateBlock(quote).blocks.splice(quoteLength - 1, 1); const marker = createSelectionMarker(); const newParagraph = createParagraph(false /* isImplicit */); newParagraph.segments.push(marker); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts b/packages/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts index a1b6e73c04e..f831316a291 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts @@ -1,9 +1,9 @@ import { normalizeContentModel } from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, DeleteResult, FormatContentModelContext, IEditor, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; /** @@ -12,7 +12,7 @@ import type { */ export function handleKeyboardEventResult( editor: IEditor, - model: ContentModelDocument, + model: ReadonlyContentModelDocument, rawEvent: KeyboardEvent, result: DeleteResult, context: FormatContentModelContext diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts index 52b1b8ff5df..7c274f2f0cc 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts @@ -5,11 +5,11 @@ import { handleTabOnTable } from './tabUtils/handleTabOnTable'; import { handleTabOnTableCell } from './tabUtils/handleTabOnTableCell'; import { setModelIndentation } from 'roosterjs-content-model-api'; import type { - ContentModelDocument, ContentModelListItem, ContentModelTableCell, FormatContentModelContext, IEditor, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; /** @@ -51,7 +51,7 @@ export function keyboardTab(editor: IEditor, rawEvent: KeyboardEvent) { * - If it is a list item, call handleTabOnList to handle the tab key. */ function handleTab( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, rawEvent: KeyboardEvent, context: FormatContentModelContext ) { diff --git a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts index 2cec2fefc77..010f73b9d0a 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnList.ts @@ -1,9 +1,9 @@ import { handleTabOnParagraph } from './handleTabOnParagraph'; import { setModelIndentation } from 'roosterjs-content-model-api'; import type { - ContentModelDocument, - ContentModelListItem, FormatContentModelContext, + ReadonlyContentModelDocument, + ReadonlyContentModelListItem, } from 'roosterjs-content-model-types'; /** @@ -12,8 +12,8 @@ import type { * @internal */ export function handleTabOnList( - model: ContentModelDocument, - listItem: ContentModelListItem, + model: ReadonlyContentModelDocument, + listItem: ReadonlyContentModelListItem, rawEvent: KeyboardEvent, context?: FormatContentModelContext ) { @@ -36,14 +36,14 @@ export function handleTabOnList( } } -function isMarkerAtStartOfBlock(listItem: ContentModelListItem) { +function isMarkerAtStartOfBlock(listItem: ReadonlyContentModelListItem) { return ( listItem.blocks[0].blockType == 'Paragraph' && listItem.blocks[0].segments[0].segmentType == 'SelectionMarker' ); } -function findSelectedParagraph(listItem: ContentModelListItem) { +function findSelectedParagraph(listItem: ReadonlyContentModelListItem) { return listItem.blocks.filter( block => block.blockType == 'Paragraph' && block.segments.some(segment => segment.isSelected) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts index 0a3c89bbcaa..1ab0c732fc5 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnParagraph.ts @@ -1,9 +1,14 @@ -import { createSelectionMarker, createText } from 'roosterjs-content-model-dom'; import { setModelIndentation } from 'roosterjs-content-model-api'; +import { + createSelectionMarker, + createText, + mutateBlock, + mutateSegment, +} from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, - ContentModelParagraph, FormatContentModelContext, + ReadonlyContentModelDocument, + ReadonlyContentModelParagraph, } from 'roosterjs-content-model-types'; const tabSpaces = '    '; @@ -25,8 +30,8 @@ const space = ' '; * 5. When the selection is not collapsed, but all segments are selected, call setModelIndention function to outdent the whole paragraph */ export function handleTabOnParagraph( - model: ContentModelDocument, - paragraph: ContentModelParagraph, + model: ReadonlyContentModelDocument, + paragraph: ReadonlyContentModelParagraph, rawEvent: KeyboardEvent, context?: FormatContentModelContext ) { @@ -70,7 +75,8 @@ export function handleTabOnParagraph( firstSelectedSegment.format ); const marker = createSelectionMarker(firstSelectedSegment.format); - paragraph.segments.splice( + + mutateBlock(paragraph).segments.splice( firstSelectedSegmentIndex, lastSelectedSegmentIndex - firstSelectedSegmentIndex + 1, spaceText, @@ -83,19 +89,25 @@ export function handleTabOnParagraph( const markerIndex = paragraph.segments.findIndex( segment => segment.segmentType === 'SelectionMarker' ); + if (!rawEvent.shiftKey) { const markerFormat = paragraph.segments[markerIndex].format; const tabText = createText(tabSpaces, markerFormat); - paragraph.segments.splice(markerIndex, 0, tabText); + + mutateBlock(paragraph).segments.splice(markerIndex, 0, tabText); } else { const tabText = paragraph.segments[markerIndex - 1]; const tabSpacesLength = tabSpaces.length; + if (tabText.segmentType == 'Text') { const tabSpaceTextLength = tabText.text.length - tabSpacesLength; + if (tabText.text === tabSpaces) { - paragraph.segments.splice(markerIndex - 1, 1); + mutateBlock(paragraph).segments.splice(markerIndex - 1, 1); } else if (tabText.text.substring(tabSpaceTextLength) === tabSpaces) { - tabText.text = tabText.text.substring(0, tabSpaceTextLength); + mutateSegment(paragraph, tabText, text => { + text.text = text.text.substring(0, tabSpaceTextLength); + }); } else { return false; } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTable.ts b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTable.ts index 5224a6c75ad..c512ea02c16 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTable.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTable.ts @@ -1,12 +1,15 @@ import { getFirstSelectedTable } from 'roosterjs-content-model-dom'; import { setModelIndentation } from 'roosterjs-content-model-api'; -import type { ContentModelDocument, ContentModelTable } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelDocument, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; /** * When the whole table is selected, indent or outdent the whole table with setModelIndentation. * @internal */ -export function handleTabOnTable(model: ContentModelDocument, rawEvent: KeyboardEvent) { +export function handleTabOnTable(model: ReadonlyContentModelDocument, rawEvent: KeyboardEvent) { const tableModel = getFirstSelectedTable(model)[0]; if (tableModel && isWholeTableSelected(tableModel)) { setModelIndentation(model, rawEvent.shiftKey ? 'outdent' : 'indent'); @@ -16,7 +19,7 @@ export function handleTabOnTable(model: ContentModelDocument, rawEvent: Keyboard return false; } -function isWholeTableSelected(tableModel: ContentModelTable) { +function isWholeTableSelected(tableModel: ReadonlyContentModelTable) { return ( tableModel.rows[0]?.cells[0]?.isSelected && tableModel.rows[tableModel.rows.length - 1]?.cells[tableModel.widths.length - 1]?.isSelected diff --git a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTableCell.ts b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTableCell.ts index 9e5387c9905..fb32e9023b6 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTableCell.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/tabUtils/handleTabOnTableCell.ts @@ -2,28 +2,36 @@ import { clearSelectedCells, insertTableRow } from 'roosterjs-content-model-api' import { createSelectionMarker, getFirstSelectedTable, + mutateBlock, normalizeTable, setParagraphNotImplicit, setSelection, } from 'roosterjs-content-model-dom'; -import type { ContentModelDocument, ContentModelTableCell } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelDocument, + ReadonlyContentModelTableCell, +} from 'roosterjs-content-model-types'; /** * When the cursor is on the last cell of a table, add new row and focus first new cell. * @internal */ export function handleTabOnTableCell( - model: ContentModelDocument, - cell: ContentModelTableCell, + model: ReadonlyContentModelDocument, + cell: ReadonlyContentModelTableCell, rawEvent: KeyboardEvent ) { - const tableModel = getFirstSelectedTable(model)[0]; + const readonlyTableModel = getFirstSelectedTable(model)[0]; + // Check if cursor is on last cell of the table if ( !rawEvent.shiftKey && - tableModel && - tableModel.rows[tableModel.rows.length - 1]?.cells[tableModel.widths.length - 1] === cell + readonlyTableModel && + readonlyTableModel.rows[readonlyTableModel.rows.length - 1]?.cells[ + readonlyTableModel.widths.length - 1 + ] === cell ) { + const tableModel = mutateBlock(readonlyTableModel); insertTableRow(tableModel, 'insertBelow'); // Clear Table selection @@ -40,7 +48,7 @@ export function handleTabOnTableCell( if (markerParagraph.blockType == 'Paragraph') { const marker = createSelectionMarker(model.format); - markerParagraph.segments.unshift(marker); + mutateBlock(markerParagraph).segments.unshift(marker); setParagraphNotImplicit(markerParagraph); setSelection(tableModel.rows[tableModel.rows.length - 1].cells[0], marker); } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts b/packages/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts index f99390a79bc..ad6a4a75258 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts @@ -1,9 +1,6 @@ import { isGeneralSegment } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, - ContentModelBlockGroup, ContentModelParagraph, - ContentModelSegment, ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, ReadonlyContentModelSegment, @@ -16,18 +13,18 @@ export type BlockAndPath = { /** * The sibling block */ - block: ContentModelBlock; + block: ReadonlyContentModelBlock; /** * Path of this sibling block */ - path: ContentModelBlockGroup[]; + path: ReadonlyContentModelBlockGroup[]; /** * If the input block is under a general segment, it is possible there are sibling segments under the same paragraph. * Use this property to return the sibling sibling under the same paragraph */ - siblingSegment?: ContentModelSegment; + siblingSegment?: ReadonlyContentModelSegment; }; /** @@ -55,8 +52,8 @@ export type ReadonlyBlockAndPath = { * @internal */ export function getLeafSiblingBlock( - path: ContentModelBlockGroup[], - block: ContentModelBlock, + path: ReadonlyContentModelBlockGroup[], + block: ReadonlyContentModelBlock, isNext: boolean ): BlockAndPath | null; diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index a3840002df5..403127175cf 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -8,6 +8,7 @@ import { setProcessor } from '../utils/setProcessor'; import type { WordMetadata } from './WordMetadata'; import type { BeforePasteEvent, + ContentModelBlockFormat, ContentModelListItemLevelFormat, ContentModelTableFormat, DomToModelContext, @@ -15,6 +16,10 @@ import type { FormatParser, } from 'roosterjs-content-model-types'; +const PERCENTAGE_REGEX = /%/; +// Default line height in browsers according to https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#normal +const DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE = 1.2; + /** * @internal * Handles Pasted content when source is Word Desktop @@ -27,6 +32,7 @@ export function processPastedContentFromWordDesktop( const metadataMap: Map = getStyleMetadata(ev, trustedHTMLHandler); setProcessor(ev.domToModelOption, 'element', wordDesktopElementProcessor(metadataMap)); + addParser(ev.domToModelOption, 'block', adjustPercentileLineHeight); addParser(ev.domToModelOption, 'block', removeNegativeTextIndentParser); addParser(ev.domToModelOption, 'listLevel', listLevelParser); addParser(ev.domToModelOption, 'container', wordTableParser); @@ -50,6 +56,20 @@ const wordDesktopElementProcessor = ( }; }; +function adjustPercentileLineHeight(format: ContentModelBlockFormat, element: HTMLElement): void { + //If the line height is less than the browser default line height, line between the text is going to be too narrow + let parsedLineHeight: number; + if ( + PERCENTAGE_REGEX.test(element.style.lineHeight) && + !isNaN((parsedLineHeight = parseInt(element.style.lineHeight))) + ) { + format.lineHeight = ( + DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE * + (parsedLineHeight / 100) + ).toString(); + } +} + function listLevelParser( format: ContentModelListItemLevelFormat, element: HTMLElement, diff --git a/packages/roosterjs-content-model-plugins/lib/picker/getQueryString.ts b/packages/roosterjs-content-model-plugins/lib/picker/getQueryString.ts index e5a581bb1e2..c52ddc76e9e 100644 --- a/packages/roosterjs-content-model-plugins/lib/picker/getQueryString.ts +++ b/packages/roosterjs-content-model-plugins/lib/picker/getQueryString.ts @@ -1,12 +1,15 @@ import { splitTextSegment } from '../pluginUtils/splitTextSegment'; -import type { ContentModelParagraph, ContentModelText } from 'roosterjs-content-model-types'; +import type { + ContentModelText, + ShallowMutableContentModelParagraph, +} from 'roosterjs-content-model-types'; /** * @internal */ export function getQueryString( triggerCharacter: string, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, previousSegment: ContentModelText, splittedSegmentResult?: ContentModelText[] ): string { diff --git a/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts b/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts index d407a8d04e0..4f7a034e8ae 100644 --- a/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts +++ b/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts @@ -1,12 +1,15 @@ import { createText } from 'roosterjs-content-model-dom'; -import type { ContentModelParagraph, ContentModelText } from 'roosterjs-content-model-types'; +import type { + ContentModelText, + ShallowMutableContentModelParagraph, +} from 'roosterjs-content-model-types'; /** * @internal */ export function splitTextSegment( textSegment: ContentModelText, - parent: ContentModelParagraph, + parent: ShallowMutableContentModelParagraph, start: number, end: number ): ContentModelText { diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts index 0a421907cdb..ed296dc3900 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/TableEditor.ts @@ -100,7 +100,10 @@ export class TableEditor { .some(feature => feature?.div == node); } - onMouseMove(x: number, y: number) { + /** + * public only for testing purposes + */ + public onMouseMove(x: number, y: number) { // Get whole table rect const tableRect = normalizeRect(this.table.getBoundingClientRect()); diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts index 759025c1493..8bf50431012 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/CellResizer.ts @@ -7,9 +7,10 @@ import { normalizeRect, MIN_ALLOWED_TABLE_CELL_WIDTH, normalizeTable, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; -import type { ContentModelTable, IEditor } from 'roosterjs-content-model-types'; +import type { IEditor, ReadonlyContentModelTable } from 'roosterjs-content-model-types'; const CELL_RESIZER_WIDTH = 4; /** @@ -45,18 +46,18 @@ export function createCellResizer( (anchorContainer || document.body).appendChild(div); - const context: DragAndDropContext = { editor, td, table, isRTL, zoomScale, onStart }; + const context: CellResizerContext = { editor, td, table, isRTL, zoomScale, onStart }; const setPosition = isHorizontal ? setHorizontalPosition : setVerticalPosition; setPosition(context, div); - const handler: DragAndDropHandler = { + const handler: DragAndDropHandler = { onDragStart, // Horizontal modifies row height, vertical modifies column width onDragging: isHorizontal ? onDraggingHorizontal : onDraggingVertical, onDragEnd: onEnd, }; - const featureHandler = new DragAndDropHelper( + const featureHandler = new DragAndDropHelper( div, context, setPosition, @@ -68,7 +69,11 @@ export function createCellResizer( return { node: td, div, featureHandler }; } -interface DragAndDropContext { +/** + * @internal + * Exported for testing + */ +export interface CellResizerContext { editor: IEditor; td: HTMLTableCellElement; table: HTMLTableElement; @@ -77,15 +82,23 @@ interface DragAndDropContext { onStart: () => void; } -interface DragAndDropInitValue { - cmTable: ContentModelTable | undefined; +/** + * @internal + * Exported for testing + */ +export interface CellResizerInitValue { + cmTable: ReadonlyContentModelTable | undefined; anchorColumn: number | undefined; anchorRow: number | undefined; anchorRowHeight: number; allWidths: number[]; } -function onDragStart(context: DragAndDropContext, event: MouseEvent): DragAndDropInitValue { +/** + * @internal + * Exported for testing + */ +export function onDragStart(context: CellResizerContext, event: MouseEvent): CellResizerInitValue { const { td, onStart } = context; const rect = normalizeRect(td.getBoundingClientRect()); @@ -131,10 +144,14 @@ function onDragStart(context: DragAndDropContext, event: MouseEvent): DragAndDro } } -function onDraggingHorizontal( - context: DragAndDropContext, +/** + * @internal + * Exported for testing + */ +export function onDraggingHorizontal( + context: CellResizerContext, event: MouseEvent, - initValue: DragAndDropInitValue, + initValue: CellResizerInitValue, deltaX: number, deltaY: number ) { @@ -144,7 +161,7 @@ function onDraggingHorizontal( // Assign new widths and heights to the CM table if (cmTable && anchorRow != undefined) { // Modify the CM Table size - cmTable.rows[anchorRow].height = (anchorRowHeight ?? 0) + deltaY; + mutateBlock(cmTable).rows[anchorRow].height = (anchorRowHeight ?? 0) + deltaY; // Normalize the table normalizeTable(cmTable); @@ -162,10 +179,14 @@ function onDraggingHorizontal( } } -function onDraggingVertical( - context: DragAndDropContext, +/** + * @internal + * Exported for testing + */ +export function onDraggingVertical( + context: CellResizerContext, event: MouseEvent, - initValue: DragAndDropInitValue, + initValue: CellResizerInitValue, deltaX: number ) { const { table, isRTL } = context; @@ -173,13 +194,15 @@ function onDraggingVertical( // Assign new widths and heights to the CM table if (cmTable && anchorColumn != undefined) { + const mutableTable = mutateBlock(cmTable); + // Modify the CM Table size const lastColumn = anchorColumn == cmTable.widths.length - 1; const change = deltaX * (isRTL ? -1 : 1); // This is the last column if (lastColumn) { // Only the last column changes - cmTable.widths[anchorColumn] = allWidths[anchorColumn] + change; + mutableTable.widths[anchorColumn] = allWidths[anchorColumn] + change; } else { // Any other two columns const anchorChange = allWidths[anchorColumn] + change; @@ -190,8 +213,8 @@ function onDraggingVertical( ) { return false; } - cmTable.widths[anchorColumn] = anchorChange; - cmTable.widths[anchorColumn + 1] = nextAnchorChange; + mutableTable.widths[anchorColumn] = anchorChange; + mutableTable.widths[anchorColumn + 1] = nextAnchorChange; } // Normalize the table @@ -211,7 +234,7 @@ function onDraggingVertical( } } -function setHorizontalPosition(context: DragAndDropContext, trigger: HTMLElement) { +function setHorizontalPosition(context: CellResizerContext, trigger: HTMLElement) { const { td } = context; const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { @@ -223,7 +246,7 @@ function setHorizontalPosition(context: DragAndDropContext, trigger: HTMLElement } } -function setVerticalPosition(context: DragAndDropContext, trigger: HTMLElement) { +function setVerticalPosition(context: CellResizerContext, trigger: HTMLElement) { const { td, isRTL } = context; const rect = normalizeRect(td.getBoundingClientRect()); if (rect) { diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts index 6e0dfeeb63d..b0d77d3d0f7 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableInserter.ts @@ -92,7 +92,11 @@ export function createTableInserter( return null; } -class TableInsertHandler implements Disposable { +/** + * @internal + * Exported for test only + */ +export class TableInsertHandler implements Disposable { private disposer: undefined | (() => void); constructor( private div: HTMLDivElement, diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts index cacc513e56e..6cef76ca3a3 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts @@ -6,21 +6,24 @@ import type { TableEditFeature } from './TableEditFeature'; import type { OnTableEditorCreatedCallback } from '../../OnTableEditorCreatedCallback'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; import { + cloneModel, createContentModelDocument, createSelectionMarker, getFirstSelectedTable, isNodeOfType, mergeModel, + mutateBlock, normalizeRect, setParagraphNotImplicit, setSelection, } from 'roosterjs-content-model-dom'; import type { - ContentModelTable, DOMInsertPoint, DOMSelection, IEditor, + ReadonlyContentModelTable, Rect, + ShallowMutableContentModelDocument, } from 'roosterjs-content-model-types'; const TABLE_MOVER_LENGTH = 12; @@ -124,7 +127,7 @@ export interface TableMoverContext { * Exported for testing */ export interface TableMoverInitValue { - cmTable: ContentModelTable | undefined; + cmTable: ReadonlyContentModelTable | undefined; initialSelection: DOMSelection | null; tableRect: HTMLDivElement; } @@ -325,14 +328,14 @@ export function onDragEnd( const [oldTable, path] = getFirstSelectedTable(model); if (oldTable) { const index = path[0].blocks.indexOf(oldTable); - path[0].blocks.splice(index, 1); + mutateBlock(path[0]).blocks.splice(index, 1); } if (ip && initValue?.cmTable) { // Insert new table - const doc = createContentModelDocument(); - doc.blocks.push(initValue.cmTable); - insertionSuccess = !!mergeModel(model, doc, context, { + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); + doc.blocks.push(mutateBlock(initValue.cmTable)); + insertionSuccess = !!mergeModel(model, cloneModel(doc), context, { mergeFormat: 'none', insertPosition: ip, }); @@ -347,7 +350,7 @@ export function onDragEnd( if (markerParagraph?.blockType == 'Paragraph') { const marker = createSelectionMarker(model.format); - markerParagraph.segments.unshift(marker); + mutateBlock(markerParagraph).segments.unshift(marker); setParagraphNotImplicit(markerParagraph); setSelection(FirstCell, marker); } diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts index 9e256cad719..144af8ea229 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableResizer.ts @@ -1,10 +1,15 @@ import { createElement } from '../../../pluginUtils/CreateElement/createElement'; import { DragAndDropHelper } from '../../../pluginUtils/DragAndDrop/DragAndDropHelper'; import { getCMTableFromTable } from '../utils/getTableFromContentModel'; -import { isNodeOfType, normalizeRect, normalizeTable } from 'roosterjs-content-model-dom'; +import { + isNodeOfType, + mutateBlock, + normalizeRect, + normalizeTable, +} from 'roosterjs-content-model-dom'; import type { TableEditFeature } from './TableEditFeature'; import type { OnTableEditorCreatedCallback } from '../../OnTableEditorCreatedCallback'; -import type { ContentModelTable, IEditor, Rect } from 'roosterjs-content-model-types'; +import type { IEditor, ReadonlyContentModelTable, Rect } from 'roosterjs-content-model-types'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; const TABLE_RESIZER_LENGTH = 12; @@ -49,7 +54,7 @@ export function createTableResizer( (anchorContainer || document.body).appendChild(div); - const context: DragAndDropContext = { + const context: TableResizerContext = { isRTL, table, zoomScale, @@ -79,14 +84,14 @@ export function createTableResizer( return { node: table, div, featureHandler }; } -class TableResizer extends DragAndDropHelper { +class TableResizer extends DragAndDropHelper { private disposer: undefined | (() => void); constructor( trigger: HTMLElement, - context: DragAndDropContext, - onSubmit: (context: DragAndDropContext, trigger: HTMLElement) => void, - handler: DragAndDropHandler, + context: TableResizerContext, + onSubmit: (context: TableResizerContext, trigger: HTMLElement) => void, + handler: DragAndDropHandler, zoomScale: number, forceMobile?: boolean, onTableEditorCreated?: OnTableEditorCreatedCallback @@ -102,7 +107,11 @@ class TableResizer extends DragAndDropHelper { diff --git a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index e6db9e6eca4..c7a175d4093 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -58,7 +58,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 4); + expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index f14038afee5..07f17b4039d 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -144,7 +144,7 @@ describe('processPastedContentFromWordDesktopTest', () => { runTest(source, { blockGroupType: 'Document', blocks: [] }, true); }); - it('Dont remove Line height less than default', () => { + it('adjust Line height less than default', () => { let source = '

Test

'; runTest( source, @@ -154,7 +154,7 @@ describe('processPastedContentFromWordDesktopTest', () => { { segments: [{ text: 'Test', segmentType: 'Text', format: {} }], blockType: 'Paragraph', - format: { marginTop: '1em', marginBottom: '1em', lineHeight: '102%' }, + format: { marginTop: '1em', marginBottom: '1em', lineHeight: '1.224' }, decorator: { tagName: 'p', format: {} }, }, ], @@ -211,7 +211,7 @@ describe('processPastedContentFromWordDesktopTest', () => { }); }); - it('Remove Line height, percentage greater than default', () => { + it('Adjust Line height, percentage greater than default 2', () => { let source = '

Test

'; runTest(source, { blockGroupType: 'Document', @@ -222,7 +222,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: {}, tagName: 'p', }, - format: { marginTop: '1em', marginBottom: '1em', lineHeight: '122%' }, + format: { marginTop: '1em', marginBottom: '1em', lineHeight: '1.464' }, segments: [ { segmentType: 'Text', diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts index caa8e27a6a9..d7f0e5f2d37 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/TableEditTestHelper.ts @@ -1,14 +1,10 @@ import * as TestHelper from '../TestHelper'; +import { ContentModelTable, IEditor } from 'roosterjs-content-model-types'; import { DOMEventHandlerFunction } from 'roosterjs-editor-types'; -import { getObjectKeys, normalizeTable } from 'roosterjs-content-model-dom'; +import { normalizeTable } from 'roosterjs-content-model-dom'; import { TableEditFeatureName } from '../../lib/tableEdit/editors/features/TableEditFeatureName'; +import { TableEditor } from '../../lib/tableEdit/editors/TableEditor'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; -import { - ContentModelTable, - DOMEventRecord, - EditorCore, - IEditor, -} from 'roosterjs-content-model-types'; /** * Function to be called before each Table Edit test @@ -23,37 +19,17 @@ export function beforeTableTest( ) { const plugin = new TableEditPlugin('.' + anchorContainerSelector, undefined, disabledFeatures); - let handler: Record = {}; - const attachDomEvent = jasmine - .createSpy('attachDomEvent') - .and.callFake((core: EditorCore, eventMap: Record>) => { - getObjectKeys(eventMap || {}).forEach(key => { - const eventname = key as keyof HTMLElementEventMap; - const { beforeDispatch } = eventMap[key]; - const onEvent = (event: HTMLElementEventMap[typeof eventname]) => { - beforeDispatch && beforeDispatch(event); - }; - handler[eventname] = onEvent; - }); - return () => { - handler = {}; - }; - }); - - const coreApiOverride = { - attachDomEvent, - }; const editor = TestHelper.initEditor( TEST_ID, [plugin], undefined, - coreApiOverride, + undefined, anchorContainerSelector ); plugin.initialize(editor); - return { editor, plugin, handler }; + return { editor, plugin }; } /** @@ -62,7 +38,11 @@ export function beforeTableTest( * @param plugin The plugin to be disposed * @param TEST_ID The id of the editor div */ -export function afterTableTest(editor: IEditor, plugin: TableEditPlugin, TEST_ID: string) { +export function afterTableTest( + editor: IEditor, + plugin: TableEditor | TableEditPlugin, + TEST_ID: string +) { plugin.dispose(); !editor.isDisposed() && editor.dispose(); TestHelper.removeElement(TEST_ID); diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/cellResizerTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/cellResizerTest.ts index d1a47fbc6b5..8ee0b13b051 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/cellResizerTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/cellResizerTest.ts @@ -1,157 +1,252 @@ -import { ContentModelTable, DOMEventHandlerFunction, IEditor } from 'roosterjs-content-model-types'; +import { ContentModelTable, EditorOptions, IEditor } from 'roosterjs-content-model-types'; +import { Editor } from 'roosterjs-content-model-core'; +import { getCMTableFromTable } from '../../lib/tableEdit/editors/utils/getTableFromContentModel'; +import { getCurrentTable } from './TableEditTestHelper'; import { getModelTable } from './tableData'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; import { - afterTableTest, - beforeTableTest, - getCellRect, - getCurrentTable, - initialize, - moveAndResize, -} from './TableEditTestHelper'; + CellResizerContext, + CellResizerInitValue, + onDragStart, + onDraggingHorizontal, + onDraggingVertical, +} from '../../lib/tableEdit/editors/features/CellResizer'; describe('Cell Resizer tests', () => { let editor: IEditor; - let plugin: TableEditPlugin; - const TEST_ID = 'cellResizerTest'; - let handler: Record; + let id = 'tableCellResizerContainerId'; + let targetId = 'tableCellResizerTestId'; + let tableEdit: TableEditPlugin; + let node: HTMLDivElement; + const cmTable: ContentModelTable = getModelTable(targetId); beforeEach(() => { - const setup = beforeTableTest(TEST_ID); - editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; + document.body.innerHTML = ''; + node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + tableEdit = new TableEditPlugin(); + + let options: EditorOptions = { + plugins: [tableEdit], + initialModel: { + blockGroupType: 'Document', + blocks: [{ ...cmTable }], + format: {}, + }, + }; + + editor = new Editor(node, options); }); afterEach(() => { - afterTableTest(editor, plugin, TEST_ID); + editor.dispose(); + const div = document.getElementById(id); + div?.parentNode?.removeChild(div); + node.parentElement?.removeChild(node); }); - /************************ Resizing row related tests ************************/ - - function resizeRowTest( - table: ContentModelTable, - growth: number, - cellRow: number, - cellColumn: number - ) { - initialize(editor, table); - const delta = 50 * growth; - const cellRect = getCellRect(editor, cellRow, cellColumn); - const targetPos: number = cellRect.bottom + delta; - - const beforeHeight = getCurrentTable(editor).rows[cellRow].getBoundingClientRect().height; - moveAndResize( - { x: cellRect.left + cellRect.width / 2, y: cellRect.bottom }, - { x: cellRect.left + cellRect.width / 2, y: targetPos }, - 'horizontal', - editor, - handler, - TEST_ID - ); - const afterHeight = getCurrentTable(editor).rows[cellRow].getBoundingClientRect().height; - - growth > 0 - ? expect(afterHeight).toBeGreaterThan(beforeHeight) - : expect(afterHeight).toBeLessThan(beforeHeight); - } - - it('increases the height of the first row', () => { - resizeRowTest(getModelTable(), 1, 0, 0); - }); - - it('increases the height of the last row', () => { - const MODEL_TABLE = getModelTable(); - resizeRowTest(MODEL_TABLE, 1, MODEL_TABLE.rows.length - 1, MODEL_TABLE.widths.length - 1); - }); + it('Resize - onDragStart', () => { + //Arrange + node.style.height = '500px'; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + if (!target) { + fail('Table not found'); + return; + } - it('decreases the height of the first row', () => { - resizeRowTest(getModelTable(), -1, 0, 0); + const targetTd = (target as HTMLTableElement).rows[0].cells[0]; + + const onStartSpy = jasmine.createSpy('onStart'); + const context: CellResizerContext = { + editor: editor, + td: targetTd as HTMLTableCellElement, + table: target as HTMLTableElement, + isRTL: false, + zoomScale: 1, + onStart: onStartSpy, + }; + const editorCMTable = getCMTableFromTable(editor, target as HTMLTableElement); + + //Act + const initvalue = onDragStart(context, {} as MouseEvent); + + //Assert + expect(onStartSpy).toHaveBeenCalled(); + expect(initvalue.cmTable).toEqual(editorCMTable); + expect(initvalue.allWidths).toEqual(editorCMTable.widths); + expect(initvalue.anchorColumn).toEqual(0); + expect(initvalue.anchorRow).toEqual(0); + expect(initvalue.anchorRowHeight).toEqual(editorCMTable.rows[0].height); }); - it('decreases the height of the last row', () => { - const MODEL_TABLE = getModelTable(); - resizeRowTest(MODEL_TABLE, -1, MODEL_TABLE.rows.length - 1, MODEL_TABLE.widths.length - 1); - }); + describe('Resize - onDragging', () => { + /************************ Resizing row related tests ************************/ + + function resizeRowTest(growth: number, cellRow: number, cellColumn: number) { + //Arrange + node.style.height = '500px'; + node.style.width = '500px'; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + if (!target) { + fail('Table not found'); + return; + } + + const initValue: CellResizerInitValue = { + cmTable: cmTable, + anchorColumn: cellColumn, + anchorRow: cellRow, + anchorRowHeight: cmTable.rows[cellRow].height, + allWidths: cmTable.widths, + }; + + const targetTd = (target as HTMLTableElement).rows[cellRow].cells[cellColumn]; + + const onStartSpy = jasmine.createSpy('onStart'); + const context: CellResizerContext = { + editor: editor, + td: targetTd as HTMLTableCellElement, + table: target as HTMLTableElement, + isRTL: false, + zoomScale: 1, + onStart: onStartSpy, + }; + const delta = 10 * growth; + const beforeHeight = getCurrentTable(editor).rows[cellRow].getBoundingClientRect() + .height; + + //Act + const dragHResult = onDraggingHorizontal( + context, + {} as MouseEvent, + initValue, + 0, + delta + ); + + //Assert + const afterHeight = getCurrentTable(editor).rows[cellRow].getBoundingClientRect() + .height; + expect(dragHResult).toBeTrue(); + growth > 0 + ? expect(afterHeight).toBeGreaterThan(beforeHeight) + : expect(afterHeight).toBeLessThan(beforeHeight); + } - /************************ Resizing column related tests ************************/ - - function resizeColumnTest( - table: ContentModelTable, - direction: number, - cellRow: number, - cellColumn: number - ) { - initialize(editor, table); - const delta = 20 * direction; - const cellRect = getCellRect(editor, cellRow, cellColumn); - const targetPos: number = cellRect.right + delta; - - const beforeWidth = getCurrentTable(editor).rows[cellRow].cells[ - cellColumn - ].getBoundingClientRect().width; - const beforeNextWidth = - cellColumn < table.widths.length - 1 - ? getCurrentTable(editor).rows[cellRow].cells[ - cellColumn + 1 - ].getBoundingClientRect().width - : undefined; - - moveAndResize( - { x: cellRect.right, y: cellRect.top + cellRect.height / 2 }, - { x: targetPos, y: cellRect.top + cellRect.height / 2 }, - 'vertical', - editor, - handler, - TEST_ID - ); - - const afterWidth = getCurrentTable(editor).rows[cellRow].cells[ - cellColumn - ].getBoundingClientRect().width; - const afterNextWidth = - cellColumn < table.widths.length - 1 - ? getCurrentTable(editor).rows[cellRow].cells[ - cellColumn + 1 - ].getBoundingClientRect().width - : undefined; - - direction > 0 - ? expect(afterWidth).toBeGreaterThan(beforeWidth) - : expect(afterWidth).toBeLessThan(beforeWidth); - - if (beforeNextWidth && afterNextWidth) { - direction > 0 - ? expect(afterNextWidth).toBeLessThan(beforeNextWidth) - : expect(afterNextWidth).toBeGreaterThan(beforeNextWidth); + it('increases the height of the first row', () => { + resizeRowTest(1, 0, 0); + }); + + it('increases the height of the last row', () => { + const MODEL_TABLE = cmTable; + resizeRowTest(1, MODEL_TABLE.rows.length - 1, MODEL_TABLE.widths.length - 1); + }); + + it('decreases the height of the first row', () => { + resizeRowTest(-1, 0, 0); + }); + + it('decreases the height of the last row', () => { + const MODEL_TABLE = cmTable; + resizeRowTest(-1, MODEL_TABLE.rows.length - 1, MODEL_TABLE.widths.length - 1); + }); + + /************************ Resizing column related tests ************************/ + + function resizeColumnTest(growth: number, cellRow: number, cellColumn: number) { + //Arrange + node.style.height = '500px'; + node.style.width = '500px'; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + if (!target) { + fail('Table not found'); + return; + } + + const initValue: CellResizerInitValue = { + cmTable: cmTable, + anchorColumn: cellColumn, + anchorRow: cellRow, + anchorRowHeight: cmTable.rows[cellRow].height, + allWidths: cmTable.widths, + }; + + const targetTd = (target as HTMLTableElement).rows[cellRow].cells[cellColumn]; + + const onStartSpy = jasmine.createSpy('onStart'); + const context: CellResizerContext = { + editor: editor, + td: targetTd as HTMLTableCellElement, + table: target as HTMLTableElement, + isRTL: false, + zoomScale: 1, + onStart: onStartSpy, + }; + const delta = 10 * growth; + const beforeWidth = getCurrentTable(editor).rows[cellRow].cells[ + cellColumn + ].getBoundingClientRect().width; + const beforeNextWidth = + cellColumn < cmTable.widths.length - 1 + ? getCurrentTable(editor).rows[cellRow].cells[ + cellColumn + 1 + ].getBoundingClientRect().width + : undefined; + + //Act + const dragVResult = onDraggingVertical(context, {} as MouseEvent, initValue, delta); + + //Assert + const afterWidth = getCurrentTable(editor).rows[cellRow].cells[ + cellColumn + ].getBoundingClientRect().width; + const afterNextWidth = + cellColumn < cmTable.widths.length - 1 + ? getCurrentTable(editor).rows[cellRow].cells[ + cellColumn + 1 + ].getBoundingClientRect().width + : undefined; + expect(dragVResult).toBeTrue(); + growth > 0 + ? expect(afterWidth).toBeGreaterThan(beforeWidth) + : expect(afterWidth).toBeLessThan(beforeWidth); + + if (beforeNextWidth && afterNextWidth) { + growth > 0 + ? expect(afterNextWidth).toBeLessThan(beforeNextWidth) + : expect(afterNextWidth).toBeGreaterThan(beforeNextWidth); + } } - } - it('increases the width of the first column', () => { - resizeColumnTest(getModelTable(), 1, 0, 0); - }); + it('increases the width of the first column', () => { + resizeColumnTest(1, 0, 0); + }); - it('increases the width of the last column', () => { - const MODEL_TABLE = getModelTable(); - resizeColumnTest( - MODEL_TABLE, - 1, - MODEL_TABLE.rows.length - 1, - MODEL_TABLE.widths.length - 1 - ); - }); + it('increases the width of the last column', () => { + const MODEL_TABLE = cmTable; + resizeColumnTest(1, MODEL_TABLE.rows.length - 1, MODEL_TABLE.widths.length - 1); + }); - it('decreases the width of the first column', () => { - resizeColumnTest(getModelTable(), -1, 0, 0); - }); + it('decreases the width of the first column', () => { + resizeColumnTest(-1, 0, 0); + }); - it('decreases the width of the last column', () => { - const MODEL_TABLE = getModelTable(); - resizeColumnTest( - MODEL_TABLE, - -1, - MODEL_TABLE.rows.length - 1, - MODEL_TABLE.widths.length - 1 - ); + it('decreases the width of the last column', () => { + const MODEL_TABLE = cmTable; + resizeColumnTest(-1, MODEL_TABLE.rows.length - 1, MODEL_TABLE.widths.length - 1); + }); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableData.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableData.ts index 802f58c1713..09e97fe36e5 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableData.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableData.ts @@ -12,7 +12,7 @@ export const DEFAULT_TABLE_MERGED = /** * Regular 3 x 3 Table */ -export function getModelTable(): ContentModelTable { +export function getModelTable(id?: string): ContentModelTable { /* * —————————————— * | a1 | b1 | c1 | @@ -233,7 +233,7 @@ export function getModelTable(): ContentModelTable { ], }, ], - format: {}, + format: id ? { id: id } : {}, widths: [50, 50, 50], dataset: {}, }; @@ -242,7 +242,7 @@ export function getModelTable(): ContentModelTable { /** * 3 x 3 Table with merged central column */ -export function getMergedCenterColumnTable(): ContentModelTable { +export function getMergedCenterColumnTable(id?: string): ContentModelTable { /* * —————————————— * | a1 | | c1 | @@ -439,7 +439,7 @@ export function getMergedCenterColumnTable(): ContentModelTable { ], }, ], - format: {}, + format: id ? { id: id } : {}, widths: [50, 50, 50], dataset: {}, }; @@ -448,7 +448,7 @@ export function getMergedCenterColumnTable(): ContentModelTable { /** * 3 x 3 Table with merged central row */ -export function getMergedCenterRowTable(): ContentModelTable { +export function getMergedCenterRowTable(id?: string): ContentModelTable { /* * —————————————— * | a1 | b1 | c1 | @@ -645,7 +645,7 @@ export function getMergedCenterRowTable(): ContentModelTable { ], }, ], - format: {}, + format: id ? { id: id } : {}, widths: [50, 50, 50], dataset: {}, }; @@ -654,7 +654,7 @@ export function getMergedCenterRowTable(): ContentModelTable { /** * 2 x 2 Table with merged top row */ -export function getMergedTopRowTable(): ContentModelTable { +export function getMergedTopRowTable(id?: string): ContentModelTable { /* * ————————— * | a1 | @@ -750,7 +750,7 @@ export function getMergedTopRowTable(): ContentModelTable { ], }, ], - format: {}, + format: id ? { id: id } : {}, widths: [50, 50], dataset: {}, }; @@ -759,7 +759,7 @@ export function getMergedTopRowTable(): ContentModelTable { /** * 2 x 2 Table with merged first column */ -export function getMergedFirstColumnTable(): ContentModelTable { +export function getMergedFirstColumnTable(id?: string): ContentModelTable { /* * ————————— * | a1 | b1 | @@ -855,7 +855,7 @@ export function getMergedFirstColumnTable(): ContentModelTable { ], }, ], - format: {}, + format: id ? { id: id } : {}, widths: [50, 50], dataset: {}, }; diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts index f9981981a22..df3a3aca3b6 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditPluginTest.ts @@ -45,6 +45,34 @@ describe('TableEditPlugin', () => { document.body = document.createElement('body'); }); + it('onPluginEvent - input', () => { + const disposerSpy = spyOn(plugin, 'setTableEditor').and.callThrough(); + plugin.onPluginEvent({ eventType: 'input', rawEvent: null }); + expect(disposerSpy).toHaveBeenCalledWith(null); + }); + + it('onPluginEvent - contentChanged', () => { + const disposerSpy = spyOn(plugin, 'setTableEditor').and.callThrough(); + plugin.onPluginEvent({ eventType: 'contentChanged', source: null }); + expect(disposerSpy).toHaveBeenCalledWith(null); + }); + + it('onPluginEvent - scroll', () => { + const disposerSpy = spyOn(plugin, 'setTableEditor').and.callThrough(); + plugin.onPluginEvent({ + eventType: 'scroll', + rawEvent: null, + scrollContainer: editor.getScrollContainer(), + }); + expect(disposerSpy).toHaveBeenCalledWith(null); + }); + + it('onPluginEvent - zoomChanged', () => { + const disposerSpy = spyOn(plugin, 'setTableEditor').and.callThrough(); + plugin.onPluginEvent({ eventType: 'zoomChanged', newZoomScale: 1 }); + expect(disposerSpy).toHaveBeenCalledWith(null); + }); + it('setTableEditor - Dismiss table editor on mouse out', () => { const ele = createElement( { diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts index 22122bdf5bd..7cbe0623633 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableEditorTest.ts @@ -1,133 +1,178 @@ -import { DOMEventHandlerFunction, IEditor } from 'roosterjs-content-model-types'; -import { getModelTable } from './tableData'; +import { afterTableTest, beforeTableTest, getCellRect, initialize } from './TableEditTestHelper'; +import { ContentModelTable, IEditor } from 'roosterjs-content-model-types'; +import { getMergedFirstColumnTable, getMergedTopRowTable, getModelTable } from './tableData'; import { TABLE_MOVER_ID } from '../../lib/tableEdit/editors/features/TableMover'; import { TABLE_RESIZER_ID } from '../../lib/tableEdit/editors/features/TableResizer'; import { TableEditFeatureName } from '../../lib/tableEdit/editors/features/TableEditFeatureName'; -import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; +import { TableEditor } from '../../lib/tableEdit/editors/TableEditor'; import { HORIZONTAL_INSERTER_ID, VERTICAL_INSERTER_ID, } from '../../lib/tableEdit/editors/features/TableInserter'; -import { - afterTableTest, - beforeTableTest, - getCellRect, - initialize, - mouseToPoint, -} from './TableEditTestHelper'; import { HORIZONTAL_RESIZER_ID, VERTICAL_RESIZER_ID, } from '../../lib/tableEdit/editors/features/CellResizer'; -describe('TableEdit', () => { +describe('TableEditor', () => { describe('disableFeatures', () => { const insideTheOffset = 5; let editor: IEditor; - let plugin: TableEditPlugin; - let handler: Record; + let table: HTMLTableElement; + let tEditor: TableEditor; const TEST_ID = 'test'; - function runDisableFeatureSetup(featuresToDisable: TableEditFeatureName[]) { - // Create editor, plugin, and table + function runDisableFeatureSetup( + cmTable: ContentModelTable, + featuresToDisable: TableEditFeatureName[], + anchorContainer?: HTMLElement + ) { const setup = beforeTableTest(TEST_ID, undefined, featuresToDisable); editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; - return initialize(editor, getModelTable()); + const rect = initialize(editor, cmTable); + table = editor.getDOMHelper().queryElements('table')[0]; + const contentDiv = editor.getDocument().getElementById(TEST_ID); + tEditor = new TableEditor( + editor, + table, + () => {}, + anchorContainer, + contentDiv, + undefined, + featuresToDisable + ); + return rect; } + /************************ Disable features tests ************************/ + it('Disable Horizontal Inserter', () => { - const tableRect = runDisableFeatureSetup(['HorizontalTableInserter']); + const tableRect = runDisableFeatureSetup(getModelTable(), ['HorizontalTableInserter']); // Move mouse to bottom left of table - mouseToPoint({ x: tableRect.left - insideTheOffset, y: tableRect.bottom }, handler); + tEditor.onMouseMove(tableRect.left - insideTheOffset, tableRect.bottom); const feature = editor.getDocument().getElementById(HORIZONTAL_INSERTER_ID); expect(!!feature).toBe(false); }); it('Disable Vertical Inserter', () => { - const tableRect = runDisableFeatureSetup(['VerticalTableInserter']); + const tableRect = runDisableFeatureSetup(getModelTable(), ['VerticalTableInserter']); // Move mouse to top right of table - mouseToPoint({ x: tableRect.right, y: tableRect.top - insideTheOffset }, handler); + tEditor.onMouseMove(tableRect.right, tableRect.top - insideTheOffset); const feature = editor.getDocument().getElementById(VERTICAL_INSERTER_ID); expect(!!feature).toBe(false); }); it('Disable Horizontal Resizer', () => { - const tableRect = runDisableFeatureSetup(['CellResizer']); + const tableRect = runDisableFeatureSetup(getModelTable(), ['CellResizer']); // Move mouse to bottom right of table - mouseToPoint({ x: tableRect.right - insideTheOffset, y: tableRect.bottom }, handler); + tEditor.onMouseMove(tableRect.right - insideTheOffset, tableRect.bottom); const feature = editor.getDocument().getElementById(HORIZONTAL_RESIZER_ID); expect(!!feature).toBe(false); }); it('Disable Vertical Resizer', () => { - const tableRect = runDisableFeatureSetup(['CellResizer']); + const tableRect = runDisableFeatureSetup(getModelTable(), ['CellResizer']); // Move mouse to bottom right of table - mouseToPoint({ x: tableRect.right, y: tableRect.bottom - insideTheOffset }, handler); + tEditor.onMouseMove(tableRect.right, tableRect.bottom - insideTheOffset); const feature = editor.getDocument().getElementById(VERTICAL_RESIZER_ID); expect(!!feature).toBe(false); }); it('Disable Table Resizer', () => { - const tableRect = runDisableFeatureSetup(['TableResizer']); + const tableRect = runDisableFeatureSetup(getModelTable(), ['TableResizer']); // Move mouse to center of table - mouseToPoint( - { - x: tableRect.left + tableRect.width / 2, - y: tableRect.top + tableRect.height / 2, - }, - handler + tEditor.onMouseMove( + tableRect.left + tableRect.width / 2, + tableRect.top + tableRect.height / 2 ); const feature = editor.getDocument().getElementById(TABLE_RESIZER_ID); expect(!!feature).toBe(false); }); - //Not reliable - xit('Disable Table Mover', () => { - const tableRect = runDisableFeatureSetup(['TableMover', 'TableSelector']); + it('Disable Table Mover', () => { + const tableRect = runDisableFeatureSetup(getModelTable(), [ + 'TableMover', + 'TableSelector', + ]); // Move mouse to center of table - mouseToPoint( - { - x: tableRect.left + tableRect.width / 2, - y: tableRect.top + tableRect.height / 2, - }, - handler + tEditor.onMouseMove( + tableRect.left + tableRect.width / 2, + tableRect.top + tableRect.height / 2 ); const feature = editor.getDocument().getElementById(TABLE_MOVER_ID); expect(!!feature).toBe(false); }); + /************************ Table Inserter tests ************************/ + + it('Not add table inserter if cursor on top left corner', () => { + const tableRect = runDisableFeatureSetup(getModelTable(), []); + // Move mouse to top left of table + tEditor.onMouseMove(tableRect.left - insideTheOffset, tableRect.top - insideTheOffset); + const featureH = editor.getDocument().getElementById(HORIZONTAL_INSERTER_ID); + const featureV = editor.getDocument().getElementById(VERTICAL_INSERTER_ID); + expect(!!featureH).toBe(false); + expect(!!featureV).toBe(false); + }); + + it('Not add table inserter if cursor on top middle of merged top row', () => { + const tableRect = runDisableFeatureSetup(getMergedTopRowTable(), []); + // Move mouse to top middle of table + tEditor.onMouseMove( + tableRect.left + tableRect.width / 2, + tableRect.top - insideTheOffset + ); + const featureH = editor.getDocument().getElementById(HORIZONTAL_INSERTER_ID); + const featureV = editor.getDocument().getElementById(VERTICAL_INSERTER_ID); + expect(!!featureH).toBe(false); + expect(!!featureV).toBe(false); + }); + + it('Add table inserter if cursor on left middle of merged first column', () => { + const tableRect = runDisableFeatureSetup(getMergedFirstColumnTable(), []); + // Move mouse to left middle of table + tEditor.onMouseMove( + tableRect.left - insideTheOffset, + tableRect.top + tableRect.height / 2 + ); + const featureH = editor.getDocument().getElementById(HORIZONTAL_INSERTER_ID); + const featureV = editor.getDocument().getElementById(VERTICAL_INSERTER_ID); + expect(!!featureH).toBe(true); + expect(!!featureV).toBe(false); + }); + afterEach(() => { - afterTableTest(editor, plugin, TEST_ID); + afterTableTest(editor, tEditor, TEST_ID); }); }); describe('anchorContainer', () => { let editor: IEditor; - let plugin: TableEditPlugin; + let tEditor: TableEditor; + let table: any; const TEST_ID = 'cellResizerTest'; const ANCHOR_CLASS = 'anchor_' + TEST_ID; - let handler: Record; afterEach(() => { - afterTableTest(editor, plugin, TEST_ID); + afterTableTest(editor, tEditor, TEST_ID); }); it('Table editor features, resizer and mover, inserted on anchor', () => { - // Create editor, plugin, and table const setup = beforeTableTest(TEST_ID, ANCHOR_CLASS); editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; initialize(editor, getModelTable()); + table = editor.getDOMHelper().queryElements('table')[0]; + const contentDiv = editor.getDocument().getElementById(TEST_ID); + const anchor = editor + .getDocument() + .getElementsByClassName(ANCHOR_CLASS)[0] as HTMLElement; + tEditor = new TableEditor(editor, table, () => {}, anchor, contentDiv, undefined); // Move mouse to the first cell const cellRect = getCellRect(editor, 0, 0); - mouseToPoint({ x: cellRect.left, y: cellRect.bottom }, handler); + tEditor.onMouseMove(cellRect.left, cellRect.bottom); // Look for table mover and resizer on the anchor - const anchor = editor.getDocument().getElementsByClassName(ANCHOR_CLASS)[0]; const mover = anchor?.querySelector('#' + TABLE_MOVER_ID); const resizer = anchor?.querySelector('#' + TABLE_RESIZER_ID); expect(!!mover).toBe(true); @@ -135,19 +180,21 @@ describe('TableEdit', () => { }); it('Table editor features, resizer and mover, not inserted on anchor', () => { - // Create editor, plugin, and table const setup = beforeTableTest(TEST_ID); editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; initialize(editor, getModelTable()); + table = editor.getDOMHelper().queryElements('table')[0]; + const contentDiv = editor.getDocument().getElementById(TEST_ID); + const anchor = editor + .getDocument() + .getElementsByClassName(ANCHOR_CLASS)[0] as HTMLElement; + tEditor = new TableEditor(editor, table, () => {}, anchor, contentDiv, undefined); // Move mouse to the first cell const cellRect = getCellRect(editor, 0, 0); - mouseToPoint({ x: cellRect.left, y: cellRect.bottom }, handler); + tEditor.onMouseMove(cellRect.left, cellRect.bottom); // Look for table mover and resizer on the anchor - const anchor = editor.getDocument().getElementsByClassName(ANCHOR_CLASS)[0]; const mover = anchor?.querySelector('#' + TABLE_MOVER_ID); const resizer = anchor?.querySelector('#' + TABLE_RESIZER_ID); expect(!!mover).toBe(false); diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts index 74386776bf7..0e416eb3a48 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts @@ -1,124 +1,108 @@ import * as getIntersectedRect from '../../lib/pluginUtils/Rect/getIntersectedRect'; -import { DOMEventHandlerFunction, IEditor } from 'roosterjs-content-model-types'; -import { getMergedFirstColumnTable, getMergedTopRowTable, getModelTable } from './tableData'; -import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; +import { Editor } from 'roosterjs-content-model-core'; +import { getModelTable } from './tableData'; import { HORIZONTAL_INSERTER_ID, + TableInsertHandler, VERTICAL_INSERTER_ID, createTableInserter, } from '../../lib/tableEdit/editors/features/TableInserter'; -import { - Position, - afterTableTest, - beforeTableTest, - getCurrentTable, - getTableColumns, - getTableRows, - initialize, -} from './TableEditTestHelper'; +import { ContentModelTable, EditorOptions, IEditor } from 'roosterjs-content-model-types'; + +import { getCurrentTable, getTableColumns, getTableRows } from './TableEditTestHelper'; describe('Table Inserter tests', () => { let editor: IEditor; - let plugin: TableEditPlugin; - const insideTheOffset = 5; - const TEST_ID = 'inserterTest'; - let handler: Record; - - beforeEach(() => { - const setup = beforeTableTest(TEST_ID); - editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; - }); + let id = 'tableInserterContainerId'; + let targetId = 'tableInserterTestId'; + let tInserter: TableInsertHandler; + let node: HTMLDivElement; + + function initialize(table: ContentModelTable) { + document.body.innerHTML = ''; + node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + + let options: EditorOptions = { + plugins: [], + initialModel: { + blockGroupType: 'Document', + blocks: [{ ...table }], + format: {}, + }, + }; + + editor = new Editor(node, options); + } afterEach(() => { - afterTableTest(editor, plugin, TEST_ID); + !editor.isDisposed && editor.dispose(); + tInserter && tInserter.dispose(); + const div = document.getElementById(id); + div?.parentNode?.removeChild(div); + node.parentElement?.removeChild(node); }); - function isClickInsideInserter(click: Position, rect: DOMRect) { - return ( - click.x >= rect.left && - click.x <= rect.right && - click.y >= rect.top && - click.y <= rect.bottom - ); - } + function runInserterTest( + table: ContentModelTable, + inserterType: string, + colIndex: number, + rowIndex: number + ) { + //Arrange + initialize(table); + const nodeHeight = 1000; + const nodeWidth = 1000; + node.style.height = `${nodeHeight}px`; + node.style.width = `${nodeWidth}px`; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + if (!target) { + fail('Table not found'); + return; + } - function runInserterTest(inserterType: string, mouseEnd: Position) { - handler.mousemove( - new MouseEvent('mousemove', { - clientX: mouseEnd.x, - clientY: mouseEnd.y, - }) + const targetTd = (target as HTMLTableElement).rows[rowIndex].cells[colIndex]; + const div = document.createElement('div'); + const onInsertSpy = jasmine.createSpy('onInsert'); + const beforeTable = getCurrentTable(editor); + const rows = getTableRows(beforeTable); + const cols = getTableColumns(beforeTable); + tInserter = new TableInsertHandler( + div, + targetTd, + target as HTMLTableElement, + inserterType == HORIZONTAL_INSERTER_ID, + editor, + onInsertSpy ); - const inserter = editor.getDocument().getElementById(inserterType); - if (!!inserter) { - const inserterRect = inserter.getBoundingClientRect(); - if (!isClickInsideInserter(mouseEnd, inserterRect)) { - // Inserter is visible, but pointer is not over it - return 'not clickable'; - } - let table = getCurrentTable(editor); - const rows = getTableRows(table); - const cols = getTableColumns(table); - inserter.dispatchEvent(new MouseEvent('click')); - - table = getCurrentTable(editor); - const newRows = getTableRows(table); - const newCols = getTableColumns(table); - expect(newRows).toBe(inserterType == VERTICAL_INSERTER_ID ? rows : rows + 1); - expect(newCols).toBe(inserterType == HORIZONTAL_INSERTER_ID ? cols : cols + 1); - } - return !!inserter ? 'found' : 'not found'; + //Act + div.click(); + + //Assert + expect(onInsertSpy).toHaveBeenCalled(); + const afterTable = getCurrentTable(editor); + const newRows = getTableRows(afterTable); + const newCols = getTableColumns(afterTable); + expect(newRows).toBe(inserterType == VERTICAL_INSERTER_ID ? rows : rows + 1); + expect(newCols).toBe(inserterType == HORIZONTAL_INSERTER_ID ? cols : cols + 1); } - it('adds a new column if the vertical inserter is detected and clicked', () => { - const rect = initialize(editor, getModelTable()); - const inserterFound = runInserterTest(VERTICAL_INSERTER_ID, { - x: rect.right, - y: rect.top - insideTheOffset, - }); - expect(inserterFound).toBe('found'); - }); - - it('adds a new row if the horizontal inserter is detected and clicked', () => { - const rect = initialize(editor, getModelTable()); - const inserterFound = runInserterTest(HORIZONTAL_INSERTER_ID, { - x: rect.left - insideTheOffset, - y: rect.bottom, - }); - expect(inserterFound).toBe('found'); - }); - - it('does not add inserter if top left corner hovered', () => { - const rect = initialize(editor, getModelTable()); - const inserterFound = runInserterTest(VERTICAL_INSERTER_ID, { - x: rect.left - insideTheOffset, - y: rect.top - insideTheOffset, - }); - expect(inserterFound).toBe('not found'); - }); - - it('does not add new column if top middle clicked on merged top row', () => { - const rect = initialize(editor, getMergedTopRowTable()); - const inserterFound = runInserterTest(VERTICAL_INSERTER_ID, { - x: (rect.right - rect.left) / 2 + 10, - y: rect.top - insideTheOffset, - }); - expect(inserterFound).toBe('not clickable'); + it('adds a new column', () => { + runInserterTest(getModelTable(targetId), VERTICAL_INSERTER_ID, 0, 0); }); - it('does not add new row if left middle clicked on merged first column', () => { - const rect = initialize(editor, getMergedFirstColumnTable()); - const inserterFound = runInserterTest(HORIZONTAL_INSERTER_ID, { - x: rect.left - insideTheOffset, - y: (rect.bottom - rect.top) / 2, - }); - expect(inserterFound).toBe('not clickable'); + it('adds a new row', () => { + runInserterTest(getModelTable(targetId), HORIZONTAL_INSERTER_ID, 0, 0); }); it('Customize table inserter', () => { + initialize(getModelTable(targetId)); spyOn(getIntersectedRect, 'getIntersectedRect').and.returnValue({ bottom: 10, left: 10, @@ -173,6 +157,7 @@ describe('Table Inserter tests', () => { }); it('Customize table inserter, do not customize editortype is not in the cb', () => { + initialize(getModelTable(targetId)); spyOn(getIntersectedRect, 'getIntersectedRect').and.returnValue({ bottom: 10, left: 10, diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableResizerTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableResizerTest.ts index 7acb5c00941..75ae8d9fc5d 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableResizerTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableResizerTest.ts @@ -1,204 +1,293 @@ import * as getIntersectedRect from '../../lib/pluginUtils/Rect/getIntersectedRect'; -import { createTableResizer } from '../../lib/tableEdit/editors/features/TableResizer'; +import { ContentModelTable, EditorOptions, IEditor } from 'roosterjs-content-model-types'; +import { Editor } from 'roosterjs-content-model-core'; +import { getCMTableFromTable } from '../../lib/tableEdit/editors/utils/getTableFromContentModel'; +import { getCurrentTable, getTableRectSet, Position, resizeDirection } from './TableEditTestHelper'; import { getModelTable } from './tableData'; import { TableEditPlugin } from '../../lib/tableEdit/TableEditPlugin'; - -import { - ContentModelTable, - DOMEventHandlerFunction, - IEditor, - PluginEvent, -} from 'roosterjs-content-model-types'; import { - Position, - afterTableTest, - beforeTableTest, - getCellRect, - getCurrentTable, - getTableRectSet, - initialize, - moveAndResize, - resizeDirection, -} from './TableEditTestHelper'; - -const TABLE_RESIZER_ID = '_Table_Resizer'; - -xdescribe('Table Resizer tests', () => { + createTableResizer, + TableResizerInitValue, + onDragging, + onDragStart, + TableResizerContext, + onDragEnd, +} from '../../lib/tableEdit/editors/features/TableResizer'; + +describe('Table Resizer tests', () => { let editor: IEditor; - let plugin: TableEditPlugin; - const TEST_ID = 'resizerTest'; - let handler: Record; + let id = 'tableResizerContainerId'; + let targetId = 'tableResizerTestId'; + let tableEdit: TableEditPlugin; + let node: HTMLDivElement; + const cmTable: ContentModelTable = getModelTable(targetId); beforeEach(() => { - const setup = beforeTableTest(TEST_ID); - editor = setup.editor; - plugin = setup.plugin; - handler = setup.handler; + document.body.innerHTML = ''; + node = document.createElement('div'); + node.id = id; + document.body.insertBefore(node, document.body.childNodes[0]); + tableEdit = new TableEditPlugin(); + + let options: EditorOptions = { + plugins: [tableEdit], + initialModel: { + blockGroupType: 'Document', + blocks: [{ ...cmTable }], + format: {}, + }, + }; + + editor = new Editor(node, options); }); afterEach(() => { - afterTableTest(editor, plugin, TEST_ID); + editor.dispose(); + tableEdit.dispose(); + const div = document.getElementById(id); + div?.parentNode?.removeChild(div); + node.parentElement?.removeChild(node); }); - /************************** Resizer removing tests **************************/ + it('Resize - onDragStart', () => { + //Arrange + node.style.height = '500px'; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); - function removeResizerTest(pluginEvent: PluginEvent) { - let resizer: HTMLElement | null = null; - plugin.initialize(editor); - initialize(editor, getModelTable()); - const cellRect = getCellRect(editor, 0, 0); - handler.mousemove( - new MouseEvent('mousemove', { clientX: cellRect?.right, clientY: cellRect?.bottom }) - ); - resizer = editor.getDocument().getElementById(TABLE_RESIZER_ID); - expect(!!resizer).toBe(true); - plugin.onPluginEvent(pluginEvent); - resizer = editor.getDocument().getElementById(TABLE_RESIZER_ID); - expect(!!resizer).toBe(false); - } - - it('removes table resizer on input', () => { - const pluginEvent: PluginEvent = { - eventType: 'input', - rawEvent: null, - }; - removeResizerTest(pluginEvent); - }); + if (!target) { + fail('Table not found'); + return; + } - it('removes table resizer on content change', () => { - const pluginEvent: PluginEvent = { - eventType: 'contentChanged', - source: null, + const div = document.createElement('div'); + const onStartSpy = jasmine.createSpy('onStart'); + const context: TableResizerContext = { + table: target as HTMLTableElement, + isRTL: false, + zoomScale: 1, + onStart: onStartSpy, + onEnd: () => false, + div: div, + editor: editor, + contentDiv: node, }; - removeResizerTest(pluginEvent); - }); - it('removes table resizer on scrolling', () => { - const pluginEvent: PluginEvent = { - eventType: 'scroll', - scrollContainer: editor.getScrollContainer(), - rawEvent: null, - }; - removeResizerTest(pluginEvent); + const editorCMTable = getCMTableFromTable(editor, target as HTMLTableElement); + + const heights: number[] = []; + editorCMTable?.rows.forEach(row => { + heights.push(row.height); + }); + const tableRect = target.getBoundingClientRect(); + + //Act + const initvalue = onDragStart(context, {} as MouseEvent); + + //Assert + expect(onStartSpy).toHaveBeenCalled(); + expect(initvalue.cmTable).toEqual(editorCMTable); + expect(initvalue.originalRect).toEqual(tableRect); + expect(initvalue.originalHeights).toEqual(heights); + expect(initvalue.originalWidths).toEqual(editorCMTable.widths); }); - /************************ Resizing table related tests ************************/ - - function resizeWholeTableTest( - table: ContentModelTable, - growth: number, - direction: resizeDirection - ) { - const delta = 20 * growth; - const tableRect = initialize(editor, table); - const mouseStart = { x: tableRect.right + 3, y: tableRect.bottom + 3 }; - let mouseEnd: Position = { x: 0, y: 0 }; - switch (direction) { - case 'horizontal': - mouseEnd = { x: tableRect.right + 3 + delta, y: tableRect.bottom + 3 }; - break; - case 'vertical': - mouseEnd = { x: tableRect.right + 3, y: tableRect.bottom + 3 + delta }; - break; - case 'both': - mouseEnd = { x: tableRect.right + 3 + delta, y: tableRect.bottom + 3 + delta }; - break; + describe('Resize - onDragging', () => { + function resizeWholeTableTest(growth: number, direction: resizeDirection) { + //Arrange + const nodeHeight = 1000; + const nodeWidth = 1000; + node.style.height = `${nodeHeight}px`; + node.style.width = `${nodeWidth}px`; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + if (!target) { + fail('Table not found'); + return; + } + + const div = document.createElement('div'); + + const context: TableResizerContext = { + table: target as HTMLTableElement, + isRTL: false, + zoomScale: 1, + onStart: () => {}, + onEnd: () => false, + div: div, + editor: editor, + contentDiv: node, + }; + + const heights: number[] = []; + cmTable?.rows.forEach(row => { + heights.push(row.height); + }); + const tableRect = target.getBoundingClientRect(); + + const initValue: TableResizerInitValue = { + originalRect: tableRect, + originalHeights: heights, + originalWidths: cmTable.widths, + cmTable: cmTable, + }; + const beforeSize = getTableRectSet(getCurrentTable(editor)); + const delta = 10 * growth; + let mouseEnd: Position = { x: 0, y: 0 }; + switch (direction) { + case 'horizontal': + mouseEnd = { x: 3 * delta, y: 0 }; + break; + case 'vertical': + mouseEnd = { x: 0, y: 3 * delta }; + break; + case 'both': + mouseEnd = { x: 3 * delta, y: 3 * delta }; + break; + } + + //Act + const result = onDragging(context, {} as MouseEvent, initValue, mouseEnd.x, mouseEnd.y); + + //Assert + expect(result).toBeTrue(); + const afterSize = getTableRectSet(getCurrentTable(editor)); + compareTableRects(beforeSize, afterSize, growth, direction); } - const beforeSize = getTableRectSet(getCurrentTable(editor)); - moveAndResize(mouseStart, mouseEnd, 'both', editor, handler, TEST_ID); - const afterSize = getTableRectSet(getCurrentTable(editor)); - compareTableRects(beforeSize, afterSize, growth, direction); - } - - function verifyTableRectChange( - rect1: DOMRect, - rect2: DOMRect, - growth: number, - direction: resizeDirection - ): boolean { - switch (direction) { - case 'horizontal': - return growth > 0 ? rect1.width < rect2.width : rect1.width > rect2.width; - case 'vertical': - return growth > 0 ? rect1.height < rect2.height : rect1.height > rect2.height; - case 'both': - return growth > 0 - ? rect1.width < rect2.width && rect1.height < rect2.height - : rect1.width > rect2.width && rect1.height > rect2.height; + + function verifyTableRectChange( + rect1: DOMRect, + rect2: DOMRect, + growth: number, + direction: resizeDirection + ): boolean { + switch (direction) { + case 'horizontal': + return growth > 0 ? rect1.width < rect2.width : rect1.width > rect2.width; + case 'vertical': + return growth > 0 ? rect1.height < rect2.height : rect1.height > rect2.height; + case 'both': + return growth > 0 + ? rect1.width < rect2.width && rect1.height < rect2.height + : rect1.width > rect2.width && rect1.height > rect2.height; + } } - } - - function verifyCellRectChange( - rect1: DOMRect, - rect2: DOMRect, - growth: number, - direction: resizeDirection - ): boolean { - switch (direction) { - case 'horizontal': - return rect1.top == rect2.top && rect1.bottom == rect2.bottom && growth > 0 - ? rect1.left <= rect2.left && rect1.right <= rect2.right - : rect1.left >= rect2.left && rect1.right >= rect2.right; - case 'vertical': - return rect1.left == rect2.left && rect1.right == rect2.right && growth > 0 - ? rect1.top <= rect2.top && rect1.bottom <= rect2.bottom - : rect1.top >= rect2.top && rect1.bottom >= rect2.bottom; - case 'both': - return growth > 0 - ? rect1.left <= rect2.left && - rect1.right <= rect2.right && - rect1.top <= rect2.top && - rect1.bottom <= rect2.bottom - : rect1.left >= rect2.left && - rect1.right >= rect2.right && - rect1.top >= rect2.top && - rect1.bottom >= rect2.bottom; + + function verifyCellRectChange( + rect1: DOMRect, + rect2: DOMRect, + growth: number, + direction: resizeDirection + ): boolean { + switch (direction) { + case 'horizontal': + return rect1.top == rect2.top && rect1.bottom == rect2.bottom && growth > 0 + ? rect1.left <= rect2.left && rect1.right <= rect2.right + : rect1.left >= rect2.left && rect1.right >= rect2.right; + case 'vertical': + return rect1.left == rect2.left && rect1.right == rect2.right && growth > 0 + ? rect1.top <= rect2.top && rect1.bottom <= rect2.bottom + : rect1.top >= rect2.top && rect1.bottom >= rect2.bottom; + case 'both': + return growth > 0 + ? rect1.left <= rect2.left && + rect1.right <= rect2.right && + rect1.top <= rect2.top && + rect1.bottom <= rect2.bottom + : rect1.left >= rect2.left && + rect1.right >= rect2.right && + rect1.top >= rect2.top && + rect1.bottom >= rect2.bottom; + } + } + + function compareTableRects( + beforeTableRectSet1: DOMRect[], + afterTableRectSet2: DOMRect[], + growth: number, + direction: resizeDirection + ) { + expect(beforeTableRectSet1.length).toBe(afterTableRectSet2.length); + beforeTableRectSet1.forEach((rect, i) => { + i == 0 + ? expect( + verifyTableRectChange(rect, afterTableRectSet2[i], growth, direction) + ).toBe(true) // Verify a change to whole table size + : expect( + verifyCellRectChange(rect, afterTableRectSet2[i], growth, direction) + ).toBe( + true // Verify a change to each cell size + ); + }); } - } - - function compareTableRects( - beforeTableRectSet1: DOMRect[], - afterTableRectSet2: DOMRect[], - growth: number, - direction: resizeDirection - ) { - expect(beforeTableRectSet1.length).toBe(afterTableRectSet2.length); - beforeTableRectSet1.forEach((rect, i) => { - i == 0 - ? expect( - verifyTableRectChange(rect, afterTableRectSet2[i], growth, direction) - ).toBe(true) // Verify a change to whole table size - : expect(verifyCellRectChange(rect, afterTableRectSet2[i], growth, direction)).toBe( - true // Verify a change to each cell size - ); + + it('increases the width of the table', () => { + resizeWholeTableTest(1, 'horizontal'); }); - } - it('increases the width of the table', () => { - resizeWholeTableTest(getModelTable(), 1, 'horizontal'); - }); + it('increases the height of the table', () => { + resizeWholeTableTest(1, 'vertical'); + }); - it('increases the height of the table', () => { - resizeWholeTableTest(getModelTable(), 1, 'vertical'); - }); + it('increases the width and height of the table', () => { + resizeWholeTableTest(1, 'both'); + }); - it('increases the width and height of the table', () => { - resizeWholeTableTest(getModelTable(), 1, 'both'); - }); + it('decreases the width of the table', () => { + resizeWholeTableTest(-1, 'horizontal'); + }); - it('decreases the width of the table', () => { - resizeWholeTableTest(getModelTable(), -1, 'horizontal'); - }); + it('decreases the height of the table', () => { + resizeWholeTableTest(-1, 'vertical'); + }); - it('decreases the height of the table', () => { - resizeWholeTableTest(getModelTable(), -1, 'vertical'); + it('decreases the width and height of the table', () => { + resizeWholeTableTest(-1, 'both'); + }); }); - it('decreases the width and height of the table', () => { - resizeWholeTableTest(getModelTable(), -1, 'both'); + it('Resize - onDragEnd', () => { + //Arrange + node.style.height = '500px'; + node.style.overflowX = 'auto'; + node.scrollTop = 0; + const target = document.getElementById(targetId); + editor.focus(); + + if (!target) { + fail('Table not found'); + return; + } + + const div = document.createElement('div'); + const styleSpy = spyOnProperty(div, 'style').and.callThrough(); + const onEndSpy = jasmine.createSpy('onEnd'); + const context: TableResizerContext = { + table: target as HTMLTableElement, + isRTL: false, + zoomScale: 1, + onStart: () => {}, + onEnd: onEndSpy, + div: div, + editor: editor, + contentDiv: node, + }; + + //Act + const result = onDragEnd(context, {} as MouseEvent, undefined); + + //Assert + expect(onEndSpy).toHaveBeenCalled(); + expect(result).toBeFalse(); + expect(styleSpy).toHaveBeenCalled(); }); - it('Customize table inserter', () => { + it('Customize table resizer', () => { spyOn(getIntersectedRect, 'getIntersectedRect').and.returnValue({ bottom: 10, left: 10, @@ -210,17 +299,18 @@ xdescribe('Table Resizer tests', () => { const changeCb = jasmine.createSpy('changeCb'); //Act const result = createTableResizer( - { + ({ getBoundingClientRect: () => { return { bottom: 10, height: 10, left: 10, right: 10, + top: 10, }; - ownerDocument: document; }, - }, + ownerDocument: document, + }), editor, false, () => {}, @@ -241,7 +331,7 @@ xdescribe('Table Resizer tests', () => { expect(changeCb).toHaveBeenCalled(); }); - it('Customize table inserter, do not customize wrong editor type', () => { + it('Customize table resizer, do not customize wrong editor type', () => { spyOn(getIntersectedRect, 'getIntersectedRect').and.returnValue({ bottom: 10, left: 10, @@ -253,17 +343,18 @@ xdescribe('Table Resizer tests', () => { const changeCb = jasmine.createSpy('changeCb'); //Act const result = createTableResizer( - { + ({ getBoundingClientRect: () => { return { bottom: 10, height: 10, left: 10, right: 10, + top: 10, }; - ownerDocument: document; }, - }, + ownerDocument: document, + }), editor, false, () => {}, diff --git a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts index 79ccc8ff3aa..2fbe371b9cc 100644 --- a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts +++ b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts @@ -2,7 +2,10 @@ import type { DomToModelOptionForSanitizing } from '../context/DomToModelOption' import type { PasteType } from '../enum/PasteType'; import type { ClipboardData } from '../parameter/ClipboardData'; import type { BasePluginEvent } from './BasePluginEvent'; -import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; +import type { + ContentModelDocument, + ShallowMutableContentModelDocument, +} from '../contentModel/blockGroup/ContentModelDocument'; import type { InsertPoint } from '../selection/InsertPoint'; /** @@ -12,7 +15,7 @@ import type { InsertPoint } from '../selection/InsertPoint'; * @returns Insert point after merge */ export type MergePastedContentFunc = ( - target: ContentModelDocument, + target: ShallowMutableContentModelDocument, source: ContentModelDocument ) => InsertPoint | null; diff --git a/packages/roosterjs-editor-adapter/lib/README.md b/packages/roosterjs-editor-adapter/lib/README.md new file mode 100644 index 00000000000..c80fe95215f --- /dev/null +++ b/packages/roosterjs-editor-adapter/lib/README.md @@ -0,0 +1,45 @@ +# RoosterJS Editor Adapter + +## Introduction + +For RoosterJS v9 packages be compatible with old (8._) versions, you can use EditorAdapter class from the following package which can act as a 8._ Editor. +roosterjs-editor-adapter: Provide a adapter class EditorAdapter to work with Editor (9.\*) and legacy plugins (via EditorAdapterOptions.legacyPlugins) by providing a Translation of Content Model plugin events that are going to be compatible with RoosterJS v8 plugins + +## Implementation + +Install the Rooster JS Editor Adapter package + +```sh +npm install roosterjs-editor-adapter +``` + +Import the new package in the file you create the editor instance + +```js +import { EditorAdapter } from 'roosterjs-editor-adapter'; +``` + +Change from `Editor` class to use `EditorAdapter` + +Before + +```js +const options: EditorOptions = { + plugins: [], /// Array of V8 plugins + ... +} +new Editor(div, options); +``` + +After + +```ts +const options: EditorAdapterOptions = { + legacyPlugins: [], /// Array of V8 plugins + plugins: [], /// Array of v9 plugins + ... +} +return new EditorAdapter(div, options); +``` + +Now your v8 RoosterJS Plugins should still work even if you are using the new RoosterJS v9.