From c30c84afa86d08438b95b707f91724d8b9055448 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 29 Apr 2024 16:57:33 -0700 Subject: [PATCH 01/66] KeyboardEnter --- .../lib/edit/EditPlugin.ts | 4 + .../lib/edit/deleteSteps/deleteEmptyQuote.ts | 65 ++++++++++----- .../lib/edit/inputSteps/handleEnterOnList.ts | 81 ++++++------------- .../edit/inputSteps/handleEnterOnParagraph.ts | 20 +++++ .../lib/edit/keyboardEnter.ts | 53 ++++++++++++ .../lib/edit/keyboardInput.ts | 27 +------ .../lib/edit/utils/splitParagraph.ts | 34 ++++++++ 7 files changed, 180 insertions(+), 104 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts create mode 100644 packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts create mode 100644 packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts index 59cfe12ccc8..5350dccfaff 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts @@ -1,4 +1,5 @@ import { keyboardDelete } from './keyboardDelete'; +import { keyboardEnter } from './keyboardEnter'; import { keyboardInput } from './keyboardInput'; import { keyboardTab } from './keyboardTab'; import type { @@ -104,6 +105,9 @@ export class EditPlugin implements EditorPlugin { break; case 'Enter': + keyboardEnter(editor, rawEvent); + break; + default: keyboardInput(editor, rawEvent); break; 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 cb337857438..647073f25be 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts @@ -1,13 +1,13 @@ import { - createParagraph, - createSelectionMarker, unwrapBlock, getClosestAncestorBlockGroupIndex, isBlockGroupOfType, + createFormatContainer, } from 'roosterjs-content-model-dom'; import type { ContentModelBlockGroup, ContentModelFormatContainer, + ContentModelParagraph, DeleteSelectionStep, } from 'roosterjs-content-model-types'; @@ -16,13 +16,10 @@ import type { */ export const deleteEmptyQuote: DeleteSelectionStep = context => { const { deleteResult } = context; - if ( - deleteResult == 'nothingToDelete' || - deleteResult == 'notDeleted' || - deleteResult == 'range' - ) { + + if (deleteResult == 'nothingToDelete' || deleteResult == 'notDeleted') { const { insertPoint, formatContext } = context; - const { path } = insertPoint; + const { path, paragraph } = insertPoint; const rawEvent = formatContext?.rawEvent as KeyboardEvent; const index = getClosestAncestorBlockGroupIndex( path, @@ -35,6 +32,7 @@ export const deleteEmptyQuote: DeleteSelectionStep = context => { const parent = path[index + 1]; const quoteBlockIndex = parent.blocks.indexOf(quote); const blockQuote = parent.blocks[quoteBlockIndex]; + if ( isBlockGroupOfType(blockQuote, 'FormatContainer') && blockQuote.tagName === 'blockquote' @@ -43,8 +41,11 @@ export const deleteEmptyQuote: DeleteSelectionStep = context => { unwrapBlock(parent, blockQuote); rawEvent?.preventDefault(); context.deleteResult = 'range'; - } else if (isSelectionOnEmptyLine(blockQuote) && rawEvent?.key === 'Enter') { - insertNewLine(blockQuote, parent, quoteBlockIndex); + } else if ( + isSelectionOnEmptyLine(blockQuote, paragraph) && + rawEvent?.key === 'Enter' + ) { + insertNewLine(blockQuote, parent, quoteBlockIndex, paragraph); rawEvent?.preventDefault(); context.deleteResult = 'range'; } @@ -63,25 +64,45 @@ const isEmptyQuote = (quote: ContentModelFormatContainer) => { ); }; -const isSelectionOnEmptyLine = (quote: ContentModelFormatContainer) => { - const quoteLength = quote.blocks.length; - const lastParagraph = quote.blocks[quoteLength - 1]; - if (lastParagraph && lastParagraph.blockType === 'Paragraph') { - return lastParagraph.segments.every( +const isSelectionOnEmptyLine = ( + quote: ContentModelFormatContainer, + paragraph: ContentModelParagraph +) => { + const paraIndex = quote.blocks.indexOf(paragraph); + + if (paraIndex >= 0) { + return paragraph.segments.every( s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br' ); + } else { + return false; } }; const insertNewLine = ( quote: ContentModelFormatContainer, parent: ContentModelBlockGroup, - index: number + quoteIndex: number, + paragraph: ContentModelParagraph ) => { - const quoteLength = quote.blocks.length; - quote.blocks.splice(quoteLength - 1, 1); - const marker = createSelectionMarker(); - const newParagraph = createParagraph(false /* isImplicit */); - newParagraph.segments.push(marker); - parent.blocks.splice(index + 1, 0, newParagraph); + const paraIndex = quote.blocks.indexOf(paragraph); + + if (paraIndex >= 0) { + if (paraIndex < quote.blocks.length - 1) { + const newQuote = createFormatContainer(quote.tagName, quote.format); + + newQuote.blocks.push( + ...quote.blocks.splice(paraIndex + 1, quote.blocks.length - paraIndex - 1) + ); + + parent.blocks.splice(quoteIndex + 1, 0, newQuote); + } + + parent.blocks.splice(quoteIndex + 1, 0, paragraph); + quote.blocks.splice(paraIndex, 1); + + if (quote.blocks.length == 0) { + parent.blocks.splice(quoteIndex, 0); + } + } }; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 869d20ac8f5..19a1b70ae73 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -1,13 +1,10 @@ import { getListAnnounceData } from 'roosterjs-content-model-api'; +import { splitParagraph } from '../utils/splitParagraph'; import { - createBr, createListItem, createListLevel, - createParagraph, createSelectionMarker, normalizeContentModel, - normalizeParagraph, - setParagraphNotImplicit, getClosestAncestorBlockGroupIndex, isBlockGroupOfType, } from 'roosterjs-content-model-dom'; @@ -15,7 +12,6 @@ import type { ContentModelBlockGroup, ContentModelListItem, DeleteSelectionStep, - InsertPoint, ValidDeleteSelectionContext, } from 'roosterjs-content-model-types'; @@ -23,25 +19,35 @@ import type { * @internal */ export const handleEnterOnList: DeleteSelectionStep = context => { - const { deleteResult } = context; - if ( - deleteResult == 'nothingToDelete' || - deleteResult == 'notDeleted' || - deleteResult == 'range' - ) { - const { insertPoint, formatContext } = context; + const { deleteResult, insertPoint } = context; + + if (deleteResult == 'notDeleted' || deleteResult == 'nothingToDelete') { const { path } = insertPoint; - const rawEvent = formatContext?.rawEvent; const index = getClosestAncestorBlockGroupIndex(path, ['ListItem'], ['TableCell']); - const listItem = path[index]; const listParent = path[index + 1]; + const parentBlock = path[index]; + + if (parentBlock?.blockGroupType === 'ListItem' && listParent) { + let listItem: ContentModelListItem = parentBlock; + + if (isEmptyListItem(listItem)) { + listItem.levels.pop(); + } else { + listItem = createNewListItem(context, listItem, listParent); + + if (context.formatContext) { + context.formatContext.announceData = getListAnnounceData([ + listItem, + ...path.slice(index + 1), + ]); + } + } - if (listItem && listItem.blockGroupType === 'ListItem' && listParent) { const listIndex = listParent.blocks.indexOf(listItem); const nextBlock = listParent.blocks[listIndex + 1]; - if (deleteResult == 'range' && nextBlock) { + if (nextBlock) { normalizeContentModel(listParent); const nextListItem = listParent.blocks[listIndex + 1]; @@ -75,22 +81,8 @@ export const handleEnterOnList: DeleteSelectionStep = context => { context.lastParagraph = undefined; } - } else if (deleteResult !== 'range') { - if (isEmptyListItem(listItem)) { - listItem.levels.pop(); - } else { - const newListItem = createNewListItem(context, listItem, listParent); - - if (context.formatContext) { - context.formatContext.announceData = getListAnnounceData([ - newListItem, - ...path.slice(index + 1), - ]); - } - } } - rawEvent?.preventDefault(); context.deleteResult = 'range'; } } @@ -113,7 +105,7 @@ const createNewListItem = ( ) => { const { insertPoint } = context; const listIndex = listParent.blocks.indexOf(listItem); - const newParagraph = createNewParagraph(insertPoint); + const newParagraph = splitParagraph(insertPoint); const levels = createNewListLevel(listItem); const newListItem = createListItem(levels, insertPoint.marker.format); @@ -138,30 +130,3 @@ const createNewListLevel = (listItem: ContentModelListItem) => { ); }); }; - -const createNewParagraph = (insertPoint: InsertPoint) => { - const { paragraph, marker } = insertPoint; - const newParagraph = createParagraph( - false /*isImplicit*/, - paragraph.format, - paragraph.segmentFormat - ); - - const markerIndex = paragraph.segments.indexOf(marker); - const segments = paragraph.segments.splice( - markerIndex, - paragraph.segments.length - markerIndex - ); - - newParagraph.segments.push(...segments); - - setParagraphNotImplicit(paragraph); - - if (paragraph.segments.every(x => x.segmentType == 'SelectionMarker')) { - paragraph.segments.push(createBr(marker.format)); - } - - normalizeParagraph(newParagraph); - - return newParagraph; -}; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts new file mode 100644 index 00000000000..312966a5fa7 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts @@ -0,0 +1,20 @@ +import { splitParagraph } from '../utils/splitParagraph'; +import type { DeleteSelectionStep } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export const handleEnterOnParagraph: DeleteSelectionStep = context => { + const { paragraph, path } = context.insertPoint; + const paraIndex = path[0]?.blocks.indexOf(paragraph) ?? -1; + + if (context.deleteResult == 'notDeleted' && paraIndex >= 0) { + const newPara = splitParagraph(context.insertPoint); + + path[0].blocks.splice(paraIndex + 1, 0, newPara); + + context.deleteResult = 'range'; + context.lastParagraph = newPara; + context.insertPoint.paragraph = newPara; + } +}; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts new file mode 100644 index 00000000000..5120c060d36 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -0,0 +1,53 @@ +import { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote'; +import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom'; +import { handleEnterOnList } from './inputSteps/handleEnterOnList'; +import { handleEnterOnParagraph } from './inputSteps/handleEnterOnParagraph'; +import type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { + const selection = editor.getDOMSelection(); + + editor.formatContentModel( + (model, context) => { + const result = deleteSelection(model, getInputSteps(selection, rawEvent), context); + + if (result.deleteResult == 'range') { + // We have deleted something, next input should inherit the segment format from deleted content, so set pending format here + context.newPendingFormat = result.insertPoint?.marker.format; + + normalizeContentModel(model); + + rawEvent.preventDefault(); + return true; + } else { + return false; + } + }, + { + rawEvent, + } + ); +} + +function getInputSteps(selection: DOMSelection | null, rawEvent: KeyboardEvent) { + const result: DeleteSelectionStep[] = [clearDeleteResult]; + + if (selection && selection.type != 'table') { + if (rawEvent.shiftKey) { + result.push(handleEnterOnParagraph); + } else { + result.push(handleEnterOnList, deleteEmptyQuote, handleEnterOnParagraph); + } + } + + return result; +} + +const clearDeleteResult: DeleteSelectionStep = context => { + // For ENTER key, although we may have deleted something, since we still need to split the line, we always treat it as not delete + // so further delete steps can keep working + context.deleteResult = 'notDeleted'; +}; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index a25f2d6beaa..dc7db829ad2 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -1,6 +1,4 @@ -import { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote'; import { deleteSelection, isModifierKey, normalizeContentModel } from 'roosterjs-content-model-dom'; -import { handleEnterOnList } from './inputSteps/handleEnterOnList'; import type { DOMSelection, IEditor } from 'roosterjs-content-model-types'; /** @@ -14,11 +12,7 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) { editor.formatContentModel( (model, context) => { - const result = deleteSelection(model, getInputSteps(selection, rawEvent), context); - - // We have deleted selection then we will let browser to handle the input. - // With this combined operation, we don't wan to mass up the cached model so clear it - context.clearModelCache = true; + const result = deleteSelection(model, [], context); // Skip undo snapshot here and add undo snapshot before the operation so that we don't add another undo snapshot in middle of this replace operation context.skipUndoSnapshot = true; @@ -44,27 +38,12 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) { } } -function getInputSteps(selection: DOMSelection | null, rawEvent: KeyboardEvent) { - return shouldHandleEnterKey(selection, rawEvent) ? [handleEnterOnList, deleteEmptyQuote] : []; -} - function shouldInputWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) { if (!selection) { return false; // Nothing to delete - } else if ( - !isModifierKey(rawEvent) && - (rawEvent.key == 'Enter' || rawEvent.key == 'Space' || rawEvent.key.length == 1) - ) { - return ( - selection.type != 'range' || - !selection.range.collapsed || - shouldHandleEnterKey(selection, rawEvent) - ); + } else if (!isModifierKey(rawEvent) && (rawEvent.key == 'Space' || rawEvent.key.length == 1)) { + return selection.type != 'range' || !selection.range.collapsed; } else { return false; } } - -const shouldHandleEnterKey = (selection: DOMSelection | null, rawEvent: KeyboardEvent) => { - return selection && selection.type == 'range' && rawEvent.key == 'Enter' && !rawEvent.shiftKey; -}; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts new file mode 100644 index 00000000000..da31ec0a6b3 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts @@ -0,0 +1,34 @@ +import { + createBr, + createParagraph, + normalizeParagraph, + setParagraphNotImplicit, +} from 'roosterjs-content-model-dom'; +import type { InsertPoint } from 'roosterjs-content-model-types'; + +export function splitParagraph(insertPoint: InsertPoint) { + const { paragraph, marker } = insertPoint; + const newParagraph = createParagraph( + false /*isImplicit*/, + paragraph.format, + paragraph.segmentFormat + ); + + const markerIndex = paragraph.segments.indexOf(marker); + const segments = paragraph.segments.splice( + markerIndex, + paragraph.segments.length - markerIndex + ); + + newParagraph.segments.push(...segments); + + setParagraphNotImplicit(paragraph); + + if (paragraph.segments.every(x => x.segmentType == 'SelectionMarker')) { + paragraph.segments.push(createBr(marker.format)); + } + + normalizeParagraph(newParagraph); + + return newParagraph; +} From 98655527a72fb8156141112c63bb4ea631aa7ea5 Mon Sep 17 00:00:00 2001 From: JiuqingSong Date: Tue, 30 Apr 2024 09:31:02 -0700 Subject: [PATCH 02/66] fix comment --- .../lib/edit/utils/splitParagraph.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts index da31ec0a6b3..e6f4c38bbf3 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts @@ -1,11 +1,17 @@ import { - createBr, createParagraph, normalizeParagraph, setParagraphNotImplicit, } from 'roosterjs-content-model-dom'; import type { InsertPoint } from 'roosterjs-content-model-types'; +/** + * @internal + * Split the given paragraph from insert point into two paragraphs, + * and move the selection marker to the beginning of the second paragraph + * @param insertPoint The input insert point which includes the paragraph and selection marker + * @returns The new paragraph it created + */ export function splitParagraph(insertPoint: InsertPoint) { const { paragraph, marker } = insertPoint; const newParagraph = createParagraph( @@ -24,10 +30,9 @@ export function splitParagraph(insertPoint: InsertPoint) { setParagraphNotImplicit(paragraph); - if (paragraph.segments.every(x => x.segmentType == 'SelectionMarker')) { - paragraph.segments.push(createBr(marker.format)); - } + insertPoint.paragraph = newParagraph; + normalizeParagraph(paragraph); normalizeParagraph(newParagraph); return newParagraph; From a8af3674b0094dd7168a9d0b8c88ddd09d63e9e1 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 30 Apr 2024 16:08:40 -0700 Subject: [PATCH 03/66] fix test --- .../lib/modelApi/editing/deleteSelection.ts | 6 +- .../lib/edit/inputSteps/handleEnterOnList.ts | 33 +-- .../lib/edit/utils/splitParagraph.ts | 6 +- .../edit/deleteSteps/deleteEmptyQuoteTest.ts | 277 ----------------- .../edit/inputSteps/handleEnterOnListTest.ts | 280 ++++++++++++------ .../test/edit/keyboardInputTest.ts | 81 ----- 6 files changed, 201 insertions(+), 482 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts index 20ff0629c65..5373bd63af3 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts @@ -41,16 +41,18 @@ function isValidDeleteSelectionContext( // If we end up with multiple paragraphs impacted, we need to merge them function mergeParagraphAfterDelete(context: DeleteSelectionContext) { const { insertPoint, deleteResult, lastParagraph, lastTableContext } = context; + const prevParagraph = insertPoint?.paragraph; if ( insertPoint && + prevParagraph && deleteResult != 'notDeleted' && deleteResult != 'nothingToDelete' && lastParagraph && - lastParagraph != insertPoint.paragraph && + lastParagraph != prevParagraph && lastTableContext == insertPoint.tableContext ) { - insertPoint.paragraph.segments.push(...lastParagraph.segments); + prevParagraph.segments.push(...lastParagraph.segments); lastParagraph.segments = []; } } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 19a1b70ae73..3646a3a07b7 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -3,8 +3,6 @@ import { splitParagraph } from '../utils/splitParagraph'; import { createListItem, createListLevel, - createSelectionMarker, - normalizeContentModel, getClosestAncestorBlockGroupIndex, isBlockGroupOfType, } from 'roosterjs-content-model-dom'; @@ -48,38 +46,15 @@ export const handleEnterOnList: DeleteSelectionStep = context => { const nextBlock = listParent.blocks[listIndex + 1]; if (nextBlock) { - normalizeContentModel(listParent); - const nextListItem = listParent.blocks[listIndex + 1]; if ( isBlockGroupOfType(nextListItem, 'ListItem') && nextListItem.levels[0] ) { - nextListItem.levels.forEach((level, index) => { + nextListItem.levels.forEach(level => { level.format.startNumberOverride = undefined; - level.dataset = listItem.levels[index] - ? listItem.levels[index].dataset - : {}; }); - - const lastParagraph = listItem.blocks[listItem.blocks.length - 1]; - const nextParagraph = nextListItem.blocks[0]; - - if ( - nextParagraph.blockType === 'Paragraph' && - lastParagraph.blockType === 'Paragraph' && - lastParagraph.segments[lastParagraph.segments.length - 1].segmentType === - 'SelectionMarker' - ) { - lastParagraph.segments.pop(); - - nextParagraph.segments.unshift( - createSelectionMarker(insertPoint.marker.format) - ); - } - - context.lastParagraph = undefined; } } @@ -105,15 +80,19 @@ const createNewListItem = ( ) => { const { insertPoint } = context; const listIndex = listParent.blocks.indexOf(listItem); + const currentPara = insertPoint.paragraph; const newParagraph = splitParagraph(insertPoint); const levels = createNewListLevel(listItem); const newListItem = createListItem(levels, insertPoint.marker.format); newListItem.blocks.push(newParagraph); insertPoint.paragraph = newParagraph; - context.lastParagraph = newParagraph; listParent.blocks.splice(listIndex + 1, 0, newListItem); + if (context.lastParagraph == currentPara) { + context.lastParagraph = newParagraph; + } + return newListItem; }; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts index e6f4c38bbf3..9fd83d3668a 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts @@ -1,4 +1,5 @@ import { + createBr, createParagraph, normalizeParagraph, setParagraphNotImplicit, @@ -26,6 +27,10 @@ export function splitParagraph(insertPoint: InsertPoint) { paragraph.segments.length - markerIndex ); + if (paragraph.segments.length == 0) { + paragraph.segments.push(createBr(marker.format)); + } + newParagraph.segments.push(...segments); setParagraphNotImplicit(paragraph); @@ -33,7 +38,6 @@ export function splitParagraph(insertPoint: InsertPoint) { insertPoint.paragraph = newParagraph; normalizeParagraph(paragraph); - normalizeParagraph(newParagraph); return newParagraph; } diff --git a/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts b/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts index fbe4719c2fe..3494db5d813 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts @@ -1,8 +1,6 @@ import { ContentModelDocument } from 'roosterjs-content-model-types'; import { deleteEmptyQuote } from '../../../lib/edit/deleteSteps/deleteEmptyQuote'; import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom'; -import { editingTestCommon } from '../editingTestCommon'; -import { keyboardInput } from '../../../lib/edit/keyboardInput'; describe('deleteEmptyQuote', () => { function runTest( @@ -131,278 +129,3 @@ describe('deleteEmptyQuote', () => { runTest(model, model, 'notDeleted'); }); }); - -describe('deleteEmptyQuote - keyboardInput', () => { - function runTest( - input: ContentModelDocument, - key: string, - expectedResult: ContentModelDocument, - doNotCallDefaultFormat: boolean = false, - calledTimes: number = 1 - ) { - const preventDefault = jasmine.createSpy('preventDefault'); - const mockedEvent = ({ - key: key, - shiftKey: false, - preventDefault, - } as any) as KeyboardEvent; - - let editor: any; - - editingTestCommon( - undefined, - newEditor => { - editor = newEditor; - - editor.getDOMSelection = () => ({ - type: 'range', - range: { - collapsed: true, - }, - }); - - keyboardInput(editor, mockedEvent); - }, - input, - expectedResult, - calledTimes, - doNotCallDefaultFormat - ); - } - - it('should delete empty quote when press Enter', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - { - segmentType: 'Br', - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: {}, - }, - ], - format: { - marginTop: '1em', - marginRight: '40px', - marginBottom: '1em', - marginLeft: '40px', - paddingLeft: '10px', - borderLeft: '3px solid rgb(200, 200, 200)', - }, - }, - ], - format: {}, - }; - const expectedTestModel: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - { - segmentType: 'Br', - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - segmentFormat: { textColor: 'rgb(102, 102, 102)' }, - format: {}, - }, - ], - format: {}, - }; - - runTest(model, 'Enter', expectedTestModel); - }); - - it('should exit quote when press Enter', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: {}, - segmentFormat: { - textColor: 'rgb(102, 102, 102)', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - { - segmentType: 'Br', - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: {}, - segmentFormat: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: { - marginTop: '1em', - marginRight: '40px', - marginBottom: '1em', - marginLeft: '40px', - paddingLeft: '10px', - borderLeft: '3px solid rgb(200, 200, 200)', - }, - }, - ], - format: {}, - }; - const expectedTestModel: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: {}, - segmentFormat: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: { - marginTop: '1em', - marginRight: '40px', - marginBottom: '1em', - marginLeft: '40px', - paddingLeft: '10px', - borderLeft: '3px solid rgb(200, 200, 200)', - }, - }, - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, - }; - - runTest(model, 'Enter', expectedTestModel); - }); - - it('should not exit quote when press Enter', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: {}, - segmentFormat: { - textColor: 'rgb(102, 102, 102)', - }, - }, - ], - format: { - marginTop: '1em', - marginRight: '40px', - marginBottom: '1em', - marginLeft: '40px', - paddingLeft: '10px', - borderLeft: '3px solid rgb(200, 200, 200)', - }, - }, - ], - format: {}, - }; - - runTest(model, 'Enter', model, false, 0); - }); -}); diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index f1d394a1c40..69616999988 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -2,7 +2,7 @@ import * as getListAnnounceData from 'roosterjs-content-model-api/lib/modelApi/l import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom'; import { editingTestCommon } from '../editingTestCommon'; import { handleEnterOnList } from '../../../lib/edit/inputSteps/handleEnterOnList'; -import { keyboardInput } from '../../../lib/edit/keyboardInput'; +import { keyboardEnter } from '../../../lib/edit/keyboardEnter'; import { ContentModelDocument, ContentModelListItem, @@ -30,7 +30,11 @@ describe('handleEnterOnList', () => { newEntities: [], newImages: [], }; - const result = deleteSelection(model, [handleEnterOnList], context); + const result = deleteSelection( + model, + [context => (context.deleteResult = 'notDeleted'), handleEnterOnList], + context + ); normalizeContentModel(model); expect(model).toEqual(expectedModel); @@ -961,6 +965,48 @@ describe('handleEnterOnList', () => { ], format: {}, }; + const listItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + listStyleType: 'decimal', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; const expectedModel: ContentModelDocument = { blockGroupType: 'Document', blocks: [ @@ -971,11 +1017,6 @@ describe('handleEnterOnList', () => { { blockType: 'Paragraph', segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, { segmentType: 'Br', format: {}, @@ -1004,6 +1045,7 @@ describe('handleEnterOnList', () => { }, format: {}, }, + listItem, { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -1044,7 +1086,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range', null); + runTest(model, expectedModel, 'range', listItem); }); it('enter on multiple list items with selected text', () => { @@ -1210,6 +1252,48 @@ describe('handleEnterOnList', () => { ], format: {}, }; + const listItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + listStyleType: 'decimal', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; const expectedModel: ContentModelDocument = { blockGroupType: 'Document', blocks: [ @@ -1256,11 +1340,6 @@ describe('handleEnterOnList', () => { { blockType: 'Paragraph', segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, { segmentType: 'Br', format: {}, @@ -1289,6 +1368,7 @@ describe('handleEnterOnList', () => { }, format: {}, }, + listItem, { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -1312,7 +1392,6 @@ describe('handleEnterOnList', () => { marginTop: '0px', marginBottom: '0px', listStyleType: 'decimal', - startNumberOverride: undefined, }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -1329,7 +1408,7 @@ describe('handleEnterOnList', () => { ], format: {}, }; - runTest(model, expectedModel, 'range', null); + runTest(model, expectedModel, 'range', listItem); }); it('expanded range mixed list with paragraph', () => { @@ -1436,45 +1515,52 @@ describe('handleEnterOnList', () => { ], format: {}, }; - const expectedModel: ContentModelDocument = { - blockGroupType: 'Document', + const listItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', blocks: [ { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ + blockType: 'Paragraph', + segments: [ { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'te', - format: {}, - }, - ], + segmentType: 'SelectionMarker', + isSelected: true, format: {}, }, - ], - levels: [ { - listType: 'OL', - format: { - marginTop: '0px', - marginBottom: '0px', - listStyleType: 'decimal', - }, - dataset: { - editingInfo: '{"orderedStyleType":1}', - }, + segmentType: 'Text', + text: 'st', + format: {}, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, format: {}, }, + ], + levels: [ + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + listStyleType: 'decimal', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -1482,14 +1568,9 @@ describe('handleEnterOnList', () => { { blockType: 'Paragraph', segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, { segmentType: 'Text', - text: 'st', + text: 'te', format: {}, }, ], @@ -1503,7 +1584,6 @@ describe('handleEnterOnList', () => { marginTop: '0px', marginBottom: '0px', listStyleType: 'decimal', - startNumberOverride: undefined, }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -1517,10 +1597,11 @@ describe('handleEnterOnList', () => { }, format: {}, }, + listItem, ], format: {}, }; - runTest(model, expectedModel, 'range', null); + runTest(model, expectedModel, 'range', listItem); }); it('expanded range with mixed list with paragraph | different styles', () => { @@ -1628,46 +1709,51 @@ describe('handleEnterOnList', () => { ], format: {}, }; - const expectedModel: ContentModelDocument = { - blockGroupType: 'Document', + const listItem: ContentModelListItem = { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', blocks: [ { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ + blockType: 'Paragraph', + segments: [ { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'te', - format: {}, - }, - ], + segmentType: 'SelectionMarker', + isSelected: true, format: {}, }, - ], - levels: [ { - listType: 'OL', - format: { - marginTop: '0px', - marginBottom: '0px', - }, - dataset: { - editingInfo: '{"orderedStyleType":3}', - }, + segmentType: 'Text', + text: 'st', + format: {}, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, - format: {}, - }, + format: {}, + }, + ], + levels: [ + { + listType: 'OL', format: { - listStyleType: '"1) "', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: { + editingInfo: '{"orderedStyleType":3}', }, }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }; + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', @@ -1675,14 +1761,9 @@ describe('handleEnterOnList', () => { { blockType: 'Paragraph', segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, { segmentType: 'Text', - text: 'st', + text: 'te', format: {}, }, ], @@ -1695,8 +1776,6 @@ describe('handleEnterOnList', () => { format: { marginTop: '0px', marginBottom: '0px', - startNumberOverride: undefined, - listStyleType: 'lower-alpha', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -1708,16 +1787,19 @@ describe('handleEnterOnList', () => { isSelected: false, format: {}, }, - format: {}, + format: { + listStyleType: '"1) "', + }, }, + listItem, ], format: {}, }; - runTest(model, expectedModel, 'range', null); + runTest(model, expectedModel, 'range', listItem); }); }); -describe(' handleEnterOnList - keyboardInput', () => { +describe('handleEnterOnList - keyboardEnter', () => { function runTest( input: ContentModelDocument, isShiftKey: boolean, @@ -1746,7 +1828,7 @@ describe(' handleEnterOnList - keyboardInput', () => { }, }); - keyboardInput(editor, mockedEvent); + keyboardEnter(editor, mockedEvent); }, input, expectedResult, @@ -2312,11 +2394,21 @@ describe(' handleEnterOnList - keyboardInput', () => { text: 'test', format: {}, }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ { segmentType: 'SelectionMarker', isSelected: true, format: {}, }, + { + segmentType: 'Br', + format: {}, + }, ], format: {}, }, @@ -2344,6 +2436,6 @@ describe(' handleEnterOnList - keyboardInput', () => { ], format: {}, }; - runTest(input, true, expected, true, 0); + runTest(input, true, expected, false, 1); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts b/packages/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts index 3a4d6ef809d..b2435499c9f 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts @@ -1,7 +1,5 @@ import * as deleteSelection from 'roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; -import { deleteEmptyQuote } from '../../lib/edit/deleteSteps/deleteEmptyQuote'; -import { handleEnterOnList } from '../../lib/edit/inputSteps/handleEnterOnList'; import { keyboardInput } from '../../lib/edit/keyboardInput'; import { ContentModelDocument, @@ -107,7 +105,6 @@ describe('keyboardInput', () => { deletedEntities: [], newEntities: [], newImages: [], - clearModelCache: true, skipUndoSnapshot: true, }); expect(normalizeContentModelSpy).not.toHaveBeenCalled(); @@ -139,7 +136,6 @@ describe('keyboardInput', () => { deletedEntities: [], newEntities: [], newImages: [], - clearModelCache: true, skipUndoSnapshot: true, newPendingFormat: undefined, }); @@ -169,7 +165,6 @@ describe('keyboardInput', () => { deletedEntities: [], newEntities: [], newImages: [], - clearModelCache: true, skipUndoSnapshot: true, newPendingFormat: undefined, }); @@ -199,7 +194,6 @@ describe('keyboardInput', () => { deletedEntities: [], newEntities: [], newImages: [], - clearModelCache: true, skipUndoSnapshot: true, newPendingFormat: undefined, }); @@ -285,7 +279,6 @@ describe('keyboardInput', () => { deletedEntities: [], newEntities: [], newImages: [], - clearModelCache: true, skipUndoSnapshot: true, newPendingFormat: undefined, }); @@ -319,36 +312,6 @@ describe('keyboardInput', () => { expect(normalizeContentModelSpy).not.toHaveBeenCalled(); }); - it('Enter input, table selection, no modifier key, deleteSelection returns range', () => { - getDOMSelectionSpy.and.returnValue({ - type: 'table', - }); - deleteSelectionSpy.and.returnValue({ - deleteResult: 'range', - }); - - const rawEvent = { - key: 'Enter', - } as any; - - keyboardInput(editor, rawEvent); - - expect(getDOMSelectionSpy).toHaveBeenCalled(); - expect(takeSnapshotSpy).toHaveBeenCalled(); - expect(formatContentModelSpy).toHaveBeenCalled(); - expect(deleteSelectionSpy).toHaveBeenCalledWith(mockedModel, [], mockedContext); - expect(formatResult).toBeTrue(); - expect(mockedContext).toEqual({ - deletedEntities: [], - newEntities: [], - newImages: [], - clearModelCache: true, - skipUndoSnapshot: true, - newPendingFormat: undefined, - }); - expect(normalizeContentModelSpy).toHaveBeenCalledWith(mockedModel); - }); - it('Letter input, expanded selection, no modifier key, deleteSelection returns range, has segment format', () => { const mockedFormat = 'FORMAT' as any; getDOMSelectionSpy.and.returnValue({ @@ -381,50 +344,6 @@ describe('keyboardInput', () => { deletedEntities: [], newEntities: [], newImages: [], - clearModelCache: true, - skipUndoSnapshot: true, - newPendingFormat: mockedFormat, - }); - expect(normalizeContentModelSpy).toHaveBeenCalledWith(mockedModel); - }); - - it('Enter key input on collapsed range', () => { - const mockedFormat = 'FORMAT' as any; - getDOMSelectionSpy.and.returnValue({ - type: 'range', - range: { - collapsed: true, - }, - }); - deleteSelectionSpy.and.returnValue({ - deleteResult: 'range', - insertPoint: { - marker: { - format: mockedFormat, - }, - }, - }); - - const rawEvent = { - key: 'Enter', - } as any; - - keyboardInput(editor, rawEvent); - - expect(getDOMSelectionSpy).toHaveBeenCalled(); - expect(takeSnapshotSpy).toHaveBeenCalled(); - expect(formatContentModelSpy).toHaveBeenCalled(); - expect(deleteSelectionSpy).toHaveBeenCalledWith( - mockedModel, - [handleEnterOnList, deleteEmptyQuote], - mockedContext - ); - expect(formatResult).toBeTrue(); - expect(mockedContext).toEqual({ - deletedEntities: [], - newEntities: [], - newImages: [], - clearModelCache: true, skipUndoSnapshot: true, newPendingFormat: mockedFormat, }); From ac1a2c1824fc687049842ab6b74fafb2f44a4a8b Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 1 May 2024 10:00:57 -0700 Subject: [PATCH 04/66] improve --- .../lib/modelApi/editing/deleteSelection.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts index 5373bd63af3..20ff0629c65 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts @@ -41,18 +41,16 @@ function isValidDeleteSelectionContext( // If we end up with multiple paragraphs impacted, we need to merge them function mergeParagraphAfterDelete(context: DeleteSelectionContext) { const { insertPoint, deleteResult, lastParagraph, lastTableContext } = context; - const prevParagraph = insertPoint?.paragraph; if ( insertPoint && - prevParagraph && deleteResult != 'notDeleted' && deleteResult != 'nothingToDelete' && lastParagraph && - lastParagraph != prevParagraph && + lastParagraph != insertPoint.paragraph && lastTableContext == insertPoint.tableContext ) { - prevParagraph.segments.push(...lastParagraph.segments); + insertPoint.paragraph.segments.push(...lastParagraph.segments); lastParagraph.segments = []; } } From bc99d9d65a614d77ac657c3a9c95feffacad6cd7 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 1 May 2024 17:08:00 -0700 Subject: [PATCH 05/66] Let Content Model cache handle child list change --- .../ribbon/plugin/createRibbonPlugin.ts | 5 +- .../utils/formatSegmentWithContentModel.ts | 6 +- .../lib/command/paste/mergePasteContent.ts | 16 +- .../setContentModel/setContentModel.ts | 3 +- .../lib/corePlugin/cache/CachePlugin.ts | 23 ++- .../corePlugin/cache/EmptySegmentFormat.ts | 18 +++ .../lib/corePlugin/cache/domIndexerImpl.ts | 147 +++++++++++++++++- .../corePlugin/cache/textMutationObserver.ts | 78 ++++++++-- .../lib/context/DomIndexer.ts | 7 + .../lib/context/TextMutationObserver.ts | 4 +- .../lib/index.ts | 1 + .../parameter/ReconcileChildListContext.ts | 32 ++++ 12 files changed, 297 insertions(+), 43 deletions(-) create mode 100644 packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts create mode 100644 packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts diff --git a/demo/scripts/controlsV2/roosterjsReact/ribbon/plugin/createRibbonPlugin.ts b/demo/scripts/controlsV2/roosterjsReact/ribbon/plugin/createRibbonPlugin.ts index af9c1fe5147..260307bf584 100644 --- a/demo/scripts/controlsV2/roosterjsReact/ribbon/plugin/createRibbonPlugin.ts +++ b/demo/scripts/controlsV2/roosterjsReact/ribbon/plugin/createRibbonPlugin.ts @@ -1,9 +1,8 @@ -import { getFormatState } from 'roosterjs-content-model-api'; +import { ContentModelFormatState, IEditor, PluginEvent } from 'roosterjs-content-model-types'; import { getObjectKeys } from 'roosterjs-content-model-dom'; import { LocalizedStrings, UIUtilities } from 'roosterjs-react'; import { RibbonButton } from '../type/RibbonButton'; import type { RibbonPlugin } from '../type/RibbonPlugin'; -import { ContentModelFormatState, IEditor, PluginEvent } from 'roosterjs-content-model-types'; class RibbonPluginImpl implements RibbonPlugin { private editor: IEditor | null = null; @@ -151,7 +150,7 @@ class RibbonPluginImpl implements RibbonPlugin { private updateFormat() { if (this.editor && this.onFormatChanged) { - const newFormatState = getFormatState(this.editor); + const newFormatState = {}; // = getFormatState(this.editor); if ( !this.formatState || diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts index 8031a24b6d6..4d4aa634fd1 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts @@ -40,9 +40,9 @@ export function formatSegmentWithContentModel( model, !!includingFormatHolder ); - let isCollapsedSelection = - segmentAndParagraphs.length == 1 && - segmentAndParagraphs[0][0].segmentType == 'SelectionMarker'; + let isCollapsedSelection = segmentAndParagraphs.every( + x => x[0].segmentType == 'SelectionMarker' + ); if (isCollapsedSelection) { const para = segmentAndParagraphs[0][1]; diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 9f191f235c9..4e132a061e1 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -1,4 +1,5 @@ import { createDomToModelContextForSanitizing } from '../createModelFromHtml/createDomToModelContextForSanitizing'; +import { EmptySegmentFormat } from '../../corePlugin/cache/EmptySegmentFormat'; import { ChangeSource, cloneModel, @@ -12,25 +13,10 @@ import type { ClipboardData, CloneModelOptions, ContentModelDocument, - ContentModelSegmentFormat, IEditor, MergeModelOption, } from 'roosterjs-content-model-types'; -const EmptySegmentFormat: Required = { - backgroundColor: '', - fontFamily: '', - fontSize: '', - fontWeight: '', - italic: false, - letterSpacing: '', - lineHeight: '', - strikethrough: false, - superOrSubScriptSequence: '', - textColor: '', - underline: false, -}; - const CloneOption: CloneModelOptions = { includeCachedElement: (node, type) => (type == 'cache' ? undefined : node), }; diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts index 1c5909d5bf3..240b0329d95 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts @@ -46,8 +46,7 @@ export const setContentModel: SetContentModel = (core, model, option, onNodeCrea } // Clear pending mutations since we will use our latest model object to replace existing cache - core.cache.textMutationObserver?.flushMutations(); - core.cache.cachedModel = model; + core.cache.textMutationObserver?.flushMutations(model); } return selection; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index 199209cf817..a36f27c6b0d 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -8,6 +8,7 @@ import type { PluginEvent, PluginWithState, EditorOptions, + ContentModelDocument, } from 'roosterjs-content-model-types'; /** @@ -27,7 +28,12 @@ class CachePlugin implements PluginWithState { ? {} : { domIndexer: domIndexerImpl, - textMutationObserver: createTextMutationObserver(contentDiv, this.onMutation), + textMutationObserver: createTextMutationObserver( + contentDiv, + domIndexerImpl, + this.onMutation, + this.onSkipMutation + ), }; } @@ -122,6 +128,13 @@ class CachePlugin implements PluginWithState { } }; + private onSkipMutation = (newModel: ContentModelDocument) => { + if (!this.editor?.isInShadowEdit()) { + this.state.cachedModel = newModel; + this.state.cachedSelection = undefined; + } + }; + private onNativeSelectionChange = () => { if (this.editor?.hasFocus()) { this.updateCachedModel(this.editor); @@ -129,7 +142,9 @@ class CachePlugin implements PluginWithState { }; private invalidateCache() { - if (!this.editor?.isInShadowEdit()) { + if (!this.editor?.isInShadowEdit() && this.state.cachedModel) { + console.error('Clear cache'); + this.state.cachedModel = undefined; this.state.cachedSelection = undefined; } @@ -156,9 +171,13 @@ class CachePlugin implements PluginWithState { this.invalidateCache(); } else { updateCachedSelection(this.state, newRangeEx); + + console.log('Successfully reconcile' + JSON.stringify(this.state.cachedModel)); } } else { this.state.cachedSelection = cachedSelection; + + console.log('Selection not changed'); } } } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts new file mode 100644 index 00000000000..5408d42f816 --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts @@ -0,0 +1,18 @@ +import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export const EmptySegmentFormat: Required = { + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, +}; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 5c42d7460ee..ba65d41b764 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -1,6 +1,8 @@ +import { EmptySegmentFormat } from './EmptySegmentFormat'; import { createSelectionMarker, createText, + getObjectKeys, isNodeOfType, setSelection, } from 'roosterjs-content-model-dom'; @@ -9,6 +11,7 @@ import type { ContentModelDocument, ContentModelParagraph, ContentModelSegment, + ContentModelSegmentFormat, ContentModelSelectionMarker, ContentModelTable, ContentModelTableRow, @@ -16,6 +19,7 @@ import type { DomIndexer, DOMSelection, RangeSelectionForCache, + ReconcileChildListContext, Selectable, } from 'roosterjs-content-model-types'; @@ -47,6 +51,10 @@ function isIndexedSegment(node: Node): node is IndexedSegmentNode { ); } +function getIndexedSegmentItem(node: Node | null): SegmentItem | null { + return node && isIndexedSegment(node) ? node.__roosterjsContentModel : null; +} + function onSegment( segmentNode: Node, paragraph: ContentModelParagraph, @@ -67,9 +75,7 @@ function onParagraph(paragraphElement: HTMLElement) { if (!previousText) { previousText = child; } else { - const item = isIndexedSegment(previousText) - ? previousText.__roosterjsContentModel - : undefined; + const item = getIndexedSegmentItem(previousText); if (item && isIndexedSegment(child)) { item.segments = item.segments.concat(child.__roosterjsContentModel.segments); @@ -184,9 +190,10 @@ function reconcileNodeSelection(node: Node, offset: number): Selectable | undefi function insertMarker(node: Node | null, isAfter: boolean): Selectable | undefined { let marker: ContentModelSelectionMarker | undefined; + const segmentItem = node && getIndexedSegmentItem(node); - if (node && isIndexedSegment(node)) { - const { paragraph, segments } = node.__roosterjsContentModel; + if (segmentItem) { + const { paragraph, segments } = segmentItem; const index = paragraph.segments.indexOf(segments[0]); if (index >= 0) { @@ -288,6 +295,135 @@ function reconcileTextSelection( return selectable; } +function reconcileChildList( + addedNodes: ArrayLike, + removedNodes: ArrayLike, + context: ReconcileChildListContext +): boolean { + let canHandle = true; + + // First process added nodes + const addedNode = addedNodes[0]; + + if (addedNodes.length == 1 && isNodeOfType(addedNode, 'TEXT_NODE')) { + canHandle = reconcileAddedNode(addedNode, context); + } else if (addedNodes.length > 0) { + canHandle = false; + } + + // Second, process removed nodes + const removedNode = removedNodes[0]; + + if (canHandle && removedNodes.length == 1) { + canHandle = reconcileRemovedNode(removedNode, context); + } else if (removedNodes.length > 0) { + canHandle = false; + } + + return canHandle; +} + +function reconcileAddedNode(node: Text, context: ReconcileChildListContext): boolean { + let segmentItem: SegmentItem | null = null; + let index = -1; + let existingSegment: ContentModelSegment; + const { previousSibling, nextSibling } = node; + + if ( + (segmentItem = getIndexedSegmentItem(previousSibling)) && + (existingSegment = segmentItem.segments[segmentItem.segments.length - 1]) && + (index = segmentItem.paragraph.segments.indexOf(existingSegment)) >= 0 + ) { + // When we can find indexed segment before current one, use it as the insert index + indexNode(segmentItem.paragraph, index + 1, node, existingSegment.format); + } else if ( + (segmentItem = getIndexedSegmentItem(nextSibling)) && + (existingSegment = segmentItem.segments[0]) && + (index = segmentItem.paragraph.segments.indexOf(existingSegment)) >= 0 + ) { + // When we can find indexed segment after current one, use it as the insert index + indexNode(segmentItem.paragraph, index, node, existingSegment.format); + } else if (context.paragraph && context.segIndex >= 0) { + // When there is indexed paragraph from removed nodes, we can use it as the insert index + indexNode(context.paragraph, context.segIndex, node, context.format); + } else if (context.pendingTextNode === undefined) { + // When we can't find the insert index, set current node as pending node + // so later we can pick it up when we have enough info when processing removed node + // Only do this when pendingTextNode is undefined. If it is null it means there was already a pending node before + // and in that case we should return false since we can't handle two pending text node + context.pendingTextNode = node; + } else { + return false; + } + + return true; +} + +function reconcileRemovedNode(node: Node, context: ReconcileChildListContext): boolean { + let segmentItem: SegmentItem | null = null; + let removingSegment: ContentModelSegment; + + if ( + context.segIndex < 0 && + !context.paragraph && // No previous removed segment or related paragraph found, and + (segmentItem = getIndexedSegmentItem(node)) && // The removed node is indexed, and + (removingSegment = segmentItem.segments[0]) // There is at least one related segment + ) { + // Now we can remove the indexed segment from the paragraph, and remember it, later we may need to use it + context.format = removingSegment.format; + context.paragraph = segmentItem.paragraph; + context.segIndex = segmentItem.paragraph.segments.indexOf(segmentItem.segments[0]); + + for (let i = 0; i < segmentItem.segments.length; i++) { + const index = segmentItem.paragraph.segments.indexOf(segmentItem.segments[i]); + + if (index >= 0) { + segmentItem.paragraph.segments.splice(index, 1); + } + } + + if (context.pendingTextNode) { + // If we have pending text node added but not indexed, do it now + indexNode( + context.paragraph, + context.segIndex, + context.pendingTextNode, + segmentItem.segments[0].format + ); + + // Set to null since we have processed it. + // Next time we see a pending node we know we have already processed one so it is a situation we cannot handle + context.pendingTextNode = null; + } + + return true; + } else { + return false; + } +} + +function indexNode( + paragraph: ContentModelParagraph, + index: number, + textNode: Text, + format?: ContentModelSegmentFormat +) { + let copiedFormat = format ? { ...format } : undefined; + + if (copiedFormat) { + getObjectKeys(copiedFormat).forEach(key => { + if (EmptySegmentFormat[key] === undefined) { + delete copiedFormat[key]; + } + }); + } + + const text = createText(textNode.textContent ?? '', copiedFormat); + + paragraph.segments.splice(index, 0, text); + onSegment(textNode, paragraph, [text]); +} + /** * @internal * Implementation of DomIndexer @@ -297,4 +433,5 @@ export const domIndexerImpl: DomIndexer = { onParagraph, onTable, reconcileSelection, + reconcileChildList, }; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts index 42790400a69..4d215895188 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts @@ -1,11 +1,18 @@ -import type { TextMutationObserver } from 'roosterjs-content-model-types'; +import type { + ContentModelDocument, + DomIndexer, + ReconcileChildListContext, + TextMutationObserver, +} from 'roosterjs-content-model-types'; class TextMutationObserverImpl implements TextMutationObserver { private observer: MutationObserver; constructor( private contentDiv: HTMLDivElement, - private onMutation: (isTextChangeOnly: boolean) => void + private domIndexer: DomIndexer, + private onMutation: (isTextChangeOnly: boolean) => void, + private onSkipMutation: (newModel: ContentModelDocument) => void ) { this.observer = new MutationObserver(this.onMutationInternal); } @@ -23,21 +30,66 @@ class TextMutationObserverImpl implements TextMutationObserver { this.observer.disconnect(); } - flushMutations() { + flushMutations(model: ContentModelDocument) { const mutations = this.observer.takeRecords(); - this.onMutationInternal(mutations); + if (model) { + this.onSkipMutation(model); + } else { + this.onMutationInternal(mutations); + } } private onMutationInternal = (mutations: MutationRecord[]) => { - const firstTarget = mutations[0]?.target; + const context: ReconcileChildListContext = { + segIndex: -1, + }; + let canHandle = true; + let firstTarget: Node | null = null; + let lastTextChangeNode: Node | null = null; + + for (let i = 0; i < mutations.length && canHandle; i++) { + const mutation = mutations[i]; + + switch (mutation.type) { + case 'attributes': + if (mutation.target != this.contentDiv) { + // We cannot handle attributes changes on editor content for now + canHandle = false; + } + break; + + case 'characterData': + if (lastTextChangeNode && lastTextChangeNode != mutation.target) { + // Multiple text nodes got changed, we don't know how to handle it + canHandle = false; + } else { + lastTextChangeNode = mutation.target; + this.onMutation(true /*textOnly*/); + } + break; - if (firstTarget) { - const isTextChangeOnly = mutations.every( - mutation => mutation.type == 'characterData' && mutation.target == firstTarget - ); + case 'childList': + if (!firstTarget) { + firstTarget = mutation.target; + } else if (firstTarget != mutation.target) { + canHandle = false; + } + + if (canHandle) { + canHandle = this.domIndexer.reconcileChildList( + mutation.addedNodes, + mutation.removedNodes, + context + ); + } + + break; + } + } - this.onMutation(isTextChangeOnly); + if (!canHandle || context.pendingTextNode) { + this.onMutation(false /*textOnly*/); } }; } @@ -47,7 +99,9 @@ class TextMutationObserverImpl implements TextMutationObserver { */ export function createTextMutationObserver( contentDiv: HTMLDivElement, - onMutation: (isTextChangeOnly: boolean) => void + domIndexer: DomIndexer, + onMutation: (isTextChangeOnly: boolean) => void, + onSkipMutation: (newModel: ContentModelDocument) => void ): TextMutationObserver { - return new TextMutationObserverImpl(contentDiv, onMutation); + return new TextMutationObserverImpl(contentDiv, domIndexer, onMutation, onSkipMutation); } diff --git a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts index 0b8c5125b47..b55a9e9f925 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts @@ -1,3 +1,4 @@ +import type { ReconcileChildListContext } from '../parameter/ReconcileChildListContext'; import type { CacheSelection } from '../pluginState/CachePluginState'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { ContentModelParagraph } from '../block/ContentModelParagraph'; @@ -47,4 +48,10 @@ export interface DomIndexer { newSelection: DOMSelection, oldSelection?: CacheSelection ) => boolean; + + reconcileChildList: ( + addedNodes: ArrayLike, + removedNodes: ArrayLike, + context: ReconcileChildListContext + ) => boolean; } diff --git a/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts b/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts index 96fc813ada0..88588d7b0c1 100644 --- a/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts +++ b/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts @@ -1,3 +1,5 @@ +import type { ContentModelDocument } from '../group/ContentModelDocument'; + /** * A wrapper of MutationObserver to observe text change from editor */ @@ -15,5 +17,5 @@ export interface TextMutationObserver { /** * Flush all pending mutations that have not be handled in order to ignore them */ - flushMutations(): void; + flushMutations(newModel?: ContentModelDocument): void; } diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index df428bb8d0a..4c931578067 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -309,6 +309,7 @@ export { NodeTypeMap } from './parameter/NodeTypeMap'; export { TypeOfBlockGroup } from './parameter/TypeOfBlockGroup'; export { OperationalBlocks } from './parameter/OperationalBlocks'; export { ParsedTable, ParsedTableCell } from './parameter/ParsedTable'; +export { ReconcileChildListContext } from './parameter/ReconcileChildListContext'; export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent'; export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent'; diff --git a/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts b/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts new file mode 100644 index 00000000000..a95b71f74d1 --- /dev/null +++ b/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts @@ -0,0 +1,32 @@ +import type { ContentModelParagraph } from '../block/ContentModelParagraph'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; + +/** + * Context object used by DomIndexer when reconcile mutations with child list + */ +export interface ReconcileChildListContext { + /** + * Index of segment in current paragraph + */ + segIndex: number; + + /** + * The current paragraph that we are handling + */ + paragraph?: ContentModelParagraph; + + /** + * Text node that is added from mutation but has not been handled. This can happen when we first see an added node then later we see a removed one. + * e.g. Type text in an empty paragraph (<div><br></div>), so a text node will be added and <BR> will be removed. + * Set to a valid text node means we need to handle it later. If it is finally not handled, that means we need to clear cache + * Set to undefined (initial value) means no pending text node is hit yet (valid case) + * Set to null means there was a pending text node which is already handled, so if we see another pending text node, + * we should clear cache since we don't know how to handle it + */ + pendingTextNode?: Text | null; + + /** + * Format of the removed segment, this will be used as the format for newly created segment + */ + format?: ContentModelSegmentFormat; +} From 8304dc9c1905d3f02725e5ef58a9ebb9d3c4824f Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 6 May 2024 10:28:54 -0700 Subject: [PATCH 06/66] Scroll caret into view when call formatContentModel --- .../lib/command/paste/mergePasteContent.ts | 1 + .../formatContentModel/formatContentModel.ts | 21 ++++++++++++-- .../formatContentModelTest.ts | 28 +++++++++++++++++++ .../lib/edit/keyboardDelete.ts | 1 + .../lib/edit/keyboardInput.ts | 1 + .../parameter/FormatContentModelOptions.ts | 5 ++++ 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 9f191f235c9..9cd3302ad4b 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -94,6 +94,7 @@ export function mergePasteContent( { changeSource: ChangeSource.Paste, getChangeData: () => clipboardData, + scrollCaretIntoView: true, apiName: 'paste', } ); diff --git a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts index 6515ff40b50..bcebd23f23b 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts @@ -1,4 +1,4 @@ -import { ChangeSource } from 'roosterjs-content-model-dom'; +import { ChangeSource, getSelectionRootNode } from 'roosterjs-content-model-dom'; import type { ChangedEntity, ContentChangedEvent, @@ -24,8 +24,15 @@ export const formatContentModel: FormatContentModel = ( options, domToModelOptions ) => { - const { apiName, onNodeCreated, getChangeData, changeSource, rawEvent, selectionOverride } = - options || {}; + const { + apiName, + onNodeCreated, + getChangeData, + changeSource, + rawEvent, + selectionOverride, + scrollCaretIntoView, + } = options || {}; const model = core.api.createContentModel(core, domToModelOptions, selectionOverride); const context: FormatContentModelContext = { newEntities: [], @@ -63,6 +70,14 @@ export const formatContentModel: FormatContentModel = ( handlePendingFormat(core, context, selection); + if (selection && scrollCaretIntoView) { + const selectionRoot = getSelectionRootNode(selection); + const rootElement = + selectionRoot && core.domHelper.findClosestElementAncestor(selectionRoot); + + rootElement?.scrollIntoView(); + } + const eventData: ContentChangedEvent = { eventType: 'contentChanged', contentModel: clearModelCache ? undefined : model, diff --git a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts index dc1a0afae0c..09996b39e09 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts @@ -21,6 +21,7 @@ describe('formatContentModel', () => { let hasFocus: jasmine.Spy; let getClientWidth: jasmine.Spy; let announce: jasmine.Spy; + let findClosestElementAncestor: jasmine.Spy; const apiName = 'mockedApi'; const mockedContainer = 'C' as any; @@ -42,6 +43,7 @@ describe('formatContentModel', () => { hasFocus = jasmine.createSpy('hasFocus'); getClientWidth = jasmine.createSpy('getClientWidth'); announce = jasmine.createSpy('announce'); + findClosestElementAncestor = jasmine.createSpy('findClosestElementAncestor '); core = ({ api: { @@ -62,6 +64,7 @@ describe('formatContentModel', () => { domHelper: { hasFocus, getClientWidth, + findClosestElementAncestor, }, } as any) as EditorCore; }); @@ -549,6 +552,31 @@ describe('formatContentModel', () => { }); expect(announce).not.toHaveBeenCalled(); }); + + it('Has scrollCaretIntoView, and callback return true', () => { + const scrollIntoViewSpy = jasmine.createSpy('scrollIntoView'); + const mockedImage = { scrollIntoView: scrollIntoViewSpy } as any; + + findClosestElementAncestor.and.returnValue(mockedImage); + setContentModel.and.returnValue({ + type: 'image', + image: mockedImage, + }); + formatContentModel( + core, + (model, context) => { + context.clearModelCache = true; + return true; + }, + { + scrollCaretIntoView: true, + apiName, + } + ); + + expect(findClosestElementAncestor).toHaveBeenCalledWith(mockedImage); + expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1); + }); }); describe('Editor does not have focus', () => { diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts index e4fadcc1a1b..44c73db93aa 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts @@ -49,6 +49,7 @@ export function keyboardDelete(editor: IEditor, rawEvent: KeyboardEvent) { rawEvent, changeSource: ChangeSource.Keyboard, getChangeData: () => rawEvent.which, + scrollCaretIntoView: true, apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey', } ); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index a25f2d6beaa..91ecf99647e 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -36,6 +36,7 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) { } }, { + scrollCaretIntoView: true, rawEvent, } ); diff --git a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts index c2ac135af77..ec43eae1ed1 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts @@ -38,6 +38,11 @@ export interface FormatContentModelOptions { * When specified, use this selection range to override current selection inside editor */ selectionOverride?: DOMSelection; + + /** + * When pass to true, scroll the editing caret into view after write DOM tree if need + */ + scrollCaretIntoView?: boolean; } /** From 65913c0d30149fb7e63b6ca217b792d115c83dd5 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 6 May 2024 11:08:00 -0700 Subject: [PATCH 07/66] scroll caret into view --- .../roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index 5120c060d36..6f5d17e1c3b 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -28,6 +28,7 @@ export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { }, { rawEvent, + scrollCaretIntoView: true, } ); } From 7cc5a1b84a94fd4473b48b312cd43a95e6cfcc25 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 13 May 2024 11:39:04 -0700 Subject: [PATCH 08/66] Readonly types (3rd try --- .../lib/command/paste/mergePasteContent.ts | 4 +- .../lib/modelApi/editing/cloneModel.ts | 3 +- .../modelApi/editing/getSegmentTextFormat.ts | 3 +- .../contentModel/block/ContentModelBlock.ts | 35 ++- .../block/ContentModelBlockBase.ts | 48 +++- .../contentModel/block/ContentModelDivider.ts | 33 ++- .../block/ContentModelParagraph.ts | 53 +++- .../contentModel/block/ContentModelTable.ts | 41 +++- .../block/ContentModelTableRow.ts | 46 +++- .../blockGroup/ContentModelBlockGroup.ts | 26 +- .../blockGroup/ContentModelBlockGroupBase.ts | 29 ++- .../blockGroup/ContentModelDocument.ts | 37 ++- .../blockGroup/ContentModelFormatContainer.ts | 43 +++- .../blockGroup/ContentModelGeneralBlock.ts | 50 +++- .../blockGroup/ContentModelListItem.ts | 48 +++- .../blockGroup/ContentModelTableCell.ts | 53 +++- .../lib/contentModel/common/MutableMark.ts | 29 +++ .../lib/contentModel/common/MutableType.ts | 98 ++++++++ .../lib/contentModel/common/ReadonlyMark.ts | 29 +++ .../lib/contentModel/common/Selectable.ts | 15 +- .../decorator/ContentModelCode.ts | 25 +- .../decorator/ContentModelDecorator.ts | 14 +- .../decorator/ContentModelLink.ts | 30 ++- .../decorator/ContentModelListLevel.ts | 44 +++- .../ContentModelParagraphDecorator.ts | 39 ++- .../contentModel/entity/ContentModelEntity.ts | 62 ++++- .../format/ContentModelBlockFormat.ts | 17 +- .../format/ContentModelCodeFormat.ts | 14 +- .../format/ContentModelDividerFormat.ts | 19 +- .../format/ContentModelEntityFormat.ts | 15 +- .../format/ContentModelFormatBase.ts | 4 +- .../ContentModelFormatContainerFormat.ts | 26 +- .../format/ContentModelHyperLinkFormat.ts | 17 +- .../format/ContentModelImageFormat.ts | 23 +- .../format/ContentModelListItemFormat.ts | 17 +- .../format/ContentModelListItemLevelFormat.ts | 17 +- .../format/ContentModelSegmentFormat.ts | 17 +- .../format/ContentModelTableCellFormat.ts | 23 +- .../format/ContentModelTableFormat.ts | 23 +- .../format/ContentModelWithDataset.ts | 18 +- .../format/ContentModelWithFormat.ts | 10 + .../format/metadata/DatasetFormat.ts | 5 + .../contentModel/segment/ContentModelBr.ts | 10 +- .../segment/ContentModelGeneralSegment.ts | 30 ++- .../contentModel/segment/ContentModelImage.ts | 37 ++- .../segment/ContentModelSegment.ts | 29 ++- .../segment/ContentModelSegmentBase.ts | 59 ++++- .../segment/ContentModelSelectionMarker.ts | 11 +- .../contentModel/segment/ContentModelText.ts | 21 +- .../lib/index.ts | 232 ++++++++++++++---- 50 files changed, 1379 insertions(+), 252 deletions(-) create mode 100644 packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts create mode 100644 packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts create mode 100644 packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 9cd3302ad4b..b8125a6097c 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -12,12 +12,12 @@ import type { ClipboardData, CloneModelOptions, ContentModelDocument, - ContentModelSegmentFormat, + ContentModelSegmentFormatCommon, IEditor, MergeModelOption, } from 'roosterjs-content-model-types'; -const EmptySegmentFormat: Required = { +const EmptySegmentFormat: Required = { backgroundColor: '', fontFamily: '', fontSize: '', diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts index 2bf5dc15344..95ee0cee0ed 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts @@ -265,12 +265,13 @@ function cloneListItem( item: ContentModelListItem, options: CloneModelOptions ): ContentModelListItem { - const { formatHolder, levels } = item; + const { formatHolder, levels, cachedElement } = item; return Object.assign( { formatHolder: cloneSelectionMarker(formatHolder), levels: levels.map(cloneListLevel), + cachedElement: handleCachedElement(cachedElement, 'cache', options), }, cloneBlockBase(item), cloneBlockGroupBase(item, options) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts index c8928b96cf9..cd91dbbde22 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts @@ -22,9 +22,10 @@ export function getSegmentTextFormat(segment: ContentModelSegment): ContentModel } const removeUndefinedValues = (format: ContentModelSegmentFormat): ContentModelSegmentFormat => { - const textFormat: Record = {}; + const textFormat: Record = {}; Object.keys(format).filter(key => { const value = format[key as keyof ContentModelSegmentFormat]; + if (value !== undefined) { textFormat[key] = value; } diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts index 59179bdd73c..c4256a3bc06 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts @@ -1,10 +1,19 @@ -import type { ContentModelDivider } from './ContentModelDivider'; -import type { ContentModelEntity } from '../entity/ContentModelEntity'; -import type { ContentModelFormatContainer } from '../blockGroup/ContentModelFormatContainer'; -import type { ContentModelGeneralBlock } from '../blockGroup/ContentModelGeneralBlock'; -import type { ContentModelListItem } from '../blockGroup/ContentModelListItem'; -import type { ContentModelParagraph } from './ContentModelParagraph'; -import type { ContentModelTable } from './ContentModelTable'; +import type { ContentModelDivider, ReadonlyContentModelDivider } from './ContentModelDivider'; +import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; +import type { + ContentModelFormatContainer, + ReadonlyContentModelFormatContainer, +} from '../blockGroup/ContentModelFormatContainer'; +import type { + ContentModelGeneralBlock, + ReadonlyContentModelGeneralBlock, +} from '../blockGroup/ContentModelGeneralBlock'; +import type { + ContentModelListItem, + ReadonlyContentModelListItem, +} from '../blockGroup/ContentModelListItem'; +import type { ContentModelParagraph, ReadonlyContentModelParagraph } from './ContentModelParagraph'; +import type { ContentModelTable, ReadonlyContentModelTable } from './ContentModelTable'; /** * A union type of Content Model Block @@ -17,3 +26,15 @@ export type ContentModelBlock = | ContentModelParagraph | ContentModelEntity | ContentModelDivider; + +/** + * A union type of Content Model Block (Readonly) + */ +export type ReadonlyContentModelBlock = + | ReadonlyContentModelFormatContainer + | ReadonlyContentModelListItem + | ReadonlyContentModelGeneralBlock + | ReadonlyContentModelTable + | ReadonlyContentModelParagraph + | ReadonlyContentModelEntity + | ReadonlyContentModelDivider; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts index 8e05b45151f..6d0a4f3e532 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts @@ -1,16 +1,48 @@ -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; +import type { MutableMark } from '../common/MutableMark'; +import type { + ContentModelBlockFormat, + ReadonlyContentModelBlockFormat, +} from '../format/ContentModelBlockFormat'; import type { ContentModelBlockType } from './BlockType'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; /** - * Base type of a block + * Common part of base type of a block */ -export interface ContentModelBlockBase< - T extends ContentModelBlockType, - TFormat extends ContentModelBlockFormat = ContentModelBlockFormat -> extends ContentModelWithFormat { +export interface ContentModelBlockBaseCommon { /** * Type of this block */ - blockType: T; + readonly blockType: T; } + +/** + * Base type of a block + */ +export interface ContentModelBlockBase< + T extends ContentModelBlockType, + TFormat extends ContentModelBlockFormat = ContentModelBlockFormat, + TCacheElement extends HTMLElement = HTMLElement +> + extends MutableMark, + ContentModelBlockBaseCommon, + ContentModelWithFormat, + ContentModelBlockWithCache {} + +/** + * Base type of a block (Readonly) + */ +export interface ReadonlyContentModelBlockBase< + T extends ContentModelBlockType, + TFormat extends ReadonlyContentModelBlockFormat = ReadonlyContentModelBlockFormat, + TCacheElement extends HTMLElement = HTMLElement +> + extends ReadonlyMark, + ContentModelBlockBaseCommon, + ReadonlyContentModelWithFormat, + ContentModelBlockWithCache {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts index 23fe4386273..1bf01b5f435 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts @@ -1,15 +1,14 @@ -import type { ContentModelBlockBase } from './ContentModelBlockBase'; -import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ContentModelDividerFormat } from '../format/ContentModelDividerFormat'; -import type { Selectable } from '../common/Selectable'; +import type { ContentModelBlockBase, ReadonlyContentModelBlockBase } from './ContentModelBlockBase'; +import type { + ContentModelDividerFormat, + ReadonlyContentModelDividerFormat, +} from '../format/ContentModelDividerFormat'; +import type { ReadonlySelectable, Selectable } from '../common/Selectable'; /** - * Content Model of horizontal divider + * Common part of Content Model of horizontal divider */ -export interface ContentModelDivider - extends Selectable, - ContentModelBlockWithCache, - ContentModelBlockBase<'Divider', ContentModelDividerFormat> { +export interface ContentModelDividerCommon { /** * Tag name of this element, either HR or DIV */ @@ -20,3 +19,19 @@ export interface ContentModelDivider */ size?: string; } + +/** + * Content Model of horizontal divider + */ +export interface ContentModelDivider + extends Selectable, + ContentModelDividerCommon, + ContentModelBlockBase<'Divider', ContentModelDividerFormat> {} + +/** + * Content Model of horizontal divider (Readonly) + */ +export interface ReadonlyContentModelDivider + extends ReadonlySelectable, + ReadonlyContentModelBlockBase<'Divider', ReadonlyContentModelDividerFormat>, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts index cb47999a4a4..4deb7725e7d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts @@ -1,14 +1,33 @@ -import type { ContentModelBlockBase } from './ContentModelBlockBase'; -import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ContentModelParagraphDecorator } from '../decorator/ContentModelParagraphDecorator'; -import type { ContentModelSegment } from '../segment/ContentModelSegment'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelBlockBase, ReadonlyContentModelBlockBase } from './ContentModelBlockBase'; +import type { + ContentModelParagraphDecorator, + ReadonlyContentModelParagraphDecorator, +} from '../decorator/ContentModelParagraphDecorator'; +import type { + ContentModelSegment, + ReadonlyContentModelSegment, +} from '../segment/ContentModelSegment'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; + +/** + * Common part of Content Model of Paragraph + */ +export interface ContentModelParagraphCommon { + /** + * Whether this block was created from a block HTML element or just some simple segment between other block elements. + * True means it doesn't have a related block element, false means it was from a block element + */ + isImplicit?: boolean; +} /** * Content Model of Paragraph */ export interface ContentModelParagraph - extends ContentModelBlockWithCache, + extends ContentModelParagraphCommon, ContentModelBlockBase<'Paragraph'> { /** * Segments within this paragraph @@ -24,10 +43,26 @@ export interface ContentModelParagraph * Decorator info for this paragraph, used by heading and P tags */ decorator?: ContentModelParagraphDecorator; +} + +/** + * Content Model of Paragraph (Readonly) + */ +export interface ReadonlyContentModelParagraph + extends ReadonlyContentModelBlockBase<'Paragraph'>, + Readonly { + /** + * Segments within this paragraph + */ + readonly segments: ReadonlyArray; /** - * Whether this block was created from a block HTML element or just some simple segment between other block elements. - * True means it doesn't have a related block element, false means it was from a block element + * Segment format on this paragraph. This is mostly used for default format */ - isImplicit?: boolean; + readonly segmentFormat?: ReadonlyContentModelSegmentFormat; + + /** + * Decorator info for this paragraph, used by heading and P tags + */ + readonly decorator?: ReadonlyContentModelParagraphDecorator; } diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts index c9c70ebe6c4..db8ecebcd24 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts @@ -1,17 +1,21 @@ -import type { ContentModelBlockBase } from './ContentModelBlockBase'; -import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ContentModelTableFormat } from '../format/ContentModelTableFormat'; -import type { ContentModelTableRow } from './ContentModelTableRow'; -import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; +import type { ContentModelBlockBase, ReadonlyContentModelBlockBase } from './ContentModelBlockBase'; +import type { + ContentModelTableFormat, + ReadonlyContentModelTableFormat, +} from '../format/ContentModelTableFormat'; +import type { ContentModelTableRow, ReadonlyContentModelTableRow } from './ContentModelTableRow'; +import type { + ContentModelWithDataset, + ReadonlyContentModelWithDataset, +} from '../format/ContentModelWithDataset'; import type { TableMetadataFormat } from '../format/metadata/TableMetadataFormat'; /** * Content Model of Table */ export interface ContentModelTable - extends ContentModelBlockBase<'Table', ContentModelTableFormat>, - ContentModelWithDataset, - ContentModelBlockWithCache { + extends ContentModelBlockBase<'Table', ContentModelTableFormat, HTMLTableElement>, + ContentModelWithDataset { /** * Widths of each column */ @@ -22,3 +26,24 @@ export interface ContentModelTable */ rows: ContentModelTableRow[]; } + +/** + * Content Model of Table (Readonly) + */ +export interface ReadonlyContentModelTable + extends ReadonlyContentModelBlockBase< + 'Table', + ReadonlyContentModelTableFormat, + HTMLTableElement + >, + ReadonlyContentModelWithDataset { + /** + * Widths of each column + */ + readonly widths: ReadonlyArray; + + /** + * Cells of this table + */ + readonly rows: ReadonlyArray; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts index 7a5327feb01..39443c5ad9d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts @@ -1,21 +1,53 @@ -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { + ContentModelBlockFormat, + ReadonlyContentModelBlockFormat, +} from '../format/ContentModelBlockFormat'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ContentModelTableCell } from '../blockGroup/ContentModelTableCell'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { + ContentModelTableCell, + ReadonlyContentModelTableCell, +} from '../blockGroup/ContentModelTableCell'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; /** - * Content Model of Table + * Common part of Content Model of Table */ -export interface ContentModelTableRow - extends ContentModelBlockWithCache, - ContentModelWithFormat { +export interface ContentModelTableRowCommon { /** * Heights of each row */ height: number; +} +/** + * Content Model of Table + */ +export interface ContentModelTableRow + extends MutableMark, + ContentModelTableRowCommon, + ContentModelBlockWithCache, + ContentModelWithFormat { /** * Cells of this table */ cells: ContentModelTableCell[]; } + +/** + * Content Model of Table (Readonly) + */ +export interface ReadonlyContentModelTableRow + extends ReadonlyMark, + ContentModelBlockWithCache, + ReadonlyContentModelWithFormat, + Readonly { + /** + * Cells of this table + */ + readonly cells: ReadonlyArray; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts index 9462d998e10..aa908291f8e 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts @@ -1,8 +1,14 @@ -import type { ContentModelDocument } from './ContentModelDocument'; -import type { ContentModelFormatContainer } from './ContentModelFormatContainer'; -import type { ContentModelGeneralBlock } from './ContentModelGeneralBlock'; -import type { ContentModelListItem } from './ContentModelListItem'; -import type { ContentModelTableCell } from './ContentModelTableCell'; +import type { ContentModelDocument, ReadonlyContentModelDocument } from './ContentModelDocument'; +import type { + ContentModelFormatContainer, + ReadonlyContentModelFormatContainer, +} from './ContentModelFormatContainer'; +import type { + ContentModelGeneralBlock, + ReadonlyContentModelGeneralBlock, +} from './ContentModelGeneralBlock'; +import type { ContentModelListItem, ReadonlyContentModelListItem } from './ContentModelListItem'; +import type { ContentModelTableCell, ReadonlyContentModelTableCell } from './ContentModelTableCell'; /** * The union type of Content Model Block Group @@ -13,3 +19,13 @@ export type ContentModelBlockGroup = | ContentModelListItem | ContentModelTableCell | ContentModelGeneralBlock; + +/** + * The union type of Content Model Block Group (Readonly) + */ +export type ReadonlyContentModelBlockGroup = + | ReadonlyContentModelDocument + | ReadonlyContentModelFormatContainer + | ReadonlyContentModelListItem + | ReadonlyContentModelTableCell + | ReadonlyContentModelGeneralBlock; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts index 648f9b1bd70..f401cf27bd6 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts @@ -1,17 +1,38 @@ -import type { ContentModelBlock } from '../block/ContentModelBlock'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { ContentModelBlock, ReadonlyContentModelBlock } from '../block/ContentModelBlock'; import type { ContentModelBlockGroupType } from './BlockGroupType'; /** - * Base type of Content Model Block Group + * Common part of base type of Content Model Block Group */ -export interface ContentModelBlockGroupBase { +export interface ContentModelBlockGroupBaseCommon { /** * Type of this block group */ - blockGroupType: T; + readonly blockGroupType: T; +} +/** + * Base type of Content Model Block Group + */ +export interface ContentModelBlockGroupBase + extends MutableMark, + ContentModelBlockGroupBaseCommon { /** * Blocks under this group */ blocks: ContentModelBlock[]; } + +/** + * Base type of Content Model Block Group (Readonly) + */ +export interface ReadonlyContentModelBlockGroupBase + extends ReadonlyMark, + ContentModelBlockGroupBaseCommon { + /** + * Blocks under this group + */ + readonly blocks: ReadonlyArray; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts index 7f1eac6188c..63b888b13ac 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts @@ -1,15 +1,38 @@ -import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { + ContentModelBlockGroupBase, + ReadonlyContentModelBlockGroupBase, +} from './ContentModelBlockGroupBase'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; /** - * Content Model document entry point + * Common part of Content Model document entry point */ -export interface ContentModelDocument - extends ContentModelBlockGroupBase<'Document'>, - Partial> { +export interface ContentModelDocumentCommon { /** * Whether the selection in model (if any) is a revert selection (end is before start) */ hasRevertedRangeSelection?: boolean; } + +/** + * Content Model document entry point + */ +export interface ContentModelDocument + extends ContentModelDocumentCommon, + ContentModelBlockGroupBase<'Document'>, + Partial> {} + +/** + * Content Model document entry point (Readonly) + */ +export interface ReadonlyContentModelDocument + extends ReadonlyContentModelBlockGroupBase<'Document'>, + Partial>, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts index 5abe704bdea..eeb0393043a 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts @@ -1,15 +1,20 @@ -import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; -import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; -import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ContentModelFormatContainerFormat } from '../format/ContentModelFormatContainerFormat'; +import type { + ContentModelBlockBase, + ReadonlyContentModelBlockBase, +} from '../block/ContentModelBlockBase'; +import type { + ContentModelBlockGroupBase, + ReadonlyContentModelBlockGroupBase, +} from './ContentModelBlockGroupBase'; +import type { + ContentModelFormatContainerFormat, + ReadonlyContentModelFormatContainerFormat, +} from '../format/ContentModelFormatContainerFormat'; /** - * Content Model of Format Container + * Common part of Content Model of Format Container */ -export interface ContentModelFormatContainer - extends ContentModelBlockWithCache, - ContentModelBlockGroupBase<'FormatContainer'>, - ContentModelBlockBase<'BlockGroup', ContentModelFormatContainerFormat> { +export interface ContentModelFormatContainerCommon { /** * Tag name of this container */ @@ -21,3 +26,23 @@ export interface ContentModelFormatContainer */ zeroFontSize?: boolean; } + +/** + * Content Model of Format Container + */ +export interface ContentModelFormatContainer + extends ContentModelFormatContainerCommon, + ContentModelBlockGroupBase<'FormatContainer'>, + ContentModelBlockBase<'BlockGroup', ContentModelFormatContainerFormat, HTMLElement> {} + +/** + * Content Model of Format Container (Readonly) + */ +export interface ReadonlyContentModelFormatContainer + extends ReadonlyContentModelBlockGroupBase<'FormatContainer'>, + ReadonlyContentModelBlockBase< + 'BlockGroup', + ReadonlyContentModelFormatContainerFormat, + HTMLElement + >, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts index ff3899baa4f..1f39af631d8 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts @@ -1,18 +1,48 @@ -import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { Selectable } from '../common/Selectable'; +import type { + ContentModelBlockBase, + ReadonlyContentModelBlockBase, +} from '../block/ContentModelBlockBase'; +import type { + ContentModelBlockFormat, + ReadonlyContentModelBlockFormat, +} from '../format/ContentModelBlockFormat'; +import type { + ContentModelBlockGroupBase, + ReadonlyContentModelBlockGroupBase, +} from './ContentModelBlockGroupBase'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; +import type { ReadonlySelectable, Selectable } from '../common/Selectable'; /** - * Content Model for general Block element + * Common part of Content Model for general Block element */ -export interface ContentModelGeneralBlock - extends Selectable, - ContentModelBlockGroupBase<'General'>, - ContentModelBlockBase<'BlockGroup', ContentModelBlockFormat & ContentModelSegmentFormat> { +export interface ContentModelGeneralBlockCommon { /** * A reference to original HTML node that this model was created from */ element: HTMLElement; } + +/** + * Content Model for general Block element + */ +export interface ContentModelGeneralBlock + extends Selectable, + ContentModelGeneralBlockCommon, + ContentModelBlockGroupBase<'General'>, + ContentModelBlockBase<'BlockGroup', ContentModelBlockFormat & ContentModelSegmentFormat> {} + +/** + * Content Model for general Block element (Readonly) + */ +export interface ReadonlyContentModelGeneralBlock + extends ReadonlySelectable, + ReadonlyContentModelBlockGroupBase<'General'>, + ReadonlyContentModelBlockBase< + 'BlockGroup', + ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + >, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts index a7b7eabd5ff..5f29ec1cc57 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts @@ -1,15 +1,30 @@ -import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; -import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; -import type { ContentModelListItemFormat } from '../format/ContentModelListItemFormat'; -import type { ContentModelListLevel } from '../decorator/ContentModelListLevel'; -import type { ContentModelSelectionMarker } from '../segment/ContentModelSelectionMarker'; +import type { + ContentModelBlockBase, + ReadonlyContentModelBlockBase, +} from '../block/ContentModelBlockBase'; +import type { + ContentModelBlockGroupBase, + ReadonlyContentModelBlockGroupBase, +} from './ContentModelBlockGroupBase'; +import type { + ContentModelListItemFormat, + ReadonlyContentModelListItemFormat, +} from '../format/ContentModelListItemFormat'; +import type { + ContentModelListLevel, + ReadonlyContentModelListLevel, +} from '../decorator/ContentModelListLevel'; +import type { + ContentModelSelectionMarker, + ReadonlyContentModelSelectionMarker, +} from '../segment/ContentModelSelectionMarker'; /** * Content Model of List Item */ export interface ContentModelListItem extends ContentModelBlockGroupBase<'ListItem'>, - ContentModelBlockBase<'BlockGroup', ContentModelListItemFormat> { + ContentModelBlockBase<'BlockGroup', ContentModelListItemFormat, HTMLLIElement> { /** * Type of this list, either ordered or unordered */ @@ -20,3 +35,24 @@ export interface ContentModelListItem */ formatHolder: ContentModelSelectionMarker; } + +/** + * Content Model of List Item (Readonly) + */ +export interface ReadonlyContentModelListItem + extends ReadonlyContentModelBlockGroupBase<'ListItem'>, + ReadonlyContentModelBlockBase< + 'BlockGroup', + ReadonlyContentModelListItemFormat, + HTMLLIElement + > { + /** + * Type of this list, either ordered or unordered + */ + readonly levels: ReadonlyArray; + + /** + * A dummy segment to hold format of this list item + */ + readonly formatHolder: ReadonlyContentModelSelectionMarker; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts index 4e51590c48b..d231cee39ea 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts @@ -1,20 +1,27 @@ import type { TableCellMetadataFormat } from '../format/metadata/TableCellMetadataFormat'; -import type { ContentModelBlockGroupBase } from './ContentModelBlockGroupBase'; +import type { + ContentModelBlockGroupBase, + ReadonlyContentModelBlockGroupBase, +} from './ContentModelBlockGroupBase'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ContentModelTableCellFormat } from '../format/ContentModelTableCellFormat'; -import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; -import type { Selectable } from '../common/Selectable'; +import type { + ContentModelTableCellFormat, + ReadonlyContentModelTableCellFormat, +} from '../format/ContentModelTableCellFormat'; +import type { + ContentModelWithDataset, + ReadonlyContentModelWithDataset, +} from '../format/ContentModelWithDataset'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; +import type { ReadonlySelectable, Selectable } from '../common/Selectable'; /** - * Content Model of Table Cell + * Common part of Content Model of Table Cell */ -export interface ContentModelTableCell - extends Selectable, - ContentModelBlockGroupBase<'TableCell'>, - ContentModelWithFormat, - ContentModelWithDataset, - ContentModelBlockWithCache { +export interface ContentModelTableCellCommon { /** * Whether this cell is spanned from left cell */ @@ -30,3 +37,25 @@ export interface ContentModelTableCell */ isHeader?: boolean; } + +/** + * Content Model of Table Cell + */ +export interface ContentModelTableCell + extends Selectable, + ContentModelTableCellCommon, + ContentModelBlockGroupBase<'TableCell'>, + ContentModelWithFormat, + ContentModelWithDataset, + ContentModelBlockWithCache {} + +/** + * Content Model of Table Cell (Readonly) + */ +export interface ReadonlyContentModelTableCell + extends ReadonlySelectable, + ReadonlyContentModelBlockGroupBase<'TableCell'>, + ReadonlyContentModelWithFormat, + ReadonlyContentModelWithDataset, + ContentModelBlockWithCache, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts new file mode 100644 index 00000000000..ae5b46d684a --- /dev/null +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts @@ -0,0 +1,29 @@ +/** + * A tag type to mark a content model type as mutable. + * + * This is generally a workaround to https://github.com/microsoft/TypeScript/issues/13347 + * + * In order to know if a block has been changed, we want to mark all blocks, blocks groups, segments and their members as readonly, + * When we want to change a block/segment/block group, we need to call a function to convert it to mutable. Inside this function + * we can make some change to the object (e.g. remove cached element if any) so later we know this object is changed. + * So that we expect there is a build time error if we assign a readonly object to a function that accepts mutable object only. + * However this does not happen today. + * + * To workaround it, we manually add a hidden member (dummy) to all mutable types, and add another member with readonly array type to + * readonly types. When we assign readonly object to mutable one, compiler will fail to build since the two arrays are not matching. + * So that we can know where to fix from build time. And since the dummy value is optional, it won't break existing creator code. + * + * @example + * let readonly: ReadonlyMark = {}; + * let mutable: MutableMark = {}; + * + * readonly = mutable; // OK + * mutable = readonly; // Error: Type 'ReadonlyMark' is not assignable to type 'MutableMark'. + */ +export type MutableMark = { + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + */ + readonly dummy?: never[]; +}; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts new file mode 100644 index 00000000000..cbe8a351149 --- /dev/null +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts @@ -0,0 +1,98 @@ +import type { ContentModelBr, ReadonlyContentModelBr } from '../segment/ContentModelBr'; +import type { ContentModelCode, ReadonlyContentModelCode } from '../decorator/ContentModelCode'; +import type { + ContentModelDivider, + ReadonlyContentModelDivider, +} from '../block/ContentModelDivider'; +import type { + ContentModelDocument, + ReadonlyContentModelDocument, +} from '../blockGroup/ContentModelDocument'; +import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; +import type { + ContentModelFormatContainer, + ReadonlyContentModelFormatContainer, +} from '../blockGroup/ContentModelFormatContainer'; +import type { + ContentModelGeneralBlock, + ReadonlyContentModelGeneralBlock, +} from '../blockGroup/ContentModelGeneralBlock'; +import type { + ContentModelGeneralSegment, + ReadonlyContentModelGeneralSegment, +} from '../segment/ContentModelGeneralSegment'; +import type { ContentModelImage, ReadonlyContentModelImage } from '../segment/ContentModelImage'; +import type { ContentModelLink, ReadonlyContentModelLink } from '../decorator/ContentModelLink'; +import type { + ContentModelListItem, + ReadonlyContentModelListItem, +} from '../blockGroup/ContentModelListItem'; +import type { + ContentModelListLevel, + ReadonlyContentModelListLevel, +} from '../decorator/ContentModelListLevel'; +import type { + ContentModelParagraph, + ReadonlyContentModelParagraph, +} from '../block/ContentModelParagraph'; +import type { + ContentModelParagraphDecorator, + ReadonlyContentModelParagraphDecorator, +} from '../decorator/ContentModelParagraphDecorator'; +import type { + ContentModelSelectionMarker, + ReadonlyContentModelSelectionMarker, +} from '../segment/ContentModelSelectionMarker'; +import type { ContentModelTable, ReadonlyContentModelTable } from '../block/ContentModelTable'; +import type { + ContentModelTableCell, + ReadonlyContentModelTableCell, +} from '../blockGroup/ContentModelTableCell'; +import type { + ContentModelTableRow, + ReadonlyContentModelTableRow, +} from '../block/ContentModelTableRow'; +import type { ContentModelText, ReadonlyContentModelText } from '../segment/ContentModelText'; + +/** + * Get mutable type from its related readonly type + */ +export type MutableType = T extends ReadonlyContentModelGeneralSegment + ? ContentModelGeneralSegment + : T extends ReadonlyContentModelSelectionMarker + ? ContentModelSelectionMarker + : T extends ReadonlyContentModelImage + ? ContentModelImage + : T extends ReadonlyContentModelEntity + ? ContentModelEntity + : T extends ReadonlyContentModelText + ? ContentModelText + : T extends ReadonlyContentModelBr + ? ContentModelBr + : T extends ReadonlyContentModelParagraph + ? ContentModelParagraph + : T extends ReadonlyContentModelTable + ? ContentModelTable + : T extends ReadonlyContentModelTableRow + ? ContentModelTableRow + : T extends ReadonlyContentModelTableCell + ? ContentModelTableCell + : T extends ReadonlyContentModelFormatContainer + ? ContentModelFormatContainer + : T extends ReadonlyContentModelListItem + ? ContentModelListItem + : T extends ReadonlyContentModelListLevel + ? ContentModelListLevel + : T extends ReadonlyContentModelDivider + ? ContentModelDivider + : T extends ReadonlyContentModelDocument + ? ContentModelDocument + : T extends ReadonlyContentModelGeneralBlock + ? ContentModelGeneralBlock + : T extends ReadonlyContentModelParagraphDecorator + ? ContentModelParagraphDecorator + : T extends ReadonlyContentModelLink + ? ContentModelLink + : T extends ReadonlyContentModelCode + ? ContentModelCode + : never; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts new file mode 100644 index 00000000000..e4568edf284 --- /dev/null +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts @@ -0,0 +1,29 @@ +/** + * A tag type to mark a content model type as readonly. + * + * This is generally a workaround to https://github.com/microsoft/TypeScript/issues/13347 + * + * In order to know if a block has been changed, we want to mark all blocks, blocks groups, segments and their members as readonly, + * When we want to change a block/segment/block group, we need to call a function to convert it to mutable. Inside this function + * we can make some change to the object (e.g. remove cached element if any) so later we know this object is changed. + * So that we expect there is a build time error if we assign a readonly object to a function that accepts mutable object only. + * However this does not happen today. + * + * To workaround it, we manually add a hidden member (dummy) to all mutable types, and add another member with readonly array type to + * readonly types. When we assign readonly object to mutable one, compiler will fail to build since the two arrays are not matching. + * So that we can know where to fix from build time. And since the dummy value is optional, it won't break existing creator code. + * + * @example + * let readonly: ReadonlyMark = {}; + * let mutable: MutableMark = {}; + * + * readonly = mutable; // OK + * mutable = readonly; // Error: Type 'ReadonlyMark' is not assignable to type 'MutableMark'. + */ +export type ReadonlyMark = { + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + */ + readonly dummy?: ReadonlyArray; +}; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts index 2e28a98e1e2..4ceef92d5e8 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts @@ -1,9 +1,22 @@ +import type { MutableMark } from './MutableMark'; +import type { ReadonlyMark } from './ReadonlyMark'; + /** * Represents a selectable Content Model object */ -export interface Selectable { +export interface Selectable extends MutableMark { /** * Whether this model object is selected */ isSelected?: boolean; } + +/** + * Represents a selectable Content Model object (Readonly) + */ +export interface ReadonlySelectable extends ReadonlyMark { + /** + * Whether this model object is selected + */ + readonly isSelected?: boolean; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts index b124d0e61b5..77406c02033 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts @@ -1,9 +1,28 @@ -import type { ContentModelCodeFormat } from '../format/ContentModelCodeFormat'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { + ContentModelCodeFormat, + ReadonlyContentModelCodeFormat, +} from '../format/ContentModelCodeFormat'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; /** * Represent code info of Content Model. * ContentModelCode is a decorator but not a standalone model type, instead it need to be put inside a ContentModelSegment * since code is also a kind of segment, with some extra information */ -export interface ContentModelCode extends ContentModelWithFormat {} +export interface ContentModelCode + extends MutableMark, + ContentModelWithFormat {} + +/** + * Represent code info of Content Model. (Readonly) + * ContentModelCode is a decorator but not a standalone model type, instead it need to be put inside a ContentModelSegment + * since code is also a kind of segment, with some extra information + */ +export interface ReadonlyContentModelCode + extends ReadonlyMark, + ReadonlyContentModelWithFormat {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelDecorator.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelDecorator.ts index b8fe53b3f2d..44e2a394627 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelDecorator.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelDecorator.ts @@ -1,8 +1,16 @@ -import type { ContentModelCode } from './ContentModelCode'; -import type { ContentModelLink } from './ContentModelLink'; -import type { ContentModelListLevel } from './ContentModelListLevel'; +import type { ContentModelCode, ReadonlyContentModelCode } from './ContentModelCode'; +import type { ContentModelLink, ReadonlyContentModelLink } from './ContentModelLink'; +import type { ContentModelListLevel, ReadonlyContentModelListLevel } from './ContentModelListLevel'; /** * Union type for segment decorators */ export type ContentModelDecorator = ContentModelLink | ContentModelCode | ContentModelListLevel; + +/** + * Union type for segment decorators (Readonly) + */ +export type ReadonlyContentModelDecorator = + | ReadonlyContentModelLink + | ReadonlyContentModelCode + | ReadonlyContentModelListLevel; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts index c0622fba7ef..441fdf4d846 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts @@ -1,6 +1,17 @@ -import type { ContentModelHyperLinkFormat } from '../format/ContentModelHyperLinkFormat'; -import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { + ContentModelHyperLinkFormat, + ReadonlyContentModelHyperLinkFormat, +} from '../format/ContentModelHyperLinkFormat'; +import type { + ContentModelWithDataset, + ReadonlyContentModelWithDataset, +} from '../format/ContentModelWithDataset'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; /** * Represent link info of Content Model. @@ -8,5 +19,16 @@ import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; * since link is also a kind of segment, with some extra information */ export interface ContentModelLink - extends ContentModelWithFormat, + extends MutableMark, + ContentModelWithFormat, ContentModelWithDataset {} + +/** + * Represent link info of Content Model (Readonly). + * ContentModelLink is not a standalone model type, instead it need to be put inside a ContentModelSegment + * since link is also a kind of segment, with some extra information + */ +export interface ReadonlyContentModelLink + extends ReadonlyMark, + ReadonlyContentModelWithFormat, + ReadonlyContentModelWithDataset {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts index 206d541701e..16a91a1e23d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts @@ -1,16 +1,46 @@ -import type { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; -import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { + ContentModelListItemLevelFormat, + ReadonlyContentModelListItemLevelFormat, +} from '../format/ContentModelListItemLevelFormat'; +import type { + ContentModelWithDataset, + ReadonlyContentModelWithDataset, +} from '../format/ContentModelWithDataset'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; import type { ListMetadataFormat } from '../format/metadata/ListMetadataFormat'; /** - * Content Model of List Level + * Common part of Content Model of List Level */ -export interface ContentModelListLevel - extends ContentModelWithFormat, - ContentModelWithDataset { +export interface ContentModelListLevelCommon { /** * Type of a list, order (OL) or unordered (UL) */ listType: 'OL' | 'UL'; } + +/** + * Content Model of List Level + */ +export interface ContentModelListLevel + extends MutableMark, + ContentModelBlockWithCache, + ContentModelListLevelCommon, + ContentModelWithFormat, + ContentModelWithDataset {} + +/** + * Content Model of List Level (Readonly) + */ +export interface ReadonlyContentModelListLevel + extends ReadonlyMark, + ContentModelBlockWithCache, + ReadonlyContentModelWithFormat, + ReadonlyContentModelWithDataset, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts index b3c154b6ff1..21794a2280c 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts @@ -1,15 +1,40 @@ -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; /** - * Represent decorator for a paragraph in Content Model - * A decorator of paragraph can represent a heading, or a P tag that act likes a paragraph but with some extra format info - * since heading is also a kind of paragraph, with some extra information + * Common part of decorator for a paragraph in Content Model */ -export interface ContentModelParagraphDecorator - extends ContentModelWithFormat { +export interface ContentModelParagraphDecoratorCommon { /** * Tag name of this paragraph */ tagName: string; } + +/** + * Represent decorator for a paragraph in Content Model + * A decorator of paragraph can represent a heading, or a P tag that act likes a paragraph but with some extra format info + * since heading is also a kind of paragraph, with some extra information + */ +export interface ContentModelParagraphDecorator + extends MutableMark, + ContentModelParagraphDecoratorCommon, + ContentModelWithFormat {} + +/** + * Represent decorator for a paragraph in Content Model (Readonly) + * A decorator of paragraph can represent a heading, or a P tag that act likes a paragraph but with some extra format info + * since heading is also a kind of paragraph, with some extra information + */ +export interface ReadonlyContentModelParagraphDecorator + extends ReadonlyMark, + ReadonlyContentModelWithFormat, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts index 897a572920e..5a9889a5175 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts @@ -1,22 +1,66 @@ -import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelEntityFormat } from '../format/ContentModelEntityFormat'; -import type { ContentModelSegmentBase } from '../segment/ContentModelSegmentBase'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { + ContentModelBlockBase, + ReadonlyContentModelBlockBase, +} from '../block/ContentModelBlockBase'; +import type { + ContentModelBlockFormat, + ReadonlyContentModelBlockFormat, +} from '../format/ContentModelBlockFormat'; +import type { + ContentModelEntityFormat, + ReadonlyContentModelEntityFormat, +} from '../format/ContentModelEntityFormat'; +import type { + ContentModelSegmentBase, + ReadonlyContentModelSegmentBase, +} from '../segment/ContentModelSegmentBase'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; /** - * Content Model of Entity + * Common part of Content Model of Entity */ -export interface ContentModelEntity - extends ContentModelBlockBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat>, - ContentModelSegmentBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat> { +export interface ContentModelEntityCommon { /** * The wrapper DOM node of this entity which holds the info CSS classes of this entity */ wrapper: HTMLElement; +} +/** + * Content Model of Entity + */ +export interface ContentModelEntity + extends MutableMark, + ContentModelEntityCommon, + ContentModelBlockBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat>, + ContentModelSegmentBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat> { /** * Format of this entity */ entityFormat: ContentModelEntityFormat; } + +/** + * Content Model of Entity (Readonly) + */ +export interface ReadonlyContentModelEntity + extends ReadonlyMark, + Readonly, + ReadonlyContentModelBlockBase< + 'Entity', + ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + >, + ReadonlyContentModelSegmentBase< + 'Entity', + ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + > { + /** + * Format of this entity + */ + readonly entityFormat: ReadonlyContentModelEntityFormat; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts index d09450d161f..60dddcc6bf9 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts @@ -1,3 +1,5 @@ +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { BorderFormat } from './formatParts/BorderFormat'; import type { DirectionFormat } from './formatParts/DirectionFormat'; @@ -10,9 +12,9 @@ import type { TextIndentFormat } from './formatParts/TextIndentFormat'; import type { WhiteSpaceFormat } from './formatParts/WhiteSpaceFormat'; /** - * The format object for a paragraph in Content Model + * Common part of format object for a paragraph in Content Model */ -export type ContentModelBlockFormat = BackgroundColorFormat & +export type ContentModelBlockFormatCommon = BackgroundColorFormat & DirectionFormat & TextAlignFormat & HtmlAlignFormat & @@ -22,3 +24,14 @@ export type ContentModelBlockFormat = BackgroundColorFormat & WhiteSpaceFormat & BorderFormat & TextIndentFormat; + +/** + * The format object for a paragraph in Content Model + */ +export type ContentModelBlockFormat = MutableMark & ContentModelBlockFormatCommon; + +/** + * The format object for a paragraph in Content Model (Readonly) + */ +export type ReadonlyContentModelBlockFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts index e674a4f7552..739bf3e3ee5 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts @@ -1,7 +1,19 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { FontFamilyFormat } from './formatParts/FontFamilyFormat'; +/** + * Common part of format object for a code element in Content Model + */ +export type ContentModelCodeFormatCommon = FontFamilyFormat & DisplayFormat; + /** * The format object for a code element in Content Model */ -export type ContentModelCodeFormat = FontFamilyFormat & DisplayFormat; +export type ContentModelCodeFormat = MutableMark & ContentModelCodeFormatCommon; + +/** + * The format object for a code element in Content Model (Readonly) + */ +export type ReadonlyContentModelCodeFormat = ReadonlyMark & Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts index bec64102174..d803cba7601 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts @@ -1,8 +1,23 @@ -import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; +/** + * Common part of format object for a divider in Content Model + */ +export type ContentModelDividerFormatCommon = DisplayFormat & + SizeFormat & + ContentModelBlockFormatCommon; + /** * The format object for a divider in Content Model */ -export type ContentModelDividerFormat = ContentModelBlockFormat & DisplayFormat & SizeFormat; +export type ContentModelDividerFormat = MutableMark & ContentModelDividerFormatCommon; + +/** + * The format object for a divider in Content Model (Readonly) + */ +export type ReadonlyContentModelDividerFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts index 0570d49228d..1c474999fba 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts @@ -1,7 +1,20 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { IdFormat } from './formatParts/IdFormat'; import type { EntityInfoFormat } from './formatParts/EntityInfoFormat'; +/** + * Common part of format object for an entity in Content Model + */ +export type ContentModelEntityFormatCommon = EntityInfoFormat & IdFormat; + /** * The format object for an entity in Content Model */ -export type ContentModelEntityFormat = EntityInfoFormat & IdFormat; +export type ContentModelEntityFormat = MutableMark & ContentModelEntityFormatCommon; + +/** + * The format object for an entity in Content Model (Readonly) + */ +export type ReadonlyContentModelEntityFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts index b0cce2e2ecd..313fb57025e 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts @@ -4,12 +4,14 @@ * So that we can use a single level copy ({...object}) to easily clone a format object */ export type ContentModelFormatBase< - V extends string | number | boolean | undefined | null = + V extends string | number | boolean | undefined | null | never[] | ReadonlyArray = | string | number | boolean | undefined | null + | never[] + | ReadonlyArray > = { [key: string]: V; }; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts index e5dfb905fc5..a85ef5e2804 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts @@ -1,12 +1,26 @@ -import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; -import type { ContentModelSegmentFormat } from './ContentModelSegmentFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; +import type { ContentModelSegmentFormatCommon } from './ContentModelSegmentFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; +/** + * Common part of type for FormatContainer + */ +export type ContentModelFormatContainerFormatCommon = SizeFormat & + DisplayFormat & + ContentModelSegmentFormatCommon & + ContentModelBlockFormatCommon; + /** * Type for FormatContainer */ -export type ContentModelFormatContainerFormat = ContentModelBlockFormat & - ContentModelSegmentFormat & - SizeFormat & - DisplayFormat; +export type ContentModelFormatContainerFormat = MutableMark & + ContentModelFormatContainerFormatCommon; + +/** + * Type for FormatContainer (Readonly) + */ +export type ReadonlyContentModelFormatContainerFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts index 129cc55d84f..282a7af1408 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts @@ -1,3 +1,5 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { BorderFormat } from './formatParts/BorderFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; @@ -10,9 +12,9 @@ import type { TextColorFormat } from './formatParts/TextColorFormat'; import type { UnderlineFormat } from './formatParts/UnderlineFormat'; /** - * The format object for a hyperlink in Content Model + * Common part of format object for a hyperlink in Content Model */ -export type ContentModelHyperLinkFormat = LinkFormat & +export type ContentModelHyperLinkFormatCommon = LinkFormat & TextColorFormat & BackgroundColorFormat & UnderlineFormat & @@ -22,3 +24,14 @@ export type ContentModelHyperLinkFormat = LinkFormat & BorderFormat & SizeFormat & TextAlignFormat; + +/** + * The format object for a hyperlink in Content Model + */ +export type ContentModelHyperLinkFormat = MutableMark & ContentModelHyperLinkFormatCommon; + +/** + * The format object for a hyperlink in Content Model (Readonly) + */ +export type ReadonlyContentModelHyperLinkFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts index 6f9ba413a85..ee96be64bb5 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts @@ -1,6 +1,8 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { BorderFormat } from './formatParts/BorderFormat'; import type { BoxShadowFormat } from './formatParts/BoxShadowFormat'; -import type { ContentModelSegmentFormat } from './ContentModelSegmentFormat'; +import type { ContentModelSegmentFormatCommon } from './ContentModelSegmentFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { FloatFormat } from './formatParts/FloatFormat'; import type { IdFormat } from './formatParts/IdFormat'; @@ -10,10 +12,9 @@ import type { SizeFormat } from './formatParts/SizeFormat'; import type { VerticalAlignFormat } from './formatParts/VerticalAlignFormat'; /** - * The format object for an image in Content Model + * Common part of format object for an image in Content Model */ -export type ContentModelImageFormat = ContentModelSegmentFormat & - IdFormat & +export type ContentModelImageFormatCommon = IdFormat & SizeFormat & MarginFormat & PaddingFormat & @@ -21,4 +22,16 @@ export type ContentModelImageFormat = ContentModelSegmentFormat & BoxShadowFormat & DisplayFormat & FloatFormat & - VerticalAlignFormat; + VerticalAlignFormat & + ContentModelSegmentFormatCommon; + +/** + * The format object for an image in Content Model + */ +export type ContentModelImageFormat = MutableMark & ContentModelImageFormatCommon; + +/** + * The format object for an image in Content Model (Readonly) + */ +export type ReadonlyContentModelImageFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts index f2d7b550b62..69903df63b5 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts @@ -1,3 +1,5 @@ +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { DirectionFormat } from './formatParts/DirectionFormat'; import type { LineHeightFormat } from './formatParts/LineHeightFormat'; @@ -8,9 +10,9 @@ import type { TextAlignFormat } from './formatParts/TextAlignFormat'; import type { TextIndentFormat } from './formatParts/TextIndentFormat'; /** - * The format object for a list item in Content Model + * Common part of format object for a list item in Content Model */ -export type ContentModelListItemFormat = DirectionFormat & +export type ContentModelListItemFormatCommon = DirectionFormat & LineHeightFormat & MarginFormat & PaddingFormat & @@ -18,3 +20,14 @@ export type ContentModelListItemFormat = DirectionFormat & ListStyleFormat & TextIndentFormat & BackgroundColorFormat; + +/** + * The format object for a list item in Content Model + */ +export type ContentModelListItemFormat = MutableMark & ContentModelListItemFormatCommon; + +/** + * The format object for a list item in Content Model (Readonly) + */ +export type ReadonlyContentModelListItemFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts index c0ea2bf60b6..98c559a1583 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts @@ -1,3 +1,5 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { DirectionFormat } from './formatParts/DirectionFormat'; import type { ListStyleFormat } from './formatParts/ListStyleFormat'; import type { ListThreadFormat } from './formatParts/ListThreadFormat'; @@ -6,11 +8,22 @@ import type { PaddingFormat } from './formatParts/PaddingFormat'; import type { TextAlignFormat } from './formatParts/TextAlignFormat'; /** - * The format object for a list level in Content Model + * Common part of format object for a list level in Content Model */ -export type ContentModelListItemLevelFormat = ListThreadFormat & +export type ContentModelListItemLevelFormatCommon = ListThreadFormat & DirectionFormat & TextAlignFormat & MarginFormat & PaddingFormat & ListStyleFormat; + +/** + * The format object for a list level in Content Model + */ +export type ContentModelListItemLevelFormat = MutableMark & ContentModelListItemLevelFormatCommon; + +/** + * The format object for a list level in Content Model (Readonly) + */ +export type ReadonlyContentModelListItemLevelFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts index d8552973cef..789de2d021d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts @@ -1,3 +1,5 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { BoldFormat } from './formatParts/BoldFormat'; import type { FontFamilyFormat } from './formatParts/FontFamilyFormat'; @@ -11,9 +13,9 @@ import type { TextColorFormat } from './formatParts/TextColorFormat'; import type { UnderlineFormat } from './formatParts/UnderlineFormat'; /** - * The format object for a segment in Content Model + * Common part of format object for a segment in Content Model */ -export type ContentModelSegmentFormat = TextColorFormat & +export type ContentModelSegmentFormatCommon = TextColorFormat & BackgroundColorFormat & LetterSpacingFormat & FontSizeFormat & @@ -24,3 +26,14 @@ export type ContentModelSegmentFormat = TextColorFormat & StrikeFormat & SuperOrSubScriptFormat & LineHeightFormat; + +/** + * The format object for a segment in Content Model + */ +export type ContentModelSegmentFormat = MutableMark & ContentModelSegmentFormatCommon; + +/** + * The format object for a segment in Content Model (Readonly) + */ +export type ReadonlyContentModelSegmentFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts index 651535c95f0..592606bdee2 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts @@ -1,16 +1,29 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { BorderBoxFormat } from './formatParts/BorderBoxFormat'; -import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; +import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; import type { TextColorFormat } from './formatParts/TextColorFormat'; import type { VerticalAlignFormat } from './formatParts/VerticalAlignFormat'; import type { WordBreakFormat } from '../format/formatParts/WordBreakFormat'; /** - * Format of table cell + * Common part of format of table cell */ -export type ContentModelTableCellFormat = ContentModelBlockFormat & - BorderBoxFormat & +export type ContentModelTableCellFormatCommon = BorderBoxFormat & VerticalAlignFormat & WordBreakFormat & TextColorFormat & - SizeFormat; + SizeFormat & + ContentModelBlockFormatCommon; + +/** + * Format of table cell + */ +export type ContentModelTableCellFormat = MutableMark & ContentModelTableCellFormatCommon; + +/** + * Format of table cell (Readonly) + */ +export type ReadonlyContentModelTableCellFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts index 7d84b3fe376..b5e3f11a48c 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts @@ -1,6 +1,8 @@ +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; import type { BorderBoxFormat } from './formatParts/BorderBoxFormat'; import type { BorderFormat } from './formatParts/BorderFormat'; -import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; +import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { IdFormat } from './formatParts/IdFormat'; import type { MarginFormat } from './formatParts/MarginFormat'; @@ -9,14 +11,25 @@ import type { TableLayoutFormat } from './formatParts/TableLayoutFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; /** - * Format of Table + * Common part of format of Table */ -export type ContentModelTableFormat = ContentModelBlockFormat & - IdFormat & +export type ContentModelTableFormatCommon = IdFormat & BorderFormat & BorderBoxFormat & SpacingFormat & MarginFormat & DisplayFormat & TableLayoutFormat & - SizeFormat; + SizeFormat & + ContentModelBlockFormatCommon; + +/** + * Format of Table + */ +export type ContentModelTableFormat = MutableMark & ContentModelTableFormatCommon; + +/** + * Format of Table (Readonly) + */ +export type ReadonlyContentModelTableFormat = ReadonlyMark & + Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts index 0fe8f417c78..255cb1553f2 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts @@ -1,11 +1,23 @@ -import type { DatasetFormat } from './metadata/DatasetFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { DatasetFormat, ReadonlyDatasetFormat } from './metadata/DatasetFormat'; /** * Represents base format of an element that supports dataset and/or metadata */ -export interface ContentModelWithDataset { +export type ContentModelWithDataset = MutableMark & { /** * dataset of this element */ dataset: DatasetFormat; -} +}; + +/** + * Represents base format of an element that supports dataset and/or metadata (Readonly) + */ +export type ReadonlyContentModelWithDataset = ReadonlyMark & { + /** + * dataset of this element + */ + readonly dataset: ReadonlyDatasetFormat; +}; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithFormat.ts index 53f52e67522..cfbe0d6b821 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithFormat.ts @@ -9,3 +9,13 @@ export interface ContentModelWithFormat { */ format: T; } + +/** + * Represent a content model with format (Readonly) + */ +export interface ReadonlyContentModelWithFormat { + /** + * Format of this model + */ + readonly format: Readonly; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/DatasetFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/DatasetFormat.ts index e71cadf3c37..11625671ba6 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/DatasetFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/metadata/DatasetFormat.ts @@ -2,3 +2,8 @@ * Represents dataset format of Content Model */ export type DatasetFormat = Record; + +/** + * Represents dataset format of Content Model (Readonly) + */ +export type ReadonlyDatasetFormat = Readonly>; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelBr.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelBr.ts index d8bf1b04871..e414626c817 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelBr.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelBr.ts @@ -1,6 +1,14 @@ -import type { ContentModelSegmentBase } from './ContentModelSegmentBase'; +import type { + ContentModelSegmentBase, + ReadonlyContentModelSegmentBase, +} from './ContentModelSegmentBase'; /** * Content Model of BR */ export interface ContentModelBr extends ContentModelSegmentBase<'Br'> {} + +/** + * Content Model of BR (Readonly) + */ +export interface ReadonlyContentModelBr extends ReadonlyContentModelSegmentBase<'Br'> {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts index bbd0741c7ac..e3287e700a9 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts @@ -1,7 +1,19 @@ -import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; -import type { ContentModelGeneralBlock } from '../blockGroup/ContentModelGeneralBlock'; -import type { ContentModelSegmentBase } from './ContentModelSegmentBase'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { + ContentModelBlockFormat, + ReadonlyContentModelBlockFormat, +} from '../format/ContentModelBlockFormat'; +import type { + ContentModelGeneralBlock, + ReadonlyContentModelGeneralBlock, +} from '../blockGroup/ContentModelGeneralBlock'; +import type { + ContentModelSegmentBase, + ReadonlyContentModelSegmentBase, +} from './ContentModelSegmentBase'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; /** * Content Model of general Segment @@ -9,3 +21,13 @@ import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFor export interface ContentModelGeneralSegment extends ContentModelGeneralBlock, ContentModelSegmentBase<'General', ContentModelBlockFormat & ContentModelSegmentFormat> {} + +/** + * Content Model of general Segment (Readonly) + */ +export interface ReadonlyContentModelGeneralSegment + extends ReadonlyContentModelGeneralBlock, + ReadonlyContentModelSegmentBase< + 'General', + ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + > {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts index 8b457ce4f0c..f349d9e481e 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts @@ -1,14 +1,21 @@ -import type { ContentModelImageFormat } from '../format/ContentModelImageFormat'; -import type { ContentModelSegmentBase } from './ContentModelSegmentBase'; -import type { ContentModelWithDataset } from '../format/ContentModelWithDataset'; +import type { + ContentModelImageFormat, + ReadonlyContentModelImageFormat, +} from '../format/ContentModelImageFormat'; +import type { + ContentModelSegmentBase, + ReadonlyContentModelSegmentBase, +} from './ContentModelSegmentBase'; +import type { + ContentModelWithDataset, + ReadonlyContentModelWithDataset, +} from '../format/ContentModelWithDataset'; import type { ImageMetadataFormat } from '../format/metadata/ImageMetadataFormat'; /** - * Content Model of IMG + * Common part of Content Model of IMG */ -export interface ContentModelImage - extends ContentModelSegmentBase<'Image', ContentModelImageFormat>, - ContentModelWithDataset { +export interface ContentModelImageCommon { /** * Image source of this IMG element */ @@ -29,3 +36,19 @@ export interface ContentModelImage */ isSelectedAsImageSelection?: boolean; } + +/** + * Content Model of IMG + */ +export interface ContentModelImage + extends ContentModelImageCommon, + ContentModelSegmentBase<'Image', ContentModelImageFormat>, + ContentModelWithDataset {} + +/** + * Content Model of IMG (Readonly) + */ +export interface ReadonlyContentModelImage + extends ReadonlyContentModelSegmentBase<'Image', ReadonlyContentModelImageFormat>, + ReadonlyContentModelWithDataset, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts index 77fd2c68cad..687fb511796 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts @@ -1,9 +1,15 @@ -import type { ContentModelBr } from './ContentModelBr'; -import type { ContentModelEntity } from '../entity/ContentModelEntity'; -import type { ContentModelGeneralSegment } from './ContentModelGeneralSegment'; -import type { ContentModelImage } from './ContentModelImage'; -import type { ContentModelSelectionMarker } from './ContentModelSelectionMarker'; -import type { ContentModelText } from './ContentModelText'; +import type { ContentModelBr, ReadonlyContentModelBr } from './ContentModelBr'; +import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; +import type { + ContentModelGeneralSegment, + ReadonlyContentModelGeneralSegment, +} from './ContentModelGeneralSegment'; +import type { ContentModelImage, ReadonlyContentModelImage } from './ContentModelImage'; +import type { + ContentModelSelectionMarker, + ReadonlyContentModelSelectionMarker, +} from './ContentModelSelectionMarker'; +import type { ContentModelText, ReadonlyContentModelText } from './ContentModelText'; /** * Union type of Content Model Segment @@ -15,3 +21,14 @@ export type ContentModelSegment = | ContentModelGeneralSegment | ContentModelEntity | ContentModelImage; + +/** + * Union type of Content Model Segment (Readonly) + */ +export type ReadonlyContentModelSegment = + | ReadonlyContentModelSelectionMarker + | ReadonlyContentModelText + | ReadonlyContentModelBr + | ReadonlyContentModelGeneralSegment + | ReadonlyContentModelEntity + | ReadonlyContentModelImage; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts index 22df2695372..23687afb195 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts @@ -1,9 +1,27 @@ -import type { ContentModelCode } from '../decorator/ContentModelCode'; -import type { ContentModelLink } from '../decorator/ContentModelLink'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark } from '../common/MutableMark'; +import type { ContentModelCode, ReadonlyContentModelCode } from '../decorator/ContentModelCode'; +import type { ContentModelLink, ReadonlyContentModelLink } from '../decorator/ContentModelLink'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegmentFormat, +} from '../format/ContentModelSegmentFormat'; import type { ContentModelSegmentType } from './SegmentType'; -import type { ContentModelWithFormat } from '../format/ContentModelWithFormat'; -import type { Selectable } from '../common/Selectable'; +import type { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from '../format/ContentModelWithFormat'; +import type { ReadonlySelectable, Selectable } from '../common/Selectable'; + +/** + * Common part of base type of Content Model Segment + */ +export interface ContentModelSegmentBaseCommon { + /** + * Type of this segment + */ + readonly segmentType: T; +} /** * Base type of Content Model Segment @@ -11,19 +29,40 @@ import type { Selectable } from '../common/Selectable'; export interface ContentModelSegmentBase< T extends ContentModelSegmentType, TFormat extends ContentModelSegmentFormat = ContentModelSegmentFormat -> extends Selectable, ContentModelWithFormat { +> + extends MutableMark, + Selectable, + ContentModelWithFormat, + ContentModelSegmentBaseCommon { /** - * Type of this segment + * Hyperlink info */ - segmentType: T; + link?: ContentModelLink; + /** + * Code info + */ + code?: ContentModelCode; +} + +/** + * Base type of Content Model Segment (Readonly) + */ +export interface ReadonlyContentModelSegmentBase< + T extends ContentModelSegmentType, + TFormat extends ReadonlyContentModelSegmentFormat = ReadonlyContentModelSegmentFormat +> + extends ReadonlyMark, + ReadonlySelectable, + ReadonlyContentModelWithFormat, + Readonly> { /** * Hyperlink info */ - link?: ContentModelLink; + readonly link?: ReadonlyContentModelLink; /** * Code info */ - code?: ContentModelCode; + readonly code?: ReadonlyContentModelCode; } diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSelectionMarker.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSelectionMarker.ts index e99db906635..2e1f9b400eb 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSelectionMarker.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSelectionMarker.ts @@ -1,6 +1,15 @@ -import type { ContentModelSegmentBase } from './ContentModelSegmentBase'; +import type { + ContentModelSegmentBase, + ReadonlyContentModelSegmentBase, +} from './ContentModelSegmentBase'; /** * Content Model of Selection Marker */ export interface ContentModelSelectionMarker extends ContentModelSegmentBase<'SelectionMarker'> {} + +/** + * Content Model of Selection Marker (Readonly) + */ +export interface ReadonlyContentModelSelectionMarker + extends ReadonlyContentModelSegmentBase<'SelectionMarker'> {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelText.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelText.ts index 5cd7c79f19b..7cee676ad6c 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelText.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelText.ts @@ -1,11 +1,26 @@ -import type { ContentModelSegmentBase } from './ContentModelSegmentBase'; +import type { + ContentModelSegmentBase, + ReadonlyContentModelSegmentBase, +} from './ContentModelSegmentBase'; /** - * Content Model for Text + * Common port of Content Model for Text */ -export interface ContentModelText extends ContentModelSegmentBase<'Text'> { +export interface ContentModelTextCommon { /** * Text content of this segment */ text: string; } + +/** + * Content Model for Text + */ +export interface ContentModelText extends ContentModelTextCommon, ContentModelSegmentBase<'Text'> {} + +/** + * Content Model for Text (Readonly) + */ +export interface ReadonlyContentModelText + extends ReadonlyContentModelSegmentBase<'Text'>, + Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index a6b748c4c68..4f4dd6d1abb 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -1,19 +1,73 @@ -export { ContentModelSegmentFormat } from './contentModel/format/ContentModelSegmentFormat'; -export { ContentModelWithFormat } from './contentModel/format/ContentModelWithFormat'; -export { ContentModelTableFormat } from './contentModel/format/ContentModelTableFormat'; -export { ContentModelWithDataset } from './contentModel/format/ContentModelWithDataset'; -export { ContentModelBlockFormat } from './contentModel/format/ContentModelBlockFormat'; -export { ContentModelTableCellFormat } from './contentModel/format/ContentModelTableCellFormat'; -export { ContentModelListItemFormat } from './contentModel/format/ContentModelListItemFormat'; -export { ContentModelListItemLevelFormat } from './contentModel/format/ContentModelListItemLevelFormat'; -export { ContentModelHyperLinkFormat } from './contentModel/format/ContentModelHyperLinkFormat'; -export { ContentModelCodeFormat } from './contentModel/format/ContentModelCodeFormat'; -export { ContentModelFormatContainerFormat } from './contentModel/format/ContentModelFormatContainerFormat'; -export { ContentModelDividerFormat } from './contentModel/format/ContentModelDividerFormat'; +export { + ContentModelSegmentFormat, + ContentModelSegmentFormatCommon, + ReadonlyContentModelSegmentFormat, +} from './contentModel/format/ContentModelSegmentFormat'; +export { + ContentModelWithFormat, + ReadonlyContentModelWithFormat, +} from './contentModel/format/ContentModelWithFormat'; +export { + ContentModelTableFormat, + ContentModelTableFormatCommon, + ReadonlyContentModelTableFormat, +} from './contentModel/format/ContentModelTableFormat'; +export { + ContentModelWithDataset, + ReadonlyContentModelWithDataset, +} from './contentModel/format/ContentModelWithDataset'; +export { + ContentModelBlockFormat, + ContentModelBlockFormatCommon, + ReadonlyContentModelBlockFormat, +} from './contentModel/format/ContentModelBlockFormat'; +export { + ContentModelTableCellFormat, + ContentModelTableCellFormatCommon, + ReadonlyContentModelTableCellFormat, +} from './contentModel/format/ContentModelTableCellFormat'; +export { + ContentModelListItemFormat, + ContentModelListItemFormatCommon, + ReadonlyContentModelListItemFormat, +} from './contentModel/format/ContentModelListItemFormat'; +export { + ContentModelListItemLevelFormat, + ContentModelListItemLevelFormatCommon, + ReadonlyContentModelListItemLevelFormat, +} from './contentModel/format/ContentModelListItemLevelFormat'; +export { + ContentModelHyperLinkFormat, + ContentModelHyperLinkFormatCommon, + ReadonlyContentModelHyperLinkFormat, +} from './contentModel/format/ContentModelHyperLinkFormat'; +export { + ContentModelCodeFormat, + ContentModelCodeFormatCommon, + ReadonlyContentModelCodeFormat, +} from './contentModel/format/ContentModelCodeFormat'; +export { + ContentModelFormatContainerFormat, + ContentModelFormatContainerFormatCommon, + ReadonlyContentModelFormatContainerFormat, +} from './contentModel/format/ContentModelFormatContainerFormat'; +export { + ContentModelDividerFormat, + ContentModelDividerFormatCommon, + ReadonlyContentModelDividerFormat, +} from './contentModel/format/ContentModelDividerFormat'; export { ContentModelFormatBase } from './contentModel/format/ContentModelFormatBase'; export { ContentModelFormatMap } from './contentModel/format/ContentModelFormatMap'; -export { ContentModelImageFormat } from './contentModel/format/ContentModelImageFormat'; -export { ContentModelEntityFormat } from './contentModel/format/ContentModelEntityFormat'; +export { + ContentModelImageFormat, + ContentModelImageFormatCommon, + ReadonlyContentModelImageFormat, +} from './contentModel/format/ContentModelImageFormat'; +export { + ContentModelEntityFormat, + ContentModelEntityFormatCommon, + ReadonlyContentModelEntityFormat, +} from './contentModel/format/ContentModelEntityFormat'; export { FormatHandlerTypeMap, FormatKey } from './contentModel/format/FormatHandlerTypeMap'; export { BackgroundColorFormat } from './contentModel/format/formatParts/BackgroundColorFormat'; @@ -50,7 +104,7 @@ export { ListStyleFormat } from './contentModel/format/formatParts/ListStyleForm export { FloatFormat } from './contentModel/format/formatParts/FloatFormat'; export { EntityInfoFormat } from './contentModel/format/formatParts/EntityInfoFormat'; -export { DatasetFormat } from './contentModel/format/metadata/DatasetFormat'; +export { DatasetFormat, ReadonlyDatasetFormat } from './contentModel/format/metadata/DatasetFormat'; export { TableMetadataFormat } from './contentModel/format/metadata/TableMetadataFormat'; export { ListMetadataFormat } from './contentModel/format/metadata/ListMetadataFormat'; export { @@ -89,39 +143,133 @@ export { DeleteResult } from './enum/DeleteResult'; export { InsertEntityPosition } from './enum/InsertEntityPosition'; export { ExportContentMode } from './enum/ExportContentMode'; -export { ContentModelBlock } from './contentModel/block/ContentModelBlock'; -export { ContentModelParagraph } from './contentModel/block/ContentModelParagraph'; -export { ContentModelTable } from './contentModel/block/ContentModelTable'; -export { ContentModelDivider } from './contentModel/block/ContentModelDivider'; -export { ContentModelBlockBase } from './contentModel/block/ContentModelBlockBase'; +export { + ContentModelBlock, + ReadonlyContentModelBlock, +} from './contentModel/block/ContentModelBlock'; +export { + ContentModelParagraph, + ContentModelParagraphCommon, + ReadonlyContentModelParagraph, +} from './contentModel/block/ContentModelParagraph'; +export { + ContentModelTable, + ReadonlyContentModelTable, +} from './contentModel/block/ContentModelTable'; +export { + ContentModelDivider, + ContentModelDividerCommon, + ReadonlyContentModelDivider, +} from './contentModel/block/ContentModelDivider'; +export { + ContentModelBlockBase, + ContentModelBlockBaseCommon, + ReadonlyContentModelBlockBase, +} from './contentModel/block/ContentModelBlockBase'; export { ContentModelBlockWithCache } from './contentModel/common/ContentModelBlockWithCache'; -export { ContentModelTableRow } from './contentModel/block/ContentModelTableRow'; +export { + ContentModelTableRow, + ContentModelTableRowCommon, + ReadonlyContentModelTableRow, +} from './contentModel/block/ContentModelTableRow'; -export { ContentModelEntity } from './contentModel/entity/ContentModelEntity'; +export { + ContentModelEntity, + ContentModelEntityCommon, + ReadonlyContentModelEntity, +} from './contentModel/entity/ContentModelEntity'; + +export { + ContentModelDocument, + ContentModelDocumentCommon, + ReadonlyContentModelDocument, +} from './contentModel/blockGroup/ContentModelDocument'; +export { + ContentModelBlockGroupBase, + ContentModelBlockGroupBaseCommon, + ReadonlyContentModelBlockGroupBase, +} from './contentModel/blockGroup/ContentModelBlockGroupBase'; +export { + ContentModelFormatContainer, + ContentModelFormatContainerCommon, + ReadonlyContentModelFormatContainer, +} from './contentModel/blockGroup/ContentModelFormatContainer'; +export { + ContentModelGeneralBlock, + ContentModelGeneralBlockCommon, + ReadonlyContentModelGeneralBlock, +} from './contentModel/blockGroup/ContentModelGeneralBlock'; +export { + ContentModelListItem, + ReadonlyContentModelListItem, +} from './contentModel/blockGroup/ContentModelListItem'; +export { + ContentModelTableCell, + ContentModelTableCellCommon, + ReadonlyContentModelTableCell, +} from './contentModel/blockGroup/ContentModelTableCell'; +export { + ContentModelBlockGroup, + ReadonlyContentModelBlockGroup, +} from './contentModel/blockGroup/ContentModelBlockGroup'; -export { ContentModelDocument } from './contentModel/blockGroup/ContentModelDocument'; -export { ContentModelBlockGroupBase } from './contentModel/blockGroup/ContentModelBlockGroupBase'; -export { ContentModelFormatContainer } from './contentModel/blockGroup/ContentModelFormatContainer'; -export { ContentModelGeneralBlock } from './contentModel/blockGroup/ContentModelGeneralBlock'; -export { ContentModelListItem } from './contentModel/blockGroup/ContentModelListItem'; -export { ContentModelTableCell } from './contentModel/blockGroup/ContentModelTableCell'; -export { ContentModelBlockGroup } from './contentModel/blockGroup/ContentModelBlockGroup'; +export { ContentModelBr, ReadonlyContentModelBr } from './contentModel/segment/ContentModelBr'; +export { + ContentModelGeneralSegment, + ReadonlyContentModelGeneralSegment, +} from './contentModel/segment/ContentModelGeneralSegment'; +export { + ContentModelImage, + ContentModelImageCommon, + ReadonlyContentModelImage, +} from './contentModel/segment/ContentModelImage'; +export { + ContentModelText, + ContentModelTextCommon, + ReadonlyContentModelText, +} from './contentModel/segment/ContentModelText'; +export { + ContentModelSelectionMarker, + ReadonlyContentModelSelectionMarker, +} from './contentModel/segment/ContentModelSelectionMarker'; +export { + ContentModelSegmentBase, + ContentModelSegmentBaseCommon, + ReadonlyContentModelSegmentBase, +} from './contentModel/segment/ContentModelSegmentBase'; +export { + ContentModelSegment, + ReadonlyContentModelSegment, +} from './contentModel/segment/ContentModelSegment'; -export { ContentModelBr } from './contentModel/segment/ContentModelBr'; -export { ContentModelGeneralSegment } from './contentModel/segment/ContentModelGeneralSegment'; -export { ContentModelImage } from './contentModel/segment/ContentModelImage'; -export { ContentModelText } from './contentModel/segment/ContentModelText'; -export { ContentModelSelectionMarker } from './contentModel/segment/ContentModelSelectionMarker'; -export { ContentModelSegmentBase } from './contentModel/segment/ContentModelSegmentBase'; -export { ContentModelSegment } from './contentModel/segment/ContentModelSegment'; +export { + ContentModelCode, + ReadonlyContentModelCode, +} from './contentModel/decorator/ContentModelCode'; +export { + ContentModelLink, + ReadonlyContentModelLink, +} from './contentModel/decorator/ContentModelLink'; +export { + ContentModelParagraphDecorator, + ContentModelParagraphDecoratorCommon, + ReadonlyContentModelParagraphDecorator, +} from './contentModel/decorator/ContentModelParagraphDecorator'; +export { + ContentModelDecorator, + ReadonlyContentModelDecorator, +} from './contentModel/decorator/ContentModelDecorator'; +export { + ContentModelListLevel, + ContentModelListLevelCommon, + ReadonlyContentModelListLevel, +} from './contentModel/decorator/ContentModelListLevel'; -export { ContentModelCode } from './contentModel/decorator/ContentModelCode'; -export { ContentModelLink } from './contentModel/decorator/ContentModelLink'; -export { ContentModelParagraphDecorator } from './contentModel/decorator/ContentModelParagraphDecorator'; -export { ContentModelDecorator } from './contentModel/decorator/ContentModelDecorator'; -export { ContentModelListLevel } from './contentModel/decorator/ContentModelListLevel'; +export { Selectable, ReadonlySelectable } from './contentModel/common/Selectable'; +export { ReadonlyMark } from './contentModel/common/ReadonlyMark'; +export { MutableMark } from './contentModel/common/MutableMark'; +export { MutableType } from './contentModel/common/MutableType'; -export { Selectable } from './contentModel/common/Selectable'; export { DOMSelection, SelectionType, From 3ff07793285c320434b1e0702660b4a8fee6a9e7 Mon Sep 17 00:00:00 2001 From: jiuqingsong Date: Tue, 14 May 2024 08:35:54 -0700 Subject: [PATCH 09/66] Improve --- .../block/ContentModelBlockBase.ts | 7 +- .../contentModel/block/ContentModelDivider.ts | 7 +- .../block/ContentModelParagraph.ts | 7 +- .../contentModel/block/ContentModelTable.ts | 11 +-- .../block/ContentModelTableRow.ts | 7 +- .../blockGroup/ContentModelDocument.ts | 7 +- .../blockGroup/ContentModelFormatContainer.ts | 11 +-- .../blockGroup/ContentModelGeneralBlock.ts | 12 +--- .../blockGroup/ContentModelListItem.ts | 11 +-- .../blockGroup/ContentModelTableCell.ts | 7 +- .../decorator/ContentModelCode.ts | 7 +- .../decorator/ContentModelLink.ts | 7 +- .../decorator/ContentModelListLevel.ts | 7 +- .../ContentModelParagraphDecorator.ts | 7 +- .../contentModel/entity/ContentModelEntity.ts | 21 ++---- .../format/ContentModelBlockFormat.ts | 17 +---- .../format/ContentModelCodeFormat.ts | 14 +--- .../format/ContentModelDividerFormat.ts | 19 +---- .../format/ContentModelEntityFormat.ts | 15 +--- .../format/ContentModelFormatBase.ts | 4 +- .../ContentModelFormatContainerFormat.ts | 26 ++----- .../format/ContentModelHyperLinkFormat.ts | 17 +---- .../format/ContentModelImageFormat.ts | 23 ++---- .../format/ContentModelListItemFormat.ts | 17 +---- .../format/ContentModelListItemLevelFormat.ts | 17 +---- .../format/ContentModelSegmentFormat.ts | 17 +---- .../format/ContentModelTableCellFormat.ts | 23 ++---- .../format/ContentModelTableFormat.ts | 23 ++---- .../format/ContentModelWithDataset.ts | 6 +- .../segment/ContentModelGeneralSegment.ts | 12 +--- .../contentModel/segment/ContentModelImage.ts | 7 +- .../segment/ContentModelSegmentBase.ts | 7 +- .../lib/index.ts | 72 ++++--------------- 33 files changed, 92 insertions(+), 380 deletions(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts index 6d0a4f3e532..6ccf329e98c 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts @@ -1,10 +1,7 @@ import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { MutableMark } from '../common/MutableMark'; -import type { - ContentModelBlockFormat, - ReadonlyContentModelBlockFormat, -} from '../format/ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockType } from './BlockType'; import type { ContentModelWithFormat, @@ -39,7 +36,7 @@ export interface ContentModelBlockBase< */ export interface ReadonlyContentModelBlockBase< T extends ContentModelBlockType, - TFormat extends ReadonlyContentModelBlockFormat = ReadonlyContentModelBlockFormat, + TFormat extends ContentModelBlockFormat = ContentModelBlockFormat, TCacheElement extends HTMLElement = HTMLElement > extends ReadonlyMark, diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts index 1bf01b5f435..c38c9d15734 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelDivider.ts @@ -1,8 +1,5 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase } from './ContentModelBlockBase'; -import type { - ContentModelDividerFormat, - ReadonlyContentModelDividerFormat, -} from '../format/ContentModelDividerFormat'; +import type { ContentModelDividerFormat } from '../format/ContentModelDividerFormat'; import type { ReadonlySelectable, Selectable } from '../common/Selectable'; /** @@ -33,5 +30,5 @@ export interface ContentModelDivider */ export interface ReadonlyContentModelDivider extends ReadonlySelectable, - ReadonlyContentModelBlockBase<'Divider', ReadonlyContentModelDividerFormat>, + ReadonlyContentModelBlockBase<'Divider', ContentModelDividerFormat>, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts index 4deb7725e7d..987c7033a3b 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts @@ -7,10 +7,7 @@ import type { ContentModelSegment, ReadonlyContentModelSegment, } from '../segment/ContentModelSegment'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; /** * Common part of Content Model of Paragraph @@ -59,7 +56,7 @@ export interface ReadonlyContentModelParagraph /** * Segment format on this paragraph. This is mostly used for default format */ - readonly segmentFormat?: ReadonlyContentModelSegmentFormat; + readonly segmentFormat?: ContentModelSegmentFormat; /** * Decorator info for this paragraph, used by heading and P tags diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts index db8ecebcd24..b13dfeb8975 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts @@ -1,8 +1,5 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase } from './ContentModelBlockBase'; -import type { - ContentModelTableFormat, - ReadonlyContentModelTableFormat, -} from '../format/ContentModelTableFormat'; +import type { ContentModelTableFormat } from '../format/ContentModelTableFormat'; import type { ContentModelTableRow, ReadonlyContentModelTableRow } from './ContentModelTableRow'; import type { ContentModelWithDataset, @@ -31,11 +28,7 @@ export interface ContentModelTable * Content Model of Table (Readonly) */ export interface ReadonlyContentModelTable - extends ReadonlyContentModelBlockBase< - 'Table', - ReadonlyContentModelTableFormat, - HTMLTableElement - >, + extends ReadonlyContentModelBlockBase<'Table', ContentModelTableFormat, HTMLTableElement>, ReadonlyContentModelWithDataset { /** * Widths of each column diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts index 39443c5ad9d..449ae86076d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts @@ -1,9 +1,6 @@ import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { MutableMark } from '../common/MutableMark'; -import type { - ContentModelBlockFormat, - ReadonlyContentModelBlockFormat, -} from '../format/ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelTableCell, @@ -44,7 +41,7 @@ export interface ContentModelTableRow export interface ReadonlyContentModelTableRow extends ReadonlyMark, ContentModelBlockWithCache, - ReadonlyContentModelWithFormat, + ReadonlyContentModelWithFormat, Readonly { /** * Cells of this table diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts index 63b888b13ac..3dc7175dc62 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts @@ -2,10 +2,7 @@ import type { ContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { ContentModelWithFormat, ReadonlyContentModelWithFormat, @@ -34,5 +31,5 @@ export interface ContentModelDocument */ export interface ReadonlyContentModelDocument extends ReadonlyContentModelBlockGroupBase<'Document'>, - Partial>, + Partial>, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts index eeb0393043a..c8573111673 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts @@ -6,10 +6,7 @@ import type { ContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; -import type { - ContentModelFormatContainerFormat, - ReadonlyContentModelFormatContainerFormat, -} from '../format/ContentModelFormatContainerFormat'; +import type { ContentModelFormatContainerFormat } from '../format/ContentModelFormatContainerFormat'; /** * Common part of Content Model of Format Container @@ -40,9 +37,5 @@ export interface ContentModelFormatContainer */ export interface ReadonlyContentModelFormatContainer extends ReadonlyContentModelBlockGroupBase<'FormatContainer'>, - ReadonlyContentModelBlockBase< - 'BlockGroup', - ReadonlyContentModelFormatContainerFormat, - HTMLElement - >, + ReadonlyContentModelBlockBase<'BlockGroup', ContentModelFormatContainerFormat, HTMLElement>, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts index 1f39af631d8..caefb55a56d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts @@ -2,18 +2,12 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase, } from '../block/ContentModelBlockBase'; -import type { - ContentModelBlockFormat, - ReadonlyContentModelBlockFormat, -} from '../format/ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { ReadonlySelectable, Selectable } from '../common/Selectable'; /** @@ -43,6 +37,6 @@ export interface ReadonlyContentModelGeneralBlock ReadonlyContentModelBlockGroupBase<'General'>, ReadonlyContentModelBlockBase< 'BlockGroup', - ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + ContentModelBlockFormat & ContentModelSegmentFormat >, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts index 5f29ec1cc57..a801f9d41e4 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts @@ -6,10 +6,7 @@ import type { ContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; -import type { - ContentModelListItemFormat, - ReadonlyContentModelListItemFormat, -} from '../format/ContentModelListItemFormat'; +import type { ContentModelListItemFormat } from '../format/ContentModelListItemFormat'; import type { ContentModelListLevel, ReadonlyContentModelListLevel, @@ -41,11 +38,7 @@ export interface ContentModelListItem */ export interface ReadonlyContentModelListItem extends ReadonlyContentModelBlockGroupBase<'ListItem'>, - ReadonlyContentModelBlockBase< - 'BlockGroup', - ReadonlyContentModelListItemFormat, - HTMLLIElement - > { + ReadonlyContentModelBlockBase<'BlockGroup', ContentModelListItemFormat, HTMLLIElement> { /** * Type of this list, either ordered or unordered */ diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts index d231cee39ea..0f8674a5861 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts @@ -4,10 +4,7 @@ import type { ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { - ContentModelTableCellFormat, - ReadonlyContentModelTableCellFormat, -} from '../format/ContentModelTableCellFormat'; +import type { ContentModelTableCellFormat } from '../format/ContentModelTableCellFormat'; import type { ContentModelWithDataset, ReadonlyContentModelWithDataset, @@ -55,7 +52,7 @@ export interface ContentModelTableCell export interface ReadonlyContentModelTableCell extends ReadonlySelectable, ReadonlyContentModelBlockGroupBase<'TableCell'>, - ReadonlyContentModelWithFormat, + ReadonlyContentModelWithFormat, ReadonlyContentModelWithDataset, ContentModelBlockWithCache, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts index 77406c02033..353cdc61fee 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts @@ -1,9 +1,6 @@ import type { MutableMark } from '../common/MutableMark'; import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { - ContentModelCodeFormat, - ReadonlyContentModelCodeFormat, -} from '../format/ContentModelCodeFormat'; +import type { ContentModelCodeFormat } from '../format/ContentModelCodeFormat'; import type { ContentModelWithFormat, ReadonlyContentModelWithFormat, @@ -25,4 +22,4 @@ export interface ContentModelCode */ export interface ReadonlyContentModelCode extends ReadonlyMark, - ReadonlyContentModelWithFormat {} + ReadonlyContentModelWithFormat {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts index 441fdf4d846..18856a29424 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts @@ -1,9 +1,6 @@ import type { MutableMark } from '../common/MutableMark'; import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { - ContentModelHyperLinkFormat, - ReadonlyContentModelHyperLinkFormat, -} from '../format/ContentModelHyperLinkFormat'; +import type { ContentModelHyperLinkFormat } from '../format/ContentModelHyperLinkFormat'; import type { ContentModelWithDataset, ReadonlyContentModelWithDataset, @@ -30,5 +27,5 @@ export interface ContentModelLink */ export interface ReadonlyContentModelLink extends ReadonlyMark, - ReadonlyContentModelWithFormat, + ReadonlyContentModelWithFormat, ReadonlyContentModelWithDataset {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts index 16a91a1e23d..ccb346e9845 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts @@ -1,10 +1,7 @@ import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { MutableMark } from '../common/MutableMark'; import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { - ContentModelListItemLevelFormat, - ReadonlyContentModelListItemLevelFormat, -} from '../format/ContentModelListItemLevelFormat'; +import type { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; import type { ContentModelWithDataset, ReadonlyContentModelWithDataset, @@ -41,6 +38,6 @@ export interface ContentModelListLevel export interface ReadonlyContentModelListLevel extends ReadonlyMark, ContentModelBlockWithCache, - ReadonlyContentModelWithFormat, + ReadonlyContentModelWithFormat, ReadonlyContentModelWithDataset, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts index 21794a2280c..b838b7f01bc 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts @@ -1,9 +1,6 @@ import type { MutableMark } from '../common/MutableMark'; import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { ContentModelWithFormat, ReadonlyContentModelWithFormat, @@ -36,5 +33,5 @@ export interface ContentModelParagraphDecorator */ export interface ReadonlyContentModelParagraphDecorator extends ReadonlyMark, - ReadonlyContentModelWithFormat, + ReadonlyContentModelWithFormat, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts index 5a9889a5175..a44731bc54f 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts @@ -4,22 +4,13 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase, } from '../block/ContentModelBlockBase'; -import type { - ContentModelBlockFormat, - ReadonlyContentModelBlockFormat, -} from '../format/ContentModelBlockFormat'; -import type { - ContentModelEntityFormat, - ReadonlyContentModelEntityFormat, -} from '../format/ContentModelEntityFormat'; +import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; +import type { ContentModelEntityFormat } from '../format/ContentModelEntityFormat'; import type { ContentModelSegmentBase, ReadonlyContentModelSegmentBase, } from '../segment/ContentModelSegmentBase'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; /** * Common part of Content Model of Entity @@ -53,14 +44,14 @@ export interface ReadonlyContentModelEntity Readonly, ReadonlyContentModelBlockBase< 'Entity', - ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + ContentModelBlockFormat & ContentModelSegmentFormat >, ReadonlyContentModelSegmentBase< 'Entity', - ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + ContentModelBlockFormat & ContentModelSegmentFormat > { /** * Format of this entity */ - readonly entityFormat: ReadonlyContentModelEntityFormat; + readonly entityFormat: Readonly; } diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts index 60dddcc6bf9..d09450d161f 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelBlockFormat.ts @@ -1,5 +1,3 @@ -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { BorderFormat } from './formatParts/BorderFormat'; import type { DirectionFormat } from './formatParts/DirectionFormat'; @@ -12,9 +10,9 @@ import type { TextIndentFormat } from './formatParts/TextIndentFormat'; import type { WhiteSpaceFormat } from './formatParts/WhiteSpaceFormat'; /** - * Common part of format object for a paragraph in Content Model + * The format object for a paragraph in Content Model */ -export type ContentModelBlockFormatCommon = BackgroundColorFormat & +export type ContentModelBlockFormat = BackgroundColorFormat & DirectionFormat & TextAlignFormat & HtmlAlignFormat & @@ -24,14 +22,3 @@ export type ContentModelBlockFormatCommon = BackgroundColorFormat & WhiteSpaceFormat & BorderFormat & TextIndentFormat; - -/** - * The format object for a paragraph in Content Model - */ -export type ContentModelBlockFormat = MutableMark & ContentModelBlockFormatCommon; - -/** - * The format object for a paragraph in Content Model (Readonly) - */ -export type ReadonlyContentModelBlockFormat = ReadonlyMark & - Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts index 739bf3e3ee5..e674a4f7552 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelCodeFormat.ts @@ -1,19 +1,7 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { FontFamilyFormat } from './formatParts/FontFamilyFormat'; -/** - * Common part of format object for a code element in Content Model - */ -export type ContentModelCodeFormatCommon = FontFamilyFormat & DisplayFormat; - /** * The format object for a code element in Content Model */ -export type ContentModelCodeFormat = MutableMark & ContentModelCodeFormatCommon; - -/** - * The format object for a code element in Content Model (Readonly) - */ -export type ReadonlyContentModelCodeFormat = ReadonlyMark & Readonly; +export type ContentModelCodeFormat = FontFamilyFormat & DisplayFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts index d803cba7601..bec64102174 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelDividerFormat.ts @@ -1,23 +1,8 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; -import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; -/** - * Common part of format object for a divider in Content Model - */ -export type ContentModelDividerFormatCommon = DisplayFormat & - SizeFormat & - ContentModelBlockFormatCommon; - /** * The format object for a divider in Content Model */ -export type ContentModelDividerFormat = MutableMark & ContentModelDividerFormatCommon; - -/** - * The format object for a divider in Content Model (Readonly) - */ -export type ReadonlyContentModelDividerFormat = ReadonlyMark & - Readonly; +export type ContentModelDividerFormat = ContentModelBlockFormat & DisplayFormat & SizeFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts index 1c474999fba..0570d49228d 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelEntityFormat.ts @@ -1,20 +1,7 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { IdFormat } from './formatParts/IdFormat'; import type { EntityInfoFormat } from './formatParts/EntityInfoFormat'; -/** - * Common part of format object for an entity in Content Model - */ -export type ContentModelEntityFormatCommon = EntityInfoFormat & IdFormat; - /** * The format object for an entity in Content Model */ -export type ContentModelEntityFormat = MutableMark & ContentModelEntityFormatCommon; - -/** - * The format object for an entity in Content Model (Readonly) - */ -export type ReadonlyContentModelEntityFormat = ReadonlyMark & - Readonly; +export type ContentModelEntityFormat = EntityInfoFormat & IdFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts index 313fb57025e..b0cce2e2ecd 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatBase.ts @@ -4,14 +4,12 @@ * So that we can use a single level copy ({...object}) to easily clone a format object */ export type ContentModelFormatBase< - V extends string | number | boolean | undefined | null | never[] | ReadonlyArray = + V extends string | number | boolean | undefined | null = | string | number | boolean | undefined | null - | never[] - | ReadonlyArray > = { [key: string]: V; }; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts index a85ef5e2804..e5dfb905fc5 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelFormatContainerFormat.ts @@ -1,26 +1,12 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; -import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; -import type { ContentModelSegmentFormatCommon } from './ContentModelSegmentFormat'; +import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; +import type { ContentModelSegmentFormat } from './ContentModelSegmentFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; -/** - * Common part of type for FormatContainer - */ -export type ContentModelFormatContainerFormatCommon = SizeFormat & - DisplayFormat & - ContentModelSegmentFormatCommon & - ContentModelBlockFormatCommon; - /** * Type for FormatContainer */ -export type ContentModelFormatContainerFormat = MutableMark & - ContentModelFormatContainerFormatCommon; - -/** - * Type for FormatContainer (Readonly) - */ -export type ReadonlyContentModelFormatContainerFormat = ReadonlyMark & - Readonly; +export type ContentModelFormatContainerFormat = ContentModelBlockFormat & + ContentModelSegmentFormat & + SizeFormat & + DisplayFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts index 282a7af1408..129cc55d84f 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelHyperLinkFormat.ts @@ -1,5 +1,3 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { BorderFormat } from './formatParts/BorderFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; @@ -12,9 +10,9 @@ import type { TextColorFormat } from './formatParts/TextColorFormat'; import type { UnderlineFormat } from './formatParts/UnderlineFormat'; /** - * Common part of format object for a hyperlink in Content Model + * The format object for a hyperlink in Content Model */ -export type ContentModelHyperLinkFormatCommon = LinkFormat & +export type ContentModelHyperLinkFormat = LinkFormat & TextColorFormat & BackgroundColorFormat & UnderlineFormat & @@ -24,14 +22,3 @@ export type ContentModelHyperLinkFormatCommon = LinkFormat & BorderFormat & SizeFormat & TextAlignFormat; - -/** - * The format object for a hyperlink in Content Model - */ -export type ContentModelHyperLinkFormat = MutableMark & ContentModelHyperLinkFormatCommon; - -/** - * The format object for a hyperlink in Content Model (Readonly) - */ -export type ReadonlyContentModelHyperLinkFormat = ReadonlyMark & - Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts index ee96be64bb5..6f9ba413a85 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelImageFormat.ts @@ -1,8 +1,6 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { BorderFormat } from './formatParts/BorderFormat'; import type { BoxShadowFormat } from './formatParts/BoxShadowFormat'; -import type { ContentModelSegmentFormatCommon } from './ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from './ContentModelSegmentFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { FloatFormat } from './formatParts/FloatFormat'; import type { IdFormat } from './formatParts/IdFormat'; @@ -12,9 +10,10 @@ import type { SizeFormat } from './formatParts/SizeFormat'; import type { VerticalAlignFormat } from './formatParts/VerticalAlignFormat'; /** - * Common part of format object for an image in Content Model + * The format object for an image in Content Model */ -export type ContentModelImageFormatCommon = IdFormat & +export type ContentModelImageFormat = ContentModelSegmentFormat & + IdFormat & SizeFormat & MarginFormat & PaddingFormat & @@ -22,16 +21,4 @@ export type ContentModelImageFormatCommon = IdFormat & BoxShadowFormat & DisplayFormat & FloatFormat & - VerticalAlignFormat & - ContentModelSegmentFormatCommon; - -/** - * The format object for an image in Content Model - */ -export type ContentModelImageFormat = MutableMark & ContentModelImageFormatCommon; - -/** - * The format object for an image in Content Model (Readonly) - */ -export type ReadonlyContentModelImageFormat = ReadonlyMark & - Readonly; + VerticalAlignFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts index 69903df63b5..f2d7b550b62 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemFormat.ts @@ -1,5 +1,3 @@ -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { DirectionFormat } from './formatParts/DirectionFormat'; import type { LineHeightFormat } from './formatParts/LineHeightFormat'; @@ -10,9 +8,9 @@ import type { TextAlignFormat } from './formatParts/TextAlignFormat'; import type { TextIndentFormat } from './formatParts/TextIndentFormat'; /** - * Common part of format object for a list item in Content Model + * The format object for a list item in Content Model */ -export type ContentModelListItemFormatCommon = DirectionFormat & +export type ContentModelListItemFormat = DirectionFormat & LineHeightFormat & MarginFormat & PaddingFormat & @@ -20,14 +18,3 @@ export type ContentModelListItemFormatCommon = DirectionFormat & ListStyleFormat & TextIndentFormat & BackgroundColorFormat; - -/** - * The format object for a list item in Content Model - */ -export type ContentModelListItemFormat = MutableMark & ContentModelListItemFormatCommon; - -/** - * The format object for a list item in Content Model (Readonly) - */ -export type ReadonlyContentModelListItemFormat = ReadonlyMark & - Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts index 98c559a1583..c0ea2bf60b6 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelListItemLevelFormat.ts @@ -1,5 +1,3 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { DirectionFormat } from './formatParts/DirectionFormat'; import type { ListStyleFormat } from './formatParts/ListStyleFormat'; import type { ListThreadFormat } from './formatParts/ListThreadFormat'; @@ -8,22 +6,11 @@ import type { PaddingFormat } from './formatParts/PaddingFormat'; import type { TextAlignFormat } from './formatParts/TextAlignFormat'; /** - * Common part of format object for a list level in Content Model + * The format object for a list level in Content Model */ -export type ContentModelListItemLevelFormatCommon = ListThreadFormat & +export type ContentModelListItemLevelFormat = ListThreadFormat & DirectionFormat & TextAlignFormat & MarginFormat & PaddingFormat & ListStyleFormat; - -/** - * The format object for a list level in Content Model - */ -export type ContentModelListItemLevelFormat = MutableMark & ContentModelListItemLevelFormatCommon; - -/** - * The format object for a list level in Content Model (Readonly) - */ -export type ReadonlyContentModelListItemLevelFormat = ReadonlyMark & - Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts index 789de2d021d..d8552973cef 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelSegmentFormat.ts @@ -1,5 +1,3 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import type { BoldFormat } from './formatParts/BoldFormat'; import type { FontFamilyFormat } from './formatParts/FontFamilyFormat'; @@ -13,9 +11,9 @@ import type { TextColorFormat } from './formatParts/TextColorFormat'; import type { UnderlineFormat } from './formatParts/UnderlineFormat'; /** - * Common part of format object for a segment in Content Model + * The format object for a segment in Content Model */ -export type ContentModelSegmentFormatCommon = TextColorFormat & +export type ContentModelSegmentFormat = TextColorFormat & BackgroundColorFormat & LetterSpacingFormat & FontSizeFormat & @@ -26,14 +24,3 @@ export type ContentModelSegmentFormatCommon = TextColorFormat & StrikeFormat & SuperOrSubScriptFormat & LineHeightFormat; - -/** - * The format object for a segment in Content Model - */ -export type ContentModelSegmentFormat = MutableMark & ContentModelSegmentFormatCommon; - -/** - * The format object for a segment in Content Model (Readonly) - */ -export type ReadonlyContentModelSegmentFormat = ReadonlyMark & - Readonly; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts index 592606bdee2..651535c95f0 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableCellFormat.ts @@ -1,29 +1,16 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { BorderBoxFormat } from './formatParts/BorderBoxFormat'; -import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; import type { TextColorFormat } from './formatParts/TextColorFormat'; import type { VerticalAlignFormat } from './formatParts/VerticalAlignFormat'; import type { WordBreakFormat } from '../format/formatParts/WordBreakFormat'; /** - * Common part of format of table cell + * Format of table cell */ -export type ContentModelTableCellFormatCommon = BorderBoxFormat & +export type ContentModelTableCellFormat = ContentModelBlockFormat & + BorderBoxFormat & VerticalAlignFormat & WordBreakFormat & TextColorFormat & - SizeFormat & - ContentModelBlockFormatCommon; - -/** - * Format of table cell - */ -export type ContentModelTableCellFormat = MutableMark & ContentModelTableCellFormatCommon; - -/** - * Format of table cell (Readonly) - */ -export type ReadonlyContentModelTableCellFormat = ReadonlyMark & - Readonly; + SizeFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts index b5e3f11a48c..7d84b3fe376 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelTableFormat.ts @@ -1,8 +1,6 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { BorderBoxFormat } from './formatParts/BorderBoxFormat'; import type { BorderFormat } from './formatParts/BorderFormat'; -import type { ContentModelBlockFormatCommon } from './ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; import type { IdFormat } from './formatParts/IdFormat'; import type { MarginFormat } from './formatParts/MarginFormat'; @@ -11,25 +9,14 @@ import type { TableLayoutFormat } from './formatParts/TableLayoutFormat'; import type { SizeFormat } from './formatParts/SizeFormat'; /** - * Common part of format of Table + * Format of Table */ -export type ContentModelTableFormatCommon = IdFormat & +export type ContentModelTableFormat = ContentModelBlockFormat & + IdFormat & BorderFormat & BorderBoxFormat & SpacingFormat & MarginFormat & DisplayFormat & TableLayoutFormat & - SizeFormat & - ContentModelBlockFormatCommon; - -/** - * Format of Table - */ -export type ContentModelTableFormat = MutableMark & ContentModelTableFormatCommon; - -/** - * Format of Table (Readonly) - */ -export type ReadonlyContentModelTableFormat = ReadonlyMark & - Readonly; + SizeFormat; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts index 255cb1553f2..f79e379ee04 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts @@ -1,11 +1,9 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; import type { DatasetFormat, ReadonlyDatasetFormat } from './metadata/DatasetFormat'; /** * Represents base format of an element that supports dataset and/or metadata */ -export type ContentModelWithDataset = MutableMark & { +export type ContentModelWithDataset = { /** * dataset of this element */ @@ -15,7 +13,7 @@ export type ContentModelWithDataset = MutableMark & { /** * Represents base format of an element that supports dataset and/or metadata (Readonly) */ -export type ReadonlyContentModelWithDataset = ReadonlyMark & { +export type ReadonlyContentModelWithDataset = { /** * dataset of this element */ diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts index e3287e700a9..cca9d00d8e0 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts @@ -1,7 +1,4 @@ -import type { - ContentModelBlockFormat, - ReadonlyContentModelBlockFormat, -} from '../format/ContentModelBlockFormat'; +import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, @@ -10,10 +7,7 @@ import type { ContentModelSegmentBase, ReadonlyContentModelSegmentBase, } from './ContentModelSegmentBase'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; /** * Content Model of general Segment @@ -29,5 +23,5 @@ export interface ReadonlyContentModelGeneralSegment extends ReadonlyContentModelGeneralBlock, ReadonlyContentModelSegmentBase< 'General', - ReadonlyContentModelBlockFormat & ReadonlyContentModelSegmentFormat + ContentModelBlockFormat & ContentModelSegmentFormat > {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts index f349d9e481e..9b7e531d728 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelImage.ts @@ -1,7 +1,4 @@ -import type { - ContentModelImageFormat, - ReadonlyContentModelImageFormat, -} from '../format/ContentModelImageFormat'; +import type { ContentModelImageFormat } from '../format/ContentModelImageFormat'; import type { ContentModelSegmentBase, ReadonlyContentModelSegmentBase, @@ -49,6 +46,6 @@ export interface ContentModelImage * Content Model of IMG (Readonly) */ export interface ReadonlyContentModelImage - extends ReadonlyContentModelSegmentBase<'Image', ReadonlyContentModelImageFormat>, + extends ReadonlyContentModelSegmentBase<'Image', ContentModelImageFormat>, ReadonlyContentModelWithDataset, Readonly {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts index 23687afb195..4c6f2636a25 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts @@ -2,10 +2,7 @@ import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { MutableMark } from '../common/MutableMark'; import type { ContentModelCode, ReadonlyContentModelCode } from '../decorator/ContentModelCode'; import type { ContentModelLink, ReadonlyContentModelLink } from '../decorator/ContentModelLink'; -import type { - ContentModelSegmentFormat, - ReadonlyContentModelSegmentFormat, -} from '../format/ContentModelSegmentFormat'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { ContentModelSegmentType } from './SegmentType'; import type { ContentModelWithFormat, @@ -50,7 +47,7 @@ export interface ContentModelSegmentBase< */ export interface ReadonlyContentModelSegmentBase< T extends ContentModelSegmentType, - TFormat extends ReadonlyContentModelSegmentFormat = ReadonlyContentModelSegmentFormat + TFormat extends ContentModelSegmentFormat = ContentModelSegmentFormat > extends ReadonlyMark, ReadonlySelectable, diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 4f4dd6d1abb..16ea23b8d07 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -1,73 +1,25 @@ -export { - ContentModelSegmentFormat, - ContentModelSegmentFormatCommon, - ReadonlyContentModelSegmentFormat, -} from './contentModel/format/ContentModelSegmentFormat'; +export { ContentModelSegmentFormat } from './contentModel/format/ContentModelSegmentFormat'; export { ContentModelWithFormat, ReadonlyContentModelWithFormat, } from './contentModel/format/ContentModelWithFormat'; -export { - ContentModelTableFormat, - ContentModelTableFormatCommon, - ReadonlyContentModelTableFormat, -} from './contentModel/format/ContentModelTableFormat'; +export { ContentModelTableFormat } from './contentModel/format/ContentModelTableFormat'; export { ContentModelWithDataset, ReadonlyContentModelWithDataset, } from './contentModel/format/ContentModelWithDataset'; -export { - ContentModelBlockFormat, - ContentModelBlockFormatCommon, - ReadonlyContentModelBlockFormat, -} from './contentModel/format/ContentModelBlockFormat'; -export { - ContentModelTableCellFormat, - ContentModelTableCellFormatCommon, - ReadonlyContentModelTableCellFormat, -} from './contentModel/format/ContentModelTableCellFormat'; -export { - ContentModelListItemFormat, - ContentModelListItemFormatCommon, - ReadonlyContentModelListItemFormat, -} from './contentModel/format/ContentModelListItemFormat'; -export { - ContentModelListItemLevelFormat, - ContentModelListItemLevelFormatCommon, - ReadonlyContentModelListItemLevelFormat, -} from './contentModel/format/ContentModelListItemLevelFormat'; -export { - ContentModelHyperLinkFormat, - ContentModelHyperLinkFormatCommon, - ReadonlyContentModelHyperLinkFormat, -} from './contentModel/format/ContentModelHyperLinkFormat'; -export { - ContentModelCodeFormat, - ContentModelCodeFormatCommon, - ReadonlyContentModelCodeFormat, -} from './contentModel/format/ContentModelCodeFormat'; -export { - ContentModelFormatContainerFormat, - ContentModelFormatContainerFormatCommon, - ReadonlyContentModelFormatContainerFormat, -} from './contentModel/format/ContentModelFormatContainerFormat'; -export { - ContentModelDividerFormat, - ContentModelDividerFormatCommon, - ReadonlyContentModelDividerFormat, -} from './contentModel/format/ContentModelDividerFormat'; +export { ContentModelBlockFormat } from './contentModel/format/ContentModelBlockFormat'; +export { ContentModelTableCellFormat } from './contentModel/format/ContentModelTableCellFormat'; +export { ContentModelListItemFormat } from './contentModel/format/ContentModelListItemFormat'; +export { ContentModelListItemLevelFormat } from './contentModel/format/ContentModelListItemLevelFormat'; +export { ContentModelHyperLinkFormat } from './contentModel/format/ContentModelHyperLinkFormat'; +export { ContentModelCodeFormat } from './contentModel/format/ContentModelCodeFormat'; +export { ContentModelFormatContainerFormat } from './contentModel/format/ContentModelFormatContainerFormat'; +export { ContentModelDividerFormat } from './contentModel/format/ContentModelDividerFormat'; export { ContentModelFormatBase } from './contentModel/format/ContentModelFormatBase'; export { ContentModelFormatMap } from './contentModel/format/ContentModelFormatMap'; -export { - ContentModelImageFormat, - ContentModelImageFormatCommon, - ReadonlyContentModelImageFormat, -} from './contentModel/format/ContentModelImageFormat'; -export { - ContentModelEntityFormat, - ContentModelEntityFormatCommon, - ReadonlyContentModelEntityFormat, -} from './contentModel/format/ContentModelEntityFormat'; +export { ContentModelImageFormat } from './contentModel/format/ContentModelImageFormat'; +export { ContentModelEntityFormat } from './contentModel/format/ContentModelEntityFormat'; export { FormatHandlerTypeMap, FormatKey } from './contentModel/format/FormatHandlerTypeMap'; export { BackgroundColorFormat } from './contentModel/format/formatParts/BackgroundColorFormat'; From 950a4ae6913c5a3426517a5badb471541f8b7af9 Mon Sep 17 00:00:00 2001 From: jiuqingsong Date: Tue, 14 May 2024 08:47:34 -0700 Subject: [PATCH 10/66] fix build --- .../lib/command/paste/mergePasteContent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index b8125a6097c..9cd3302ad4b 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -12,12 +12,12 @@ import type { ClipboardData, CloneModelOptions, ContentModelDocument, - ContentModelSegmentFormatCommon, + ContentModelSegmentFormat, IEditor, MergeModelOption, } from 'roosterjs-content-model-types'; -const EmptySegmentFormat: Required = { +const EmptySegmentFormat: Required = { backgroundColor: '', fontFamily: '', fontSize: '', From 2574fe2dbde60cd56bf4b39412647086ec46e93f Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 13:31:37 -0700 Subject: [PATCH 11/66] Improve --- .../roosterjs-content-model-dom/lib/index.ts | 1 + .../lib/modelApi/common/mutate.ts | 99 +++++++++++++++++++ .../blockGroup/ContentModelBlockGroup.ts | 30 +++++- .../blockGroup/ContentModelBlockGroupBase.ts | 33 +++++-- .../blockGroup/ContentModelDocument.ts | 15 ++- .../blockGroup/ContentModelFormatContainer.ts | 19 +++- .../blockGroup/ContentModelGeneralBlock.ts | 14 ++- .../blockGroup/ContentModelListItem.ts | 22 ++++- .../blockGroup/ContentModelTableCell.ts | 24 +++-- .../lib/contentModel/common/MutableType.ts | 24 ++--- .../format/ContentModelWithDataset.ts | 6 +- .../segment/ContentModelGeneralSegment.ts | 8 ++ .../lib/index.ts | 8 ++ 13 files changed, 261 insertions(+), 42 deletions(-) create mode 100644 packages/roosterjs-content-model-dom/lib/modelApi/common/mutate.ts diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index d2f8789135a..d871e3d46f2 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -55,6 +55,7 @@ export { createDivider } from './modelApi/creators/createDivider'; export { createListLevel } from './modelApi/creators/createListLevel'; export { createEmptyModel } from './modelApi/creators/createEmptyModel'; +export { mutateBlock, mutateSegments, mutateSegment } from './modelApi/common/mutate'; export { addBlock } from './modelApi/common/addBlock'; export { addCode } from './modelApi/common/addDecorators'; export { addLink } from './modelApi/common/addDecorators'; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/mutate.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/mutate.ts new file mode 100644 index 00000000000..699bb3e7616 --- /dev/null +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/mutate.ts @@ -0,0 +1,99 @@ +import type { + ContentModelParagraph, + ContentModelSegment, + MutableType, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelListItem, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; + +/** + * Convert a readonly block to mutable block, clear cached element if exist + * @param block The block to convert from + * @returns The same block object of its related mutable type + */ +export function mutateBlock( + block: T +): MutableType { + if (block.cachedElement) { + delete block.cachedElement; + } + + if (isTable(block)) { + block.rows.forEach(row => { + delete row.cachedElement; + }); + } else if (isListItem(block)) { + block.levels.forEach(level => delete level.cachedElement); + } + + const result = (block as unknown) as MutableType; + + return result; +} + +/** + * Convert segments of a readonly paragraph to be mutable. + * Segments that are not belong to the given paragraph will be skipped + * @param paragraph The readonly paragraph to convert from + * @param segments The segments to convert from + */ +export function mutateSegments( + paragraph: ReadonlyContentModelParagraph, + segments: ReadonlyContentModelSegment[] +): [ContentModelParagraph, ContentModelSegment[], number[]] { + const mutablePara = mutateBlock(paragraph); + const result: [ContentModelParagraph, ContentModelSegment[], number[]] = [mutablePara, [], []]; + + if (segments) { + segments.forEach(segment => { + const index = paragraph.segments.indexOf(segment); + + if (index >= 0) { + result[1].push(mutablePara.segments[index]); + result[2].push(index); + } + }); + } + + return result; +} + +/** + * Convert a readonly segment to be mutable, together with its owner paragraph + * If the segment does not belong to the given paragraph, return null for the segment + * @param paragraph The readonly paragraph to convert from + * @param segment The segment to convert from + */ +export function mutateSegment( + paragraph: ReadonlyContentModelParagraph, + segment: T, + callback?: (segment: MutableType, paragraph: ContentModelParagraph, index: number) => void +): [ContentModelParagraph, MutableType | null, number] { + const [mutablePara, mutableSegments, indexes] = mutateSegments(paragraph, [segment]); + const mutableSegment = + (mutableSegments[0] as ReadonlyContentModelSegment) == segment + ? (mutableSegments[0] as MutableType) + : null; + + if (callback && mutableSegment) { + callback(mutableSegments[0] as MutableType, mutablePara, indexes[0]); + } + + return [mutablePara, mutableSegment, indexes[0] ?? -1]; +} + +function isTable( + obj: ReadonlyContentModelBlockGroup | ReadonlyContentModelBlock +): obj is ReadonlyContentModelTable { + return (obj as ReadonlyContentModelTable).blockType == 'Table'; +} + +function isListItem( + obj: ReadonlyContentModelBlockGroup | ReadonlyContentModelBlock +): obj is ReadonlyContentModelListItem { + return (obj as ReadonlyContentModelListItem).blockGroupType == 'ListItem'; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts index aa908291f8e..9be2dd2b791 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts @@ -1,14 +1,28 @@ -import type { ContentModelDocument, ReadonlyContentModelDocument } from './ContentModelDocument'; +import type { + ContentModelDocument, + MutableContentModelDocument, + ReadonlyContentModelDocument, +} from './ContentModelDocument'; import type { ContentModelFormatContainer, + MutableContentModelFormatContainer, ReadonlyContentModelFormatContainer, } from './ContentModelFormatContainer'; import type { ContentModelGeneralBlock, + MutableContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, } from './ContentModelGeneralBlock'; -import type { ContentModelListItem, ReadonlyContentModelListItem } from './ContentModelListItem'; -import type { ContentModelTableCell, ReadonlyContentModelTableCell } from './ContentModelTableCell'; +import type { + ContentModelListItem, + MutableContentModelListItem, + ReadonlyContentModelListItem, +} from './ContentModelListItem'; +import type { + ContentModelTableCell, + MutableContentModelTableCell, + ReadonlyContentModelTableCell, +} from './ContentModelTableCell'; /** * The union type of Content Model Block Group @@ -29,3 +43,13 @@ export type ReadonlyContentModelBlockGroup = | ReadonlyContentModelListItem | ReadonlyContentModelTableCell | ReadonlyContentModelGeneralBlock; + +/** + * The union type of Content Model Block Group (Single level mutable) + */ +export type MutableContentModelBlockGroup = + | MutableContentModelDocument + | MutableContentModelFormatContainer + | MutableContentModelListItem + | MutableContentModelTableCell + | MutableContentModelGeneralBlock; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts index f401cf27bd6..61e72267938 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts @@ -1,3 +1,4 @@ +import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { MutableMark } from '../common/MutableMark'; import type { ContentModelBlock, ReadonlyContentModelBlock } from '../block/ContentModelBlock'; @@ -6,7 +7,10 @@ import type { ContentModelBlockGroupType } from './BlockGroupType'; /** * Common part of base type of Content Model Block Group */ -export interface ContentModelBlockGroupBaseCommon { +export interface ContentModelBlockGroupBaseCommon< + T extends ContentModelBlockGroupType, + TElement extends HTMLElement = HTMLElement +> extends ContentModelBlockWithCache { /** * Type of this block group */ @@ -16,9 +20,10 @@ export interface ContentModelBlockGroupBaseCommon - extends MutableMark, - ContentModelBlockGroupBaseCommon { +export interface ContentModelBlockGroupBase< + T extends ContentModelBlockGroupType, + TElement extends HTMLElement = HTMLElement +> extends MutableMark, ContentModelBlockGroupBaseCommon { /** * Blocks under this group */ @@ -28,11 +33,25 @@ export interface ContentModelBlockGroupBase - extends ReadonlyMark, - ContentModelBlockGroupBaseCommon { +export interface ReadonlyContentModelBlockGroupBase< + T extends ContentModelBlockGroupType, + TElement extends HTMLElement = HTMLElement +> extends ReadonlyMark, ContentModelBlockGroupBaseCommon { /** * Blocks under this group */ readonly blocks: ReadonlyArray; } + +/** + * Base type of Content Model Block Group (Readonly) + */ +export interface MutableContentModelBlockGroupBase< + T extends ContentModelBlockGroupType, + TElement extends HTMLElement = HTMLElement +> extends MutableMark, ContentModelBlockGroupBaseCommon { + /** + * Blocks under this group + */ + blocks: ReadonlyContentModelBlock[]; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts index 3dc7175dc62..2571557eacb 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts @@ -1,5 +1,6 @@ import type { ContentModelBlockGroupBase, + MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -30,6 +31,14 @@ export interface ContentModelDocument * Content Model document entry point (Readonly) */ export interface ReadonlyContentModelDocument - extends ReadonlyContentModelBlockGroupBase<'Document'>, - Partial>, - Readonly {} + extends Readonly, + ReadonlyContentModelBlockGroupBase<'Document'>, + Partial> {} + +/** + * Content Model document entry point (Single level mutable) + */ +export interface MutableContentModelDocument + extends ContentModelDocumentCommon, + MutableContentModelBlockGroupBase<'Document'>, + Partial> {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts index c8573111673..445075ec6f9 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts @@ -4,6 +4,7 @@ import type { } from '../block/ContentModelBlockBase'; import type { ContentModelBlockGroupBase, + MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelFormatContainerFormat } from '../format/ContentModelFormatContainerFormat'; @@ -36,6 +37,18 @@ export interface ContentModelFormatContainer * Content Model of Format Container (Readonly) */ export interface ReadonlyContentModelFormatContainer - extends ReadonlyContentModelBlockGroupBase<'FormatContainer'>, - ReadonlyContentModelBlockBase<'BlockGroup', ContentModelFormatContainerFormat, HTMLElement>, - Readonly {} + extends Readonly, + ReadonlyContentModelBlockGroupBase<'FormatContainer'>, + ReadonlyContentModelBlockBase< + 'BlockGroup', + ContentModelFormatContainerFormat, + HTMLElement + > {} + +/** + * Content Model of Format Container (Single level mutable) + */ +export interface MutableContentModelFormatContainer + extends ContentModelFormatContainerCommon, + MutableContentModelBlockGroupBase<'FormatContainer'>, + ContentModelBlockBase<'BlockGroup', ContentModelFormatContainerFormat, HTMLElement> {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts index caefb55a56d..c05051a6f99 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts @@ -5,6 +5,7 @@ import type { import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockGroupBase, + MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -34,9 +35,18 @@ export interface ContentModelGeneralBlock */ export interface ReadonlyContentModelGeneralBlock extends ReadonlySelectable, + Readonly, ReadonlyContentModelBlockGroupBase<'General'>, ReadonlyContentModelBlockBase< 'BlockGroup', ContentModelBlockFormat & ContentModelSegmentFormat - >, - Readonly {} + > {} + +/** + * Content Model for general Block element (Single level mutable) + */ +export interface MutableContentModelGeneralBlock + extends Selectable, + ContentModelGeneralBlockCommon, + MutableContentModelBlockGroupBase<'General'>, + ContentModelBlockBase<'BlockGroup', ContentModelBlockFormat & ContentModelSegmentFormat> {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts index a801f9d41e4..05b3ce10720 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts @@ -4,6 +4,7 @@ import type { } from '../block/ContentModelBlockBase'; import type { ContentModelBlockGroupBase, + MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelListItemFormat } from '../format/ContentModelListItemFormat'; @@ -20,7 +21,7 @@ import type { * Content Model of List Item */ export interface ContentModelListItem - extends ContentModelBlockGroupBase<'ListItem'>, + extends ContentModelBlockGroupBase<'ListItem', HTMLLIElement>, ContentModelBlockBase<'BlockGroup', ContentModelListItemFormat, HTMLLIElement> { /** * Type of this list, either ordered or unordered @@ -37,7 +38,7 @@ export interface ContentModelListItem * Content Model of List Item (Readonly) */ export interface ReadonlyContentModelListItem - extends ReadonlyContentModelBlockGroupBase<'ListItem'>, + extends ReadonlyContentModelBlockGroupBase<'ListItem', HTMLLIElement>, ReadonlyContentModelBlockBase<'BlockGroup', ContentModelListItemFormat, HTMLLIElement> { /** * Type of this list, either ordered or unordered @@ -49,3 +50,20 @@ export interface ReadonlyContentModelListItem */ readonly formatHolder: ReadonlyContentModelSelectionMarker; } + +/** + * Content Model of List Item (Single level mutable) + */ +export interface MutableContentModelListItem + extends MutableContentModelBlockGroupBase<'ListItem', HTMLLIElement>, + ContentModelBlockBase<'BlockGroup', ContentModelListItemFormat, HTMLLIElement> { + /** + * Type of this list, either ordered or unordered + */ + readonly levels: ReadonlyArray; + + /** + * A dummy segment to hold format of this list item + */ + readonly formatHolder: ReadonlyContentModelSelectionMarker; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts index 0f8674a5861..2dfa3eb467a 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts @@ -1,9 +1,9 @@ import type { TableCellMetadataFormat } from '../format/metadata/TableCellMetadataFormat'; import type { ContentModelBlockGroupBase, + MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; -import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { ContentModelTableCellFormat } from '../format/ContentModelTableCellFormat'; import type { ContentModelWithDataset, @@ -41,18 +41,26 @@ export interface ContentModelTableCellCommon { export interface ContentModelTableCell extends Selectable, ContentModelTableCellCommon, - ContentModelBlockGroupBase<'TableCell'>, + ContentModelBlockGroupBase<'TableCell', HTMLTableCellElement>, ContentModelWithFormat, - ContentModelWithDataset, - ContentModelBlockWithCache {} + ContentModelWithDataset {} /** * Content Model of Table Cell (Readonly) */ export interface ReadonlyContentModelTableCell extends ReadonlySelectable, - ReadonlyContentModelBlockGroupBase<'TableCell'>, + Readonly, + ReadonlyContentModelBlockGroupBase<'TableCell', HTMLTableCellElement>, ReadonlyContentModelWithFormat, - ReadonlyContentModelWithDataset, - ContentModelBlockWithCache, - Readonly {} + ReadonlyContentModelWithDataset {} + +/** + * Content Model of Table Cell (Single level mutable) + */ +export interface MutableContentModelTableCell + extends Selectable, + ContentModelTableCellCommon, + MutableContentModelBlockGroupBase<'TableCell', HTMLTableCellElement>, + ContentModelWithFormat, + ContentModelWithDataset {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts index cbe8a351149..658d96d44fb 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts @@ -5,26 +5,26 @@ import type { ReadonlyContentModelDivider, } from '../block/ContentModelDivider'; import type { - ContentModelDocument, + MutableContentModelDocument, ReadonlyContentModelDocument, } from '../blockGroup/ContentModelDocument'; import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; import type { - ContentModelFormatContainer, + MutableContentModelFormatContainer, ReadonlyContentModelFormatContainer, } from '../blockGroup/ContentModelFormatContainer'; import type { - ContentModelGeneralBlock, + MutableContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, } from '../blockGroup/ContentModelGeneralBlock'; import type { - ContentModelGeneralSegment, + MutableContentModelGeneralSegment, ReadonlyContentModelGeneralSegment, } from '../segment/ContentModelGeneralSegment'; import type { ContentModelImage, ReadonlyContentModelImage } from '../segment/ContentModelImage'; import type { ContentModelLink, ReadonlyContentModelLink } from '../decorator/ContentModelLink'; import type { - ContentModelListItem, + MutableContentModelListItem, ReadonlyContentModelListItem, } from '../blockGroup/ContentModelListItem'; import type { @@ -45,7 +45,7 @@ import type { } from '../segment/ContentModelSelectionMarker'; import type { ContentModelTable, ReadonlyContentModelTable } from '../block/ContentModelTable'; import type { - ContentModelTableCell, + MutableContentModelTableCell, ReadonlyContentModelTableCell, } from '../blockGroup/ContentModelTableCell'; import type { @@ -58,7 +58,7 @@ import type { ContentModelText, ReadonlyContentModelText } from '../segment/Cont * Get mutable type from its related readonly type */ export type MutableType = T extends ReadonlyContentModelGeneralSegment - ? ContentModelGeneralSegment + ? MutableContentModelGeneralSegment : T extends ReadonlyContentModelSelectionMarker ? ContentModelSelectionMarker : T extends ReadonlyContentModelImage @@ -76,19 +76,19 @@ export type MutableType = T extends ReadonlyContentModelGeneralSegment : T extends ReadonlyContentModelTableRow ? ContentModelTableRow : T extends ReadonlyContentModelTableCell - ? ContentModelTableCell + ? MutableContentModelTableCell : T extends ReadonlyContentModelFormatContainer - ? ContentModelFormatContainer + ? MutableContentModelFormatContainer : T extends ReadonlyContentModelListItem - ? ContentModelListItem + ? MutableContentModelListItem : T extends ReadonlyContentModelListLevel ? ContentModelListLevel : T extends ReadonlyContentModelDivider ? ContentModelDivider : T extends ReadonlyContentModelDocument - ? ContentModelDocument + ? MutableContentModelDocument : T extends ReadonlyContentModelGeneralBlock - ? ContentModelGeneralBlock + ? MutableContentModelGeneralBlock : T extends ReadonlyContentModelParagraphDecorator ? ContentModelParagraphDecorator : T extends ReadonlyContentModelLink diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts index f79e379ee04..7de91cc8062 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts @@ -1,9 +1,11 @@ +import type { MutableMark } from '../common/MutableMark'; +import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { DatasetFormat, ReadonlyDatasetFormat } from './metadata/DatasetFormat'; /** * Represents base format of an element that supports dataset and/or metadata */ -export type ContentModelWithDataset = { +export type ContentModelWithDataset = MutableMark & { /** * dataset of this element */ @@ -13,7 +15,7 @@ export type ContentModelWithDataset = { /** * Represents base format of an element that supports dataset and/or metadata (Readonly) */ -export type ReadonlyContentModelWithDataset = { +export type ReadonlyContentModelWithDataset = ReadonlyMark & { /** * dataset of this element */ diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts index cca9d00d8e0..078776b69c2 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts @@ -1,6 +1,7 @@ import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelGeneralBlock, + MutableContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, } from '../blockGroup/ContentModelGeneralBlock'; import type { @@ -25,3 +26,10 @@ export interface ReadonlyContentModelGeneralSegment 'General', ContentModelBlockFormat & ContentModelSegmentFormat > {} + +/** + * Content Model of general Segment (Single level mutable) + */ +export interface MutableContentModelGeneralSegment + extends MutableContentModelGeneralBlock, + ContentModelSegmentBase<'General', ContentModelBlockFormat & ContentModelSegmentFormat> {} diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 16ea23b8d07..541e22bb21c 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -135,40 +135,48 @@ export { ContentModelDocument, ContentModelDocumentCommon, ReadonlyContentModelDocument, + MutableContentModelDocument, } from './contentModel/blockGroup/ContentModelDocument'; export { ContentModelBlockGroupBase, ContentModelBlockGroupBaseCommon, ReadonlyContentModelBlockGroupBase, + MutableContentModelBlockGroupBase, } from './contentModel/blockGroup/ContentModelBlockGroupBase'; export { ContentModelFormatContainer, ContentModelFormatContainerCommon, ReadonlyContentModelFormatContainer, + MutableContentModelFormatContainer, } from './contentModel/blockGroup/ContentModelFormatContainer'; export { ContentModelGeneralBlock, ContentModelGeneralBlockCommon, ReadonlyContentModelGeneralBlock, + MutableContentModelGeneralBlock, } from './contentModel/blockGroup/ContentModelGeneralBlock'; export { ContentModelListItem, ReadonlyContentModelListItem, + MutableContentModelListItem, } from './contentModel/blockGroup/ContentModelListItem'; export { ContentModelTableCell, ContentModelTableCellCommon, ReadonlyContentModelTableCell, + MutableContentModelTableCell, } from './contentModel/blockGroup/ContentModelTableCell'; export { ContentModelBlockGroup, ReadonlyContentModelBlockGroup, + MutableContentModelBlockGroup, } from './contentModel/blockGroup/ContentModelBlockGroup'; export { ContentModelBr, ReadonlyContentModelBr } from './contentModel/segment/ContentModelBr'; export { ContentModelGeneralSegment, ReadonlyContentModelGeneralSegment, + MutableContentModelGeneralSegment, } from './contentModel/segment/ContentModelGeneralSegment'; export { ContentModelImage, From 47b21f39c584aa93c9b8ff96078334357809ba23 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 13:41:44 -0700 Subject: [PATCH 12/66] improve --- .../lib/contentModel/blockGroup/ContentModelListItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts index 05b3ce10720..2d965f98157 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts @@ -65,5 +65,5 @@ export interface MutableContentModelListItem /** * A dummy segment to hold format of this list item */ - readonly formatHolder: ReadonlyContentModelSelectionMarker; + formatHolder: ContentModelSelectionMarker; } From bd6b0cbf0c95f770c5837dec4f56310458718d49 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 13:49:57 -0700 Subject: [PATCH 13/66] Improve --- .../contentModel/block/ContentModelBlock.ts | 4 +- .../lib/contentModel/common/MutableType.ts | 4 +- .../contentModel/entity/ContentModelEntity.ts | 47 +++---------------- .../segment/ContentModelSegment.ts | 4 +- .../lib/index.ts | 6 +-- 5 files changed, 13 insertions(+), 52 deletions(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts index c4256a3bc06..2422de45cd5 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts @@ -1,5 +1,5 @@ import type { ContentModelDivider, ReadonlyContentModelDivider } from './ContentModelDivider'; -import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; +import type { ContentModelEntity } from '../entity/ContentModelEntity'; import type { ContentModelFormatContainer, ReadonlyContentModelFormatContainer, @@ -36,5 +36,5 @@ export type ReadonlyContentModelBlock = | ReadonlyContentModelGeneralBlock | ReadonlyContentModelTable | ReadonlyContentModelParagraph - | ReadonlyContentModelEntity + | ContentModelEntity | ReadonlyContentModelDivider; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts index 658d96d44fb..5e1f46c34c9 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts @@ -8,7 +8,7 @@ import type { MutableContentModelDocument, ReadonlyContentModelDocument, } from '../blockGroup/ContentModelDocument'; -import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; +import type { ContentModelEntity } from '../entity/ContentModelEntity'; import type { MutableContentModelFormatContainer, ReadonlyContentModelFormatContainer, @@ -63,7 +63,7 @@ export type MutableType = T extends ReadonlyContentModelGeneralSegment ? ContentModelSelectionMarker : T extends ReadonlyContentModelImage ? ContentModelImage - : T extends ReadonlyContentModelEntity + : T extends ContentModelEntity ? ContentModelEntity : T extends ReadonlyContentModelText ? ContentModelText diff --git a/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts index a44731bc54f..897a572920e 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/entity/ContentModelEntity.ts @@ -1,57 +1,22 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; -import type { - ContentModelBlockBase, - ReadonlyContentModelBlockBase, -} from '../block/ContentModelBlockBase'; +import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelEntityFormat } from '../format/ContentModelEntityFormat'; -import type { - ContentModelSegmentBase, - ReadonlyContentModelSegmentBase, -} from '../segment/ContentModelSegmentBase'; +import type { ContentModelSegmentBase } from '../segment/ContentModelSegmentBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -/** - * Common part of Content Model of Entity - */ -export interface ContentModelEntityCommon { - /** - * The wrapper DOM node of this entity which holds the info CSS classes of this entity - */ - wrapper: HTMLElement; -} - /** * Content Model of Entity */ export interface ContentModelEntity - extends MutableMark, - ContentModelEntityCommon, - ContentModelBlockBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat>, + extends ContentModelBlockBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat>, ContentModelSegmentBase<'Entity', ContentModelBlockFormat & ContentModelSegmentFormat> { /** - * Format of this entity + * The wrapper DOM node of this entity which holds the info CSS classes of this entity */ - entityFormat: ContentModelEntityFormat; -} + wrapper: HTMLElement; -/** - * Content Model of Entity (Readonly) - */ -export interface ReadonlyContentModelEntity - extends ReadonlyMark, - Readonly, - ReadonlyContentModelBlockBase< - 'Entity', - ContentModelBlockFormat & ContentModelSegmentFormat - >, - ReadonlyContentModelSegmentBase< - 'Entity', - ContentModelBlockFormat & ContentModelSegmentFormat - > { /** * Format of this entity */ - readonly entityFormat: Readonly; + entityFormat: ContentModelEntityFormat; } diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts index 687fb511796..7fec3926408 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts @@ -1,5 +1,5 @@ import type { ContentModelBr, ReadonlyContentModelBr } from './ContentModelBr'; -import type { ContentModelEntity, ReadonlyContentModelEntity } from '../entity/ContentModelEntity'; +import type { ContentModelEntity } from '../entity/ContentModelEntity'; import type { ContentModelGeneralSegment, ReadonlyContentModelGeneralSegment, @@ -30,5 +30,5 @@ export type ReadonlyContentModelSegment = | ReadonlyContentModelText | ReadonlyContentModelBr | ReadonlyContentModelGeneralSegment - | ReadonlyContentModelEntity + | ContentModelEntity | ReadonlyContentModelImage; diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 541e22bb21c..975f5b4dd7e 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -125,11 +125,7 @@ export { ReadonlyContentModelTableRow, } from './contentModel/block/ContentModelTableRow'; -export { - ContentModelEntity, - ContentModelEntityCommon, - ReadonlyContentModelEntity, -} from './contentModel/entity/ContentModelEntity'; +export { ContentModelEntity } from './contentModel/entity/ContentModelEntity'; export { ContentModelDocument, From cf3ea785bd172f65529fc05a784d0494b18345d7 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 14:25:20 -0700 Subject: [PATCH 14/66] Add shallow mutable type --- .../block/ContentModelBlockBase.ts | 16 +++- .../block/ContentModelTableRow.ts | 3 +- .../blockGroup/ContentModelBlockGroup.ts | 24 +++--- .../blockGroup/ContentModelBlockGroupBase.ts | 9 +- .../blockGroup/ContentModelDocument.ts | 8 +- .../blockGroup/ContentModelFormatContainer.ts | 15 ++-- .../blockGroup/ContentModelGeneralBlock.ts | 22 +++-- .../blockGroup/ContentModelListItem.ts | 15 ++-- .../blockGroup/ContentModelTableCell.ts | 19 ++-- .../lib/contentModel/common/MutableMark.ts | 86 ++++++++++++++----- .../lib/contentModel/common/MutableType.ts | 24 +++--- .../lib/contentModel/common/ReadonlyMark.ts | 29 ------- .../lib/contentModel/common/Selectable.ts | 13 ++- .../decorator/ContentModelCode.ts | 3 +- .../decorator/ContentModelLink.ts | 3 +- .../decorator/ContentModelListLevel.ts | 3 +- .../ContentModelParagraphDecorator.ts | 3 +- .../format/ContentModelWithDataset.ts | 13 ++- .../segment/ContentModelGeneralSegment.ts | 14 +-- .../segment/ContentModelSegmentBase.ts | 31 ++++++- .../lib/index.ts | 28 +++--- 21 files changed, 239 insertions(+), 142 deletions(-) delete mode 100644 packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts index 6ccf329e98c..7fef63cb17a 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlockBase.ts @@ -1,6 +1,5 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { MutableMark } from '../common/MutableMark'; +import type { MutableMark, ReadonlyMark, ShallowMutableMark } from '../common/MutableMark'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockType } from './BlockType'; import type { @@ -43,3 +42,16 @@ export interface ReadonlyContentModelBlockBase< ContentModelBlockBaseCommon, ReadonlyContentModelWithFormat, ContentModelBlockWithCache {} + +/** + * Base type of a block (Shallow mutable) + */ +export interface ShallowMutableContentModelBlockBase< + T extends ContentModelBlockType, + TFormat extends ContentModelBlockFormat = ContentModelBlockFormat, + TCacheElement extends HTMLElement = HTMLElement +> + extends ShallowMutableMark, + ContentModelBlockBaseCommon, + ContentModelWithFormat, + ContentModelBlockWithCache {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts index 449ae86076d..309808338fd 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts @@ -1,5 +1,4 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; +import type { MutableMark, ReadonlyMark } from '../common/MutableMark'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts index 9be2dd2b791..d526e962738 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroup.ts @@ -1,27 +1,27 @@ import type { ContentModelDocument, - MutableContentModelDocument, ReadonlyContentModelDocument, + ShallowMutableContentModelDocument, } from './ContentModelDocument'; import type { ContentModelFormatContainer, - MutableContentModelFormatContainer, ReadonlyContentModelFormatContainer, + ShallowMutableContentModelFormatContainer, } from './ContentModelFormatContainer'; import type { ContentModelGeneralBlock, - MutableContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, + ShallowMutableContentModelGeneralBlock, } from './ContentModelGeneralBlock'; import type { ContentModelListItem, - MutableContentModelListItem, ReadonlyContentModelListItem, + ShallowMutableContentModelListItem, } from './ContentModelListItem'; import type { ContentModelTableCell, - MutableContentModelTableCell, ReadonlyContentModelTableCell, + ShallowMutableContentModelTableCell, } from './ContentModelTableCell'; /** @@ -45,11 +45,11 @@ export type ReadonlyContentModelBlockGroup = | ReadonlyContentModelGeneralBlock; /** - * The union type of Content Model Block Group (Single level mutable) + * The union type of Content Model Block Group (Shallow mutable) */ -export type MutableContentModelBlockGroup = - | MutableContentModelDocument - | MutableContentModelFormatContainer - | MutableContentModelListItem - | MutableContentModelTableCell - | MutableContentModelGeneralBlock; +export type ShallowMutableContentModelBlockGroup = + | ShallowMutableContentModelDocument + | ShallowMutableContentModelFormatContainer + | ShallowMutableContentModelListItem + | ShallowMutableContentModelTableCell + | ShallowMutableContentModelGeneralBlock; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts index 61e72267938..3235a5e4acb 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelBlockGroupBase.ts @@ -1,6 +1,5 @@ import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; +import type { MutableMark, ReadonlyMark, ShallowMutableMark } from '../common/MutableMark'; import type { ContentModelBlock, ReadonlyContentModelBlock } from '../block/ContentModelBlock'; import type { ContentModelBlockGroupType } from './BlockGroupType'; @@ -44,12 +43,12 @@ export interface ReadonlyContentModelBlockGroupBase< } /** - * Base type of Content Model Block Group (Readonly) + * Base type of Content Model Block Group (Shallow mutable) */ -export interface MutableContentModelBlockGroupBase< +export interface ShallowMutableContentModelBlockGroupBase< T extends ContentModelBlockGroupType, TElement extends HTMLElement = HTMLElement -> extends MutableMark, ContentModelBlockGroupBaseCommon { +> extends ShallowMutableMark, ContentModelBlockGroupBaseCommon { /** * Blocks under this group */ diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts index 2571557eacb..0584de310d6 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelDocument.ts @@ -1,7 +1,7 @@ import type { ContentModelBlockGroupBase, - MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, + ShallowMutableContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { @@ -36,9 +36,9 @@ export interface ReadonlyContentModelDocument Partial> {} /** - * Content Model document entry point (Single level mutable) + * Content Model document entry point (Shallow mutable) */ -export interface MutableContentModelDocument +export interface ShallowMutableContentModelDocument extends ContentModelDocumentCommon, - MutableContentModelBlockGroupBase<'Document'>, + ShallowMutableContentModelBlockGroupBase<'Document'>, Partial> {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts index 445075ec6f9..8ebcc01c06e 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelFormatContainer.ts @@ -1,11 +1,12 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase, + ShallowMutableContentModelBlockBase, } from '../block/ContentModelBlockBase'; import type { ContentModelBlockGroupBase, - MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, + ShallowMutableContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelFormatContainerFormat } from '../format/ContentModelFormatContainerFormat'; @@ -46,9 +47,13 @@ export interface ReadonlyContentModelFormatContainer > {} /** - * Content Model of Format Container (Single level mutable) + * Content Model of Format Container (Shallow mutable) */ -export interface MutableContentModelFormatContainer +export interface ShallowMutableContentModelFormatContainer extends ContentModelFormatContainerCommon, - MutableContentModelBlockGroupBase<'FormatContainer'>, - ContentModelBlockBase<'BlockGroup', ContentModelFormatContainerFormat, HTMLElement> {} + ShallowMutableContentModelBlockGroupBase<'FormatContainer'>, + ShallowMutableContentModelBlockBase< + 'BlockGroup', + ContentModelFormatContainerFormat, + HTMLElement + > {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts index c05051a6f99..e6f577dc99b 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelGeneralBlock.ts @@ -1,15 +1,20 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase, + ShallowMutableContentModelBlockBase, } from '../block/ContentModelBlockBase'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockGroupBase, - MutableContentModelBlockGroupBase, + ShallowMutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import type { ReadonlySelectable, Selectable } from '../common/Selectable'; +import type { + ReadonlySelectable, + Selectable, + ShallowMutableSelectable, +} from '../common/Selectable'; /** * Common part of Content Model for general Block element @@ -43,10 +48,13 @@ export interface ReadonlyContentModelGeneralBlock > {} /** - * Content Model for general Block element (Single level mutable) + * Content Model for general Block element (Shallow mutable) */ -export interface MutableContentModelGeneralBlock - extends Selectable, +export interface ShallowMutableContentModelGeneralBlock + extends ShallowMutableSelectable, ContentModelGeneralBlockCommon, - MutableContentModelBlockGroupBase<'General'>, - ContentModelBlockBase<'BlockGroup', ContentModelBlockFormat & ContentModelSegmentFormat> {} + ShallowMutableContentModelBlockGroupBase<'General'>, + ShallowMutableContentModelBlockBase< + 'BlockGroup', + ContentModelBlockFormat & ContentModelSegmentFormat + > {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts index 2d965f98157..c4f8b2fbb76 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts @@ -1,11 +1,12 @@ import type { ContentModelBlockBase, ReadonlyContentModelBlockBase, + ShallowMutableContentModelBlockBase, } from '../block/ContentModelBlockBase'; import type { ContentModelBlockGroupBase, - MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, + ShallowMutableContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelListItemFormat } from '../format/ContentModelListItemFormat'; import type { @@ -52,11 +53,15 @@ export interface ReadonlyContentModelListItem } /** - * Content Model of List Item (Single level mutable) + * Content Model of List Item (Shallow mutable) */ -export interface MutableContentModelListItem - extends MutableContentModelBlockGroupBase<'ListItem', HTMLLIElement>, - ContentModelBlockBase<'BlockGroup', ContentModelListItemFormat, HTMLLIElement> { +export interface ShallowMutableContentModelListItem + extends ShallowMutableContentModelBlockGroupBase<'ListItem', HTMLLIElement>, + ShallowMutableContentModelBlockBase< + 'BlockGroup', + ContentModelListItemFormat, + HTMLLIElement + > { /** * Type of this list, either ordered or unordered */ diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts index 2dfa3eb467a..03749696b78 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelTableCell.ts @@ -1,19 +1,24 @@ import type { TableCellMetadataFormat } from '../format/metadata/TableCellMetadataFormat'; import type { ContentModelBlockGroupBase, - MutableContentModelBlockGroupBase, ReadonlyContentModelBlockGroupBase, + ShallowMutableContentModelBlockGroupBase, } from './ContentModelBlockGroupBase'; import type { ContentModelTableCellFormat } from '../format/ContentModelTableCellFormat'; import type { ContentModelWithDataset, ReadonlyContentModelWithDataset, + ShallowMutableContentModelWithDataset, } from '../format/ContentModelWithDataset'; import type { ContentModelWithFormat, ReadonlyContentModelWithFormat, } from '../format/ContentModelWithFormat'; -import type { ReadonlySelectable, Selectable } from '../common/Selectable'; +import type { + ReadonlySelectable, + Selectable, + ShallowMutableSelectable, +} from '../common/Selectable'; /** * Common part of Content Model of Table Cell @@ -56,11 +61,11 @@ export interface ReadonlyContentModelTableCell ReadonlyContentModelWithDataset {} /** - * Content Model of Table Cell (Single level mutable) + * Content Model of Table Cell (Shallow mutable) */ -export interface MutableContentModelTableCell - extends Selectable, +export interface ShallowMutableContentModelTableCell + extends ShallowMutableSelectable, ContentModelTableCellCommon, - MutableContentModelBlockGroupBase<'TableCell', HTMLTableCellElement>, + ShallowMutableContentModelBlockGroupBase<'TableCell', HTMLTableCellElement>, ContentModelWithFormat, - ContentModelWithDataset {} + ShallowMutableContentModelWithDataset {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts index ae5b46d684a..9912a147c44 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableMark.ts @@ -1,29 +1,73 @@ +// We are using a tag type to mark a content model type as mutable. + +// This is generally a workaround to https://github.com/microsoft/TypeScript/issues/13347 + +// In order to know if a block has been changed, we want to mark all blocks, blocks groups, segments and their members as readonly, +// When we want to change a block/segment/block group, we need to call a function to convert it to mutable. Inside this function +// we can make some change to the object (e.g. remove cached element if any) so later we know this object is changed. +// So that we expect there is a build time error if we assign a readonly object to a function that accepts mutable object only. +// However this does not happen today. + +// To workaround it, we manually add a hidden member (dummy) to all mutable types, and add another member with readonly array type to +// readonly types. When we assign readonly object to mutable one, compiler will fail to build since the two arrays are not matching. +// So that we can know where to fix from build time. And since the dummy value is optional, it won't break existing creator code. + +// @example +// let readonly: ReadonlyMark = {}; +// let mutable: MutableMark = {}; + +// readonly = mutable; // OK +// mutable = readonly; // Error: Type 'ReadonlyMark' is not assignable to type 'MutableMark'. + /** - * A tag type to mark a content model type as mutable. - * - * This is generally a workaround to https://github.com/microsoft/TypeScript/issues/13347 - * - * In order to know if a block has been changed, we want to mark all blocks, blocks groups, segments and their members as readonly, - * When we want to change a block/segment/block group, we need to call a function to convert it to mutable. Inside this function - * we can make some change to the object (e.g. remove cached element if any) so later we know this object is changed. - * So that we expect there is a build time error if we assign a readonly object to a function that accepts mutable object only. - * However this does not happen today. - * - * To workaround it, we manually add a hidden member (dummy) to all mutable types, and add another member with readonly array type to - * readonly types. When we assign readonly object to mutable one, compiler will fail to build since the two arrays are not matching. - * So that we can know where to fix from build time. And since the dummy value is optional, it won't break existing creator code. - * - * @example - * let readonly: ReadonlyMark = {}; - * let mutable: MutableMark = {}; - * - * readonly = mutable; // OK - * mutable = readonly; // Error: Type 'ReadonlyMark' is not assignable to type 'MutableMark'. + * Mark an object as mutable */ export type MutableMark = { /** * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build * due to this member does not exist from source type. */ - readonly dummy?: never[]; + readonly dummy1?: never[]; + + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + */ + readonly dummy2?: never[]; +}; + +/** + * Mark an object as single level mutable (child models are still readonly) + */ +export type ShallowMutableMark = { + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + */ + readonly dummy1?: never[]; + + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + * This is used for preventing assigning ShallowMutableMark to MutableMark + */ + readonly dummy2?: ReadonlyArray; +}; + +/** + * Mark an object as readonly + */ +export type ReadonlyMark = { + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + * This is used for preventing assigning ReadonlyMark to ShallowMutableMark or MutableMark + */ + readonly dummy1?: ReadonlyArray; + + /** + * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build + * due to this member does not exist from source type. + */ + readonly dummy2?: ReadonlyArray; }; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts index 5e1f46c34c9..b8b0d695314 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts @@ -5,26 +5,26 @@ import type { ReadonlyContentModelDivider, } from '../block/ContentModelDivider'; import type { - MutableContentModelDocument, + ShallowMutableContentModelDocument, ReadonlyContentModelDocument, } from '../blockGroup/ContentModelDocument'; import type { ContentModelEntity } from '../entity/ContentModelEntity'; import type { - MutableContentModelFormatContainer, + ShallowMutableContentModelFormatContainer, ReadonlyContentModelFormatContainer, } from '../blockGroup/ContentModelFormatContainer'; import type { - MutableContentModelGeneralBlock, + ShallowMutableContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, } from '../blockGroup/ContentModelGeneralBlock'; import type { - MutableContentModelGeneralSegment, + ShallowMutableContentModelGeneralSegment, ReadonlyContentModelGeneralSegment, } from '../segment/ContentModelGeneralSegment'; import type { ContentModelImage, ReadonlyContentModelImage } from '../segment/ContentModelImage'; import type { ContentModelLink, ReadonlyContentModelLink } from '../decorator/ContentModelLink'; import type { - MutableContentModelListItem, + ShallowMutableContentModelListItem, ReadonlyContentModelListItem, } from '../blockGroup/ContentModelListItem'; import type { @@ -45,7 +45,7 @@ import type { } from '../segment/ContentModelSelectionMarker'; import type { ContentModelTable, ReadonlyContentModelTable } from '../block/ContentModelTable'; import type { - MutableContentModelTableCell, + ShallowMutableContentModelTableCell, ReadonlyContentModelTableCell, } from '../blockGroup/ContentModelTableCell'; import type { @@ -58,7 +58,7 @@ import type { ContentModelText, ReadonlyContentModelText } from '../segment/Cont * Get mutable type from its related readonly type */ export type MutableType = T extends ReadonlyContentModelGeneralSegment - ? MutableContentModelGeneralSegment + ? ShallowMutableContentModelGeneralSegment : T extends ReadonlyContentModelSelectionMarker ? ContentModelSelectionMarker : T extends ReadonlyContentModelImage @@ -76,19 +76,19 @@ export type MutableType = T extends ReadonlyContentModelGeneralSegment : T extends ReadonlyContentModelTableRow ? ContentModelTableRow : T extends ReadonlyContentModelTableCell - ? MutableContentModelTableCell + ? ShallowMutableContentModelTableCell : T extends ReadonlyContentModelFormatContainer - ? MutableContentModelFormatContainer + ? ShallowMutableContentModelFormatContainer : T extends ReadonlyContentModelListItem - ? MutableContentModelListItem + ? ShallowMutableContentModelListItem : T extends ReadonlyContentModelListLevel ? ContentModelListLevel : T extends ReadonlyContentModelDivider ? ContentModelDivider : T extends ReadonlyContentModelDocument - ? MutableContentModelDocument + ? ShallowMutableContentModelDocument : T extends ReadonlyContentModelGeneralBlock - ? MutableContentModelGeneralBlock + ? ShallowMutableContentModelGeneralBlock : T extends ReadonlyContentModelParagraphDecorator ? ContentModelParagraphDecorator : T extends ReadonlyContentModelLink diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts deleted file mode 100644 index e4568edf284..00000000000 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/ReadonlyMark.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * A tag type to mark a content model type as readonly. - * - * This is generally a workaround to https://github.com/microsoft/TypeScript/issues/13347 - * - * In order to know if a block has been changed, we want to mark all blocks, blocks groups, segments and their members as readonly, - * When we want to change a block/segment/block group, we need to call a function to convert it to mutable. Inside this function - * we can make some change to the object (e.g. remove cached element if any) so later we know this object is changed. - * So that we expect there is a build time error if we assign a readonly object to a function that accepts mutable object only. - * However this does not happen today. - * - * To workaround it, we manually add a hidden member (dummy) to all mutable types, and add another member with readonly array type to - * readonly types. When we assign readonly object to mutable one, compiler will fail to build since the two arrays are not matching. - * So that we can know where to fix from build time. And since the dummy value is optional, it won't break existing creator code. - * - * @example - * let readonly: ReadonlyMark = {}; - * let mutable: MutableMark = {}; - * - * readonly = mutable; // OK - * mutable = readonly; // Error: Type 'ReadonlyMark' is not assignable to type 'MutableMark'. - */ -export type ReadonlyMark = { - /** - * The mutable marker to mark an object as mutable. When assign readonly object to a mutable type, compile will fail to build - * due to this member does not exist from source type. - */ - readonly dummy?: ReadonlyArray; -}; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts index 4ceef92d5e8..1b498d79f2b 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts @@ -1,5 +1,4 @@ -import type { MutableMark } from './MutableMark'; -import type { ReadonlyMark } from './ReadonlyMark'; +import type { MutableMark, ReadonlyMark, ShallowMutableMark } from '../common/MutableMark'; /** * Represents a selectable Content Model object @@ -20,3 +19,13 @@ export interface ReadonlySelectable extends ReadonlyMark { */ readonly isSelected?: boolean; } + +/** + * Represents a selectable Content Model object (Shallow mutable) + */ +export interface ShallowMutableSelectable extends ShallowMutableMark { + /** + * Whether this model object is selected + */ + readonly isSelected?: boolean; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts index 353cdc61fee..d6823d9b496 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelCode.ts @@ -1,5 +1,4 @@ -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark, ReadonlyMark } from '../common/MutableMark'; import type { ContentModelCodeFormat } from '../format/ContentModelCodeFormat'; import type { ContentModelWithFormat, diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts index 18856a29424..4aa98459542 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelLink.ts @@ -1,5 +1,4 @@ -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark, ReadonlyMark } from '../common/MutableMark'; import type { ContentModelHyperLinkFormat } from '../format/ContentModelHyperLinkFormat'; import type { ContentModelWithDataset, diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts index ccb346e9845..585b0dce4ca 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelListLevel.ts @@ -1,6 +1,5 @@ import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark, ReadonlyMark } from '../common/MutableMark'; import type { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; import type { ContentModelWithDataset, diff --git a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts index b838b7f01bc..7eeebcc19b4 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/decorator/ContentModelParagraphDecorator.ts @@ -1,5 +1,4 @@ -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark, ReadonlyMark } from '../common/MutableMark'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { ContentModelWithFormat, diff --git a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts index 7de91cc8062..902f4dd2d5e 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/format/ContentModelWithDataset.ts @@ -1,5 +1,4 @@ -import type { MutableMark } from '../common/MutableMark'; -import type { ReadonlyMark } from '../common/ReadonlyMark'; +import type { MutableMark, ReadonlyMark, ShallowMutableMark } from '../common/MutableMark'; import type { DatasetFormat, ReadonlyDatasetFormat } from './metadata/DatasetFormat'; /** @@ -21,3 +20,13 @@ export type ReadonlyContentModelWithDataset = ReadonlyMark & { */ readonly dataset: ReadonlyDatasetFormat; }; + +/** + * Represents base format of an element that supports dataset and/or metadata (Readonly) + */ +export type ShallowMutableContentModelWithDataset = ShallowMutableMark & { + /** + * dataset of this element + */ + dataset: DatasetFormat; +}; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts index 078776b69c2..3bfded832e0 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelGeneralSegment.ts @@ -1,12 +1,13 @@ import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelGeneralBlock, - MutableContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, + ShallowMutableContentModelGeneralBlock, } from '../blockGroup/ContentModelGeneralBlock'; import type { ContentModelSegmentBase, ReadonlyContentModelSegmentBase, + ShallowMutableContentModelSegmentBase, } from './ContentModelSegmentBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -28,8 +29,11 @@ export interface ReadonlyContentModelGeneralSegment > {} /** - * Content Model of general Segment (Single level mutable) + * Content Model of general Segment (Shallow mutable) */ -export interface MutableContentModelGeneralSegment - extends MutableContentModelGeneralBlock, - ContentModelSegmentBase<'General', ContentModelBlockFormat & ContentModelSegmentFormat> {} +export interface ShallowMutableContentModelGeneralSegment + extends ShallowMutableContentModelGeneralBlock, + ShallowMutableContentModelSegmentBase< + 'General', + ContentModelBlockFormat & ContentModelSegmentFormat + > {} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts index 4c6f2636a25..d8bea3bf4c1 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts @@ -1,5 +1,4 @@ -import type { ReadonlyMark } from '../common/ReadonlyMark'; -import type { MutableMark } from '../common/MutableMark'; +import type { MutableMark, ReadonlyMark, ShallowMutableMark } from '../common/MutableMark'; import type { ContentModelCode, ReadonlyContentModelCode } from '../decorator/ContentModelCode'; import type { ContentModelLink, ReadonlyContentModelLink } from '../decorator/ContentModelLink'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -8,7 +7,11 @@ import type { ContentModelWithFormat, ReadonlyContentModelWithFormat, } from '../format/ContentModelWithFormat'; -import type { ReadonlySelectable, Selectable } from '../common/Selectable'; +import type { + ReadonlySelectable, + Selectable, + ShallowMutableSelectable, +} from '../common/Selectable'; /** * Common part of base type of Content Model Segment @@ -63,3 +66,25 @@ export interface ReadonlyContentModelSegmentBase< */ readonly code?: ReadonlyContentModelCode; } + +/** + * Base type of Content Model Segment (Shallow mutable) + */ +export interface ShallowMutableContentModelSegmentBase< + T extends ContentModelSegmentType, + TFormat extends ContentModelSegmentFormat = ContentModelSegmentFormat +> + extends ShallowMutableMark, + ShallowMutableSelectable, + ContentModelWithFormat, + ContentModelSegmentBaseCommon { + /** + * Hyperlink info + */ + readonly link?: ReadonlyContentModelLink; + + /** + * Code info + */ + readonly code?: ReadonlyContentModelCode; +} diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 975f5b4dd7e..5510fe6d6f7 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -7,6 +7,7 @@ export { ContentModelTableFormat } from './contentModel/format/ContentModelTable export { ContentModelWithDataset, ReadonlyContentModelWithDataset, + ShallowMutableContentModelWithDataset, } from './contentModel/format/ContentModelWithDataset'; export { ContentModelBlockFormat } from './contentModel/format/ContentModelBlockFormat'; export { ContentModelTableCellFormat } from './contentModel/format/ContentModelTableCellFormat'; @@ -117,6 +118,7 @@ export { ContentModelBlockBase, ContentModelBlockBaseCommon, ReadonlyContentModelBlockBase, + ShallowMutableContentModelBlockBase, } from './contentModel/block/ContentModelBlockBase'; export { ContentModelBlockWithCache } from './contentModel/common/ContentModelBlockWithCache'; export { @@ -131,48 +133,48 @@ export { ContentModelDocument, ContentModelDocumentCommon, ReadonlyContentModelDocument, - MutableContentModelDocument, + ShallowMutableContentModelDocument, } from './contentModel/blockGroup/ContentModelDocument'; export { ContentModelBlockGroupBase, ContentModelBlockGroupBaseCommon, ReadonlyContentModelBlockGroupBase, - MutableContentModelBlockGroupBase, + ShallowMutableContentModelBlockGroupBase, } from './contentModel/blockGroup/ContentModelBlockGroupBase'; export { ContentModelFormatContainer, ContentModelFormatContainerCommon, ReadonlyContentModelFormatContainer, - MutableContentModelFormatContainer, + ShallowMutableContentModelFormatContainer, } from './contentModel/blockGroup/ContentModelFormatContainer'; export { ContentModelGeneralBlock, ContentModelGeneralBlockCommon, ReadonlyContentModelGeneralBlock, - MutableContentModelGeneralBlock, + ShallowMutableContentModelGeneralBlock, } from './contentModel/blockGroup/ContentModelGeneralBlock'; export { ContentModelListItem, ReadonlyContentModelListItem, - MutableContentModelListItem, + ShallowMutableContentModelListItem, } from './contentModel/blockGroup/ContentModelListItem'; export { ContentModelTableCell, ContentModelTableCellCommon, ReadonlyContentModelTableCell, - MutableContentModelTableCell, + ShallowMutableContentModelTableCell, } from './contentModel/blockGroup/ContentModelTableCell'; export { ContentModelBlockGroup, ReadonlyContentModelBlockGroup, - MutableContentModelBlockGroup, + ShallowMutableContentModelBlockGroup, } from './contentModel/blockGroup/ContentModelBlockGroup'; export { ContentModelBr, ReadonlyContentModelBr } from './contentModel/segment/ContentModelBr'; export { ContentModelGeneralSegment, ReadonlyContentModelGeneralSegment, - MutableContentModelGeneralSegment, + ShallowMutableContentModelGeneralSegment, } from './contentModel/segment/ContentModelGeneralSegment'; export { ContentModelImage, @@ -192,6 +194,7 @@ export { ContentModelSegmentBase, ContentModelSegmentBaseCommon, ReadonlyContentModelSegmentBase, + ShallowMutableContentModelSegmentBase, } from './contentModel/segment/ContentModelSegmentBase'; export { ContentModelSegment, @@ -221,9 +224,12 @@ export { ReadonlyContentModelListLevel, } from './contentModel/decorator/ContentModelListLevel'; -export { Selectable, ReadonlySelectable } from './contentModel/common/Selectable'; -export { ReadonlyMark } from './contentModel/common/ReadonlyMark'; -export { MutableMark } from './contentModel/common/MutableMark'; +export { + Selectable, + ReadonlySelectable, + ShallowMutableSelectable, +} from './contentModel/common/Selectable'; +export { MutableMark, ShallowMutableMark, ReadonlyMark } from './contentModel/common/MutableMark'; export { MutableType } from './contentModel/common/MutableType'; export { From e14e506833fd578726641fa76829522142bb2c99 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 14:29:26 -0700 Subject: [PATCH 15/66] improve --- .../lib/contentModel/common/Selectable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts index 1b498d79f2b..d9aa25b08e8 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/Selectable.ts @@ -27,5 +27,5 @@ export interface ShallowMutableSelectable extends ShallowMutableMark { /** * Whether this model object is selected */ - readonly isSelected?: boolean; + isSelected?: boolean; } From 9a2d449f770c620502d087615280ec69811805cd Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 14:36:17 -0700 Subject: [PATCH 16/66] Improve --- .../lib/contentModel/blockGroup/ContentModelListItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts index c4f8b2fbb76..42029807129 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/blockGroup/ContentModelListItem.ts @@ -65,7 +65,7 @@ export interface ShallowMutableContentModelListItem /** * Type of this list, either ordered or unordered */ - readonly levels: ReadonlyArray; + levels: ContentModelListLevel[]; /** * A dummy segment to hold format of this list item From 719f5ed183a65f1f1f22495cf22ac694f9317466 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 14 May 2024 15:07:36 -0700 Subject: [PATCH 17/66] improve --- .../lib/contentModel/segment/ContentModelSegmentBase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts index d8bea3bf4c1..5f169c3eac7 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegmentBase.ts @@ -81,10 +81,10 @@ export interface ShallowMutableContentModelSegmentBase< /** * Hyperlink info */ - readonly link?: ReadonlyContentModelLink; + link?: ContentModelLink; /** * Code info */ - readonly code?: ReadonlyContentModelCode; + code?: ContentModelCode; } From caf7ef1d0d0f3b72ac88130130f061b49499bc8c Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 15 May 2024 16:39:02 -0700 Subject: [PATCH 18/66] improve --- .../contentModel/block/ContentModelBlock.ts | 21 ++++++++++++- .../contentModel/block/ContentModelTable.ts | 30 +++++++++++++++++-- .../block/ContentModelTableRow.ts | 20 +++++++++++-- .../lib/contentModel/common/MutableType.ts | 7 +++-- .../lib/index.ts | 3 ++ 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts index 2422de45cd5..3ae77cedd1c 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts @@ -3,17 +3,24 @@ import type { ContentModelEntity } from '../entity/ContentModelEntity'; import type { ContentModelFormatContainer, ReadonlyContentModelFormatContainer, + ShallowMutableContentModelFormatContainer, } from '../blockGroup/ContentModelFormatContainer'; import type { ContentModelGeneralBlock, ReadonlyContentModelGeneralBlock, + ShallowMutableContentModelGeneralBlock, } from '../blockGroup/ContentModelGeneralBlock'; import type { ContentModelListItem, ReadonlyContentModelListItem, + ShallowMutableContentModelListItem, } from '../blockGroup/ContentModelListItem'; import type { ContentModelParagraph, ReadonlyContentModelParagraph } from './ContentModelParagraph'; -import type { ContentModelTable, ReadonlyContentModelTable } from './ContentModelTable'; +import type { + ContentModelTable, + ReadonlyContentModelTable, + ShallowMutableContentModelTable, +} from './ContentModelTable'; /** * A union type of Content Model Block @@ -38,3 +45,15 @@ export type ReadonlyContentModelBlock = | ReadonlyContentModelParagraph | ContentModelEntity | ReadonlyContentModelDivider; + +/** + * A union type of Content Model Block (Shallow mutable) + */ +export type ShallowMutableContentModelBlock = + | ShallowMutableContentModelFormatContainer + | ShallowMutableContentModelListItem + | ShallowMutableContentModelGeneralBlock + | ShallowMutableContentModelTable + | ContentModelParagraph + | ContentModelEntity + | ContentModelDivider; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts index b13dfeb8975..741fd7f8abc 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTable.ts @@ -1,9 +1,18 @@ -import type { ContentModelBlockBase, ReadonlyContentModelBlockBase } from './ContentModelBlockBase'; +import type { + ContentModelBlockBase, + ReadonlyContentModelBlockBase, + ShallowMutableContentModelBlockBase, +} from './ContentModelBlockBase'; import type { ContentModelTableFormat } from '../format/ContentModelTableFormat'; -import type { ContentModelTableRow, ReadonlyContentModelTableRow } from './ContentModelTableRow'; +import type { + ContentModelTableRow, + ReadonlyContentModelTableRow, + ShallowMutableContentModelTableRow, +} from './ContentModelTableRow'; import type { ContentModelWithDataset, ReadonlyContentModelWithDataset, + ShallowMutableContentModelWithDataset, } from '../format/ContentModelWithDataset'; import type { TableMetadataFormat } from '../format/metadata/TableMetadataFormat'; @@ -40,3 +49,20 @@ export interface ReadonlyContentModelTable */ readonly rows: ReadonlyArray; } + +/** + * Content Model of Table (Shallow mutable) + */ +export interface ShallowMutableContentModelTable + extends ShallowMutableContentModelBlockBase<'Table', ContentModelTableFormat, HTMLTableElement>, + ShallowMutableContentModelWithDataset { + /** + * Widths of each column + */ + widths: number[]; + + /** + * Cells of this table + */ + rows: ShallowMutableContentModelTableRow[]; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts index 309808338fd..3dd881f7309 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelTableRow.ts @@ -1,4 +1,4 @@ -import type { MutableMark, ReadonlyMark } from '../common/MutableMark'; +import type { MutableMark, ReadonlyMark, ShallowMutableMark } from '../common/MutableMark'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import type { ContentModelBlockWithCache } from '../common/ContentModelBlockWithCache'; import type { @@ -39,11 +39,25 @@ export interface ContentModelTableRow */ export interface ReadonlyContentModelTableRow extends ReadonlyMark, + Readonly, ContentModelBlockWithCache, - ReadonlyContentModelWithFormat, - Readonly { + ReadonlyContentModelWithFormat { /** * Cells of this table */ readonly cells: ReadonlyArray; } + +/** + * Content Model of Table (Readonly) + */ +export interface ShallowMutableContentModelTableRow + extends ShallowMutableMark, + ContentModelTableRowCommon, + ContentModelBlockWithCache, + ContentModelWithFormat { + /** + * Cells of this table + */ + cells: ReadonlyContentModelTableCell[]; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts index b8b0d695314..72477a13d4a 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/common/MutableType.ts @@ -43,7 +43,10 @@ import type { ContentModelSelectionMarker, ReadonlyContentModelSelectionMarker, } from '../segment/ContentModelSelectionMarker'; -import type { ContentModelTable, ReadonlyContentModelTable } from '../block/ContentModelTable'; +import type { + ReadonlyContentModelTable, + ShallowMutableContentModelTable, +} from '../block/ContentModelTable'; import type { ShallowMutableContentModelTableCell, ReadonlyContentModelTableCell, @@ -72,7 +75,7 @@ export type MutableType = T extends ReadonlyContentModelGeneralSegment : T extends ReadonlyContentModelParagraph ? ContentModelParagraph : T extends ReadonlyContentModelTable - ? ContentModelTable + ? ShallowMutableContentModelTable : T extends ReadonlyContentModelTableRow ? ContentModelTableRow : T extends ReadonlyContentModelTableCell diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 5510fe6d6f7..242af3ef074 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -99,6 +99,7 @@ export { ExportContentMode } from './enum/ExportContentMode'; export { ContentModelBlock, ReadonlyContentModelBlock, + ShallowMutableContentModelBlock, } from './contentModel/block/ContentModelBlock'; export { ContentModelParagraph, @@ -108,6 +109,7 @@ export { export { ContentModelTable, ReadonlyContentModelTable, + ShallowMutableContentModelTable, } from './contentModel/block/ContentModelTable'; export { ContentModelDivider, @@ -125,6 +127,7 @@ export { ContentModelTableRow, ContentModelTableRowCommon, ReadonlyContentModelTableRow, + ShallowMutableContentModelTableRow, } from './contentModel/block/ContentModelTableRow'; export { ContentModelEntity } from './contentModel/entity/ContentModelEntity'; From 0230a26f2b27c824e2cfdbddbc906b5818d0eb18 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 10:32:03 -0700 Subject: [PATCH 19/66] add test --- .../test/modelApi/common/mutateTest.ts | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 packages/roosterjs-content-model-dom/test/modelApi/common/mutateTest.ts diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/mutateTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/mutateTest.ts new file mode 100644 index 00000000000..d4b31594cc9 --- /dev/null +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/mutateTest.ts @@ -0,0 +1,235 @@ +import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createListItem } from '../../../lib/modelApi/creators/createListItem'; +import { createListLevel } from '../../../lib/modelApi/creators/createListLevel'; +import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; +import { createTable } from '../../../lib/modelApi/creators/createTable'; +import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; +import { createText } from '../../../lib/modelApi/creators/createText'; +import { mutateBlock, mutateSegment, mutateSegments } from '../../../lib/modelApi/common/mutate'; + +const mockedCache = 'CACHE' as any; + +describe('mutate', () => { + it('mutate a block without cache', () => { + const block = {} as any; + + const mutatedBlock = mutateBlock(block); + + expect(mutatedBlock).toBe(block); + expect(mutatedBlock).toEqual({} as any); + }); + + it('mutate a block with cache', () => { + const block = {} as any; + + block.cachedElement = mockedCache; + + const mutatedBlock = mutateBlock(block); + + expect(mutatedBlock).toBe(block); + expect(mutatedBlock).toEqual({} as any); + }); + + it('mutate a block group with cache', () => { + const doc = createContentModelDocument(); + const para = createParagraph(); + + doc.cachedElement = mockedCache; + para.cachedElement = mockedCache; + + doc.blocks.push(para); + + const mutatedBlock = mutateBlock(doc); + + expect(mutatedBlock).toBe(doc); + expect(mutatedBlock).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [], + format: {}, + cachedElement: mockedCache, + }, + ], + } as any); + }); + + it('mutate a table', () => { + const table = createTable(1); + const cell = createTableCell(); + const para = createParagraph(); + + table.cachedElement = mockedCache; + table.rows[0].cachedElement = mockedCache; + cell.cachedElement = mockedCache; + para.cachedElement = mockedCache; + + cell.blocks.push(para); + table.rows[0].cells.push(cell); + + const mutatedBlock = mutateBlock(table); + + expect(mutatedBlock).toBe(table); + expect(mutatedBlock).toEqual({ + blockType: 'Table', + rows: [ + { + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [], + format: {}, + cachedElement: mockedCache, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + cachedElement: mockedCache, + }, + ], + height: 0, + format: {}, + }, + ], + format: {}, + widths: [], + dataset: {}, + } as any); + }); + + it('mutate a list', () => { + const level = createListLevel('OL'); + const list = createListItem([level]); + const para = createParagraph(); + + level.cachedElement = mockedCache; + list.cachedElement = mockedCache; + para.cachedElement = mockedCache; + + list.blocks.push(para); + + const mutatedBlock = mutateBlock(list); + + expect(mutatedBlock).toBe(list); + expect(mutatedBlock).toEqual({ + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [], + format: {}, + cachedElement: mockedCache, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + } as any); + }); +}); + +describe('mutateSegments', () => { + it('empty paragraph', () => { + const para = createParagraph(); + + para.cachedElement = mockedCache; + + const result = mutateSegments(para, []); + + expect(result).toEqual([para, [], []]); + expect(result[0].cachedElement).toBeUndefined(); + }); + + it('Paragraph with correct segments', () => { + const para = createParagraph(); + + para.cachedElement = mockedCache; + + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + + para.segments.push(text1, text2, text3, text4); + + const result = mutateSegments(para, [text2, text4]); + + expect(result).toEqual([para, [text2, text4], [1, 3]]); + expect(result[0].cachedElement).toBeUndefined(); + }); + + it('Paragraph with incorrect segments', () => { + const para = createParagraph(); + + para.cachedElement = mockedCache; + + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + + para.segments.push(text1, text2, text3); + + const result = mutateSegments(para, [text2, text4]); + + expect(result).toEqual([para, [text2], [1]]); + expect(result[0].cachedElement).toBeUndefined(); + }); +}); + +describe('mutateSegment', () => { + let callbackSpy: jasmine.Spy; + + beforeEach(() => { + callbackSpy = jasmine.createSpy('callback'); + }); + + it('Paragraph with correct segment', () => { + const para = createParagraph(); + + para.cachedElement = mockedCache; + + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + + para.segments.push(text1, text2, text3); + + const result = mutateSegment(para, text2, callbackSpy); + + expect(result).toEqual([para, text2, 1]); + expect(result[0].cachedElement).toBeUndefined(); + expect(callbackSpy).toHaveBeenCalledTimes(1); + expect(callbackSpy).toHaveBeenCalledWith(text2, para, 1); + }); + + it('Paragraph with incorrect segment', () => { + const para = createParagraph(); + + para.cachedElement = mockedCache; + + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + + para.segments.push(text1, text3); + + const result = mutateSegment(para, text2, callbackSpy); + + expect(result).toEqual([para, null, -1]); + expect(result[0].cachedElement).toBeUndefined(); + expect(callbackSpy).toHaveBeenCalledTimes(0); + }); +}); From 46fbe45625a3467c97875eda223cd4ce004244b8 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 15:34:38 -0700 Subject: [PATCH 20/66] Readonly types step 2 --- .../components/format/MetadataView.tsx | 10 +- .../list/findListItemsInSameThread.ts | 43 +++++++-- .../createContentModelTest.ts | 5 +- .../test/editor/EditorTest.ts | 4 +- .../roosterjs-content-model-dom/lib/index.ts | 17 +++- .../lib/modelApi/editing/cloneModel.ts | 70 ++++++++++---- .../getClosestAncestorBlockGroupIndex.ts | 3 +- .../modelApi/metadata/updateImageMetadata.ts | 16 +++- .../modelApi/metadata/updateListMetadata.ts | 17 +++- .../lib/modelApi/metadata/updateMetadata.ts | 41 +++++--- .../metadata/updateTableCellMetadata.ts | 20 +++- .../modelApi/metadata/updateTableMetadata.ts | 18 +++- .../modelApi/selection/collectSelections.ts | 90 ++++++++++++++++-- .../modelApi/selection/iterateSelections.ts | 43 +++++++-- .../modelApi/typeCheck/isBlockGroupOfType.ts | 25 +++++ .../modelApi/typeCheck/isGeneralSegment.ts | 14 +++ .../editing/retrieveModelFormatStateTest.ts | 44 ++++----- .../metadata/updateImageMetadataTest.ts | 72 +++++++++++++- .../metadata/updateListMetadataTest.ts | 45 ++++++++- .../modelApi/metadata/updateMetadataTest.ts | 95 ++++++++++++++++++- .../metadata/updateTableCellMetadataTest.ts | 58 ++++++++++- .../metadata/updateTableMetadataTest.ts | 77 ++++++++++++++- .../lib/index.ts | 8 +- .../lib/parameter/IterateSelectionsOption.ts | 35 ++++++- .../lib/parameter/OperationalBlocks.ts | 30 +++++- .../lib/parameter/TypeOfBlockGroup.ts | 16 +++- .../lib/selection/TableSelectionContext.ts | 30 +++++- 27 files changed, 822 insertions(+), 124 deletions(-) diff --git a/demo/scripts/controlsV2/sidePane/contentModel/components/format/MetadataView.tsx b/demo/scripts/controlsV2/sidePane/contentModel/components/format/MetadataView.tsx index 40618e31f1c..493f20902f9 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/components/format/MetadataView.tsx +++ b/demo/scripts/controlsV2/sidePane/contentModel/components/format/MetadataView.tsx @@ -1,13 +1,19 @@ import * as React from 'react'; -import { ContentModelWithDataset } from 'roosterjs-content-model-types'; import { FormatRenderer } from './utils/FormatRenderer'; +import { + ContentModelWithDataset, + ShallowMutableContentModelWithDataset, +} from 'roosterjs-content-model-types'; const styles = require('./FormatView.scss'); export function MetadataView(props: { model: ContentModelWithDataset; renderers: FormatRenderer[]; - updater: (model: ContentModelWithDataset, callback: (format: T | null) => T | null) => void; + updater: ( + model: ShallowMutableContentModelWithDataset, + callback: (format: T | null) => T | null + ) => void; }) { const { model, renderers, updater } = props; const metadata = React.useRef(null); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts index 3ad7824f584..14b9f4ff99f 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts @@ -1,14 +1,34 @@ -import type { ContentModelBlockGroup, ContentModelListItem } from 'roosterjs-content-model-types'; +import type { + ContentModelBlockGroup, + ContentModelListItem, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelListItem, +} from 'roosterjs-content-model-types'; /** + * Search for all list items in the same thread as the current list item * @param model The content model * @param currentItem The current list item - * Search for all list items in the same thread as the current list item */ export function findListItemsInSameThread( group: ContentModelBlockGroup, currentItem: ContentModelListItem -): ContentModelListItem[] { +): ContentModelListItem[]; + +/** + * Search for all list items in the same thread as the current list item (Readonly) + * @param model The content model + * @param currentItem The current list item + */ +export function findListItemsInSameThread( + group: ReadonlyContentModelBlockGroup, + currentItem: ReadonlyContentModelListItem +): ReadonlyContentModelListItem[]; + +export function findListItemsInSameThread( + group: ReadonlyContentModelBlockGroup, + currentItem: ReadonlyContentModelListItem +): ReadonlyContentModelListItem[] { const items: (ContentModelListItem | null)[] = []; findListItems(group, items); @@ -16,7 +36,10 @@ export function findListItemsInSameThread( return filterListItems(items, currentItem); } -function findListItems(group: ContentModelBlockGroup, result: (ContentModelListItem | null)[]) { +function findListItems( + group: ReadonlyContentModelBlockGroup, + result: (ReadonlyContentModelListItem | null)[] +) { group.blocks.forEach(block => { switch (block.blockType) { case 'BlockGroup': @@ -56,7 +79,7 @@ function findListItems(group: ContentModelBlockGroup, result: (ContentModelListI }); } -function pushNullIfNecessary(result: (ContentModelListItem | null)[]) { +function pushNullIfNecessary(result: (ReadonlyContentModelListItem | null)[]) { const last = result[result.length - 1]; if (!last || last !== null) { @@ -65,10 +88,10 @@ function pushNullIfNecessary(result: (ContentModelListItem | null)[]) { } function filterListItems( - items: (ContentModelListItem | null)[], - currentItem: ContentModelListItem + items: (ReadonlyContentModelListItem | null)[], + currentItem: ReadonlyContentModelListItem ) { - const result: ContentModelListItem[] = []; + const result: ReadonlyContentModelListItem[] = []; const currentIndex = items.indexOf(currentItem); const levelLength = currentItem.levels.length; const isOrderedList = currentItem.levels[levelLength - 1]?.listType == 'OL'; @@ -131,7 +154,7 @@ function filterListItems( } function areListTypesCompatible( - listItems: (ContentModelListItem | null)[], + listItems: (ReadonlyContentModelListItem | null)[], currentIndex: number, compareToIndex: number ): boolean { @@ -146,7 +169,7 @@ function areListTypesCompatible( ); } -function hasStartNumberOverride(item: ContentModelListItem, levelLength: number): boolean { +function hasStartNumberOverride(item: ReadonlyContentModelListItem, levelLength: number): boolean { return item.levels .slice(0, levelLength) .some(level => level.format.startNumberOverride !== undefined); diff --git a/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts index 0b0ec01a507..bb223138b34 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/createContentModel/createContentModelTest.ts @@ -4,6 +4,7 @@ import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/d import * as updateCachedSelection from '../../../lib/corePlugin/cache/updateCachedSelection'; import { createContentModel } from '../../../lib/coreApi/createContentModel/createContentModel'; import { + ContentModelDocument, DomToModelContext, DomToModelOptionForCreateModel, EditorCore, @@ -362,7 +363,9 @@ describe('createContentModel and cache management', () => { }, } as any; - cloneModelSpy = spyOn(cloneModel, 'cloneModel').and.callFake(x => x); + cloneModelSpy = spyOn(cloneModel, 'cloneModel').and.callFake( + x => x as ContentModelDocument + ); spyOn(domToContentModel, 'domToContentModel').and.returnValue(mockedNewModel); diff --git a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts index 042f2d0c9db..cb415755e7c 100644 --- a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts @@ -278,7 +278,9 @@ describe('Editor', () => { mockedModel ); - const cloneModelSpy = spyOn(cloneModel, 'cloneModel').and.callFake(x => x); + const cloneModelSpy = spyOn(cloneModel, 'cloneModel').and.callFake( + x => x as ContentModelDocument + ); const model = editor.getContentModelCopy('clean'); expect(model).toBe(mockedModel); diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index d871e3d46f2..519aa52801b 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -15,7 +15,7 @@ export { areSameFormats } from './domToModel/utils/areSameFormats'; export { isBlockElement } from './domToModel/utils/isBlockElement'; export { buildSelectionMarker } from './domToModel/utils/buildSelectionMarker'; -export { updateMetadata, hasMetadata } from './modelApi/metadata/updateMetadata'; +export { updateMetadata, getMetadata, hasMetadata } from './modelApi/metadata/updateMetadata'; export { isNodeOfType } from './domUtils/isNodeOfType'; export { isElementOfType } from './domUtils/isElementOfType'; export { getObjectKeys } from './domUtils/getObjectKeys'; @@ -130,10 +130,17 @@ export { retrieveModelFormatState } from './modelApi/editing/retrieveModelFormat export { getListStyleTypeFromString } from './modelApi/editing/getListStyleTypeFromString'; export { getSegmentTextFormat } from './modelApi/editing/getSegmentTextFormat'; -export { updateImageMetadata } from './modelApi/metadata/updateImageMetadata'; -export { updateTableCellMetadata } from './modelApi/metadata/updateTableCellMetadata'; -export { updateTableMetadata } from './modelApi/metadata/updateTableMetadata'; -export { updateListMetadata, ListMetadataDefinition } from './modelApi/metadata/updateListMetadata'; +export { updateImageMetadata, getImageMetadata } from './modelApi/metadata/updateImageMetadata'; +export { + updateTableCellMetadata, + getTableCellMetadata, +} from './modelApi/metadata/updateTableCellMetadata'; +export { updateTableMetadata, getTableMetadata } from './modelApi/metadata/updateTableMetadata'; +export { + updateListMetadata, + getListMetadata, + ListMetadataDefinition, +} from './modelApi/metadata/updateListMetadata'; export { ChangeSource } from './constants/ChangeSource'; export { BulletListType } from './constants/BulletListType'; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts index 95ee0cee0ed..b874b04a973 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/cloneModel.ts @@ -26,6 +26,26 @@ import type { ContentModelTableRow, ContentModelListLevel, CloneModelOptions, + ReadonlyContentModelDocument, + ReadonlyContentModelBlockGroupBase, + ReadonlyContentModelBlock, + ReadonlyContentModelFormatContainer, + ReadonlyContentModelBlockBase, + ReadonlyContentModelGeneralBlock, + ReadonlyContentModelListItem, + ReadonlyContentModelSelectionMarker, + ReadonlyContentModelSegmentBase, + ReadonlyContentModelWithDataset, + ReadonlyContentModelDivider, + ReadonlyContentModelListLevel, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, + ReadonlyContentModelTable, + ReadonlyContentModelTableRow, + ReadonlyContentModelTableCell, + ReadonlyContentModelGeneralSegment, + ReadonlyContentModelImage, + ReadonlyContentModelText, } from 'roosterjs-content-model-types'; /** @@ -34,7 +54,7 @@ import type { * @param options @optional Options to specify customize the clone behavior */ export function cloneModel( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, options?: CloneModelOptions ): ContentModelDocument { const newModel: ContentModelDocument = cloneBlockGroupBase(model, options || {}); @@ -46,7 +66,10 @@ export function cloneModel( return newModel; } -function cloneBlock(block: ContentModelBlock, options: CloneModelOptions): ContentModelBlock { +function cloneBlock( + block: ReadonlyContentModelBlock, + options: CloneModelOptions +): ContentModelBlock { switch (block.blockType) { case 'BlockGroup': switch (block.blockGroupType) { @@ -70,7 +93,7 @@ function cloneBlock(block: ContentModelBlock, options: CloneModelOptions): Conte } function cloneSegment( - segment: ContentModelSegment, + segment: ReadonlyContentModelSegment, options: CloneModelOptions ): ContentModelSegment { switch (segment.segmentType) { @@ -97,14 +120,16 @@ function cloneModelWithFormat( }; } -function cloneModelWithDataset(model: ContentModelWithDataset): ContentModelWithDataset { +function cloneModelWithDataset( + model: ReadonlyContentModelWithDataset +): ContentModelWithDataset { return { dataset: Object.assign({}, model.dataset), }; } function cloneBlockBase( - block: ContentModelBlockBase + block: ReadonlyContentModelBlockBase ): ContentModelBlockBase { const { blockType } = block; @@ -117,7 +142,7 @@ function cloneBlockBase( } function cloneBlockGroupBase( - group: ContentModelBlockGroupBase, + group: ReadonlyContentModelBlockGroupBase, options: CloneModelOptions ): ContentModelBlockGroupBase { const { blockGroupType, blocks } = group; @@ -129,7 +154,7 @@ function cloneBlockGroupBase( } function cloneSegmentBase( - segment: ContentModelSegmentBase + segment: ReadonlyContentModelSegmentBase ): ContentModelSegmentBase { const { segmentType, isSelected, code, link } = segment; @@ -165,7 +190,7 @@ function cloneEntity(entity: ContentModelEntity, options: CloneModelOptions): Co } function cloneParagraph( - paragraph: ContentModelParagraph, + paragraph: ReadonlyContentModelParagraph, options: CloneModelOptions ): ContentModelParagraph { const { cachedElement, segments, isImplicit, decorator, segmentFormat } = paragraph; @@ -193,7 +218,10 @@ function cloneParagraph( return newParagraph; } -function cloneTable(table: ContentModelTable, options: CloneModelOptions): ContentModelTable { +function cloneTable( + table: ReadonlyContentModelTable, + options: CloneModelOptions +): ContentModelTable { const { cachedElement, widths, rows } = table; return Object.assign( @@ -208,7 +236,7 @@ function cloneTable(table: ContentModelTable, options: CloneModelOptions): Conte } function cloneTableRow( - row: ContentModelTableRow, + row: ReadonlyContentModelTableRow, options: CloneModelOptions ): ContentModelTableRow { const { height, cells, cachedElement } = row; @@ -224,7 +252,7 @@ function cloneTableRow( } function cloneTableCell( - cell: ContentModelTableCell, + cell: ReadonlyContentModelTableCell, options: CloneModelOptions ): ContentModelTableCell { const { cachedElement, isSelected, spanAbove, spanLeft, isHeader } = cell; @@ -244,7 +272,7 @@ function cloneTableCell( } function cloneFormatContainer( - container: ContentModelFormatContainer, + container: ReadonlyContentModelFormatContainer, options: CloneModelOptions ): ContentModelFormatContainer { const { tagName, cachedElement } = container; @@ -262,7 +290,7 @@ function cloneFormatContainer( } function cloneListItem( - item: ContentModelListItem, + item: ReadonlyContentModelListItem, options: CloneModelOptions ): ContentModelListItem { const { formatHolder, levels, cachedElement } = item; @@ -278,13 +306,13 @@ function cloneListItem( ); } -function cloneListLevel(level: ContentModelListLevel): ContentModelListLevel { +function cloneListLevel(level: ReadonlyContentModelListLevel): ContentModelListLevel { const { listType } = level; return Object.assign({ listType }, cloneModelWithFormat(level), cloneModelWithDataset(level)); } function cloneDivider( - divider: ContentModelDivider, + divider: ReadonlyContentModelDivider, options: CloneModelOptions ): ContentModelDivider { const { tagName, isSelected, cachedElement } = divider; @@ -300,7 +328,7 @@ function cloneDivider( } function cloneGeneralBlock( - general: ContentModelGeneralBlock, + general: ReadonlyContentModelGeneralBlock, options: CloneModelOptions ): ContentModelGeneralBlock { const { element } = general; @@ -314,11 +342,13 @@ function cloneGeneralBlock( ); } -function cloneSelectionMarker(marker: ContentModelSelectionMarker): ContentModelSelectionMarker { +function cloneSelectionMarker( + marker: ReadonlyContentModelSelectionMarker +): ContentModelSelectionMarker { return Object.assign({ isSelected: marker.isSelected }, cloneSegmentBase(marker)); } -function cloneImage(image: ContentModelImage): ContentModelImage { +function cloneImage(image: ReadonlyContentModelImage): ContentModelImage { const { src, alt, title, isSelectedAsImageSelection } = image; return Object.assign( @@ -329,13 +359,13 @@ function cloneImage(image: ContentModelImage): ContentModelImage { } function cloneGeneralSegment( - general: ContentModelGeneralSegment, + general: ReadonlyContentModelGeneralSegment, options: CloneModelOptions ): ContentModelGeneralSegment { return Object.assign(cloneGeneralBlock(general, options), cloneSegmentBase(general)); } -function cloneText(textSegment: ContentModelText): ContentModelText { +function cloneText(textSegment: ReadonlyContentModelText): ContentModelText { const { text } = textSegment; return Object.assign({ text }, cloneSegmentBase(textSegment)); } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/getClosestAncestorBlockGroupIndex.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/getClosestAncestorBlockGroupIndex.ts index 27a95cef4c5..b680494215a 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/getClosestAncestorBlockGroupIndex.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/getClosestAncestorBlockGroupIndex.ts @@ -1,6 +1,7 @@ import type { ContentModelBlockGroup, ContentModelBlockGroupType, + ReadonlyContentModelBlockGroup, TypeOfBlockGroup, } from 'roosterjs-content-model-types'; @@ -11,7 +12,7 @@ import type { * @param stopTypes @optional Block group types that will cause stop searching */ export function getClosestAncestorBlockGroupIndex( - path: ContentModelBlockGroup[], + path: ReadonlyContentModelBlockGroup[], blockGroupTypes: TypeOfBlockGroup[], stopTypes: ContentModelBlockGroupType[] = [] ): number { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateImageMetadata.ts b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateImageMetadata.ts index 840bfa87014..bf5ca0bb4d0 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateImageMetadata.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateImageMetadata.ts @@ -1,10 +1,14 @@ -import { updateMetadata } from './updateMetadata'; +import { getMetadata, updateMetadata } from './updateMetadata'; import { createNumberDefinition, createObjectDefinition, createStringDefinition, } from './definitionCreators'; -import type { ContentModelImage, ImageMetadataFormat } from 'roosterjs-content-model-types'; +import type { + ContentModelImage, + ImageMetadataFormat, + ReadonlyContentModelImage, +} from 'roosterjs-content-model-types'; const NumberDefinition = createNumberDefinition(); @@ -21,6 +25,14 @@ const ImageMetadataFormatDefinition = createObjectDefinition = crea true /** allowNull */ ); +/** + * Get list metadata + * @param list The list Content Model (metadata holder) + */ +export function getListMetadata( + list: ReadonlyContentModelWithDataset +): ListMetadataFormat | null { + return getMetadata(list, ListMetadataDefinition); +} + /** * Update list metadata with a callback * @param list The list Content Model (metadata holder) * @param callback The callback function used for updating metadata */ export function updateListMetadata( - list: ContentModelWithDataset, + list: ShallowMutableContentModelWithDataset, callback?: (format: ListMetadataFormat | null) => ListMetadataFormat | null ): ListMetadataFormat | null { return updateMetadata(list, callback, ListMetadataDefinition); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateMetadata.ts b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateMetadata.ts index debd77304db..1e5fce4fbd7 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateMetadata.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateMetadata.ts @@ -1,8 +1,32 @@ import { validate } from './validate'; -import type { ContentModelWithDataset, Definition } from 'roosterjs-content-model-types'; +import type { + Definition, + ReadonlyContentModelWithDataset, + ShallowMutableContentModelWithDataset, +} from 'roosterjs-content-model-types'; const EditingInfoDatasetName = 'editingInfo'; +/** + * Retrieve metadata from the given model. + * @param model The Content Model to retrieve metadata from + * @param definition Definition of this metadata type, used for validate the metadata object + * @returns Metadata of the model, or null if it does not contain a valid metadata + */ +export function getMetadata( + model: ReadonlyContentModelWithDataset, + definition?: Definition +): T | null { + const metadataString = model.dataset[EditingInfoDatasetName]; + let obj: Object | null = null; + + try { + obj = JSON.parse(metadataString); + } catch {} + + return !definition || validate(obj, definition) ? (obj as T) : null; +} + /** * Update metadata of the given model * @param model The model to update metadata to @@ -11,20 +35,11 @@ const EditingInfoDatasetName = 'editingInfo'; * @returns The metadata object if any, or null */ export function updateMetadata( - model: ContentModelWithDataset, + model: ShallowMutableContentModelWithDataset, callback?: (metadata: T | null) => T | null, definition?: Definition ): T | null { - const metadataString = model.dataset[EditingInfoDatasetName]; - let obj: T | null = null; - - try { - obj = JSON.parse(metadataString) as T; - } catch {} - - if (definition && !validate(obj, definition)) { - obj = null; - } + let obj = getMetadata(model, definition); if (callback) { obj = callback(obj); @@ -43,6 +58,6 @@ export function updateMetadata( * Check if the given model has metadata * @param model The content model to check */ -export function hasMetadata(model: ContentModelWithDataset | HTMLElement): boolean { +export function hasMetadata(model: ReadonlyContentModelWithDataset | HTMLElement): boolean { return !!model.dataset[EditingInfoDatasetName]; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableCellMetadata.ts b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableCellMetadata.ts index 274cd905063..f718596fc71 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableCellMetadata.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableCellMetadata.ts @@ -1,6 +1,10 @@ import { createBooleanDefinition, createObjectDefinition } from './definitionCreators'; -import { updateMetadata } from './updateMetadata'; -import type { ContentModelTableCell, TableCellMetadataFormat } from 'roosterjs-content-model-types'; +import { getMetadata, updateMetadata } from './updateMetadata'; +import type { + ReadonlyContentModelTableCell, + ShallowMutableContentModelTableCell, + TableCellMetadataFormat, +} from 'roosterjs-content-model-types'; const TableCellMetadataFormatDefinition = createObjectDefinition>( { @@ -12,13 +16,23 @@ const TableCellMetadataFormatDefinition = createObjectDefinition TableCellMetadataFormat | null ): TableCellMetadataFormat | null { return updateMetadata(cell, callback, TableCellMetadataFormatDefinition); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableMetadata.ts b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableMetadata.ts index 0ad6985f958..cc9bd3ef3bb 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableMetadata.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/metadata/updateTableMetadata.ts @@ -1,12 +1,16 @@ +import { getMetadata, updateMetadata } from './updateMetadata'; import { TableBorderFormat } from '../../constants/TableBorderFormat'; -import { updateMetadata } from './updateMetadata'; import { createBooleanDefinition, createNumberDefinition, createObjectDefinition, createStringDefinition, } from './definitionCreators'; -import type { ContentModelTable, TableMetadataFormat } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelTable, + ShallowMutableContentModelTable, + TableMetadataFormat, +} from 'roosterjs-content-model-types'; const NullStringDefinition = createStringDefinition( false /** isOptional */, @@ -40,13 +44,21 @@ const TableFormatDefinition = createObjectDefinition TableMetadataFormat | null ): TableMetadataFormat | null { return updateMetadata(table, callback, TableFormatDefinition); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts index eeb023bb2c1..a134fb3b771 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts @@ -12,6 +12,13 @@ import type { ContentModelTable, IterateSelectionsOption, OperationalBlocks, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, + ReadonlyOperationalBlocks, + ReadonlyTableSelectionContext, TableSelectionContext, TypeOfBlockGroup, } from 'roosterjs-content-model-types'; @@ -25,14 +32,39 @@ export function getSelectedSegmentsAndParagraphs( model: ContentModelDocument, includingFormatHolder: boolean, includingEntity?: boolean -): [ContentModelSegment, ContentModelParagraph | null, ContentModelBlockGroup[]][] { +): [ContentModelSegment, ContentModelParagraph | null, ContentModelBlockGroup[]][]; + +/** + * Get an array of selected parent paragraph and child segment pair (Readonly) + * @param model The Content Model to get selection from + * @param includingFormatHolder True means also include format holder as segment from list item, in that case paragraph will be null + */ +export function getSelectedSegmentsAndParagraphs( + model: ReadonlyContentModelDocument, + includingFormatHolder: boolean, + includingEntity?: boolean +): [ + ReadonlyContentModelSegment, + ReadonlyContentModelParagraph | null, + ReadonlyContentModelBlockGroup[] +][]; + +export function getSelectedSegmentsAndParagraphs( + model: ReadonlyContentModelDocument, + includingFormatHolder: boolean, + includingEntity?: boolean +): [ + ReadonlyContentModelSegment, + ReadonlyContentModelParagraph | null, + ReadonlyContentModelBlockGroup[] +][] { const selections = collectSelections(model, { includeListFormatHolder: includingFormatHolder ? 'allSegments' : 'never', }); const result: [ - ContentModelSegment, - ContentModelParagraph | null, - ContentModelBlockGroup[] + ReadonlyContentModelSegment, + ReadonlyContentModelParagraph | null, + ReadonlyContentModelBlockGroup[] ][] = []; selections.forEach(({ segments, block, path }) => { @@ -95,8 +127,29 @@ export function getOperationalBlocks( blockGroupTypes: TypeOfBlockGroup[], stopTypes: ContentModelBlockGroupType[], deepFirst?: boolean -): OperationalBlocks[] { - const result: OperationalBlocks[] = []; +): OperationalBlocks[]; + +/** + * Get an array of block group - block pair that is of the expected block group type from selection (Readonly) + * @param group The root block group to search + * @param blockGroupTypes The expected block group types + * @param stopTypes Block group types that will stop searching when hit + * @param deepFirst True means search in deep first, otherwise wide first + */ +export function getOperationalBlocks( + group: ReadonlyContentModelBlockGroup, + blockGroupTypes: TypeOfBlockGroup[], + stopTypes: ContentModelBlockGroupType[], + deepFirst?: boolean +): ReadonlyOperationalBlocks[]; + +export function getOperationalBlocks( + group: ReadonlyContentModelBlockGroup, + blockGroupTypes: TypeOfBlockGroup[], + stopTypes: ContentModelBlockGroupType[], + deepFirst?: boolean +): ReadonlyOperationalBlocks[] { + const result: ReadonlyOperationalBlocks[] = []; const findSequence = deepFirst ? blockGroupTypes.map(type => [type]) : [blockGroupTypes]; const selections = collectSelections(group, { includeListFormatHolder: 'never', @@ -190,11 +243,28 @@ interface SelectionInfo { tableContext?: TableSelectionContext; } +interface ReadonlySelectionInfo { + path: ReadonlyContentModelBlockGroup[]; + segments?: ReadonlyContentModelSegment[]; + block?: ReadonlyContentModelBlock; + tableContext?: ReadonlyTableSelectionContext; +} + function collectSelections( group: ContentModelBlockGroup, option?: IterateSelectionsOption -): SelectionInfo[] { - const selections: SelectionInfo[] = []; +): SelectionInfo[]; + +function collectSelections( + group: ReadonlyContentModelBlockGroup, + option?: IterateSelectionsOption +): ReadonlySelectionInfo[]; + +function collectSelections( + group: ReadonlyContentModelBlockGroup, + option?: IterateSelectionsOption +): ReadonlySelectionInfo[] { + const selections: ReadonlySelectionInfo[] = []; iterateSelections( group, @@ -212,7 +282,7 @@ function collectSelections( return selections; } -function removeUnmeaningfulSelections(selections: SelectionInfo[]) { +function removeUnmeaningfulSelections(selections: ReadonlySelectionInfo[]) { if ( selections.length > 1 && isOnlySelectionMarkerSelected(selections, false /*checkFirstParagraph*/) @@ -230,7 +300,7 @@ function removeUnmeaningfulSelections(selections: SelectionInfo[]) { } function isOnlySelectionMarkerSelected( - selections: SelectionInfo[], + selections: ReadonlySelectionInfo[], checkFirstParagraph: boolean ): boolean { const selection = selections[checkFirstParagraph ? 0 : selections.length - 1]; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts index 9fe47705b7f..05db43d0348 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts @@ -1,10 +1,12 @@ import type { ContentModelBlockGroup, ContentModelBlockWithCache, - ContentModelSegment, IterateSelectionsCallback, IterateSelectionsOption, - TableSelectionContext, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelSegment, + ReadonlyIterateSelectionsCallback, + ReadonlyTableSelectionContext, } from 'roosterjs-content-model-types'; /** @@ -17,23 +19,46 @@ export function iterateSelections( group: ContentModelBlockGroup, callback: IterateSelectionsCallback, option?: IterateSelectionsOption +): void; + +/** + * Iterate all selected elements in a given model (Readonly) + * @param group The given Content Model to iterate selection from + * @param callback The callback function to access the selected element + * @param option Option to determine how to iterate + */ +export function iterateSelections( + group: ReadonlyContentModelBlockGroup, + callback: ReadonlyIterateSelectionsCallback, + option?: IterateSelectionsOption +): void; + +export function iterateSelections( + group: ReadonlyContentModelBlockGroup, + callback: ReadonlyIterateSelectionsCallback | IterateSelectionsCallback, + option?: IterateSelectionsOption ): void { - const internalCallback: IterateSelectionsCallback = (path, tableContext, block, segments) => { + const internalCallback: ReadonlyIterateSelectionsCallback = ( + path, + tableContext, + block, + segments + ) => { if (!!(block as ContentModelBlockWithCache)?.cachedElement) { delete (block as ContentModelBlockWithCache).cachedElement; } - return callback(path, tableContext, block, segments); + return (callback as ReadonlyIterateSelectionsCallback)(path, tableContext, block, segments); }; internalIterateSelections([group], internalCallback, option); } function internalIterateSelections( - path: ContentModelBlockGroup[], - callback: IterateSelectionsCallback, + path: ReadonlyContentModelBlockGroup[], + callback: ReadonlyIterateSelectionsCallback, option?: IterateSelectionsOption, - table?: TableSelectionContext, + table?: ReadonlyTableSelectionContext, treatAllAsSelect?: boolean ): boolean { const parent = path[0]; @@ -104,7 +129,7 @@ function internalIterateSelections( continue; } - const newTable: TableSelectionContext = { + const newTable: ReadonlyTableSelectionContext = { table: block, rowIndex, colIndex, @@ -141,7 +166,7 @@ function internalIterateSelections( break; case 'Paragraph': - const segments: ContentModelSegment[] = []; + const segments: ReadonlyContentModelSegment[] = []; for (let i = 0; i < block.segments.length; i++) { const segment = block.segments[i]; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isBlockGroupOfType.ts b/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isBlockGroupOfType.ts index a851e2fe166..afadd639e39 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isBlockGroupOfType.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isBlockGroupOfType.ts @@ -1,6 +1,8 @@ import type { ContentModelBlock, ContentModelBlockGroup, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, TypeOfBlockGroup, } from 'roosterjs-content-model-types'; @@ -12,6 +14,29 @@ import type { export function isBlockGroupOfType( input: ContentModelBlock | ContentModelBlockGroup | null | undefined, type: TypeOfBlockGroup +): input is T; + +/** + * Check if the given content model block or block group is of the expected block group type (Readonly) + * @param input The object to check + * @param type The expected type + */ +export function isBlockGroupOfType( + input: ReadonlyContentModelBlock | ReadonlyContentModelBlockGroup | null | undefined, + type: TypeOfBlockGroup +): input is T; + +export function isBlockGroupOfType< + T extends ContentModelBlockGroup | ReadonlyContentModelBlockGroup +>( + input: + | ReadonlyContentModelBlock + | ReadonlyContentModelBlockGroup + | ContentModelBlock + | ContentModelBlockGroup + | null + | undefined, + type: TypeOfBlockGroup ): input is T { const item = input; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts index 56163aa0f8b..67fc9d3a518 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts @@ -1,6 +1,8 @@ import type { ContentModelBlockGroup, ContentModelGeneralSegment, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelGeneralSegment, } from 'roosterjs-content-model-types'; /** @@ -9,6 +11,18 @@ import type { */ export function isGeneralSegment( group: ContentModelBlockGroup | ContentModelGeneralSegment +): group is ContentModelGeneralSegment; + +/** + * Check if the given block group is a general segment (Readonly) + * @param group The group to check + */ +export function isGeneralSegment( + group: ReadonlyContentModelBlockGroup | ReadonlyContentModelGeneralSegment +): group is ReadonlyContentModelGeneralSegment; + +export function isGeneralSegment( + group: ReadonlyContentModelBlockGroup | ReadonlyContentModelGeneralSegment ): group is ContentModelGeneralSegment { return ( group.blockGroupType == 'General' && diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts index accfa4a4f09..6e4e485bdf5 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts @@ -60,7 +60,7 @@ describe('retrieveModelFormatState', () => { const para = createParagraph(); const marker = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [marker]); return false; }); @@ -78,7 +78,7 @@ describe('retrieveModelFormatState', () => { addCode(marker, { format: { fontFamily: 'monospace' } }); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [marker]); return false; }); @@ -100,7 +100,7 @@ describe('retrieveModelFormatState', () => { const listItem = createListItem([createListLevel('OL')]); const marker = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([listItem], undefined, para, [marker]); return false; }); @@ -123,7 +123,7 @@ describe('retrieveModelFormatState', () => { const quote = createFormatContainer('blockquote'); const marker = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([quote], undefined, para, [marker]); return false; }); @@ -146,7 +146,7 @@ describe('retrieveModelFormatState', () => { }); const marker = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [marker]); return false; }); @@ -171,7 +171,7 @@ describe('retrieveModelFormatState', () => { const para = createParagraph(false, paraFormat); const marker = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [marker]); return false; }); @@ -196,7 +196,7 @@ describe('retrieveModelFormatState', () => { table.rows[0].cells.push(cell); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback( [path], { @@ -233,7 +233,7 @@ describe('retrieveModelFormatState', () => { table.rows[0].cells.push(cell); applyTableFormat(table); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback( [path], { @@ -283,7 +283,7 @@ describe('retrieveModelFormatState', () => { table.rows[0].cells.push(cell1, cell2); model.blocks.push(table); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], { table: table, rowIndex: 0, @@ -310,7 +310,7 @@ describe('retrieveModelFormatState', () => { const marker1 = createSelectionMarker(segmentFormat); const marker2 = createSelectionMarker(); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para1, [marker1]); callback([path], undefined, para2, [marker2]); return false; @@ -345,7 +345,7 @@ describe('retrieveModelFormatState', () => { const text2 = createText('test2', { fontFamily: 'Arial' }); const result: ContentModelFormatState = {}; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [text1, text2]); return false; }); @@ -371,7 +371,7 @@ describe('retrieveModelFormatState', () => { const text2 = createText('test2', { fontFamily: 'Times' }); const result: ContentModelFormatState = {}; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [text1, text2]); return false; }); @@ -396,7 +396,7 @@ describe('retrieveModelFormatState', () => { const marker = createSelectionMarker({ fontFamily: 'Times' }); const result: ContentModelFormatState = {}; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [text, marker]); return false; }); @@ -422,7 +422,7 @@ describe('retrieveModelFormatState', () => { const divider = createDivider('hr'); const marker1 = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para1, [marker1]); callback([path], undefined, divider); return false; @@ -445,7 +445,7 @@ describe('retrieveModelFormatState', () => { const divider = createDivider('hr'); const marker1 = createSelectionMarker(segmentFormat); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, divider); callback([path], undefined, para1, [marker1]); return false; @@ -472,7 +472,7 @@ describe('retrieveModelFormatState', () => { textColor: 'block', }; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para1, [marker1]); return false; }); @@ -623,7 +623,7 @@ describe('retrieveModelFormatState', () => { image.isSelected = true; para.segments.push(image); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [image]); return false; }); @@ -663,7 +663,7 @@ describe('retrieveModelFormatState', () => { para.segments.push(image); para.segments.push(image2); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [image, image2]); return false; }); @@ -692,7 +692,7 @@ describe('retrieveModelFormatState', () => { const marker = createSelectionMarker(); para.segments.push(marker); - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [marker]); return false; }); @@ -727,7 +727,7 @@ describe('retrieveModelFormatState', () => { text1.isSelected = true; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [text1]); return false; }); @@ -761,7 +761,7 @@ describe('retrieveModelFormatState', () => { text1.isSelected = true; text2.isSelected = true; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [text1, text2]); return false; }); @@ -791,7 +791,7 @@ describe('retrieveModelFormatState', () => { text1.isSelected = true; text2.isSelected = true; - spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => { callback([path], undefined, para, [text1, text2]); return false; }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateImageMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateImageMetadataTest.ts index fa1d7115695..1912b6b21a4 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateImageMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateImageMetadataTest.ts @@ -1,5 +1,75 @@ import { ContentModelImage, ImageMetadataFormat } from 'roosterjs-content-model-types'; -import { updateImageMetadata } from '../../../lib/modelApi/metadata/updateImageMetadata'; +import { + getImageMetadata, + updateImageMetadata, +} from '../../../lib/modelApi/metadata/updateImageMetadata'; + +describe('getImageMetadataTest', () => { + it('No value', () => { + const image: ContentModelImage = { + segmentType: 'Image', + format: {}, + src: 'test', + dataset: {}, + }; + + const result = getImageMetadata(image); + + expect(result).toBeNull(); + }); + + it('Empty value', () => { + const image: ContentModelImage = { + segmentType: 'Image', + format: {}, + src: 'test', + dataset: { + editingInfo: '', + }, + }; + + const result = getImageMetadata(image); + + expect(result).toBeNull(); + }); + + it('Full valid value, return original value', () => { + const imageFormat: ImageMetadataFormat = { + widthPx: 1, + heightPx: 2, + leftPercent: 3, + rightPercent: 4, + topPercent: 5, + bottomPercent: 6, + angleRad: 7, + naturalHeight: 8, + naturalWidth: 9, + src: 'test', + }; + const image: ContentModelImage = { + segmentType: 'Image', + format: {}, + src: 'test', + dataset: { + editingInfo: JSON.stringify(imageFormat), + }, + }; + const result = getImageMetadata(image); + + expect(result).toEqual({ + widthPx: 1, + heightPx: 2, + leftPercent: 3, + rightPercent: 4, + topPercent: 5, + bottomPercent: 6, + angleRad: 7, + naturalHeight: 8, + naturalWidth: 9, + src: 'test', + }); + }); +}); describe('updateImageMetadataTest', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateListMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateListMetadataTest.ts index 528746e83e2..ed49e4af6a8 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateListMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateListMetadataTest.ts @@ -1,5 +1,48 @@ import { ContentModelWithDataset, ListMetadataFormat } from 'roosterjs-content-model-types'; -import { updateListMetadata } from '../../../lib/modelApi/metadata/updateListMetadata'; +import { + getListMetadata, + updateListMetadata, +} from '../../../lib/modelApi/metadata/updateListMetadata'; + +describe('getListMetadata', () => { + it('No value', () => { + const list: ContentModelWithDataset = { + dataset: {}, + }; + const result = getListMetadata(list); + + expect(result).toBeNull(); + }); + + it('Empty value', () => { + const list: ContentModelWithDataset = { + dataset: { + editingInfo: '', + }, + }; + const result = getListMetadata(list); + + expect(result).toBeNull(); + }); + + it('Full valid value, return original value', () => { + const listFormat: ListMetadataFormat = { + orderedStyleType: 2, + unorderedStyleType: 3, + }; + const list: ContentModelWithDataset = { + dataset: { + editingInfo: JSON.stringify(listFormat), + }, + }; + const result = getListMetadata(list); + + expect(result).toEqual({ + orderedStyleType: 2, + unorderedStyleType: 3, + }); + }); +}); describe('updateListMetadata', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateMetadataTest.ts index 081280cc381..9dc34a783b3 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateMetadataTest.ts @@ -1,6 +1,97 @@ import * as validate from '../../../lib/modelApi/metadata/validate'; -import { ContentModelWithDataset, Definition } from 'roosterjs-content-model-types'; -import { hasMetadata, updateMetadata } from '../../../lib/modelApi/metadata/updateMetadata'; +import { + getMetadata, + hasMetadata, + updateMetadata, +} from '../../../lib/modelApi/metadata/updateMetadata'; +import { + ContentModelWithDataset, + Definition, + ReadonlyContentModelWithDataset, +} from 'roosterjs-content-model-types'; + +describe('getMetadata', () => { + it('no metadata', () => { + const model: ReadonlyContentModelWithDataset = { + dataset: {}, + }; + const result = getMetadata(model); + + expect(model).toEqual({ + dataset: {}, + }); + expect(result).toBeNull(); + }); + + it('with metadata', () => { + const model: ReadonlyContentModelWithDataset = { + dataset: { + editingInfo: '{"a":"b"}', + }, + }; + + const result = getMetadata(model); + + expect(model).toEqual({ + dataset: { + editingInfo: '{"a":"b"}', + }, + }); + expect(result).toEqual({ + a: 'b', + }); + }); + + it('with metadata, pass the validation', () => { + const model: ContentModelWithDataset = { + dataset: { + editingInfo: '{"a":"b"}', + }, + }; + + const validateSpy = spyOn(validate, 'validate').and.returnValue(true); + + const mockedDefinition = 'DEFINITION' as any; + const result = getMetadata(model, mockedDefinition); + + expect(validateSpy).toHaveBeenCalledWith( + { + a: 'b', + }, + mockedDefinition + ); + expect(model).toEqual({ + dataset: { editingInfo: '{"a":"b"}' }, + }); + expect(result).toEqual({ + a: 'b', + }); + }); + + it('with metadata, fail the validation, return new value', () => { + const model: ContentModelWithDataset = { + dataset: { + editingInfo: '{"a":"b"}', + }, + }; + + const validateSpy = spyOn(validate, 'validate').and.returnValue(false); + + const mockedDefinition = 'DEFINITION' as any; + const result = getMetadata(model, mockedDefinition); + + expect(validateSpy).toHaveBeenCalledWith( + { + a: 'b', + }, + mockedDefinition + ); + expect(model).toEqual({ + dataset: { editingInfo: '{"a":"b"}' }, + }); + expect(result).toBeNull(); + }); +}); describe('updateMetadata', () => { it('no metadata', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableCellMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableCellMetadataTest.ts index bf0bbd273b3..bf43f76c1f0 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableCellMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableCellMetadataTest.ts @@ -1,5 +1,61 @@ import { ContentModelTableCell, TableCellMetadataFormat } from 'roosterjs-content-model-types'; -import { updateTableCellMetadata } from '../../../lib//modelApi/metadata/updateTableCellMetadata'; +import { + getTableCellMetadata, + updateTableCellMetadata, +} from '../../../lib//modelApi/metadata/updateTableCellMetadata'; + +describe('getTableCellMetadata', () => { + it('No value', () => { + const cell: ContentModelTableCell = { + blockGroupType: 'TableCell', + format: {}, + blocks: [], + spanAbove: false, + spanLeft: false, + dataset: {}, + }; + const result = getTableCellMetadata(cell); + + expect(result).toBeNull(); + }); + + it('Empty value', () => { + const cell: ContentModelTableCell = { + blockGroupType: 'TableCell', + format: {}, + blocks: [], + spanAbove: false, + spanLeft: false, + dataset: { + editingInfo: '', + }, + }; + const result = getTableCellMetadata(cell); + + expect(result).toBeNull(); + }); + + it('Full valid value, return original value', () => { + const cellFormat: TableCellMetadataFormat = { + bgColorOverride: true, + }; + const cell: ContentModelTableCell = { + blockGroupType: 'TableCell', + format: {}, + blocks: [], + spanAbove: false, + spanLeft: false, + dataset: { + editingInfo: JSON.stringify(cellFormat), + }, + }; + const result = getTableCellMetadata(cell); + + expect(result).toEqual({ + bgColorOverride: true, + }); + }); +}); describe('updateTableCellMetadata', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableMetadataTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableMetadataTest.ts index 250731ca03b..1501e5c9137 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableMetadataTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/metadata/updateTableMetadataTest.ts @@ -1,6 +1,81 @@ import { ContentModelTable, TableMetadataFormat } from 'roosterjs-content-model-types'; import { TableBorderFormat } from '../../../lib/constants/TableBorderFormat'; -import { updateTableMetadata } from '../../../lib/modelApi/metadata/updateTableMetadata'; +import { + getTableMetadata, + updateTableMetadata, +} from '../../../lib/modelApi/metadata/updateTableMetadata'; + +describe('getTableMetadata', () => { + it('No value', () => { + const table: ContentModelTable = { + blockType: 'Table', + format: {}, + rows: [], + widths: [], + dataset: {}, + }; + const result = getTableMetadata(table); + + expect(result).toBeNull(); + }); + + it('Empty value', () => { + const table: ContentModelTable = { + blockType: 'Table', + format: {}, + rows: [], + widths: [], + dataset: { + editingInfo: '', + }, + }; + const result = getTableMetadata(table); + + expect(result).toBeNull(); + }); + + it('Full valid value, return original value', () => { + const tableFormat: TableMetadataFormat = { + topBorderColor: 'red', + bottomBorderColor: 'blue', + verticalBorderColor: 'green', + hasHeaderRow: true, + headerRowColor: 'orange', + hasFirstColumn: true, + hasBandedColumns: false, + hasBandedRows: true, + bgColorEven: 'yellow', + bgColorOdd: 'gray', + tableBorderFormat: TableBorderFormat.Default, + verticalAlign: 'top', + }; + const table: ContentModelTable = { + blockType: 'Table', + format: {}, + rows: [], + widths: [], + dataset: { + editingInfo: JSON.stringify(tableFormat), + }, + }; + const result = getTableMetadata(table); + + expect(result).toEqual({ + topBorderColor: 'red', + bottomBorderColor: 'blue', + verticalBorderColor: 'green', + hasHeaderRow: true, + headerRowColor: 'orange', + hasFirstColumn: true, + hasBandedColumns: false, + hasBandedRows: true, + bgColorEven: 'yellow', + bgColorOdd: 'gray', + tableBorderFormat: TableBorderFormat.Default, + verticalAlign: 'top', + }); + }); +}); describe('updateTableMetadata', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 242af3ef074..d4a9614ddb6 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -245,7 +245,10 @@ export { DOMInsertPoint, } from './selection/DOMSelection'; export { InsertPoint } from './selection/InsertPoint'; -export { TableSelectionContext } from './selection/TableSelectionContext'; +export { + TableSelectionContext, + ReadonlyTableSelectionContext, +} from './selection/TableSelectionContext'; export { TableSelectionCoordinates } from './selection/TableSelectionCoordinates'; export { @@ -418,10 +421,11 @@ export { MergeModelOption } from './parameter/MergeModelOption'; export { IterateSelectionsCallback, IterateSelectionsOption, + ReadonlyIterateSelectionsCallback, } from './parameter/IterateSelectionsOption'; export { NodeTypeMap } from './parameter/NodeTypeMap'; export { TypeOfBlockGroup } from './parameter/TypeOfBlockGroup'; -export { OperationalBlocks } from './parameter/OperationalBlocks'; +export { OperationalBlocks, ReadonlyOperationalBlocks } from './parameter/OperationalBlocks'; export { ParsedTable, ParsedTableCell } from './parameter/ParsedTable'; export { ModelToTextCallback, diff --git a/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts b/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts index 7cce60f6fba..b2b010dfd96 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/IterateSelectionsOption.ts @@ -1,7 +1,19 @@ -import type { ContentModelBlock } from '../contentModel/block/ContentModelBlock'; -import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; -import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; -import type { TableSelectionContext } from '../selection/TableSelectionContext'; +import type { + ContentModelBlock, + ReadonlyContentModelBlock, +} from '../contentModel/block/ContentModelBlock'; +import type { + ContentModelBlockGroup, + ReadonlyContentModelBlockGroup, +} from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { + ContentModelSegment, + ReadonlyContentModelSegment, +} from '../contentModel/segment/ContentModelSegment'; +import type { + ReadonlyTableSelectionContext, + TableSelectionContext, +} from '../selection/TableSelectionContext'; /** * Options for iterateSelections API @@ -51,3 +63,18 @@ export type IterateSelectionsCallback = ( block?: ContentModelBlock, segments?: ContentModelSegment[] ) => void | boolean; + +/** + * The callback function type for iterateSelections (Readonly) + * @param path The block group path of current selection + * @param tableContext Table context of current selection + * @param block Block of current selection + * @param segments Segments of current selection + * @returns True to stop iterating, otherwise keep going + */ +export type ReadonlyIterateSelectionsCallback = ( + path: ReadonlyContentModelBlockGroup[], + tableContext?: ReadonlyTableSelectionContext, + block?: ReadonlyContentModelBlock, + segments?: ReadonlyContentModelSegment[] +) => void | boolean; diff --git a/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts b/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts index b83e3e515d8..ca69ff44ac6 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/OperationalBlocks.ts @@ -1,5 +1,11 @@ -import type { ContentModelBlock } from '../contentModel/block/ContentModelBlock'; -import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { + ContentModelBlock, + ReadonlyContentModelBlock, +} from '../contentModel/block/ContentModelBlock'; +import type { + ContentModelBlockGroup, + ReadonlyContentModelBlockGroup, +} from '../contentModel/blockGroup/ContentModelBlockGroup'; /** * Represent a pair of parent block group and child block @@ -20,3 +26,23 @@ export type OperationalBlocks = { */ path: ContentModelBlockGroup[]; }; + +/** + * Represent a pair of parent block group and child block (Readonly) + */ +export type ReadonlyOperationalBlocks = { + /** + * The parent block group + */ + parent: ReadonlyContentModelBlockGroup; + + /** + * The child block + */ + block: ReadonlyContentModelBlock | T; + + /** + * Selection path of this block + */ + path: ReadonlyContentModelBlockGroup[]; +}; diff --git a/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts b/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts index 5cc421f7ce3..75099cce0a9 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/TypeOfBlockGroup.ts @@ -1,9 +1,17 @@ -import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; -import type { ContentModelBlockGroupBase } from '../contentModel/blockGroup/ContentModelBlockGroupBase'; +import type { + ContentModelBlockGroup, + ReadonlyContentModelBlockGroup, +} from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { + ContentModelBlockGroupBase, + ReadonlyContentModelBlockGroupBase, +} from '../contentModel/blockGroup/ContentModelBlockGroupBase'; /** * Retrieve block group type string from a given block group */ export type TypeOfBlockGroup< - T extends ContentModelBlockGroup -> = T extends ContentModelBlockGroupBase ? U : never; + T extends ContentModelBlockGroup | ReadonlyContentModelBlockGroup +> = T extends ContentModelBlockGroupBase | ReadonlyContentModelBlockGroupBase + ? U + : never; diff --git a/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts b/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts index 50d85d6f108..9139338751a 100644 --- a/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts +++ b/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts @@ -1,4 +1,7 @@ -import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; +import type { + ContentModelTable, + ReadonlyContentModelTable, +} from '../contentModel/block/ContentModelTable'; /** * Context object for table in a selection @@ -24,3 +27,28 @@ export interface TableSelectionContext { */ isWholeTableSelected: boolean; } + +/** + * Context object for table in a selection + */ +export interface ReadonlyTableSelectionContext { + /** + * The table that contains the selection + */ + table: ReadonlyContentModelTable; + + /** + * Row Index of the selected table cell + */ + rowIndex: number; + + /** + * Column Index of the selected table cell + */ + colIndex: number; + + /** + * Whether the whole table is selected + */ + isWholeTableSelected: boolean; +} From 5f9b88bc87cdfb04d85cda639a2e4f2a747c626f Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 15:55:24 -0700 Subject: [PATCH 21/66] Readonly types step 3 --- .../roosterjs-content-model-dom/lib/index.ts | 1 + .../lib/modelApi/common/addDecorators.ts | 8 +++---- .../lib/modelApi/creators/createBr.ts | 4 ++-- .../creators/createContentModelDocument.ts | 2 +- .../lib/modelApi/creators/createDivider.ts | 4 ++-- .../lib/modelApi/creators/createEmptyModel.ts | 4 +++- .../lib/modelApi/creators/createEntity.ts | 2 +- .../creators/createFormatContainer.ts | 4 ++-- .../modelApi/creators/createGeneralSegment.ts | 4 ++-- .../lib/modelApi/creators/createImage.ts | 7 +++++-- .../lib/modelApi/creators/createListItem.ts | 6 +++--- .../lib/modelApi/creators/createListLevel.ts | 6 +++--- .../lib/modelApi/creators/createParagraph.ts | 10 ++++----- .../creators/createParagraphDecorator.ts | 4 ++-- .../creators/createSelectionMarker.ts | 4 ++-- .../lib/modelApi/creators/createTable.ts | 14 ++++++------- .../lib/modelApi/creators/createTableCell.ts | 8 +++---- .../lib/modelApi/creators/createTableRow.ts | 16 ++++++++++++++ .../lib/modelApi/creators/createText.ts | 12 +++++------ .../test/modelApi/creators/creatorsTest.ts | 21 +++++++++++++++++++ 20 files changed, 92 insertions(+), 49 deletions(-) create mode 100644 packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableRow.ts diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index d871e3d46f2..0cb7bf38bf3 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -54,6 +54,7 @@ export { createEntity } from './modelApi/creators/createEntity'; export { createDivider } from './modelApi/creators/createDivider'; export { createListLevel } from './modelApi/creators/createListLevel'; export { createEmptyModel } from './modelApi/creators/createEmptyModel'; +export { createTableRow } from './modelApi/creators/createTableRow'; export { mutateBlock, mutateSegments, mutateSegment } from './modelApi/common/mutate'; export { addBlock } from './modelApi/common/addBlock'; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts index 25ba00b0703..dd63bcbc423 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts @@ -1,14 +1,14 @@ import type { - ContentModelCode, - ContentModelLink, ContentModelSegment, DomToModelDecoratorContext, + ReadonlyContentModelCode, + ReadonlyContentModelLink, } from 'roosterjs-content-model-types'; /** * @internal */ -export function addLink(segment: ContentModelSegment, link: ContentModelLink) { +export function addLink(segment: ContentModelSegment, link: ReadonlyContentModelLink) { if (link.format.href) { segment.link = { format: { ...link.format }, @@ -22,7 +22,7 @@ export function addLink(segment: ContentModelSegment, link: ContentModelLink) { * @param segment The segment to add decorator to * @param code The code decorator to add */ -export function addCode(segment: ContentModelSegment, code: ContentModelCode) { +export function addCode(segment: ContentModelSegment, code: ReadonlyContentModelCode) { if (code.format.fontFamily) { segment.code = { format: { ...code.format }, diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createBr.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createBr.ts index f5c907211b7..55e53d373ce 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createBr.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createBr.ts @@ -4,9 +4,9 @@ import type { ContentModelBr, ContentModelSegmentFormat } from 'roosterjs-conten * Create a ContentModelBr model * @param format @optional The format of this model */ -export function createBr(format?: ContentModelSegmentFormat): ContentModelBr { +export function createBr(format?: Readonly): ContentModelBr { return { segmentType: 'Br', - format: format ? { ...format } : {}, + format: { ...format }, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createContentModelDocument.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createContentModelDocument.ts index d4ef4a4d539..ea30b78b8ba 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createContentModelDocument.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createContentModelDocument.ts @@ -8,7 +8,7 @@ import type { * @param defaultFormat @optional Default format of this model */ export function createContentModelDocument( - defaultFormat?: ContentModelSegmentFormat + defaultFormat?: Readonly ): ContentModelDocument { const result: ContentModelDocument = { blockGroupType: 'Document', diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createDivider.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createDivider.ts index f6d058dd4ea..bd0a27d6342 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createDivider.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createDivider.ts @@ -7,11 +7,11 @@ import type { ContentModelBlockFormat, ContentModelDivider } from 'roosterjs-con */ export function createDivider( tagName: 'hr' | 'div', - format?: ContentModelBlockFormat + format?: Readonly ): ContentModelDivider { return { blockType: 'Divider', tagName, - format: format ? { ...format } : {}, + format: { ...format }, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEmptyModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEmptyModel.ts index 23c3e2c439a..d255f33d1c5 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEmptyModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEmptyModel.ts @@ -11,7 +11,9 @@ import type { * Create an empty Content Model Document with initial empty line and insert point with default format * @param format @optional The default format to be applied to this Content Model */ -export function createEmptyModel(format?: ContentModelSegmentFormat): ContentModelDocument { +export function createEmptyModel( + format?: Readonly +): ContentModelDocument { const model = createContentModelDocument(format); const paragraph = createParagraph(false /*isImplicit*/, undefined /*blockFormat*/, format); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts index da66859a67b..b323e5a4f97 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts @@ -11,7 +11,7 @@ import type { ContentModelEntity, ContentModelSegmentFormat } from 'roosterjs-co export function createEntity( wrapper: HTMLElement, isReadonly: boolean = true, - segmentFormat?: ContentModelSegmentFormat, + segmentFormat?: Readonly, type?: string, id?: string ): ContentModelEntity { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createFormatContainer.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createFormatContainer.ts index 9bf73a25b2c..bc4d7962ea3 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createFormatContainer.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createFormatContainer.ts @@ -10,13 +10,13 @@ import type { */ export function createFormatContainer( tag: Lowercase, - format?: ContentModelFormatContainerFormat + format?: Readonly ): ContentModelFormatContainer { return { blockType: 'BlockGroup', blockGroupType: 'FormatContainer', tagName: tag, blocks: [], - format: { ...(format || {}) }, + format: { ...format }, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createGeneralSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createGeneralSegment.ts index e92400efd87..b21c253ab96 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createGeneralSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createGeneralSegment.ts @@ -10,13 +10,13 @@ import type { */ export function createGeneralSegment( element: HTMLElement, - format?: ContentModelSegmentFormat + format?: Readonly ): ContentModelGeneralSegment { return { blockType: 'BlockGroup', blockGroupType: 'General', segmentType: 'General', - format: format ? { ...format } : {}, + format: { ...format }, blocks: [], element: element, }; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createImage.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createImage.ts index 44c3d744d6d..1114ce41b67 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createImage.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createImage.ts @@ -5,11 +5,14 @@ import type { ContentModelImage, ContentModelImageFormat } from 'roosterjs-conte * @param src Image source * @param format @optional The format of this model */ -export function createImage(src: string, format?: ContentModelImageFormat): ContentModelImage { +export function createImage( + src: string, + format?: Readonly +): ContentModelImage { return { segmentType: 'Image', src: src, - format: format ? { ...format } : {}, + format: { ...format }, dataset: {}, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListItem.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListItem.ts index fbad6b221de..bbfec49ca13 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListItem.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListItem.ts @@ -2,8 +2,8 @@ import { createListLevel } from './createListLevel'; import { createSelectionMarker } from './createSelectionMarker'; import type { ContentModelListItem, - ContentModelListLevel, ContentModelSegmentFormat, + ReadonlyContentModelListLevel, } from 'roosterjs-content-model-types'; /** @@ -12,8 +12,8 @@ import type { * @param format @optional The format of this model */ export function createListItem( - levels: ContentModelListLevel[], - format?: ContentModelSegmentFormat + levels: ReadonlyArray, + format?: Readonly ): ContentModelListItem { const formatHolder = createSelectionMarker(format); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListLevel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListLevel.ts index 4355b02171c..25b8eb3946e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListLevel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createListLevel.ts @@ -1,7 +1,7 @@ import type { ContentModelListItemLevelFormat, ContentModelListLevel, - DatasetFormat, + ReadonlyDatasetFormat, } from 'roosterjs-content-model-types'; /** @@ -12,8 +12,8 @@ import type { */ export function createListLevel( listType: 'OL' | 'UL', - format?: ContentModelListItemLevelFormat, - dataset?: DatasetFormat + format?: Readonly, + dataset?: ReadonlyDatasetFormat ): ContentModelListLevel { return { listType, diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraph.ts index 06e3226eecd..f987bc2d66c 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraph.ts @@ -1,8 +1,8 @@ import type { ContentModelBlockFormat, ContentModelParagraph, - ContentModelParagraphDecorator, ContentModelSegmentFormat, + ReadonlyContentModelParagraphDecorator, } from 'roosterjs-content-model-types'; /** @@ -14,14 +14,14 @@ import type { */ export function createParagraph( isImplicit?: boolean, - blockFormat?: ContentModelBlockFormat, - segmentFormat?: ContentModelSegmentFormat, - decorator?: ContentModelParagraphDecorator + blockFormat?: Readonly, + segmentFormat?: Readonly, + decorator?: ReadonlyContentModelParagraphDecorator ): ContentModelParagraph { const result: ContentModelParagraph = { blockType: 'Paragraph', segments: [], - format: blockFormat ? { ...blockFormat } : {}, + format: { ...blockFormat }, }; if (segmentFormat && Object.keys(segmentFormat).length > 0) { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraphDecorator.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraphDecorator.ts index fb5e5835ed6..cb924ee780d 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraphDecorator.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createParagraphDecorator.ts @@ -10,10 +10,10 @@ import type { */ export function createParagraphDecorator( tagName: string, - format?: ContentModelSegmentFormat + format?: Readonly ): ContentModelParagraphDecorator { return { tagName: tagName.toLocaleLowerCase(), - format: { ...(format || {}) }, + format: { ...format }, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts index 88f5ac2e86a..e8ec164d896 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createSelectionMarker.ts @@ -8,11 +8,11 @@ import type { * @param format @optional The format of this model */ export function createSelectionMarker( - format?: ContentModelSegmentFormat + format?: Readonly ): ContentModelSelectionMarker { return { segmentType: 'SelectionMarker', isSelected: true, - format: format ? { ...format } : {}, + format: { ...format }, }; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTable.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTable.ts index 7a549dac31f..2d62d7cfb09 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTable.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTable.ts @@ -1,3 +1,4 @@ +import { createTableRow } from './createTableRow'; import type { ContentModelTable, ContentModelTableFormat, @@ -9,21 +10,20 @@ import type { * @param rowCount Count of rows of this table * @param format @optional The format of this model */ -export function createTable(rowCount: number, format?: ContentModelTableFormat): ContentModelTable { +export function createTable( + rowCount: number, + format?: Readonly +): ContentModelTable { const rows: ContentModelTableRow[] = []; for (let i = 0; i < rowCount; i++) { - rows.push({ - height: 0, - format: {}, - cells: [], - }); + rows.push(createTableRow()); } return { blockType: 'Table', rows, - format: { ...(format || {}) }, + format: { ...format }, widths: [], dataset: {}, }; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableCell.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableCell.ts index 79d67f76a16..cfe036d6398 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableCell.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableCell.ts @@ -1,7 +1,7 @@ import type { ContentModelTableCell, ContentModelTableCellFormat, - DatasetFormat, + ReadonlyDatasetFormat, } from 'roosterjs-content-model-types'; /** @@ -15,8 +15,8 @@ export function createTableCell( spanLeftOrColSpan?: boolean | number, spanAboveOrRowSpan?: boolean | number, isHeader?: boolean, - format?: ContentModelTableCellFormat, - dataset?: DatasetFormat + format?: Readonly, + dataset?: ReadonlyDatasetFormat ): ContentModelTableCell { const spanLeft = typeof spanLeftOrColSpan === 'number' ? spanLeftOrColSpan > 1 : !!spanLeftOrColSpan; @@ -25,7 +25,7 @@ export function createTableCell( return { blockGroupType: 'TableCell', blocks: [], - format: format ? { ...format } : {}, + format: { ...format }, spanLeft, spanAbove, isHeader: !!isHeader, diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableRow.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableRow.ts new file mode 100644 index 00000000000..3e9608b6929 --- /dev/null +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createTableRow.ts @@ -0,0 +1,16 @@ +import type { ContentModelBlockFormat, ContentModelTableRow } from 'roosterjs-content-model-types'; + +/** + * Create a ContentModelTableRow model + * @param format @optional The format of this model + */ +export function createTableRow( + format?: Readonly, + height: number = 0 +): ContentModelTableRow { + return { + height: height, + format: { ...format }, + cells: [], + }; +} diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createText.ts b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createText.ts index 6f73d09c662..c837d11f5c9 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/creators/createText.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/creators/createText.ts @@ -1,9 +1,9 @@ import { addCode, addLink } from '../common/addDecorators'; import type { - ContentModelCode, - ContentModelLink, ContentModelSegmentFormat, ContentModelText, + ReadonlyContentModelCode, + ReadonlyContentModelLink, } from 'roosterjs-content-model-types'; /** @@ -15,14 +15,14 @@ import type { */ export function createText( text: string, - format?: ContentModelSegmentFormat, - link?: ContentModelLink, - code?: ContentModelCode + format?: Readonly, + link?: ReadonlyContentModelLink, + code?: ReadonlyContentModelCode ): ContentModelText { const result: ContentModelText = { segmentType: 'Text', text: text, - format: format ? { ...format } : {}, + format: { ...format }, }; if (link) { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts index 019b533068e..c0441215e0e 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts @@ -13,6 +13,7 @@ import { createParagraphDecorator } from '../../../lib/modelApi/creators/createP import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; import { createTable } from '../../../lib/modelApi/creators/createTable'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; +import { createTableRow } from '../../../lib/modelApi/creators/createTableRow'; import { createText } from '../../../lib/modelApi/creators/createText'; import { ContentModelCode, @@ -232,6 +233,26 @@ describe('Creators', () => { }); }); + it('createTableRow', () => { + const row = createTableRow(); + + expect(row).toEqual({ + height: 0, + format: {}, + cells: [], + }); + }); + + it('createTableRow with format', () => { + const row = createTableRow({ direction: 'ltr' }, 100); + + expect(row).toEqual({ + height: 100, + format: { direction: 'ltr' }, + cells: [], + }); + }); + it('createTable', () => { const tableModel = createTable(2); From ce2e31a906d573c42fc74e2325274037bd6ef929 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 16:04:15 -0700 Subject: [PATCH 22/66] Readonly type step 4 --- .../insertEntity/InsertEntityPane.tsx | 4 +- .../lib/modelApi/list/getListAnnounceData.ts | 14 ++++-- .../lib/modelApi/table/canMergeCells.ts | 16 ++++-- .../lib/publicApi/image/changeImage.ts | 4 +- .../lib/command/paste/mergePasteContent.ts | 3 +- .../lib/corePlugin/entity/findAllEntities.ts | 4 +- .../lib/modelApi/common/addBlock.ts | 10 +++- .../lib/modelApi/common/isEmpty.ts | 32 ++++++------ .../modelApi/editing/getSegmentTextFormat.ts | 9 +++- .../editing/retrieveModelFormatState.ts | 50 ++++++++++--------- .../modelApi/selection/getSelectedCells.ts | 9 +++- .../modelApi/selection/hasSelectionInBlock.ts | 4 +- .../selection/hasSelectionInBlockGroup.ts | 4 +- .../selection/hasSelectionInSegment.ts | 4 +- .../selection/collectSelectionsTest.ts | 11 ++-- .../selection/getSelectedSegmentsTest.ts | 36 +++++++++++-- .../lib/autoFormat/list/getListTypeStyle.ts | 16 +++--- .../lib/watermark/isModelEmptyFast.ts | 4 +- 18 files changed, 151 insertions(+), 83 deletions(-) diff --git a/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx b/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx index f03ddcd9e7f..e304a6b79aa 100644 --- a/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx +++ b/demo/scripts/controlsV2/sidePane/apiPlayground/insertEntity/InsertEntityPane.tsx @@ -3,9 +3,9 @@ import { ApiPaneProps } from '../ApiPaneProps'; import { insertEntity } from 'roosterjs-content-model-api'; import { trustedHTMLHandler } from '../../../../utils/trustedHTMLHandler'; import { - ContentModelBlockGroup, ContentModelEntity, InsertEntityOptions, + ReadonlyContentModelBlockGroup, } from 'roosterjs-content-model-types'; const styles = require('./InsertEntityPane.scss'); @@ -155,7 +155,7 @@ export default class InsertEntityPane extends React.Component { switch (block.blockType) { case 'BlockGroup': diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts index 20fc55e3ee1..16fa2e84df1 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/getListAnnounceData.ts @@ -2,13 +2,14 @@ import { findListItemsInSameThread } from './findListItemsInSameThread'; import { getAutoListStyleType, getClosestAncestorBlockGroupIndex, + getListMetadata, getOrderedListNumberStr, - updateListMetadata, } from 'roosterjs-content-model-dom'; import type { AnnounceData, - ContentModelBlockGroup, ContentModelListItem, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelListItem, } from 'roosterjs-content-model-types'; /** @@ -16,7 +17,7 @@ import type { * @param path Content model path that include the list item * @returns Announce data of current list item if any, or null */ -export function getListAnnounceData(path: ContentModelBlockGroup[]): AnnounceData | null { +export function getListAnnounceData(path: ReadonlyContentModelBlockGroup[]): AnnounceData | null { const index = getClosestAncestorBlockGroupIndex(path, ['ListItem'], ['TableCell']); if (index >= 0) { @@ -27,7 +28,7 @@ export function getListAnnounceData(path: ContentModelBlockGroup[]): AnnounceDat return null; } else if (level.listType == 'OL') { const listNumber = getListNumber(path, listItem); - const metadata = updateListMetadata(level); + const metadata = getListMetadata(level); const listStyle = getAutoListStyleType( 'OL', metadata ?? {}, @@ -51,7 +52,10 @@ export function getListAnnounceData(path: ContentModelBlockGroup[]): AnnounceDat } } -function getListNumber(path: ContentModelBlockGroup[], listItem: ContentModelListItem) { +function getListNumber( + path: ReadonlyContentModelBlockGroup[], + listItem: ReadonlyContentModelListItem +) { const items = findListItemsInSameThread(path[path.length - 1], listItem); let listNumber = 0; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/canMergeCells.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/canMergeCells.ts index 622ad9c8324..1c14ab605a7 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/canMergeCells.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/canMergeCells.ts @@ -1,10 +1,10 @@ -import type { ContentModelTableRow } from 'roosterjs-content-model-types'; +import type { ReadonlyContentModelTableRow } from 'roosterjs-content-model-types'; /** * @internal */ export function canMergeCells( - rows: ContentModelTableRow[], + rows: ReadonlyContentModelTableRow[], firstRow: number, firstCol: number, lastRow: number, @@ -40,7 +40,11 @@ export function canMergeCells( return noSpanAbove && noSpanLeft && noDifferentBelowSpan && noDifferentRightSpan; } -function getBelowSpanCount(rows: ContentModelTableRow[], rowIndex: number, colIndex: number) { +function getBelowSpanCount( + rows: ReadonlyContentModelTableRow[], + rowIndex: number, + colIndex: number +) { let spanCount = 0; for (let row = rowIndex + 1; row < rows.length; row++) { @@ -54,7 +58,11 @@ function getBelowSpanCount(rows: ContentModelTableRow[], rowIndex: number, colIn return spanCount; } -function getRightSpanCount(rows: ContentModelTableRow[], rowIndex: number, colIndex: number) { +function getRightSpanCount( + rows: ReadonlyContentModelTableRow[], + rowIndex: number, + colIndex: number +) { let spanCount = 0; for (let col = colIndex + 1; col < rows[rowIndex]?.cells.length; col++) { diff --git a/packages/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts b/packages/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts index 7067c9fae8e..a14a91db4a2 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts @@ -1,5 +1,5 @@ import { formatImageWithContentModel } from '../utils/formatImageWithContentModel'; -import { readFile, updateImageMetadata } from 'roosterjs-content-model-dom'; +import { getImageMetadata, readFile } from 'roosterjs-content-model-dom'; import type { ContentModelImage, IEditor } from 'roosterjs-content-model-types'; /** @@ -14,7 +14,7 @@ export function changeImage(editor: IEditor, file: File) { readFile(file, dataUrl => { if (dataUrl && !editor.isDisposed() && selection?.type === 'image') { formatImageWithContentModel(editor, 'changeImage', (image: ContentModelImage) => { - const originalSrc = updateImageMetadata(image)?.src ?? ''; + const originalSrc = getImageMetadata(image)?.src ?? ''; const previousSrc = image.src; image.src = dataUrl; diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 55f2374297a..af149946960 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -15,6 +15,7 @@ import type { ContentModelSegmentFormat, IEditor, MergeModelOption, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; const EmptySegmentFormat: Required = { @@ -38,7 +39,7 @@ const CloneOption: CloneModelOptions = { /** * @internal */ -export function cloneModelForPaste(model: ContentModelDocument) { +export function cloneModelForPaste(model: ReadonlyContentModelDocument) { return cloneModel(model, CloneOption); } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/entity/findAllEntities.ts b/packages/roosterjs-content-model-core/lib/corePlugin/entity/findAllEntities.ts index 0f0b466eb92..b615cf49d8a 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/entity/findAllEntities.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/entity/findAllEntities.ts @@ -1,9 +1,9 @@ -import type { ChangedEntity, ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { ChangedEntity, ReadonlyContentModelBlockGroup } from 'roosterjs-content-model-types'; /** * @internal */ -export function findAllEntities(group: ContentModelBlockGroup, entities: ChangedEntity[]) { +export function findAllEntities(group: ReadonlyContentModelBlockGroup, entities: ChangedEntity[]) { group.blocks.forEach(block => { switch (block.blockType) { case 'BlockGroup': diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts index 3461d6baccb..57a51f84640 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts @@ -1,10 +1,16 @@ -import type { ContentModelBlock, ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { + ShallowMutableContentModelBlock, + ShallowMutableContentModelBlockGroup, +} from 'roosterjs-content-model-types'; /** * Add a given block to block group * @param group The block group to add block into * @param block The block to add */ -export function addBlock(group: ContentModelBlockGroup, block: ContentModelBlock) { +export function addBlock( + group: ShallowMutableContentModelBlockGroup, + block: ShallowMutableContentModelBlock +) { group.blocks.push(block); } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts index 64a1944437b..ae399702e2d 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts @@ -1,13 +1,13 @@ import type { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelSegment, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelSegment, } from 'roosterjs-content-model-types'; /** * @internal */ -export function isBlockEmpty(block: ContentModelBlock): boolean { +export function isBlockEmpty(block: ReadonlyContentModelBlock): boolean { switch (block.blockType) { case 'Paragraph': return block.segments.length == 0; @@ -29,7 +29,7 @@ export function isBlockEmpty(block: ContentModelBlock): boolean { /** * @internal */ -export function isBlockGroupEmpty(group: ContentModelBlockGroup): boolean { +export function isBlockGroupEmpty(group: ReadonlyContentModelBlockGroup): boolean { switch (group.blockGroupType) { case 'FormatContainer': // Format Container of DIV is a container for style, so we always treat it as not empty @@ -51,7 +51,7 @@ export function isBlockGroupEmpty(group: ContentModelBlockGroup): boolean { /** * @internal */ -export function isSegmentEmpty(segment: ContentModelSegment): boolean { +export function isSegmentEmpty(segment: ReadonlyContentModelSegment): boolean { switch (segment.segmentType) { case 'Text': return !segment.text; @@ -69,7 +69,7 @@ export function isSegmentEmpty(segment: ContentModelSegment): boolean { * @returns true if the model is empty. */ export function isEmpty( - model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment + model: ReadonlyContentModelBlock | ReadonlyContentModelBlockGroup | ReadonlyContentModelSegment ): boolean { if (isBlockGroup(model)) { return isBlockGroupEmpty(model); @@ -83,19 +83,19 @@ export function isEmpty( } function isSegment( - model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment -): model is ContentModelSegment { - return typeof (model).segmentType === 'string'; + model: ReadonlyContentModelBlock | ReadonlyContentModelBlockGroup | ReadonlyContentModelSegment +): model is ReadonlyContentModelSegment { + return typeof (model).segmentType === 'string'; } function isBlock( - model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment -): model is ContentModelBlock { - return typeof (model).blockType === 'string'; + model: ReadonlyContentModelBlock | ReadonlyContentModelBlockGroup | ReadonlyContentModelSegment +): model is ReadonlyContentModelBlock { + return typeof (model).blockType === 'string'; } function isBlockGroup( - model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment -): model is ContentModelBlockGroup { - return typeof (model).blockGroupType === 'string'; + model: ReadonlyContentModelBlock | ReadonlyContentModelBlockGroup | ReadonlyContentModelSegment +): model is ReadonlyContentModelBlockGroup { + return typeof (model).blockGroupType === 'string'; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts index cd91dbbde22..b8962f3205a 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat.ts @@ -1,11 +1,16 @@ -import type { ContentModelSegment, ContentModelSegmentFormat } from 'roosterjs-content-model-types'; +import type { + ContentModelSegmentFormat, + ReadonlyContentModelSegment, +} from 'roosterjs-content-model-types'; /** * Get the text format of a segment, this function will return only format that is applicable to text * @param segment The segment to get format from * @returns */ -export function getSegmentTextFormat(segment: ContentModelSegment): ContentModelSegmentFormat { +export function getSegmentTextFormat( + segment: ReadonlyContentModelSegment +): ContentModelSegmentFormat { const { fontFamily, fontSize, textColor, backgroundColor, letterSpacing, lineHeight } = segment?.format ?? {}; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts index 5cfd4d0333e..8b7cbb0a19b 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/retrieveModelFormatState.ts @@ -1,20 +1,20 @@ import { extractBorderValues } from '../../domUtils/style/borderValues'; import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; +import { getTableMetadata } from '../metadata/updateTableMetadata'; import { isBold } from '../../domUtils/style/isBold'; import { iterateSelections } from '../selection/iterateSelections'; import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit'; -import { updateTableMetadata } from '../metadata/updateTableMetadata'; import type { ContentModelFormatState, - ContentModelBlock, - ContentModelBlockGroup, - ContentModelDocument, - ContentModelFormatContainer, - ContentModelImage, - ContentModelListItem, - ContentModelParagraph, ContentModelSegmentFormat, - TableSelectionContext, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelBlock, + ReadonlyContentModelImage, + ReadonlyTableSelectionContext, + ReadonlyContentModelParagraph, + ReadonlyContentModelFormatContainer, + ReadonlyContentModelListItem, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; /** @@ -24,12 +24,12 @@ import type { * @param formatState Existing format state object, used for receiving the result */ export function retrieveModelFormatState( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, pendingFormat: ContentModelSegmentFormat | null, formatState: ContentModelFormatState ) { - let firstTableContext: TableSelectionContext | undefined; - let firstBlock: ContentModelBlock | undefined; + let firstTableContext: ReadonlyTableSelectionContext | undefined; + let firstBlock: ReadonlyContentModelBlock | undefined; let isFirst = true; let isFirstImage = true; let isFirstSegment = true; @@ -56,10 +56,11 @@ export function retrieveModelFormatState( // Segment formats segments?.forEach(segment => { if (isFirstSegment || segment.segmentType != 'SelectionMarker') { - const modelFormat = Object.assign({}, model.format); - delete modelFormat?.italic; - delete modelFormat?.underline; - delete modelFormat?.fontWeight; + const modelFormat = { ...model.format }; + + delete modelFormat.italic; + delete modelFormat.underline; + delete modelFormat.fontWeight; retrieveSegmentFormat( formatState, @@ -165,7 +166,7 @@ function retrieveSegmentFormat( function retrieveParagraphFormat( result: ContentModelFormatState, - paragraph: ContentModelParagraph, + paragraph: ReadonlyContentModelParagraph, isFirst: boolean ) { const headingLevel = parseInt((paragraph.decorator?.tagName || '').substring(1)); @@ -180,14 +181,14 @@ function retrieveParagraphFormat( function retrieveStructureFormat( result: ContentModelFormatState, - path: ContentModelBlockGroup[], + path: ReadonlyContentModelBlockGroup[], isFirst: boolean ) { const listItemIndex = getClosestAncestorBlockGroupIndex(path, ['ListItem'], []); const containerIndex = getClosestAncestorBlockGroupIndex(path, ['FormatContainer'], []); if (listItemIndex >= 0) { - const listItem = path[listItemIndex] as ContentModelListItem; + const listItem = path[listItemIndex] as ReadonlyContentModelListItem; const listType = listItem?.levels[listItem.levels.length - 1]?.listType; mergeValue(result, 'isBullet', listType == 'UL', isFirst); @@ -198,13 +199,16 @@ function retrieveStructureFormat( result, 'isBlockQuote', containerIndex >= 0 && - (path[containerIndex] as ContentModelFormatContainer)?.tagName == 'blockquote', + (path[containerIndex] as ReadonlyContentModelFormatContainer)?.tagName == 'blockquote', isFirst ); } -function retrieveTableFormat(tableContext: TableSelectionContext, result: ContentModelFormatState) { - const tableFormat = updateTableMetadata(tableContext.table); +function retrieveTableFormat( + tableContext: ReadonlyTableSelectionContext, + result: ContentModelFormatState +) { + const tableFormat = getTableMetadata(tableContext.table); result.isInTable = true; result.tableHasHeader = tableContext.table.rows.some(row => @@ -216,7 +220,7 @@ function retrieveTableFormat(tableContext: TableSelectionContext, result: Conten } } -function retrieveImageFormat(image: ContentModelImage, result: ContentModelFormatState) { +function retrieveImageFormat(image: ReadonlyContentModelImage, result: ContentModelFormatState) { const { format } = image; const borderKey = 'borderTop'; const extractedBorder = extractBorderValues(format[borderKey]); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/getSelectedCells.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/getSelectedCells.ts index beb8b46fe86..ab7484c24a5 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/getSelectedCells.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/getSelectedCells.ts @@ -1,11 +1,16 @@ import { hasSelectionInBlockGroup } from '../selection/hasSelectionInBlockGroup'; -import type { ContentModelTable, TableSelectionCoordinates } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelTable, + TableSelectionCoordinates, +} from 'roosterjs-content-model-types'; /** * Get selection coordinates of a table. If there is no selection, return null * @param table The table model to get selection from */ -export function getSelectedCells(table: ContentModelTable): TableSelectionCoordinates | null { +export function getSelectedCells( + table: ReadonlyContentModelTable +): TableSelectionCoordinates | null { let firstRow = -1; let firstColumn = -1; let lastRow = -1; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlock.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlock.ts index 8d721e4f048..395fb46da8d 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlock.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlock.ts @@ -1,12 +1,12 @@ import { hasSelectionInBlockGroup } from './hasSelectionInBlockGroup'; import { hasSelectionInSegment } from './hasSelectionInSegment'; -import type { ContentModelBlock } from 'roosterjs-content-model-types'; +import type { ReadonlyContentModelBlock } from 'roosterjs-content-model-types'; /** * Check if there is selection within the given block * @param block The block to check */ -export function hasSelectionInBlock(block: ContentModelBlock): boolean { +export function hasSelectionInBlock(block: ReadonlyContentModelBlock): boolean { switch (block.blockType) { case 'Paragraph': return block.segments.some(hasSelectionInSegment); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlockGroup.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlockGroup.ts index eb9b2199e61..e01e82da76b 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlockGroup.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInBlockGroup.ts @@ -1,11 +1,11 @@ import { hasSelectionInBlock } from './hasSelectionInBlock'; -import type { ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { ReadonlyContentModelBlockGroup } from 'roosterjs-content-model-types'; /** * Check if there is selection within the given block * @param block The block to check */ -export function hasSelectionInBlockGroup(group: ContentModelBlockGroup): boolean { +export function hasSelectionInBlockGroup(group: ReadonlyContentModelBlockGroup): boolean { if (group.blockGroupType == 'TableCell' && group.isSelected) { return true; } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInSegment.ts index 8bc58860ef8..07238f1e8dd 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/hasSelectionInSegment.ts @@ -1,11 +1,11 @@ import { hasSelectionInBlock } from './hasSelectionInBlock'; -import type { ContentModelSegment } from 'roosterjs-content-model-types'; +import type { ReadonlyContentModelSegment } from 'roosterjs-content-model-types'; /** * Check if there is selection within the given segment * @param segment The segment to check */ -export function hasSelectionInSegment(segment: ContentModelSegment): boolean { +export function hasSelectionInSegment(segment: ReadonlyContentModelSegment): boolean { return ( segment.isSelected || (segment.segmentType == 'General' && segment.blocks.some(hasSelectionInBlock)) diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts index 1a3b5c23b47..bd950e01685 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts @@ -77,6 +77,9 @@ describe('getSelectedSegmentsAndParagraphs', () => { const p1 = createParagraph(); const p2 = createParagraph(); + p1.segments.push(s1, s2); + p2.segments.push(s3, s4); + runTest( [ { @@ -132,6 +135,7 @@ describe('getSelectedSegmentsAndParagraphs', () => { const s3 = createText('test3'); const s4 = createText('test4'); const b1 = createDivider('div'); + const doc = createContentModelDocument(); runTest( [ @@ -141,16 +145,13 @@ describe('getSelectedSegmentsAndParagraphs', () => { segments: [s1, s2], }, { - path: [], + path: [doc], segments: [s3, s4], }, ], true, false, - [ - [s3, null, []], - [s4, null, []], - ] + [] ); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts index e2fa50eeb64..aea8c2ee375 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts @@ -7,8 +7,10 @@ import { TableSelectionContext, } from 'roosterjs-content-model-types'; import { + createContentModelDocument, createDivider, createEntity, + createListItem, createParagraph, createSelectionMarker, createText, @@ -52,6 +54,9 @@ describe('getSelectedSegments', () => { const p1 = createParagraph(); const p2 = createParagraph(); + p1.segments.push(s1, s2); + p2.segments.push(s3, s4); + runTest( [ { @@ -100,21 +105,46 @@ describe('getSelectedSegments', () => { const s3 = createText('test3'); const s4 = createText('test4'); const b1 = createDivider('div'); + const doc = createContentModelDocument(); runTest( [ { - path: [], + path: [doc], block: b1, segments: [s1, s2], }, { - path: [], + path: [doc], segments: [s3, s4], }, ], true, - [s3, s4] + [] + ); + }); + + it('Block with list item, include format holder', () => { + const s1 = createText('test1'); + const s2 = createText('test2'); + const b1 = createDivider('div'); + const doc = createContentModelDocument(); + const listItem = createListItem([]); + + runTest( + [ + { + path: [doc], + block: b1, + segments: [s1, s2], + }, + { + path: [listItem, doc], + segments: [listItem.formatHolder], + }, + ], + true, + [listItem.formatHolder] ); }); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/list/getListTypeStyle.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/list/getListTypeStyle.ts index 7a6c64dc584..8ab4ec1ccb0 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/list/getListTypeStyle.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/list/getListTypeStyle.ts @@ -1,9 +1,10 @@ import { findListItemsInSameThread } from 'roosterjs-content-model-api'; import { getNumberingListStyle } from './getNumberingListStyle'; import type { - ContentModelDocument, ContentModelListItem, - ContentModelParagraph, + ReadonlyContentModelDocument, + ReadonlyContentModelListItem, + ReadonlyContentModelParagraph, } from 'roosterjs-content-model-types'; import { BulletListType, @@ -26,7 +27,7 @@ interface ListTypeStyle { * @internal */ export function getListTypeStyle( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, shouldSearchForBullet: boolean = true, shouldSearchForNumbering: boolean = true ): ListTypeStyle | undefined { @@ -77,13 +78,16 @@ export function getListTypeStyle( } const getPreviousListIndex = ( - model: ContentModelDocument, - previousListItem?: ContentModelListItem + model: ReadonlyContentModelDocument, + previousListItem?: ReadonlyContentModelListItem ) => { return previousListItem ? findListItemsInSameThread(model, previousListItem).length : undefined; }; -const getPreviousListLevel = (model: ContentModelDocument, paragraph: ContentModelParagraph) => { +const getPreviousListLevel = ( + model: ReadonlyContentModelDocument, + paragraph: ReadonlyContentModelParagraph +) => { const blocks = getOperationalBlocks( model, ['ListItem'], diff --git a/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts b/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts index 4265e799e34..b17d285d964 100644 --- a/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts +++ b/packages/roosterjs-content-model-plugins/lib/watermark/isModelEmptyFast.ts @@ -1,10 +1,10 @@ -import type { ContentModelDocument } from 'roosterjs-content-model-types'; +import type { ReadonlyContentModelDocument } from 'roosterjs-content-model-types'; /** * @internal * A fast way to check if content model is empty */ -export function isModelEmptyFast(model: ContentModelDocument): boolean { +export function isModelEmptyFast(model: ReadonlyContentModelDocument): boolean { const firstBlock = model.blocks[0]; if (model.blocks.length > 1) { From 2afcf386ad00d590923365a5fc101f3e58b700e4 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 16:24:48 -0700 Subject: [PATCH 23/66] add test --- .../selection/getSelectedSegmentsTest.ts | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts index e2fa50eeb64..2a42dadc893 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts @@ -7,8 +7,10 @@ import { TableSelectionContext, } from 'roosterjs-content-model-types'; import { + createContentModelDocument, createDivider, createEntity, + createListItem, createParagraph, createSelectionMarker, createText, @@ -52,6 +54,9 @@ describe('getSelectedSegments', () => { const p1 = createParagraph(); const p2 = createParagraph(); + p1.segments.push(s1, s2); + p2.segments.push(s3, s4); + runTest( [ { @@ -100,16 +105,17 @@ describe('getSelectedSegments', () => { const s3 = createText('test3'); const s4 = createText('test4'); const b1 = createDivider('div'); + const doc = createContentModelDocument(); runTest( [ { - path: [], + path: [doc], block: b1, segments: [s1, s2], }, { - path: [], + path: [doc], segments: [s3, s4], }, ], @@ -118,6 +124,30 @@ describe('getSelectedSegments', () => { ); }); + it('Block with list item, include format holder', () => { + const s1 = createText('test1'); + const s2 = createText('test2'); + const b1 = createDivider('div'); + const doc = createContentModelDocument(); + const listItem = createListItem([]); + + runTest( + [ + { + path: [doc], + block: b1, + segments: [s1, s2], + }, + { + path: [listItem, doc], + segments: [listItem.formatHolder], + }, + ], + true, + [listItem.formatHolder] + ); + }); + it('Unmeaningful segments should be included', () => { const s1 = createText('test1'); const s2 = createText('test2'); From adb26a0c5eb65f9f069c3d325e93b59851de29a0 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 16:48:11 -0700 Subject: [PATCH 24/66] Improve --- .../test/modelApi/selection/iterateSelectionsTest.ts | 8 ++++---- .../lib/selection/TableSelectionContext.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts index 55f2664bc5d..f1761b764a3 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts @@ -1219,6 +1219,10 @@ describe('iterateSelections', () => { const marker2 = createSelectionMarker(); const cache = 'CACHE' as any; + addSegment(quote1, marker1); + para1.segments.push(marker2); + divider1.isSelected = true; + quote1.cachedElement = cache; para1.cachedElement = cache; divider1.cachedElement = cache; @@ -1226,10 +1230,6 @@ describe('iterateSelections', () => { para2.cachedElement = cache; divider2.cachedElement = cache; - addSegment(quote1, marker1); - para1.segments.push(marker2); - divider1.isSelected = true; - const doc = createContentModelDocument(); doc.blocks.push(quote1, quote2, para1, para2, divider1, divider2); diff --git a/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts b/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts index 9139338751a..25cd46f5f0a 100644 --- a/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts +++ b/packages/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts @@ -29,7 +29,7 @@ export interface TableSelectionContext { } /** - * Context object for table in a selection + * Context object for table in a selection (Readonly) */ export interface ReadonlyTableSelectionContext { /** From 85b50f27a36d59a89e843e62bd1f15df1f3ba3de Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 16 May 2024 17:02:46 -0700 Subject: [PATCH 25/66] improve --- .../modelApi/selection/collectSelections.ts | 34 +++++++++++++-- .../selection/collectSelectionsTest.ts | 5 ++- .../selection/getSelectedSegmentsTest.ts | 26 +----------- .../lib/edit/utils/getLeafSiblingBlock.ts | 41 ++++++++++++++++++- 4 files changed, 75 insertions(+), 31 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts index a134fb3b771..8537cb9c2a1 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts @@ -15,8 +15,10 @@ import type { ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ReadonlyContentModelListItem, ReadonlyContentModelParagraph, ReadonlyContentModelSegment, + ReadonlyContentModelTable, ReadonlyOperationalBlocks, ReadonlyTableSelectionContext, TableSelectionContext, @@ -191,10 +193,22 @@ export function getOperationalBlocks( */ export function getFirstSelectedTable( model: ContentModelDocument -): [ContentModelTable | undefined, ContentModelBlockGroup[]] { +): [ContentModelTable | undefined, ContentModelBlockGroup[]]; + +/** + * Get the first selected table from content model (Readonly) + * @param model The Content Model to get selection from + */ +export function getFirstSelectedTable( + model: ReadonlyContentModelDocument +): [ReadonlyContentModelTable | undefined, ReadonlyContentModelBlockGroup[]]; + +export function getFirstSelectedTable( + model: ReadonlyContentModelDocument +): [ReadonlyContentModelTable | undefined, ReadonlyContentModelBlockGroup[]] { const selections = collectSelections(model, { includeListFormatHolder: 'never' }); - let table: ContentModelTable | undefined; - let resultPath: ContentModelBlockGroup[] = []; + let table: ReadonlyContentModelTable | undefined; + let resultPath: ReadonlyContentModelBlockGroup[] = []; removeUnmeaningfulSelections(selections); @@ -224,7 +238,19 @@ export function getFirstSelectedTable( */ export function getFirstSelectedListItem( model: ContentModelDocument -): ContentModelListItem | undefined { +): ContentModelListItem | undefined; + +/** + * Get the first selected list item from content model (Readonly) + * @param model The Content Model to get selection from + */ +export function getFirstSelectedListItem( + model: ReadonlyContentModelDocument +): ReadonlyContentModelListItem | undefined; + +export function getFirstSelectedListItem( + model: ReadonlyContentModelDocument +): ReadonlyContentModelListItem | undefined { let listItem: ContentModelListItem | undefined; getOperationalBlocks(model, ['ListItem'], ['TableCell']).forEach(r => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts index bd950e01685..6738961f570 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts @@ -151,7 +151,10 @@ describe('getSelectedSegmentsAndParagraphs', () => { ], true, false, - [] + [ + [s3, null, [doc]], + [s4, null, [doc]], + ] ); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts index e6adf163368..2a42dadc893 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts @@ -120,31 +120,7 @@ describe('getSelectedSegments', () => { }, ], true, - [] - ); - }); - - it('Block with list item, include format holder', () => { - const s1 = createText('test1'); - const s2 = createText('test2'); - const b1 = createDivider('div'); - const doc = createContentModelDocument(); - const listItem = createListItem([]); - - runTest( - [ - { - path: [doc], - block: b1, - segments: [s1, s2], - }, - { - path: [listItem, doc], - segments: [listItem.formatHolder], - }, - ], - true, - [listItem.formatHolder] + [s3, s4] ); }); 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 b5913fce32c..f99390a79bc 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts @@ -4,6 +4,9 @@ import type { ContentModelBlockGroup, ContentModelParagraph, ContentModelSegment, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelSegment, } from 'roosterjs-content-model-types'; /** @@ -27,6 +30,27 @@ export type BlockAndPath = { siblingSegment?: ContentModelSegment; }; +/** + * @internal + */ +export type ReadonlyBlockAndPath = { + /** + * The sibling block + */ + block: ReadonlyContentModelBlock; + + /** + * Path of this sibling block + */ + 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?: ReadonlyContentModelSegment; +}; + /** * @internal */ @@ -34,7 +58,22 @@ export function getLeafSiblingBlock( path: ContentModelBlockGroup[], block: ContentModelBlock, isNext: boolean -): BlockAndPath | null { +): BlockAndPath | null; + +/** + * @internal (Readonly) + */ +export function getLeafSiblingBlock( + path: ReadonlyContentModelBlockGroup[], + block: ReadonlyContentModelBlock, + isNext: boolean +): ReadonlyBlockAndPath | null; + +export function getLeafSiblingBlock( + path: ReadonlyContentModelBlockGroup[], + block: ReadonlyContentModelBlock, + isNext: boolean +): ReadonlyBlockAndPath | null { const newPath = [...path]; while (newPath.length > 0) { From 6dbd78ba60145449bf919400a7b701cd63dc83c5 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 17 May 2024 12:03:43 -0700 Subject: [PATCH 26/66] improve --- .../lib/contentModel/block/ContentModelParagraph.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts index 987c7033a3b..4db9b6ba111 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts @@ -56,7 +56,7 @@ export interface ReadonlyContentModelParagraph /** * Segment format on this paragraph. This is mostly used for default format */ - readonly segmentFormat?: ContentModelSegmentFormat; + readonly segmentFormat?: Readonly; /** * Decorator info for this paragraph, used by heading and P tags From c92c7db1aa8137809516077ad793ec9b9defb9a1 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 17 May 2024 15:13:56 -0700 Subject: [PATCH 27/66] Readonly types step 5: dom package --- .../image/adjustImageSelectionTest.ts | 1 - .../publicApi/link/adjustLinkSelectionTest.ts | 2 - .../test/publicApi/link/removeLinkTest.ts | 1 - .../block/setParagraphNotImplicitTest.ts | 16 +++++ .../test/modelApi/common/addSegmentTest.ts | 30 ++++---- .../modelApi/common/ensureParagraphTest.ts | 17 +++-- .../common/normalizeContentModelTest.ts | 17 ++--- .../modelApi/common/normalizeParagraphTest.ts | 11 +-- .../modelApi/common/normalizeSegmentTest.ts | 42 ++++++++--- .../test/modelApi/common/unwrapBlockTest.ts | 16 ++++- .../modelApi/editing/applyTableFormatTest.ts | 27 +++---- .../modelApi/editing/normalizeTableTest.ts | 72 +++++++++---------- .../setTableCellBackgroundColorTest.ts | 7 +- .../modelApi/selection/setSelectionTest.ts | 1 - .../test/tableEdit/tableInserterTest.ts | 4 +- .../lib/parameter/DeleteSelectionStep.ts | 4 +- .../lib/selection/InsertPoint.ts | 8 +-- 17 files changed, 168 insertions(+), 108 deletions(-) diff --git a/packages/roosterjs-content-model-api/test/publicApi/image/adjustImageSelectionTest.ts b/packages/roosterjs-content-model-api/test/publicApi/image/adjustImageSelectionTest.ts index 5e60019d87a..a0e6b54b11f 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/image/adjustImageSelectionTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/image/adjustImageSelectionTest.ts @@ -189,7 +189,6 @@ describe('adjustImageSelection', () => { format: {}, src: 'img2', dataset: {}, - isSelectedAsImageSelection: false, }, { segmentType: 'Text', diff --git a/packages/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts b/packages/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts index fdac2d121dc..c53b8428ac0 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts @@ -154,7 +154,6 @@ describe('adjustLinkSelection', () => { link: link, dataset: {}, isSelected: true, - isSelectedAsImageSelection: false, }, { segmentType: 'Text', @@ -228,7 +227,6 @@ describe('adjustLinkSelection', () => { link: link, dataset: {}, isSelected: true, - isSelectedAsImageSelection: false, }, { segmentType: 'Text', diff --git a/packages/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts b/packages/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts index 0d0f1f081bf..ac563a97d05 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts @@ -150,7 +150,6 @@ describe('removeLink', () => { dataset: {}, format: {}, isSelected: true, - isSelectedAsImageSelection: false, }, ], }, diff --git a/packages/roosterjs-content-model-dom/test/modelApi/block/setParagraphNotImplicitTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/block/setParagraphNotImplicitTest.ts index d87520a13c7..192964ca13c 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/block/setParagraphNotImplicitTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/block/setParagraphNotImplicitTest.ts @@ -1,5 +1,6 @@ import { createDivider } from '../../../lib/modelApi/creators/createDivider'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; +import { ReadonlyContentModelParagraph } from 'roosterjs-content-model-types'; import { setParagraphNotImplicit } from '../../../lib/modelApi/block/setParagraphNotImplicit'; describe('setParagraphNotImplicit', () => { @@ -33,4 +34,19 @@ describe('setParagraphNotImplicit', () => { isImplicit: false, }); }); + + it('Readonly paragraph', () => { + const block: ReadonlyContentModelParagraph = createParagraph(true); + + block.cachedElement = {} as any; + + setParagraphNotImplicit(block); + + expect(block).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [], + isImplicit: false, + }); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/addSegmentTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/addSegmentTest.ts index 9bcde0996dc..69bf30d8277 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/addSegmentTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/addSegmentTest.ts @@ -1,15 +1,19 @@ import { addBlock } from '../../../lib/modelApi/common/addBlock'; import { addSegment } from '../../../lib/modelApi/common/addSegment'; -import { ContentModelGeneralBlock, ContentModelParagraph } from 'roosterjs-content-model-types'; import { createBr } from '../../../lib/modelApi/creators/createBr'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; import { createText } from '../../../lib/modelApi/creators/createText'; +import { + ContentModelGeneralBlock, + ContentModelParagraph, + ShallowMutableContentModelDocument, +} from 'roosterjs-content-model-types'; describe('addSegment', () => { it('Add segment to empty document', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const segment = createText('test'); const result = addSegment(doc, segment); @@ -34,7 +38,7 @@ describe('addSegment', () => { }); it('Add segment to document contains an empty paragraph', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(false); addBlock(doc, para); @@ -62,7 +66,7 @@ describe('addSegment', () => { }); it('Add segment to document contains a paragraph with existing text', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const block: ContentModelParagraph = { blockType: 'Paragraph', segments: [ @@ -104,7 +108,7 @@ describe('addSegment', () => { }); it('Add segment to document contains a paragraph with other type of block', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const div = document.createElement('div'); const block: ContentModelGeneralBlock = { blockType: 'BlockGroup', @@ -140,7 +144,7 @@ describe('addSegment', () => { }); it('Add selection marker in empty paragraph', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); doc.blocks.push(para); @@ -168,7 +172,7 @@ describe('addSegment', () => { }); it('Add selection marker after selection marker', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); @@ -198,7 +202,7 @@ describe('addSegment', () => { }); it('Add selection marker after selected segment', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const br = createBr(); @@ -229,7 +233,7 @@ describe('addSegment', () => { }); it('Add selection marker after selection marker', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); @@ -259,7 +263,7 @@ describe('addSegment', () => { }); it('Add selection marker after selection marker that is not selected', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); @@ -295,7 +299,7 @@ describe('addSegment', () => { }); it('Add unselected selection marker after selection marker', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); @@ -332,7 +336,7 @@ describe('addSegment', () => { }); it('Add selected segment after selection marker', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); @@ -364,7 +368,7 @@ describe('addSegment', () => { }); it('Add selected segment after unselected selection marker', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/ensureParagraphTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/ensureParagraphTest.ts index 5d4c57b8f81..cec013a545d 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/ensureParagraphTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/ensureParagraphTest.ts @@ -1,12 +1,15 @@ -import { ContentModelBlockFormat } from 'roosterjs-content-model-types'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDivider } from '../../../lib/modelApi/creators/createDivider'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { ensureParagraph } from '../../../lib/modelApi/common/ensureParagraph'; +import { + ContentModelBlockFormat, + ShallowMutableContentModelDocument, +} from 'roosterjs-content-model-types'; describe('ensureParagraph', () => { it('Empty group', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const result = ensureParagraph(doc); expect(doc).toEqual({ @@ -22,7 +25,7 @@ describe('ensureParagraph', () => { }); it('Empty group with format', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const format: ContentModelBlockFormat = { backgroundColor: 'red', }; @@ -43,7 +46,7 @@ describe('ensureParagraph', () => { }); it('Last block is not paragraph', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const divider = createDivider('hr'); doc.blocks.push(divider); @@ -63,9 +66,11 @@ describe('ensureParagraph', () => { }); it('Last block is paragraph', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const paragraph = createParagraph(); + paragraph.cachedElement = {} as any; + doc.blocks.push(paragraph); const result = ensureParagraph(doc); @@ -83,7 +88,7 @@ describe('ensureParagraph', () => { }); it('Last block is paragraph, do not overwrite format', () => { - const doc = createContentModelDocument(); + const doc: ShallowMutableContentModelDocument = createContentModelDocument(); const format: ContentModelBlockFormat = { backgroundColor: 'red', }; diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeContentModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeContentModelTest.ts index a821e02816a..bc3aa260c52 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeContentModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeContentModelTest.ts @@ -7,12 +7,13 @@ import { createTable } from '../../../lib/modelApi/creators/createTable'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; import { createText } from '../../../lib/modelApi/creators/createText'; import { normalizeContentModel } from '../../../lib/modelApi/common/normalizeContentModel'; +import { ReadonlyContentModelDocument } from 'roosterjs-content-model-types'; describe('normalizeContentModel', () => { it('Empty model', () => { const model = createContentModelDocument(); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -28,7 +29,7 @@ describe('normalizeContentModel', () => { para.segments.push(text); model.blocks.push(para); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -46,7 +47,7 @@ describe('normalizeContentModel', () => { para.segments.push(text1, text2, text3); model.blocks.push(para); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -83,7 +84,7 @@ describe('normalizeContentModel', () => { para2.segments.push(text, br); model.blocks.push(para1, para2); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -132,7 +133,7 @@ describe('normalizeContentModel', () => { para2.segments.push(br3, br4); model.blocks.push(para1, para2); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -186,7 +187,7 @@ describe('normalizeContentModel', () => { para2.segments.push(text, br); model.blocks.push(para1, para2); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -235,7 +236,7 @@ describe('normalizeContentModel', () => { table.rows[0].cells.push(cell); model.blocks.push(table); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', @@ -277,7 +278,7 @@ describe('normalizeContentModel', () => { listItem.blocks.push(para); model.blocks.push(listItem); - normalizeContentModel(model); + normalizeContentModel(model as ReadonlyContentModelDocument); expect(model).toEqual({ blockGroupType: 'Document', diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeParagraphTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeParagraphTest.ts index 95f6f6175f5..a28e3aa5a38 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeParagraphTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeParagraphTest.ts @@ -6,6 +6,7 @@ import { createSelectionMarker } from '../../../lib/modelApi/creators/createSele import { createText } from '../../../lib/modelApi/creators/createText'; import { normalizeContentModel } from '../../../lib/modelApi/common/normalizeContentModel'; import { normalizeParagraph } from '../../../lib/modelApi/common/normalizeParagraph'; +import { ReadonlyContentModelParagraph } from 'roosterjs-content-model-types'; describe('Normalize text that contains space', () => { function runTest(texts: string[], expected: string[], whiteSpace?: string) { @@ -360,7 +361,7 @@ describe('Normalize paragraph with segmentFormat', () => { it('Empty paragraph', () => { const paragraph = createParagraph(); - normalizeParagraph(paragraph); + normalizeParagraph(paragraph as ReadonlyContentModelParagraph); expect(paragraph).toEqual({ blockType: 'Paragraph', @@ -377,7 +378,7 @@ describe('Normalize paragraph with segmentFormat', () => { paragraph.segments.push(text); - normalizeParagraph(paragraph); + normalizeParagraph(paragraph as ReadonlyContentModelParagraph); expect(paragraph).toEqual({ blockType: 'Paragraph', @@ -405,7 +406,7 @@ describe('Normalize paragraph with segmentFormat', () => { paragraph.segments.push(text1, marker, text2); - normalizeParagraph(paragraph); + normalizeParagraph(paragraph as ReadonlyContentModelParagraph); expect(paragraph).toEqual({ blockType: 'Paragraph', @@ -443,7 +444,7 @@ describe('Normalize paragraph with segmentFormat', () => { paragraph.segments.push(text1, marker, text2); - normalizeParagraph(paragraph); + normalizeParagraph(paragraph as ReadonlyContentModelParagraph); expect(paragraph).toEqual({ blockType: 'Paragraph', @@ -482,7 +483,7 @@ describe('Normalize paragraph with segmentFormat', () => { paragraph.segments.push(text1, marker, text2); - normalizeParagraph(paragraph); + normalizeParagraph(paragraph as ReadonlyContentModelParagraph); expect(paragraph).toEqual({ blockType: 'Paragraph', diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeSegmentTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeSegmentTest.ts index e33785530fd..7faa0cc8996 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeSegmentTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/normalizeSegmentTest.ts @@ -1,6 +1,8 @@ import { createBr } from '../../../lib/modelApi/creators/createBr'; import { createImage } from '../../../lib/modelApi/creators/createImage'; +import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { createText } from '../../../lib/modelApi/creators/createText'; +import { ReadonlyContentModelParagraph } from 'roosterjs-content-model-types'; import { createNormalizeSegmentContext, normalizeSegment, @@ -10,8 +12,11 @@ describe('normalizeSegment', () => { it('With initial context, image', () => { const context = createNormalizeSegmentContext(); const image = createImage('test'); + const para = createParagraph(); - normalizeSegment(image, context); + para.segments.push(image); + + normalizeSegment(para as ReadonlyContentModelParagraph, image, context); expect(image).toEqual({ segmentType: 'Image', @@ -32,8 +37,11 @@ describe('normalizeSegment', () => { it('With initial context, regular text', () => { const context = createNormalizeSegmentContext(); const text = createText('test'); + const para = createParagraph(); + + para.segments.push(text); - normalizeSegment(text, context); + normalizeSegment(para as ReadonlyContentModelParagraph, text, context); expect(text).toEqual({ segmentType: 'Text', @@ -53,8 +61,11 @@ describe('normalizeSegment', () => { it('With initial context, br', () => { const context = createNormalizeSegmentContext(); const br = createBr(); + const para = createParagraph(); - normalizeSegment(br, context); + para.segments.push(br); + + normalizeSegment(para as ReadonlyContentModelParagraph, br, context); expect(br).toEqual({ segmentType: 'Br', @@ -73,8 +84,11 @@ describe('normalizeSegment', () => { it('Normalize an empty string', () => { const context = createNormalizeSegmentContext(); const text = createText(''); + const para = createParagraph(); + + para.segments.push(text); - normalizeSegment(text, context); + normalizeSegment(para as ReadonlyContentModelParagraph, text, context); expect(text).toEqual({ segmentType: 'Text', @@ -94,8 +108,11 @@ describe('normalizeSegment', () => { it('Normalize an string with spaces', () => { const context = createNormalizeSegmentContext(); const text = createText(' aa '); + const para = createParagraph(); - normalizeSegment(text, context); + para.segments.push(text); + + normalizeSegment(para as ReadonlyContentModelParagraph, text, context); expect(text).toEqual({ segmentType: 'Text', @@ -115,8 +132,11 @@ describe('normalizeSegment', () => { it('Normalize an string with  ', () => { const context = createNormalizeSegmentContext(); const text = createText('\u00A0\u00A0aa\u00A0\u00A0'); + const para = createParagraph(); + + para.segments.push(text); - normalizeSegment(text, context); + normalizeSegment(para as ReadonlyContentModelParagraph, text, context); expect(text).toEqual({ segmentType: 'Text', @@ -136,10 +156,13 @@ describe('normalizeSegment', () => { it('Normalize an string space and ignoreLeadingSpaces = false', () => { const context = createNormalizeSegmentContext(); const text = createText(' aa '); + const para = createParagraph(); + + para.segments.push(text); context.ignoreLeadingSpaces = false; - normalizeSegment(text, context); + normalizeSegment(para as ReadonlyContentModelParagraph, text, context); expect(text).toEqual({ segmentType: 'Text', @@ -159,10 +182,13 @@ describe('normalizeSegment', () => { it('Normalize an string space and ignoreTrailingSpaces = false', () => { const context = createNormalizeSegmentContext(); const text = createText(' aa '); + const para = createParagraph(); + + para.segments.push(text); context.ignoreTrailingSpaces = false; - normalizeSegment(text, context); + normalizeSegment(para as ReadonlyContentModelParagraph, text, context); expect(text).toEqual({ segmentType: 'Text', diff --git a/packages/roosterjs-content-model-dom/test/modelApi/common/unwrapBlockTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/common/unwrapBlockTest.ts index ad8c24cfecb..3bd0e062abb 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/common/unwrapBlockTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/common/unwrapBlockTest.ts @@ -1,7 +1,11 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createFormatContainer } from '../../../lib/modelApi/creators/createFormatContainer'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { unwrapBlock } from '../../../lib/modelApi/common/unwrapBlock'; +import { + ContentModelDocument, + ReadonlyContentModelDocument, + ReadonlyContentModelFormatContainer, +} from 'roosterjs-content-model-types'; describe('unwrapBlock', () => { it('no parent', () => { @@ -40,7 +44,10 @@ describe('unwrapBlock', () => { para.isImplicit = true; quote.blocks.push(para); - unwrapBlock(doc, quote); + unwrapBlock( + doc as ReadonlyContentModelDocument, + quote as ReadonlyContentModelFormatContainer + ); expect(doc).toEqual({ blockGroupType: 'Document', @@ -76,7 +83,10 @@ describe('unwrapBlock', () => { para.isImplicit = true; quote.blocks.push(para); - unwrapBlock(doc, quote); + unwrapBlock( + doc as ReadonlyContentModelDocument, + quote as ReadonlyContentModelFormatContainer + ); expect(doc).toEqual({ blockGroupType: 'Document', diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/applyTableFormatTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/applyTableFormatTest.ts index e7810d76afc..548505bbe12 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/applyTableFormatTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/applyTableFormatTest.ts @@ -4,6 +4,7 @@ import { ContentModelTable, ContentModelTableCell, ContentModelTableRow, + ReadonlyContentModelTable, TableMetadataFormat, } from 'roosterjs-content-model-types'; @@ -57,7 +58,7 @@ describe('applyTableFormat', () => { exportedBackgroundColors: string[][], expectedBorders: string[][][] ) { - const table = createTable(3, 4); + const table: ReadonlyContentModelTable = createTable(3, 4); applyTableFormat(table, format); @@ -457,7 +458,7 @@ describe('applyTableFormat', () => { const table = createTable(1, 1); table.rows[0].cells[0].format.backgroundColor = 'red'; - applyTableFormat(table, { + applyTableFormat(table as ReadonlyContentModelTable, { bgColorEven: 'green', }); @@ -466,7 +467,7 @@ describe('applyTableFormat', () => { table.rows[0].cells[0].dataset.editingInfo = '{"bgColorOverride":true}'; - applyTableFormat(table, { + applyTableFormat(table as ReadonlyContentModelTable, { bgColorEven: 'blue', }); @@ -476,7 +477,7 @@ describe('applyTableFormat', () => { table.rows[0].cells[0].dataset.editingInfo = '{"bgColorOverride":true}'; applyTableFormat( - table, + table as ReadonlyContentModelTable, { bgColorEven: 'yellow', }, @@ -492,7 +493,7 @@ describe('applyTableFormat', () => { table.rows[0].cells[0].format.borderLeft = '1px solid red'; // Try to apply green - applyTableFormat(table, { + applyTableFormat(table as ReadonlyContentModelTable, { topBorderColor: 'green', }); @@ -503,7 +504,7 @@ describe('applyTableFormat', () => { table.rows[0].cells[0].dataset.editingInfo = '{"borderOverride":true}'; // Try to apply blue - applyTableFormat(table, { + applyTableFormat(table as ReadonlyContentModelTable, { topBorderColor: 'blue', }); @@ -531,10 +532,10 @@ describe('applyTableFormat', () => { }; // Try to apply default format black - applyTableFormat(table, format); + applyTableFormat(table as ReadonlyContentModelTable, format); //apply HeaderRowColor - applyTableFormat(table, { ...format, hasHeaderRow: true }); + applyTableFormat(table as ReadonlyContentModelTable, { ...format, hasHeaderRow: true }); //expect HeaderRowColor text color to be applied table.rows[0].cells[0].blocks.forEach(block => { @@ -578,10 +579,10 @@ describe('applyTableFormat', () => { }; // Try to apply default format black - applyTableFormat(table, format); + applyTableFormat(table as ReadonlyContentModelTable, format); //apply HeaderRowColor - applyTableFormat(table, { ...format, hasHeaderRow: true }); + applyTableFormat(table as ReadonlyContentModelTable, { ...format, hasHeaderRow: true }); //expect HeaderRowColor text color to be applied table.rows[0].cells[0].blocks.forEach(block => { @@ -613,13 +614,13 @@ describe('applyTableFormat', () => { }; // Try to apply default format black - applyTableFormat(table, format); + applyTableFormat(table as ReadonlyContentModelTable, format); //apply HeaderRowColor - applyTableFormat(table, { ...format, hasHeaderRow: true }); + applyTableFormat(table as ReadonlyContentModelTable, { ...format, hasHeaderRow: true }); //Toggle HeaderRowColor - applyTableFormat(table, { ...format, hasHeaderRow: false }); + applyTableFormat(table as ReadonlyContentModelTable, { ...format, hasHeaderRow: false }); //expect HeaderRowColor text color to be applied table.rows[0].cells[0].blocks.forEach(block => { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/normalizeTableTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/normalizeTableTest.ts index 9e81b4d9d37..50187d7548b 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/normalizeTableTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/normalizeTableTest.ts @@ -5,9 +5,10 @@ import { ContentModelTable, ContentModelTableCellFormat, ContentModelTableFormat, + ReadonlyContentModelTable, } from 'roosterjs-content-model-types'; import { - createParagraph, + createParagraph as originalCreateParagraph, createTable as originalCreateTable, createTableCell as originalCreateTableCell, createText, @@ -15,6 +16,14 @@ import { const mockedCachedElement = {} as any; +function createParagraph(): ContentModelParagraph { + const paragraph = originalCreateParagraph(); + + paragraph.cachedElement = mockedCachedElement; + + return paragraph; +} + function createTable(rowCount: number, format?: ContentModelTableFormat): ContentModelTable { const table = originalCreateTable(rowCount, format); @@ -40,7 +49,7 @@ describe('normalizeTable', () => { it('Normalize an empty table', () => { const table = createTable(0); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -51,7 +60,6 @@ describe('normalizeTable', () => { }, widths: [], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -60,7 +68,7 @@ describe('normalizeTable', () => { table.rows[0].cells.push(createTableCell(1, 1, false)); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -88,7 +96,6 @@ describe('normalizeTable', () => { }, ], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -99,7 +106,6 @@ describe('normalizeTable', () => { }, widths: [120], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -118,7 +124,7 @@ describe('normalizeTable', () => { table.rows[0].cells.push(cell1); table.rows[1].cells.push(cell2); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -138,10 +144,10 @@ describe('normalizeTable', () => { blockType: 'Paragraph', segments: [], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -160,6 +166,7 @@ describe('normalizeTable', () => { blockType: 'Paragraph', segments: [], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, @@ -173,7 +180,6 @@ describe('normalizeTable', () => { }, widths: [120], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -204,7 +210,7 @@ describe('normalizeTable', () => { table.rows[0].cells.push(cell2); table.rows[0].cells.push(cell3); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -230,6 +236,7 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -241,10 +248,10 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, - cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -263,10 +270,10 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -277,7 +284,6 @@ describe('normalizeTable', () => { }, widths: [240, 120], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -296,7 +302,7 @@ describe('normalizeTable', () => { table.rows[0].cells.push(cell1); table.rows[0].cells.push(cell2); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -316,10 +322,10 @@ describe('normalizeTable', () => { blockType: 'Paragraph', segments: [], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -330,7 +336,6 @@ describe('normalizeTable', () => { }, widths: [240], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -362,7 +367,7 @@ describe('normalizeTable', () => { table.rows[1].cells.push(cell3); table.rows[1].cells.push(cell4); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -379,7 +384,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block1, block2], dataset: {}, - cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -389,7 +393,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -405,7 +408,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block3], dataset: {}, - cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -415,7 +417,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block4], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -426,7 +427,6 @@ describe('normalizeTable', () => { }, widths: [120, 120], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -458,7 +458,7 @@ describe('normalizeTable', () => { table.rows[1].cells.push(cell3); table.rows[1].cells.push(cell4); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -484,6 +484,7 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -495,10 +496,10 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, - cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -517,6 +518,7 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -528,10 +530,10 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -542,7 +544,6 @@ describe('normalizeTable', () => { }, widths: [120, 120], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -574,7 +575,7 @@ describe('normalizeTable', () => { table.rows[1].cells.push(cell3); table.rows[1].cells.push(cell4); - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); expect(table).toEqual({ blockType: 'Table', @@ -601,6 +602,7 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -612,6 +614,7 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -623,6 +626,7 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -634,9 +638,9 @@ describe('normalizeTable', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], - cachedElement: mockedCachedElement, }, ], }, @@ -647,7 +651,6 @@ describe('normalizeTable', () => { }, widths: [240], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -659,7 +662,7 @@ describe('normalizeTable', () => { table.rows[0].cells.push(createTableCell(1, 1, false)); - normalizeTable(table, format); + normalizeTable(table as ReadonlyContentModelTable, format); expect(table).toEqual({ blockType: 'Table', @@ -690,7 +693,6 @@ describe('normalizeTable', () => { }, ], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -701,7 +703,6 @@ describe('normalizeTable', () => { }, widths: [120], dataset: {}, - cachedElement: mockedCachedElement, }); }); @@ -720,7 +721,7 @@ describe('normalizeTable', () => { table.rows[0].height = 200; table.rows[1].height = 200; - normalizeTable(table); + normalizeTable(table as ReadonlyContentModelTable); const block: ContentModelParagraph = { blockType: 'Paragraph', @@ -748,7 +749,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block], dataset: {}, - cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -758,7 +758,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -774,7 +773,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block], dataset: {}, - cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -784,7 +782,6 @@ describe('normalizeTable', () => { format: { useBorderBox: true }, blocks: [block], dataset: {}, - cachedElement: mockedCachedElement, }, ], }, @@ -795,7 +792,6 @@ describe('normalizeTable', () => { }, widths: [100, 100], dataset: {}, - cachedElement: mockedCachedElement, }); }); }); 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 6fbe2ce6fc2..ca884d9774a 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/setTableCellBackgroundColorTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/setTableCellBackgroundColorTest.ts @@ -1,13 +1,16 @@ -import { ContentModelTableCell, ContentModelTableCellFormat } from 'roosterjs-content-model-types'; import { createTableCell as originalCreateTableCell } 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 -): ContentModelTableCell { +): ShallowMutableContentModelTableCell { const cell = originalCreateTableCell(spanLeftOrColSpan, spanAboveOrRowSpan, isHeader, format); cell.cachedElement = {} as any; diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts index c7c5d7fcefe..b87aa5d56b9 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts @@ -599,7 +599,6 @@ describe('setSelection', () => { src: '', dataset: {}, isSelected: true, - isSelectedAsImageSelection: false, }, { segmentType: 'Text', diff --git a/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts b/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts index 48e6c778b47..74386776bf7 100644 --- a/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts +++ b/packages/roosterjs-content-model-plugins/test/tableEdit/tableInserterTest.ts @@ -59,10 +59,12 @@ describe('Table Inserter tests', () => { // Inserter is visible, but pointer is not over it return 'not clickable'; } - const table = getCurrentTable(editor); + 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); diff --git a/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts b/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts index f396642b126..f6a8f15188a 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts @@ -2,7 +2,7 @@ import type { ContentModelParagraph } from '../contentModel/block/ContentModelPa import type { DeleteResult } from '../enum/DeleteResult'; import type { FormatContentModelContext } from './FormatContentModelContext'; import type { InsertPoint } from '../selection/InsertPoint'; -import type { TableSelectionContext } from '../selection/TableSelectionContext'; +import type { ReadonlyTableSelectionContext } from '../selection/TableSelectionContext'; /** * Result of deleteSelection API @@ -31,7 +31,7 @@ export interface DeleteSelectionContext extends DeleteSelectionResult { /** * Last table context after previous step */ - lastTableContext?: TableSelectionContext; + lastTableContext?: ReadonlyTableSelectionContext; /** * Format context provided by formatContentModel API diff --git a/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts b/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts index 49f1057d918..2eab4b717e3 100644 --- a/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts +++ b/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts @@ -1,7 +1,7 @@ -import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; +import type { ReadonlyContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; import type { ContentModelSelectionMarker } from '../contentModel/segment/ContentModelSelectionMarker'; -import type { TableSelectionContext } from './TableSelectionContext'; +import type { ReadonlyTableSelectionContext } from './TableSelectionContext'; /** * Represent all related information of an insert point @@ -20,10 +20,10 @@ export interface InsertPoint { /** * Block group path of this insert point, from direct parent group to the root group */ - path: ContentModelBlockGroup[]; + path: ReadonlyContentModelBlockGroup[]; /** * Table context of this insert point */ - tableContext?: TableSelectionContext; + tableContext?: ReadonlyTableSelectionContext; } From 719c9170cacac7cdcbabb96784cc7addd55166bb Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 17 May 2024 15:14:54 -0700 Subject: [PATCH 28/66] add change --- .../lib/modelApi/block/setModelIndentation.ts | 26 ++-- .../lib/modelApi/entity/insertEntityModel.ts | 13 +- .../corePlugin/copyPaste/deleteEmptyList.ts | 7 +- .../modelApi/block/setParagraphNotImplicit.ts | 7 +- .../lib/modelApi/common/addSegment.ts | 4 +- .../lib/modelApi/common/ensureParagraph.ts | 7 +- .../modelApi/common/normalizeContentModel.ts | 7 +- .../lib/modelApi/common/normalizeParagraph.ts | 28 ++-- .../lib/modelApi/common/normalizeSegment.ts | 74 +++++++---- .../lib/modelApi/common/unwrapBlock.ts | 14 +- .../lib/modelApi/editing/applyTableFormat.ts | 53 ++++---- .../lib/modelApi/editing/deleteBlock.ts | 8 +- .../editing/deleteExpandedSelection.ts | 36 ++--- .../lib/modelApi/editing/deleteSegment.ts | 33 +++-- .../lib/modelApi/editing/deleteSelection.ts | 11 +- .../lib/modelApi/editing/mergeModel.ts | 40 +++--- .../lib/modelApi/editing/normalizeTable.ts | 28 ++-- .../editing/setTableCellBackgroundColor.ts | 21 +-- .../lib/modelApi/selection/setSelection.ts | 124 ++++++++++++------ .../deleteSteps/deleteCollapsedSelection.ts | 26 ++-- .../lib/edit/deleteSteps/deleteEmptyQuote.ts | 7 +- .../lib/edit/inputSteps/handleEnterOnList.ts | 18 +-- 22 files changed, 358 insertions(+), 234 deletions(-) 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 828635044a0..7abcf8e17f8 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -4,17 +4,19 @@ import { createListLevel, getOperationalBlocks, isBlockGroupOfType, + mutateBlock, parseValueWithUnit, updateListMetadata, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, ContentModelBlockFormat, - ContentModelBlockGroup, - ContentModelDocument, ContentModelListItem, ContentModelListLevel, FormatContentModelContext, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, + ReadonlyContentModelListItem, } from 'roosterjs-content-model-types'; const IndentStepInPixel = 40; @@ -26,7 +28,7 @@ const IndentStepInPixel = 40; * Set indentation for selected list items or paragraphs */ export function setModelIndentation( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, indentation: 'indent' | 'outdent', length: number = IndentStepInPixel, context?: FormatContentModelContext @@ -37,7 +39,7 @@ export function setModelIndentation( ['TableCell'] ); const isIndent = indentation == 'indent'; - const modifiedBlocks: ContentModelBlock[] = []; + const modifiedBlocks: ReadonlyContentModelBlock[] = []; paragraphOrListItem.forEach(({ block, parent, path }) => { if (isBlockGroupOfType(block, 'ListItem')) { @@ -89,12 +91,12 @@ export function setModelIndentation( } } } else if (block) { - let currentBlock: ContentModelBlock = block; - let currentParent: ContentModelBlockGroup = parent; + let currentBlock: ReadonlyContentModelBlock = block; + let currentParent: ReadonlyContentModelBlockGroup = parent; while (currentParent && modifiedBlocks.indexOf(currentBlock) < 0) { const index = path.indexOf(currentParent); - const { format } = currentBlock; + const { format } = mutateBlock(currentBlock); const newValue = calculateMarginValue(format, isIndent, length); if (newValue !== null) { @@ -124,7 +126,7 @@ export function setModelIndentation( return paragraphOrListItem.length > 0; } -function isSelected(listItem: ContentModelListItem) { +function isSelected(listItem: ReadonlyContentModelListItem) { return listItem.blocks.some(block => { if (block.blockType == 'Paragraph') { return block.segments.some(segment => segment.isSelected); @@ -137,9 +139,9 @@ function isSelected(listItem: ContentModelListItem) { * Otherwise, the margin of the first item will be changed, and the sub list will be created, creating a unintentional margin difference between the list items. */ function isMultilevelSelection( - model: ContentModelDocument, - listItem: ContentModelListItem, - parent: ContentModelBlockGroup + model: ReadonlyContentModelDocument, + listItem: ReadonlyContentModelListItem, + parent: ReadonlyContentModelBlockGroup ) { const listIndex = parent.blocks.indexOf(listItem); for (let i = listIndex - 1; i >= 0; i--) { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts index faa023c42a8..326f144bfa8 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts @@ -6,16 +6,18 @@ import { deleteSelection, getClosestAncestorBlockGroupIndex, setSelection, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { ContentModelBlock, - ContentModelBlockGroup, ContentModelDocument, ContentModelEntity, ContentModelParagraph, FormatContentModelContext, InsertEntityPosition, InsertPoint, + ReadonlyContentModelBlock, + ShallowMutableContentModelBlockGroup, } from 'roosterjs-content-model-types'; /** @@ -30,7 +32,7 @@ export function insertEntityModel( context?: FormatContentModelContext, insertPointOverride?: InsertPoint ) { - let blockParent: ContentModelBlockGroup | undefined; + let blockParent: ShallowMutableContentModelBlockGroup | undefined; let blockIndex = -1; let insertPoint: InsertPoint | null; @@ -57,9 +59,10 @@ export function insertEntityModel( position == 'root' ? getClosestAncestorBlockGroupIndex(path, ['TableCell', 'Document']) : 0; - blockParent = path[pathIndex]; + blockParent = mutateBlock(path[pathIndex]); + const child = path[pathIndex - 1]; - const directChild: ContentModelBlock = + const directChild: ReadonlyContentModelBlock = child?.blockGroupType == 'FormatContainer' || child?.blockGroupType == 'General' || child?.blockGroupType == 'ListItem' @@ -80,7 +83,7 @@ export function insertEntityModel( blocksToInsert.push(entityModel); if (nextBlock?.blockType == 'Paragraph') { - nextParagraph = nextBlock; + nextParagraph = mutateBlock(nextBlock); } else if (!nextBlock || nextBlock.blockType == 'Entity' || focusAfterEntity) { nextParagraph = createParagraph(false /*isImplicit*/, {}, model.format); nextParagraph.segments.push(createBr(model.format)); diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts index 0031bb195a0..7aae91ee484 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts @@ -2,14 +2,15 @@ import { getClosestAncestorBlockGroupIndex, hasSelectionInBlock, hasSelectionInBlockGroup, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, DeleteSelectionContext, DeleteSelectionStep, + ReadonlyContentModelBlock, } from 'roosterjs-content-model-types'; -function isEmptyBlock(block: ContentModelBlock | undefined): boolean { +function isEmptyBlock(block: ReadonlyContentModelBlock | undefined): boolean { if (block && block.blockType == 'Paragraph') { return block.segments.every( segment => segment.segmentType !== 'SelectionMarker' && segment.segmentType == 'Br' @@ -53,7 +54,7 @@ export const deleteEmptyList: DeleteSelectionStep = (context: DeleteSelectionCon nextBlock && isEmptyBlock(nextBlock) ) { - item.levels = []; + mutateBlock(item).levels = []; } } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/block/setParagraphNotImplicit.ts b/packages/roosterjs-content-model-dom/lib/modelApi/block/setParagraphNotImplicit.ts index 949e3cb3b50..734c9999a0c 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/block/setParagraphNotImplicit.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/block/setParagraphNotImplicit.ts @@ -1,11 +1,12 @@ -import type { ContentModelBlock } from 'roosterjs-content-model-types'; +import { mutateBlock } from '../common/mutate'; +import type { ReadonlyContentModelBlock } from 'roosterjs-content-model-types'; /** * For a given block, if it is a paragraph, set it to be not-implicit * @param block The block to check */ -export function setParagraphNotImplicit(block: ContentModelBlock) { +export function setParagraphNotImplicit(block: ReadonlyContentModelBlock) { if (block.blockType == 'Paragraph' && block.isImplicit) { - block.isImplicit = false; + mutateBlock(block).isImplicit = false; } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts index d564538490a..de48b006eb9 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts @@ -1,10 +1,10 @@ import { ensureParagraph } from './ensureParagraph'; import type { ContentModelBlockFormat, - ContentModelBlockGroup, ContentModelParagraph, ContentModelSegment, ContentModelSegmentFormat, + ShallowMutableContentModelBlockGroup, } from 'roosterjs-content-model-types'; /** @@ -15,7 +15,7 @@ import type { * @returns The parent paragraph where the segment is added to */ export function addSegment( - group: ContentModelBlockGroup, + group: ShallowMutableContentModelBlockGroup, newSegment: ContentModelSegment, blockFormat?: ContentModelBlockFormat, segmentFormat?: ContentModelSegmentFormat diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts index e638f1920ce..9cadb347804 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts @@ -1,10 +1,11 @@ import { addBlock } from './addBlock'; import { createParagraph } from '../creators/createParagraph'; +import { mutateBlock } from './mutate'; import type { ContentModelBlockFormat, - ContentModelBlockGroup, ContentModelParagraph, ContentModelSegmentFormat, + ShallowMutableContentModelBlockGroup, } from 'roosterjs-content-model-types'; /** @@ -14,14 +15,14 @@ import type { * @param blockFormat Format of the paragraph. This is only used if we need to create a new paragraph */ export function ensureParagraph( - group: ContentModelBlockGroup, + group: ShallowMutableContentModelBlockGroup, blockFormat?: ContentModelBlockFormat, segmentFormat?: ContentModelSegmentFormat ): ContentModelParagraph { const lastBlock = group.blocks[group.blocks.length - 1]; if (lastBlock?.blockType == 'Paragraph') { - return lastBlock; + return mutateBlock(lastBlock); } else { const paragraph = createParagraph(true, blockFormat, segmentFormat); addBlock(group, paragraph); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel.ts index a808e975a4c..46f648c85a1 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel.ts @@ -1,7 +1,8 @@ import { isBlockEmpty } from './isEmpty'; +import { mutateBlock } from './mutate'; import { normalizeParagraph } from './normalizeParagraph'; import { unwrapBlock } from './unwrapBlock'; -import type { ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { ReadonlyContentModelBlockGroup } from 'roosterjs-content-model-types'; /** * For a given content model, normalize it to make the model be consistent. @@ -12,7 +13,7 @@ import type { ContentModelBlockGroup } from 'roosterjs-content-model-types'; * - For an empty block, remove it * @param group The root level block group of content model to normalize */ -export function normalizeContentModel(group: ContentModelBlockGroup) { +export function normalizeContentModel(group: ReadonlyContentModelBlockGroup) { for (let i = group.blocks.length - 1; i >= 0; i--) { const block = group.blocks[i]; @@ -40,7 +41,7 @@ export function normalizeContentModel(group: ContentModelBlockGroup) { } if (isBlockEmpty(block)) { - group.blocks.splice(i, 1); + mutateBlock(group).blocks.splice(i, 1); } } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts index 14616124d47..b9c120c5a83 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts @@ -2,18 +2,19 @@ import { areSameFormats } from '../../domToModel/utils/areSameFormats'; import { createBr } from '../creators/createBr'; import { isSegmentEmpty } from './isEmpty'; import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved'; +import { mutateBlock, mutateSegment } from './mutate'; import { normalizeAllSegments } from './normalizeSegment'; import type { - ContentModelParagraph, - ContentModelSegment, ContentModelSegmentFormat, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, } from 'roosterjs-content-model-types'; /** * @param paragraph The paragraph to normalize * Normalize a paragraph. If it is empty, add a BR segment to make sure it can insert content */ -export function normalizeParagraph(paragraph: ContentModelParagraph) { +export function normalizeParagraph(paragraph: ReadonlyContentModelParagraph) { const segments = paragraph.segments; if (!paragraph.isImplicit && segments.length > 0) { @@ -24,7 +25,7 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) { last.segmentType == 'SelectionMarker' && (!secondLast || secondLast.segmentType == 'Br') ) { - segments.push(createBr(last.format)); + mutateBlock(paragraph).segments.push(createBr(last.format)); } else if (segments.length > 1 && segments[segments.length - 1].segmentType == 'Br') { const noMarkerSegments = segments.filter(x => x.segmentType != 'SelectionMarker'); @@ -34,7 +35,7 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) { noMarkerSegments.length > 1 && noMarkerSegments[noMarkerSegments.length - 2].segmentType != 'Br' ) { - segments.pop(); + mutateBlock(paragraph).segments.pop(); } } } @@ -50,20 +51,21 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) { moveUpSegmentFormat(paragraph); } -function removeEmptySegments(block: ContentModelParagraph) { +function removeEmptySegments(block: ReadonlyContentModelParagraph) { for (let j = block.segments.length - 1; j >= 0; j--) { if (isSegmentEmpty(block.segments[j])) { - block.segments.splice(j, 1); + mutateBlock(block).segments.splice(j, 1); } } } -function removeEmptyLinks(paragraph: ContentModelParagraph) { +function removeEmptyLinks(paragraph: ReadonlyContentModelParagraph) { const marker = paragraph.segments.find(x => x.segmentType == 'SelectionMarker'); if (marker) { const markerIndex = paragraph.segments.indexOf(marker); const prev = paragraph.segments[markerIndex - 1]; const next = paragraph.segments[markerIndex + 1]; + if ( (prev && !prev.link && @@ -76,7 +78,9 @@ function removeEmptyLinks(paragraph: ContentModelParagraph) { !next.link && areSameFormats(next.format, marker.format)) ) { - delete marker.link; + mutateSegment(paragraph, marker, mutableMarker => { + delete mutableMarker.link; + }); } } } @@ -85,7 +89,7 @@ type FormatsToMoveUp = 'fontFamily' | 'fontSize' | 'textColor'; const formatsToMoveUp: FormatsToMoveUp[] = ['fontFamily', 'fontSize', 'textColor']; // When all segments are sharing the same segment format (font name, size and color), we can move its format to paragraph -function moveUpSegmentFormat(paragraph: ContentModelParagraph) { +function moveUpSegmentFormat(paragraph: ReadonlyContentModelParagraph) { if (!paragraph.decorator) { const segments = paragraph.segments.filter(x => x.segmentType != 'SelectionMarker'); const target = paragraph.segmentFormat || {}; @@ -96,13 +100,13 @@ function moveUpSegmentFormat(paragraph: ContentModelParagraph) { }); if (changed) { - paragraph.segmentFormat = target; + mutateBlock(paragraph).segmentFormat = target; } } } function internalMoveUpSegmentFormat( - segments: ContentModelSegment[], + segments: ReadonlyContentModelSegment[], target: ContentModelSegmentFormat, formatKey: FormatsToMoveUp ): boolean { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeSegment.ts index 59bb9be59e8..d191f4ab61c 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/normalizeSegment.ts @@ -1,8 +1,9 @@ import { hasSpacesOnly } from './hasSpacesOnly'; +import { mutateSegment } from './mutate'; import type { - ContentModelParagraph, - ContentModelSegment, - ContentModelText, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, + ReadonlyContentModelText, } from 'roosterjs-content-model-types'; const SPACE = '\u0020'; @@ -13,15 +14,15 @@ const TRAILING_SPACE_REGEX = /\u0020+$/; /** * @internal */ -export function normalizeAllSegments(paragraph: ContentModelParagraph) { +export function normalizeAllSegments(paragraph: ReadonlyContentModelParagraph) { const context = resetNormalizeSegmentContext(); paragraph.segments.forEach(segment => { - normalizeSegment(segment, context); + normalizeSegment(paragraph, segment, context); }); - normalizeTextSegments(context.textSegments, context.lastInlineSegment); - normalizeLastTextSegment(context.lastTextSegment, context.lastInlineSegment); + normalizeTextSegments(paragraph, context.textSegments, context.lastInlineSegment); + normalizeLastTextSegment(paragraph, context.lastTextSegment, context.lastInlineSegment); } /** @@ -30,24 +31,25 @@ export function normalizeAllSegments(paragraph: ContentModelParagraph) { * @param ignoreTrailingSpaces Whether we should ignore the trailing space of the text segment @default false */ export function normalizeSingleSegment( - segment: ContentModelSegment, + paragraph: ReadonlyContentModelParagraph, + segment: ReadonlyContentModelSegment, ignoreTrailingSpaces: boolean = false ) { const context = resetNormalizeSegmentContext(); context.ignoreTrailingSpaces = ignoreTrailingSpaces; - normalizeSegment(segment, context); + normalizeSegment(paragraph, segment, context); } /** * @internal Export for test only */ export interface NormalizeSegmentContext { - textSegments: ContentModelText[]; + textSegments: ReadonlyContentModelText[]; ignoreLeadingSpaces: boolean; ignoreTrailingSpaces: boolean; - lastTextSegment: ContentModelText | undefined; - lastInlineSegment: ContentModelSegment | undefined; + lastTextSegment: ReadonlyContentModelText | undefined; + lastInlineSegment: ReadonlyContentModelSegment | undefined; } /** @@ -72,11 +74,15 @@ function resetNormalizeSegmentContext( /** * @internal Export for test only */ -export function normalizeSegment(segment: ContentModelSegment, context: NormalizeSegmentContext) { +export function normalizeSegment( + paragraph: ReadonlyContentModelParagraph, + segment: ReadonlyContentModelSegment, + context: NormalizeSegmentContext +) { switch (segment.segmentType) { case 'Br': - normalizeTextSegments(context.textSegments, context.lastInlineSegment); - normalizeLastTextSegment(context.lastTextSegment, context.lastInlineSegment); + normalizeTextSegments(paragraph, context.textSegments, context.lastInlineSegment); + normalizeLastTextSegment(paragraph, context.lastTextSegment, context.lastInlineSegment); // Line ends, reset all states resetNormalizeSegmentContext(context); @@ -103,18 +109,22 @@ export function normalizeSegment(segment: ContentModelSegment, context: Normaliz if (!hasSpacesOnly(segment.text)) { if (first == SPACE) { // 1. Multiple leading space => single   or empty (depends on if previous segment ends with space) - segment.text = segment.text.replace( - LEADING_SPACE_REGEX, - context.ignoreLeadingSpaces ? '' : NONE_BREAK_SPACE - ); + mutateSegment(paragraph, segment, textSegment => { + textSegment.text = textSegment.text.replace( + LEADING_SPACE_REGEX, + context.ignoreLeadingSpaces ? '' : NONE_BREAK_SPACE + ); + }); } if (last == SPACE) { // 2. Multiple trailing space => single space - segment.text = segment.text.replace( - TRAILING_SPACE_REGEX, - context.ignoreTrailingSpaces ? SPACE : NONE_BREAK_SPACE - ); + mutateSegment(paragraph, segment, textSegment => { + textSegment.text = textSegment.text.replace( + TRAILING_SPACE_REGEX, + context.ignoreTrailingSpaces ? SPACE : NONE_BREAK_SPACE + ); + }); } } @@ -125,8 +135,9 @@ export function normalizeSegment(segment: ContentModelSegment, context: Normaliz } function normalizeTextSegments( - segments: ContentModelText[], - lastInlineSegment: ContentModelSegment | undefined + paragraph: ReadonlyContentModelParagraph, + segments: ReadonlyContentModelText[], + lastInlineSegment: ReadonlyContentModelSegment | undefined ) { segments.forEach(segment => { // 3. Segment ends with   replace it with space if the previous char is not space so that next segment can wrap @@ -139,18 +150,23 @@ function normalizeTextSegments( text.length > 1 && text.substr(-2, 1) != SPACE ) { - segment.text = text.substring(0, text.length - 1) + SPACE; + mutateSegment(paragraph, segment, textSegment => { + textSegment.text = text.substring(0, text.length - 1) + SPACE; + }); } } }); } function normalizeLastTextSegment( - segment: ContentModelText | undefined, - lastInlineSegment: ContentModelSegment | undefined + paragraph: ReadonlyContentModelParagraph, + segment: ReadonlyContentModelText | undefined, + lastInlineSegment: ReadonlyContentModelSegment | undefined ) { if (segment && segment == lastInlineSegment && segment?.text.substr(-1) == SPACE) { // 4. last text segment of the paragraph, remove trailing space - segment.text = segment.text.replace(TRAILING_SPACE_REGEX, ''); + mutateSegment(paragraph, segment, textSegment => { + textSegment.text = textSegment.text.replace(TRAILING_SPACE_REGEX, ''); + }); } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/unwrapBlock.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/unwrapBlock.ts index 312f007bfab..375f0a22117 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/unwrapBlock.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/unwrapBlock.ts @@ -1,5 +1,9 @@ +import { mutateBlock } from './mutate'; import { setParagraphNotImplicit } from '../block/setParagraphNotImplicit'; -import type { ContentModelBlock, ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, +} from 'roosterjs-content-model-types'; /** * Unwrap a given block group, move its child blocks to be under its parent group @@ -7,14 +11,16 @@ import type { ContentModelBlock, ContentModelBlockGroup } from 'roosterjs-conten * @param groupToUnwrap The block group to unwrap */ export function unwrapBlock( - parent: ContentModelBlockGroup | null, - groupToUnwrap: ContentModelBlockGroup & ContentModelBlock + parent: ReadonlyContentModelBlockGroup | null, + groupToUnwrap: ReadonlyContentModelBlockGroup & ReadonlyContentModelBlock ) { const index = parent?.blocks.indexOf(groupToUnwrap) ?? -1; if (index >= 0) { groupToUnwrap.blocks.forEach(setParagraphNotImplicit); - parent?.blocks.splice(index, 1, ...groupToUnwrap.blocks); + if (parent) { + mutateBlock(parent)?.blocks.splice(index, 1, ...groupToUnwrap.blocks.map(mutateBlock)); + } } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts index 7ef33b0ce18..95ce5d83fa1 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/applyTableFormat.ts @@ -1,13 +1,14 @@ import { BorderKeys } from '../../formatHandlers/common/borderFormatHandler'; import { combineBorderValue, extractBorderValues } from '../../domUtils/style/borderValues'; +import { mutateBlock } from '../common/mutate'; import { setTableCellBackgroundColor } from './setTableCellBackgroundColor'; import { TableBorderFormat } from '../../constants/TableBorderFormat'; import { updateTableCellMetadata } from '../metadata/updateTableCellMetadata'; import { updateTableMetadata } from '../metadata/updateTableMetadata'; import type { BorderFormat, - ContentModelTable, - ContentModelTableRow, + ReadonlyContentModelTable, + ShallowMutableContentModelTableRow, TableMetadataFormat, } from 'roosterjs-content-model-types'; @@ -39,42 +40,34 @@ type MetaOverrides = { * @param keepCellShade @optional When pass true, table cells with customized shade color will not be overwritten. @default false */ export function applyTableFormat( - table: ContentModelTable, + table: ReadonlyContentModelTable, newFormat?: TableMetadataFormat, keepCellShade?: boolean ) { - const { rows } = table; + const mutableTable = mutateBlock(table); + const { rows } = mutableTable; - updateTableMetadata(table, format => { + updateTableMetadata(mutableTable, format => { const effectiveMetadata = { ...DEFAULT_FORMAT, ...format, - ...(newFormat || {}), + ...newFormat, }; const metaOverrides: MetaOverrides = updateOverrides(rows, !keepCellShade); - delete table.cachedElement; - - clearCache(rows); formatCells(rows, effectiveMetadata, metaOverrides); setFirstColumnFormat(rows, effectiveMetadata, metaOverrides); setHeaderRowFormat(rows, effectiveMetadata, metaOverrides); - return effectiveMetadata; - }); -} - -function clearCache(rows: ContentModelTableRow[]) { - rows.forEach(row => { - row.cells.forEach(cell => { - delete cell.cachedElement; - }); - delete row.cachedElement; + return effectiveMetadata; }); } -function updateOverrides(rows: ContentModelTableRow[], removeCellShade: boolean): MetaOverrides { +function updateOverrides( + rows: ShallowMutableContentModelTableRow[], + removeCellShade: boolean +): MetaOverrides { const overrides: MetaOverrides = { bgColorOverrides: [], vAlignOverrides: [], @@ -91,7 +84,7 @@ function updateOverrides(rows: ContentModelTableRow[], removeCellShade: boolean) overrides.borderOverrides.push(borderOverrides); row.cells.forEach(cell => { - updateTableCellMetadata(cell, metadata => { + updateTableCellMetadata(mutateBlock(cell), metadata => { if (metadata && removeCellShade) { bgColorOverrides.push(false); delete metadata.bgColorOverride; @@ -172,14 +165,16 @@ const BorderFormatters: Record = * Apply vertical align, borders, and background color to all cells in the table */ function formatCells( - rows: ContentModelTableRow[], + rows: ShallowMutableContentModelTableRow[], format: TableMetadataFormat, metaOverrides: MetaOverrides ) { const { hasBandedRows, hasBandedColumns, bgColorOdd, bgColorEven } = format; rows.forEach((row, rowIndex) => { - row.cells.forEach((cell, colIndex) => { + row.cells.forEach((readonlyCell, colIndex) => { + const cell = mutateBlock(readonlyCell); + // Format Borders if ( !metaOverrides.borderOverrides[rowIndex][colIndex] && @@ -237,12 +232,14 @@ function formatCells( } function setFirstColumnFormat( - rows: ContentModelTableRow[], + rows: ShallowMutableContentModelTableRow[], format: Partial, metaOverrides: MetaOverrides ) { rows.forEach((row, rowIndex) => { - row.cells.forEach((cell, cellIndex) => { + row.cells.forEach((readonlyCell, cellIndex) => { + const cell = mutateBlock(readonlyCell); + if (format.hasFirstColumn && cellIndex === 0) { cell.isHeader = true; @@ -267,13 +264,15 @@ function setFirstColumnFormat( } function setHeaderRowFormat( - rows: ContentModelTableRow[], + rows: ShallowMutableContentModelTableRow[], format: TableMetadataFormat, metaOverrides: MetaOverrides ) { const rowIndex = 0; - rows[rowIndex]?.cells.forEach((cell, cellIndex) => { + rows[rowIndex]?.cells.forEach((readonlyCell, cellIndex) => { + const cell = mutateBlock(readonlyCell); + cell.isHeader = format.hasHeaderRow; if (format.hasHeaderRow && format.headerRowColor) { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteBlock.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteBlock.ts index 6b58dc828e9..a7acdd276a8 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteBlock.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteBlock.ts @@ -1,7 +1,7 @@ import type { - ContentModelBlock, EntityRemovalOperation, FormatContentModelContext, + ReadonlyContentModelBlock, } from 'roosterjs-content-model-types'; /** @@ -14,9 +14,9 @@ import type { * If not specified, only selected entity will be deleted */ export function deleteBlock( - blocks: ContentModelBlock[], - blockToDelete: ContentModelBlock, - replacement?: ContentModelBlock, + blocks: ReadonlyContentModelBlock[], + blockToDelete: ReadonlyContentModelBlock, + replacement?: ReadonlyContentModelBlock, context?: FormatContentModelContext, direction?: 'forward' | 'backward' ): boolean { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts index f2131c384a6..0954e6e7006 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts @@ -5,17 +5,18 @@ import { deleteBlock } from './deleteBlock'; import { deleteSegment } from './deleteSegment'; import { getSegmentTextFormat } from './getSegmentTextFormat'; import { iterateSelections } from '../selection/iterateSelections'; +import { mutateBlock, mutateSegments } from '../common/mutate'; import { setParagraphNotImplicit } from '../block/setParagraphNotImplicit'; import type { - ContentModelBlockGroup, - ContentModelDocument, ContentModelParagraph, ContentModelSelectionMarker, DeleteSelectionContext, FormatContentModelContext, InsertPoint, IterateSelectionsOption, - TableSelectionContext, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, + ReadonlyTableSelectionContext, } from 'roosterjs-content-model-types'; const DeleteSelectionIteratingOptions: IterateSelectionsOption = { @@ -30,7 +31,7 @@ const DeleteSelectionIteratingOptions: IterateSelectionsOption = { * at the first deleted position so that we know where to put cursor to after delete */ export function deleteExpandedSelection( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, formatContext?: FormatContentModelContext ): DeleteSelectionContext { const context: DeleteSelectionContext = { @@ -41,7 +42,7 @@ export function deleteExpandedSelection( iterateSelections( model, - (path, tableContext, block, segments) => { + (path, tableContext, readonlyBlock, readonlySegments) => { // Set paragraph, format and index for default position where we will put cursor to. // Later we can overwrite these info when process the selections let paragraph = createParagraph( @@ -52,13 +53,15 @@ export function deleteExpandedSelection( let markerFormat = model.format; let insertMarkerIndex = 0; - if (segments) { + if (readonlySegments && readonlyBlock?.blockType == 'Paragraph') { + const [block, segments, indexes] = mutateSegments(readonlyBlock, readonlySegments); + // Delete segments inside a paragraph - if (segments[0] && block?.blockType == 'Paragraph') { + if (segments[0]) { // Now that we have found a paragraph with selections, we can overwrite the default paragraph with this one // so we can put cursor here after delete paragraph = block; - insertMarkerIndex = paragraph.segments.indexOf(segments[0]); + insertMarkerIndex = indexes[0]; markerFormat = getSegmentTextFormat(segments[0]); context.lastParagraph = paragraph; @@ -90,25 +93,24 @@ export function deleteExpandedSelection( setParagraphNotImplicit(block); } } - } else if (block) { + } else if (readonlyBlock) { // Delete a whole block (divider, table, ...) - const blocks = path[0].blocks; + const blocks = mutateBlock(path[0]).blocks; - if (deleteBlock(blocks, block, paragraph, context.formatContext)) { + if (deleteBlock(blocks, readonlyBlock, paragraph, context.formatContext)) { context.deleteResult = 'range'; } } else if (tableContext) { // Delete a whole table cell const { table, colIndex, rowIndex } = tableContext; - const row = table.rows[rowIndex]; - const cell = row.cells[colIndex]; + const mutableTable = mutateBlock(table); + const row = mutableTable.rows[rowIndex]; + const cell = mutateBlock(row.cells[colIndex]); path = [cell, ...path]; paragraph.segments.push(createBr(model.format)); cell.blocks = [paragraph]; - delete cell.cachedElement; - delete row.cachedElement; context.deleteResult = 'range'; } @@ -130,8 +132,8 @@ export function deleteExpandedSelection( function createInsertPoint( marker: ContentModelSelectionMarker, paragraph: ContentModelParagraph, - path: ContentModelBlockGroup[], - tableContext: TableSelectionContext | undefined + path: ReadonlyContentModelBlockGroup[], + tableContext: ReadonlyTableSelectionContext | undefined ): InsertPoint { return { marker, diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSegment.ts index ee809726160..d1018ec16dc 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSegment.ts @@ -1,39 +1,43 @@ import { deleteSingleChar } from './deleteSingleChar'; import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved'; +import { mutateSegment } from '../common/mutate'; import { normalizeSingleSegment } from '../common/normalizeSegment'; import { normalizeText } from '../../domUtils/stringUtil'; import type { - ContentModelParagraph, - ContentModelSegment, EntityRemovalOperation, FormatContentModelContext, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, } from 'roosterjs-content-model-types'; /** * Delete a content model segment from current selection - * @param paragraph Parent paragraph of the segment to delete - * @param segmentToDelete The segment to delete + * @param readonlyParagraph Parent paragraph of the segment to delete + * @param readonlySegmentToDelete The segment to delete * @param context @optional Context object provided by formatContentModel API * @param direction @optional Whether this is deleting forward or backward. This is only used for deleting entity. * If not specified, only selected entity will be deleted */ export function deleteSegment( - paragraph: ContentModelParagraph, - segmentToDelete: ContentModelSegment, + readonlyParagraph: ReadonlyContentModelParagraph, + readonlySegmentToDelete: ReadonlyContentModelSegment, context?: FormatContentModelContext, direction?: 'forward' | 'backward' ): boolean { + const [paragraph, segmentToDelete, index] = mutateSegment( + readonlyParagraph, + readonlySegmentToDelete + ); const segments = paragraph.segments; - const index = segments.indexOf(segmentToDelete); const preserveWhiteSpace = isWhiteSpacePreserved(paragraph.format.whiteSpace); const isForward = direction == 'forward'; const isBackward = direction == 'backward'; if (!preserveWhiteSpace) { - normalizePreviousSegment(segments, index); + normalizePreviousSegment(paragraph, segments, index); } - switch (segmentToDelete.segmentType) { + switch (segmentToDelete?.segmentType) { case 'Br': case 'Image': case 'SelectionMarker': @@ -86,10 +90,17 @@ export function deleteSegment( } else { return false; } + + default: + return false; } } -function normalizePreviousSegment(segments: ContentModelSegment[], currentIndex: number) { +function normalizePreviousSegment( + paragraph: ReadonlyContentModelParagraph, + segments: ReadonlyArray, + currentIndex: number +) { let index = currentIndex - 1; while (segments[index]?.segmentType == 'SelectionMarker') { @@ -99,6 +110,6 @@ function normalizePreviousSegment(segments: ContentModelSegment[], currentIndex: const segment = segments[index]; if (segment) { - normalizeSingleSegment(segment); + normalizeSingleSegment(paragraph, segment); } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts index 20ff0629c65..adcd718f964 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts @@ -1,10 +1,11 @@ import { deleteExpandedSelection } from './deleteExpandedSelection'; +import { mutateBlock } from '../common/mutate'; import type { - ContentModelDocument, DeleteSelectionContext, DeleteSelectionResult, DeleteSelectionStep, FormatContentModelContext, + ReadonlyContentModelDocument, ValidDeleteSelectionContext, } from 'roosterjs-content-model-types'; @@ -16,7 +17,7 @@ import type { * @returns A DeleteSelectionResult object to specify the deletion result */ export function deleteSelection( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, additionalSteps: (DeleteSelectionStep | null)[] = [], formatContext?: FormatContentModelContext ): DeleteSelectionResult { @@ -50,7 +51,9 @@ function mergeParagraphAfterDelete(context: DeleteSelectionContext) { lastParagraph != insertPoint.paragraph && lastTableContext == insertPoint.tableContext ) { - insertPoint.paragraph.segments.push(...lastParagraph.segments); - lastParagraph.segments = []; + const mutableLastParagraph = mutateBlock(lastParagraph); + + mutateBlock(insertPoint.paragraph).segments.push(...mutableLastParagraph.segments); + mutableLastParagraph.segments = []; } } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index ed46c5c1d09..47f7abcd0b1 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -7,12 +7,12 @@ import { createTableCell } from '../creators/createTableCell'; import { deleteSelection } from './deleteSelection'; import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; import { getObjectKeys } from '../..//domUtils/getObjectKeys'; +import { mutateBlock } from '../common/mutate'; import { normalizeContentModel } from '../common/normalizeContentModel'; import { normalizeTable } from './normalizeTable'; import type { ContentModelBlock, ContentModelBlockFormat, - ContentModelBlockGroup, ContentModelDocument, ContentModelListItem, ContentModelParagraph, @@ -21,6 +21,9 @@ import type { FormatContentModelContext, InsertPoint, MergeModelOption, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; const HeadingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; @@ -34,7 +37,7 @@ const HeadingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; * @returns Insert point after merge, or null if there is no insert point */ export function mergeModel( - target: ContentModelDocument, + target: ReadonlyContentModelDocument, source: ContentModelDocument, context?: FormatContentModelContext, options?: MergeModelOption @@ -170,7 +173,9 @@ function mergeTable( const { tableContext, marker } = markerPosition; if (tableContext && source.blocks.length == 1 && source.blocks[0] == newTable) { - const { table, colIndex, rowIndex } = tableContext; + const { table: readonlyTable, colIndex, rowIndex } = tableContext; + const table = mutateBlock(readonlyTable); + for (let i = 0; i < newTable.rows.length; i++) { for (let j = 0; j < newTable.rows[i].cells.length; j++) { const newCell = newTable.rows[i].cells[j]; @@ -242,7 +247,7 @@ function mergeList(markerPosition: InsertPoint, newList: ContentModelListItem) { const blockIndex = listParent.blocks.indexOf(listItem || paragraph); if (blockIndex >= 0) { - listParent.blocks.splice(blockIndex, 0, newList); + mutateBlock(listParent).blocks.splice(blockIndex, 0, newList); } if (listItem) { @@ -267,7 +272,7 @@ function splitParagraph(markerPosition: InsertPoint, newParaFormat: ContentModel } if (paraIndex >= 0) { - path[0].blocks.splice(paraIndex + 1, 0, newParagraph); + mutateBlock(path[0]).blocks.splice(paraIndex + 1, 0, newParagraph); } const listItemIndex = getClosestAncestorBlockGroupIndex( @@ -289,7 +294,7 @@ function splitParagraph(markerPosition: InsertPoint, newParaFormat: ContentModel } if (blockIndex >= 0) { - listParent.blocks.splice(blockIndex + 1, 0, newListItem); + mutateBlock(listParent).blocks.splice(blockIndex + 1, 0, newListItem); } path[listItemIndex] = newListItem; @@ -308,21 +313,22 @@ function insertBlock(markerPosition: InsertPoint, block: ContentModelBlock) { const blockIndex = path[0].blocks.indexOf(newPara); if (blockIndex >= 0) { - path[0].blocks.splice(blockIndex, 0, block); + mutateBlock(path[0]).blocks.splice(blockIndex, 0, block); } } function applyDefaultFormat( - group: ContentModelBlockGroup, + group: ReadonlyContentModelBlockGroup, format: ContentModelSegmentFormat, applyDefaultFormatOption: 'mergeAll' | 'keepSourceEmphasisFormat' ) { group.blocks.forEach(block => { mergeBlockFormat(applyDefaultFormatOption, block); + switch (block.blockType) { case 'BlockGroup': if (block.blockGroupType == 'ListItem') { - block.formatHolder.format = mergeSegmentFormat( + mutateBlock(block).formatHolder.format = mergeSegmentFormat( applyDefaultFormatOption, format, block.formatHolder.format @@ -341,7 +347,9 @@ function applyDefaultFormat( case 'Paragraph': const paragraphFormat = block.decorator?.format || {}; - block.segments.forEach(segment => { + const paragraph = mutateBlock(block); + + paragraph.segments.forEach(segment => { if (segment.segmentType == 'General') { applyDefaultFormat(segment, format, applyDefaultFormatOption); } @@ -353,28 +361,28 @@ function applyDefaultFormat( }); if (applyDefaultFormatOption === 'keepSourceEmphasisFormat') { - delete block.decorator; + delete paragraph.decorator; } break; } }); } -function mergeBlockFormat(applyDefaultFormatOption: string, block: ContentModelBlock) { +function mergeBlockFormat(applyDefaultFormatOption: string, block: ReadonlyContentModelBlock) { if (applyDefaultFormatOption == 'keepSourceEmphasisFormat' && block.format.backgroundColor) { - delete block.format.backgroundColor; + delete mutateBlock(block).format.backgroundColor; } } function mergeSegmentFormat( applyDefaultFormatOption: 'mergeAll' | 'keepSourceEmphasisFormat', - targetformat: ContentModelSegmentFormat, + targetFormat: ContentModelSegmentFormat, sourceFormat: ContentModelSegmentFormat ): ContentModelSegmentFormat { return applyDefaultFormatOption == 'mergeAll' - ? { ...targetformat, ...sourceFormat } + ? { ...targetFormat, ...sourceFormat } : { - ...targetformat, + ...targetFormat, ...getSemanticFormat(sourceFormat), }; } 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 354decd7884..76a57ab0bcf 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts @@ -2,11 +2,12 @@ import { addBlock } from '../common/addBlock'; import { addSegment } from '../common/addSegment'; import { createBr } from '../creators/createBr'; import { createParagraph } from '../creators/createParagraph'; +import { mutateBlock } from '../common/mutate'; import type { - ContentModelSegment, ContentModelSegmentFormat, - ContentModelTable, - ContentModelTableCell, + ReadonlyContentModelSegment, + ReadonlyContentModelTable, + ReadonlyContentModelTableCell, } from 'roosterjs-content-model-types'; /** @@ -23,13 +24,15 @@ const MIN_HEIGHT = 22; * 4. Table and table row have correct width/height * 5. Spanned cell has no child blocks * 6. default format is correctly applied - * @param table The table to normalize + * @param readonlyTable The table to normalize * @param defaultSegmentFormat @optional Default segment format to apply to cell */ export function normalizeTable( - table: ContentModelTable, + readonlyTable: ReadonlyContentModelTable, defaultSegmentFormat?: ContentModelSegmentFormat ) { + const table = mutateBlock(readonlyTable); + // Always collapse border and use border box for table in roosterjs to make layout simpler const format = table.format; @@ -42,7 +45,9 @@ export function normalizeTable( // Make sure all inner cells are not header // Make sure all cells have content and width table.rows.forEach((row, rowIndex) => { - row.cells.forEach((cell, colIndex) => { + row.cells.forEach((readonlyCell, colIndex) => { + const cell = mutateBlock(readonlyCell); + if (cell.blocks.length == 0) { const format = cell.format.textColor ? { @@ -137,18 +142,21 @@ function getTableCellWidth(columns: number): number { } } -function tryMoveBlocks(targetCell: ContentModelTableCell, sourceCell: ContentModelTableCell) { +function tryMoveBlocks( + targetCell: ReadonlyContentModelTableCell, + sourceCell: ReadonlyContentModelTableCell +) { const onlyHasEmptyOrBr = sourceCell.blocks.every( block => block.blockType == 'Paragraph' && hasOnlyBrSegment(block.segments) ); if (!onlyHasEmptyOrBr) { - targetCell.blocks.push(...sourceCell.blocks); - sourceCell.blocks = []; + mutateBlock(targetCell).blocks.push(...sourceCell.blocks); + mutateBlock(sourceCell).blocks = []; } } -function hasOnlyBrSegment(segments: ContentModelSegment[]): boolean { +function hasOnlyBrSegment(segments: ReadonlyArray): boolean { segments = segments.filter(s => s.segmentType != 'SelectionMarker'); return segments.length == 0 || (segments.length == 1 && segments[0].segmentType == 'Br'); 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 96df09590b7..258e3cbb24f 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/setTableCellBackgroundColor.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/setTableCellBackgroundColor.ts @@ -1,6 +1,7 @@ +import { mutateBlock } from '../common/mutate'; import { parseColor } from '../../formatHandlers/utils/color'; import { updateTableCellMetadata } from '../metadata/updateTableCellMetadata'; -import type { ContentModelTableCell } from 'roosterjs-content-model-types'; +import type { ShallowMutableContentModelTableCell } from 'roosterjs-content-model-types'; // Using the HSL (hue, saturation and lightness) representation for RGB color values. // If the value of the lightness is less than 20, the color is dark. @@ -18,7 +19,7 @@ const Black = '#000000'; * @param applyToSegments @optional When pass true, we will also apply text color from table cell to its child blocks and segments */ export function setTableCellBackgroundColor( - cell: ContentModelTableCell, + cell: ShallowMutableContentModelTableCell, color: string | null | undefined, isColorOverride?: boolean, applyToSegments?: boolean @@ -58,9 +59,11 @@ export function setTableCellBackgroundColor( delete cell.cachedElement; } -function removeAdaptiveCellColor(cell: ContentModelTableCell) { - cell.blocks.forEach(block => { - if (block.blockType == 'Paragraph') { +function removeAdaptiveCellColor(cell: ShallowMutableContentModelTableCell) { + cell.blocks.forEach(readonlyBlock => { + if (readonlyBlock.blockType == 'Paragraph') { + const block = mutateBlock(readonlyBlock); + if ( block.segmentFormat?.textColor && shouldRemoveColor(block.segmentFormat?.textColor, cell.format.backgroundColor || '') @@ -79,10 +82,12 @@ function removeAdaptiveCellColor(cell: ContentModelTableCell) { }); } -function setAdaptiveCellColor(cell: ContentModelTableCell) { +function setAdaptiveCellColor(cell: ShallowMutableContentModelTableCell) { if (cell.format.textColor) { - cell.blocks.forEach(block => { - if (block.blockType == 'Paragraph') { + cell.blocks.forEach(readonlyBlock => { + if (readonlyBlock.blockType == 'Paragraph') { + const block = mutateBlock(readonlyBlock); + if (!block.segmentFormat?.textColor) { block.segmentFormat = { ...block.segmentFormat, diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts index ca23f3468b2..484f74d863e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts @@ -1,10 +1,14 @@ import { isGeneralSegment } from '../typeCheck/isGeneralSegment'; +import { mutateBlock, mutateSegment } from '../common/mutate'; import type { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelSegment, - ContentModelTable, - Selectable, + MutableType, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, + ReadonlyContentModelTable, + ReadonlySelectable, + ShallowMutableSelectable, TableCellCoordinate, } from 'roosterjs-content-model-types'; @@ -14,19 +18,23 @@ import type { * @param start The start selected element. If not passed, existing selection of content model will be cleared * @param end The end selected element. If not passed, only the start element will be selected. If passed, all elements between start and end elements will be selected */ -export function setSelection(group: ContentModelBlockGroup, start?: Selectable, end?: Selectable) { +export function setSelection( + group: ReadonlyContentModelBlockGroup, + start?: ReadonlySelectable, + end?: ReadonlySelectable +) { setSelectionToBlockGroup(group, false /*isInSelection*/, start || null, end || null); } function setSelectionToBlockGroup( - group: ContentModelBlockGroup, + group: ReadonlyContentModelBlockGroup, isInSelection: boolean, - start: Selectable | null, - end: Selectable | null + start: ReadonlySelectable | null, + end: ReadonlySelectable | null ): boolean { return handleSelection(isInSelection, group, start, end, isInSelection => { - if (isGeneralSegment(group)) { - setIsSelected(group, isInSelection); + if (isGeneralSegment(group) && needToSetSelection(group, isInSelection)) { + setIsSelected(mutateBlock(group), isInSelection); } group.blocks.forEach(block => { @@ -38,10 +46,10 @@ function setSelectionToBlockGroup( } function setSelectionToBlock( - block: ContentModelBlock, + block: ReadonlyContentModelBlock, isInSelection: boolean, - start: Selectable | null, - end: Selectable | null + start: ReadonlySelectable | null, + end: ReadonlySelectable | null ) { switch (block.blockType) { case 'BlockGroup': @@ -53,10 +61,14 @@ function setSelectionToBlock( case 'Divider': case 'Entity': return handleSelection(isInSelection, block, start, end, isInSelection => { - if (isInSelection) { - block.isSelected = true; - } else { - delete block.isSelected; + if (needToSetSelection(block, isInSelection)) { + const mutableBlock = mutateBlock(block); + + if (isInSelection) { + mutableBlock.isSelected = true; + } else { + delete mutableBlock.isSelected; + } } return isInSelection; @@ -73,6 +85,7 @@ function setSelectionToBlock( end, isInSelection => { return setSelectionToSegment( + block, segment, isInSelection, segmentsToDelete, @@ -84,11 +97,11 @@ function setSelectionToBlock( ); }); - while (segmentsToDelete.length > 0) { - const index = segmentsToDelete.pop()!; + let index: number | undefined; + while ((index = segmentsToDelete.pop()) !== undefined) { if (index >= 0) { - block.segments.splice(index, 1); + mutateBlock(block).segments.splice(index, 1); } } @@ -100,10 +113,10 @@ function setSelectionToBlock( } function setSelectionToTable( - table: ContentModelTable, + table: ReadonlyContentModelTable, isInSelection: boolean, - start: Selectable | null, - end: Selectable | null + start: ReadonlySelectable | null, + end: ReadonlySelectable | null ): boolean { const first = findCell(table, start); const last = end ? findCell(table, end) : first; @@ -116,7 +129,9 @@ function setSelectionToTable( const isSelected = row >= first.row && row <= last.row && col >= first.col && col <= last.col; - setIsSelected(currentCell, isSelected); + if (needToSetSelection(currentCell, isSelected)) { + setIsSelected(mutateBlock(currentCell), isSelected); + } if (!isSelected) { setSelectionToBlockGroup(currentCell, false /*isInSelection*/, start, end); @@ -134,21 +149,27 @@ function setSelectionToTable( return isInSelection; } -function findCell(table: ContentModelTable, cell: Selectable | null): TableCellCoordinate { +function findCell( + table: ReadonlyContentModelTable, + cell: ReadonlySelectable | null +): TableCellCoordinate { let col = -1; const row = cell - ? table.rows.findIndex(row => (col = (row.cells as Selectable[]).indexOf(cell)) >= 0) + ? table.rows.findIndex( + row => (col = (row.cells as ReadonlyArray).indexOf(cell)) >= 0 + ) : -1; return { row, col }; } function setSelectionToSegment( - segment: ContentModelSegment, + paragraph: ReadonlyContentModelParagraph, + segment: ReadonlyContentModelSegment, isInSelection: boolean, segmentsToDelete: number[], - start: Selectable | null, - end: Selectable | null, + start: ReadonlySelectable | null, + end: ReadonlySelectable | null, i: number ) { switch (segment.segmentType) { @@ -162,23 +183,50 @@ function setSelectionToSegment( return isInSelection; case 'General': - setIsSelected(segment, isInSelection); + internalSetSelectionToSegment(paragraph, segment, isInSelection); return segment != start && segment != end ? setSelectionToBlockGroup(segment, isInSelection, start, end) : isInSelection; case 'Image': - setIsSelected(segment, isInSelection); - segment.isSelectedAsImageSelection = start == segment && (!end || end == segment); + const isSelectedAsImageSelection = start == segment && (!end || end == segment); + + internalSetSelectionToSegment( + paragraph, + segment, + isInSelection, + !segment.isSelectedAsImageSelection != !isSelectedAsImageSelection + ? image => (image.isSelectedAsImageSelection = isSelectedAsImageSelection) + : undefined + ); + return isInSelection; default: - setIsSelected(segment, isInSelection); + internalSetSelectionToSegment(paragraph, segment, isInSelection); return isInSelection; } } -function setIsSelected(selectable: Selectable, value: boolean) { +function internalSetSelectionToSegment( + paragraph: ReadonlyContentModelParagraph, + segment: T, + isInSelection: boolean, + additionAction?: (segment: MutableType) => void +) { + if (additionAction || needToSetSelection(segment, isInSelection)) { + mutateSegment(paragraph, segment, mutableSegment => { + setIsSelected(mutableSegment, isInSelection); + additionAction?.(mutableSegment); + }); + } +} + +function needToSetSelection(selectable: ReadonlySelectable, isSelected: boolean) { + return !selectable.isSelected != !isSelected; +} + +function setIsSelected(selectable: ShallowMutableSelectable, value: boolean) { if (value) { selectable.isSelected = true; } else { @@ -190,9 +238,9 @@ function setIsSelected(selectable: Selectable, value: boolean) { function handleSelection( isInSelection: boolean, - model: ContentModelBlockGroup | ContentModelBlock | ContentModelSegment, - start: Selectable | null, - end: Selectable | null, + model: ReadonlyContentModelBlockGroup | ReadonlyContentModelBlock | ReadonlyContentModelSegment, + start: ReadonlySelectable | null, + end: ReadonlySelectable | null, callback: (isInSelection: boolean) => boolean ) { isInSelection = isInSelection || model == start; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts index fdaf459ab6f..337c6d7c4d1 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts @@ -4,15 +4,16 @@ import { deleteBlock, deleteSegment, getClosestAncestorBlockGroupIndex, + mutateBlock, setParagraphNotImplicit, } from 'roosterjs-content-model-dom'; -import type { BlockAndPath } from '../utils/getLeafSiblingBlock'; +import type { ReadonlyBlockAndPath } from '../utils/getLeafSiblingBlock'; import type { - ContentModelBlockGroup, - ContentModelDocument, ContentModelParagraph, ContentModelSegment, DeleteSelectionStep, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteSelectionStep { @@ -29,8 +30,8 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS const index = segments.indexOf(marker) + (isForward ? 1 : -1); const segmentToDelete = segments[index]; - let blockToDelete: BlockAndPath | null; - let root: ContentModelDocument | null; + let blockToDelete: ReadonlyBlockAndPath | null; + let root: ReadonlyContentModelDocument | null; if (segmentToDelete) { if (deleteSegment(paragraph, segmentToDelete, context.formatContext, direction)) { @@ -47,9 +48,11 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS setModelIndentation(root, 'outdent'); context.deleteResult = 'range'; } else if ((blockToDelete = getLeafSiblingBlock(path, paragraph, isForward))) { - const { block, path, siblingSegment } = blockToDelete; + const { block: readonlyBlock, path, siblingSegment } = blockToDelete; + + if (readonlyBlock.blockType == 'Paragraph') { + const block = mutateBlock(readonlyBlock); - if (block.blockType == 'Paragraph') { if (siblingSegment) { // When selection is under general segment, need to check if it has a sibling sibling, and delete from it if (deleteSegment(block, siblingSegment, context.formatContext, direction)) { @@ -70,7 +73,6 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS tableContext, }; context.lastParagraph = paragraph; - delete block.cachedElement; } context.deleteResult = 'range'; @@ -81,8 +83,8 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS } else { if ( deleteBlock( - path[0].blocks, - block, + mutateBlock(path[0]).blocks, + readonlyBlock, undefined /*replacement*/, context.formatContext, direction @@ -100,7 +102,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS }; } -function getRoot(path: ContentModelBlockGroup[]): ContentModelDocument | null { +function getRoot(path: ReadonlyContentModelBlockGroup[]): ReadonlyContentModelDocument | null { const lastInPath = path[path.length - 1]; return lastInPath.blockGroupType == 'Document' ? lastInPath : null; } @@ -109,7 +111,7 @@ function shouldOutdentParagraph( isForward: boolean, segments: ContentModelSegment[], paragraph: ContentModelParagraph, - path: ContentModelBlockGroup[] + path: ReadonlyContentModelBlockGroup[] ) { return ( !isForward && 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 cb337857438..8148d5f9a66 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts @@ -4,11 +4,12 @@ import { unwrapBlock, getClosestAncestorBlockGroupIndex, isBlockGroupOfType, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlockGroup, ContentModelFormatContainer, DeleteSelectionStep, + ReadonlyContentModelBlockGroup, } from 'roosterjs-content-model-types'; /** @@ -75,7 +76,7 @@ const isSelectionOnEmptyLine = (quote: ContentModelFormatContainer) => { const insertNewLine = ( quote: ContentModelFormatContainer, - parent: ContentModelBlockGroup, + parent: ReadonlyContentModelBlockGroup, index: number ) => { const quoteLength = quote.blocks.length; @@ -83,5 +84,5 @@ const insertNewLine = ( const marker = createSelectionMarker(); const newParagraph = createParagraph(false /* isImplicit */); newParagraph.segments.push(marker); - parent.blocks.splice(index + 1, 0, newParagraph); + mutateBlock(parent).blocks.splice(index + 1, 0, newParagraph); }; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 869d20ac8f5..38e5867b870 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -10,12 +10,14 @@ import { setParagraphNotImplicit, getClosestAncestorBlockGroupIndex, isBlockGroupOfType, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlockGroup, ContentModelListItem, DeleteSelectionStep, InsertPoint, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelListItem, ValidDeleteSelectionContext, } from 'roosterjs-content-model-types'; @@ -66,7 +68,7 @@ export const handleEnterOnList: DeleteSelectionStep = context => { lastParagraph.segments[lastParagraph.segments.length - 1].segmentType === 'SelectionMarker' ) { - lastParagraph.segments.pop(); + mutateBlock(lastParagraph).segments.pop(); nextParagraph.segments.unshift( createSelectionMarker(insertPoint.marker.format) @@ -77,7 +79,7 @@ export const handleEnterOnList: DeleteSelectionStep = context => { } } else if (deleteResult !== 'range') { if (isEmptyListItem(listItem)) { - listItem.levels.pop(); + mutateBlock(listItem).levels.pop(); } else { const newListItem = createNewListItem(context, listItem, listParent); @@ -96,7 +98,7 @@ export const handleEnterOnList: DeleteSelectionStep = context => { } }; -const isEmptyListItem = (listItem: ContentModelListItem) => { +const isEmptyListItem = (listItem: ReadonlyContentModelListItem) => { return ( listItem.blocks.length === 1 && listItem.blocks[0].blockType === 'Paragraph' && @@ -108,8 +110,8 @@ const isEmptyListItem = (listItem: ContentModelListItem) => { const createNewListItem = ( context: ValidDeleteSelectionContext, - listItem: ContentModelListItem, - listParent: ContentModelBlockGroup + listItem: ReadonlyContentModelListItem, + listParent: ReadonlyContentModelBlockGroup ) => { const { insertPoint } = context; const listIndex = listParent.blocks.indexOf(listItem); @@ -120,12 +122,12 @@ const createNewListItem = ( newListItem.blocks.push(newParagraph); insertPoint.paragraph = newParagraph; context.lastParagraph = newParagraph; - listParent.blocks.splice(listIndex + 1, 0, newListItem); + mutateBlock(listParent).blocks.splice(listIndex + 1, 0, newListItem); return newListItem; }; -const createNewListLevel = (listItem: ContentModelListItem) => { +const createNewListLevel = (listItem: ReadonlyContentModelListItem) => { return listItem.levels.map(level => { return createListLevel( level.listType, From acceb1df193fe230d2cb76bdcba4a3a2a5903aa2 Mon Sep 17 00:00:00 2001 From: jiuqingsong Date: Sat, 18 May 2024 22:33:26 -0700 Subject: [PATCH 29/66] improve --- .../modelApi/selection/collectSelections.ts | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts index a134fb3b771..e77bb56fb73 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts @@ -15,8 +15,10 @@ import type { ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ReadonlyContentModelListItem, ReadonlyContentModelParagraph, ReadonlyContentModelSegment, + ReadonlyContentModelTable, ReadonlyOperationalBlocks, ReadonlyTableSelectionContext, TableSelectionContext, @@ -92,7 +94,22 @@ export function getSelectedSegmentsAndParagraphs( export function getSelectedSegments( model: ContentModelDocument, includingFormatHolder: boolean -): ContentModelSegment[] { +): ContentModelSegment[]; + +/** + * Get an array of selected segments from a content model (Readonly) + * @param model The Content Model to get selection from + * @param includingFormatHolder True means also include format holder as segment from list item + */ +export function getSelectedSegments( + model: ReadonlyContentModelDocument, + includingFormatHolder: boolean +): ReadonlyContentModelSegment[]; + +export function getSelectedSegments( + model: ReadonlyContentModelDocument, + includingFormatHolder: boolean +): ReadonlyContentModelSegment[] { return getSelectedSegmentsAndParagraphs(model, includingFormatHolder).map(x => x[0]); } @@ -100,9 +117,21 @@ export function getSelectedSegments( * Get any array of selected paragraphs from a content model * @param model The Content Model to get selection from */ -export function getSelectedParagraphs(model: ContentModelDocument): ContentModelParagraph[] { +export function getSelectedParagraphs(model: ContentModelDocument): ContentModelParagraph[]; + +/** + * Get any array of selected paragraphs from a content model (Readonly) + * @param model The Content Model to get selection from + */ +export function getSelectedParagraphs( + model: ReadonlyContentModelDocument +): ReadonlyContentModelParagraph[]; + +export function getSelectedParagraphs( + model: ReadonlyContentModelDocument +): ReadonlyContentModelParagraph[] { const selections = collectSelections(model, { includeListFormatHolder: 'never' }); - const result: ContentModelParagraph[] = []; + const result: ReadonlyContentModelParagraph[] = []; removeUnmeaningfulSelections(selections); @@ -191,10 +220,22 @@ export function getOperationalBlocks( */ export function getFirstSelectedTable( model: ContentModelDocument -): [ContentModelTable | undefined, ContentModelBlockGroup[]] { +): [ContentModelTable | undefined, ContentModelBlockGroup[]]; + +/** + * Get the first selected table from content model (Readonly) + * @param model The Content Model to get selection from + */ +export function getFirstSelectedTable( + model: ReadonlyContentModelDocument +): [ReadonlyContentModelTable | undefined, ReadonlyContentModelBlockGroup[]]; + +export function getFirstSelectedTable( + model: ReadonlyContentModelDocument +): [ReadonlyContentModelTable | undefined, ReadonlyContentModelBlockGroup[]] { const selections = collectSelections(model, { includeListFormatHolder: 'never' }); - let table: ContentModelTable | undefined; - let resultPath: ContentModelBlockGroup[] = []; + let table: ReadonlyContentModelTable | undefined; + let resultPath: ReadonlyContentModelBlockGroup[] = []; removeUnmeaningfulSelections(selections); @@ -224,7 +265,19 @@ export function getFirstSelectedTable( */ export function getFirstSelectedListItem( model: ContentModelDocument -): ContentModelListItem | undefined { +): ContentModelListItem | undefined; + +/** + * Get the first selected list item from content model (Readonly) + * @param model The Content Model to get selection from + */ +export function getFirstSelectedListItem( + model: ReadonlyContentModelDocument +): ReadonlyContentModelListItem | undefined; + +export function getFirstSelectedListItem( + model: ReadonlyContentModelDocument +): ReadonlyContentModelListItem | undefined { let listItem: ContentModelListItem | undefined; getOperationalBlocks(model, ['ListItem'], ['TableCell']).forEach(r => { From f7384a4a463abcc8e40cf653fef14df66c62fada Mon Sep 17 00:00:00 2001 From: JiuqingSong Date: Mon, 20 May 2024 09:00:04 -0700 Subject: [PATCH 30/66] Readonly types step 6 --- .../lib/modelApi/block/setModelAlignment.ts | 12 ++- .../lib/modelApi/block/setModelDirection.ts | 15 +-- .../modelApi/block/toggleModelBlockQuote.ts | 21 ++-- .../lib/modelApi/common/clearModelFormat.ts | 90 +++++++++++------ .../lib/modelApi/common/wrapBlock.ts | 44 +++++---- .../lib/modelApi/entity/insertEntityModel.ts | 12 +-- .../lib/modelApi/list/setListType.ts | 41 ++++---- .../modelApi/list/setModelListStartNumber.ts | 8 +- .../lib/modelApi/list/setModelListStyle.ts | 15 ++- .../selection/adjustSegmentSelection.ts | 29 ++++-- .../selection/adjustTrailingSpaceSelection.ts | 63 ++++++------ .../modelApi/selection/adjustWordSelection.ts | 14 ++- .../selection/collapseTableSelection.ts | 8 +- .../lib/modelApi/table/alignTable.ts | 7 +- .../lib/modelApi/table/alignTableCell.ts | 24 +++-- .../lib/modelApi/table/clearSelectedCells.ts | 19 +++- .../lib/modelApi/table/deleteTable.ts | 4 +- .../lib/modelApi/table/deleteTableColumn.ts | 8 +- .../lib/modelApi/table/deleteTableRow.ts | 12 +-- .../table/ensureFocusableParagraphForTable.ts | 26 ++--- .../lib/modelApi/table/insertTableColumn.ts | 4 +- .../lib/modelApi/table/insertTableRow.ts | 8 +- .../lib/modelApi/table/mergeTableCells.ts | 12 +-- .../lib/modelApi/table/mergeTableColumn.ts | 19 ++-- .../lib/modelApi/table/mergeTableRow.ts | 23 +++-- .../table/splitTableCellHorizontally.ts | 11 +-- .../table/splitTableCellVertically.ts | 12 ++- .../publicApi/image/adjustImageSelection.ts | 7 +- .../test/modelApi/common/wrapBlockTest.ts | 96 ++++++++++++++++++- 29 files changed, 436 insertions(+), 228 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts index e9f31d283d0..9fd36a217eb 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts @@ -1,8 +1,8 @@ import { alignTable } from '../table/alignTable'; -import { getOperationalBlocks } from 'roosterjs-content-model-dom'; +import { getOperationalBlocks, mutateBlock } from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, ContentModelListItem, + ReadonlyContentModelDocument, TableAlignOperation, } from 'roosterjs-content-model-types'; @@ -50,7 +50,7 @@ const TableAlignMap: Record< * @internal */ export function setModelAlignment( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, alignment: 'left' | 'center' | 'right' | 'justify' ) { const paragraphOrListItemOrTable = getOperationalBlocks( @@ -59,8 +59,10 @@ export function setModelAlignment( ['TableCell'] ); - paragraphOrListItemOrTable.forEach(({ block }) => { + paragraphOrListItemOrTable.forEach(({ block: readonlyBlock }) => { + const block = mutateBlock(readonlyBlock); const newAlignment = ResultMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr']; + if (block.blockType === 'Table' && alignment !== 'justify') { alignTable( block, @@ -69,7 +71,7 @@ export function setModelAlignment( } else if (block) { if (block.blockType === 'BlockGroup' && block.blockGroupType === 'ListItem') { block.blocks.forEach(b => { - const { format } = b; + const { format } = mutateBlock(b); format.textAlign = newAlignment; }); } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts index 611b8bc7d27..92dd2ac840e 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts @@ -3,22 +3,23 @@ import { applyTableFormat, getOperationalBlocks, isBlockGroupOfType, + mutateBlock, updateTableCellMetadata, } from 'roosterjs-content-model-dom'; import type { BorderFormat, - ContentModelBlock, ContentModelBlockFormat, - ContentModelDocument, ContentModelListItem, MarginFormat, PaddingFormat, + ReadonlyContentModelBlock, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; /** * @internal */ -export function setModelDirection(model: ContentModelDocument, direction: 'ltr' | 'rtl') { +export function setModelDirection(model: ReadonlyContentModelDocument, direction: 'ltr' | 'rtl') { const paragraphOrListItemOrTable = getOperationalBlocks( model, ['ListItem'], @@ -29,7 +30,9 @@ export function setModelDirection(model: ContentModelDocument, direction: 'ltr' if (isBlockGroupOfType(block, 'ListItem')) { const items = findListItemsInSameThread(model, block); - items.forEach(item => { + items.forEach(readonlyItem => { + const item = mutateBlock(readonlyItem); + item.levels.forEach(level => { level.format.direction = direction; }); @@ -47,7 +50,7 @@ export function setModelDirection(model: ContentModelDocument, direction: 'ltr' function internalSetDirection( format: ContentModelBlockFormat, direction: 'ltr' | 'rtl', - block?: ContentModelBlock + block?: ReadonlyContentModelBlock ) { const wasRtl = format.direction == 'rtl'; const isRtl = direction == 'rtl'; @@ -69,7 +72,7 @@ function internalSetDirection( block.rows.forEach(row => { row.cells.forEach(cell => { // Optimise by skipping cells with unchanged borders - updateTableCellMetadata(cell, metadata => { + updateTableCellMetadata(mutateBlock(cell), metadata => { if (metadata?.borderOverride) { const storeBorderLeft = cell.format.borderLeft; setProperty(cell.format, 'borderLeft', cell.format.borderRight); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts index 917fa91805a..de89ad2be7c 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts @@ -8,20 +8,23 @@ import { } from 'roosterjs-content-model-dom'; import type { WrapBlockStep1Result } from '../common/wrapBlock'; import type { - ContentModelBlock, ContentModelBlockGroup, - ContentModelDocument, ContentModelFormatContainer, ContentModelFormatContainerFormat, ContentModelListItem, - OperationalBlocks, + ReadonlyContentModelBlock, + ReadonlyContentModelDocument, + ReadonlyContentModelFormatContainer, + ReadonlyContentModelListItem, + ReadonlyOperationalBlocks, + ShallowMutableContentModelBlock, } from 'roosterjs-content-model-types'; /** * @internal */ export function toggleModelBlockQuote( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, formatLtr: ContentModelFormatContainerFormat, formatRtl: ContentModelFormatContainerFormat ): boolean { @@ -40,7 +43,7 @@ export function toggleModelBlockQuote( createFormatContainer('blockquote', isRtl ? formatRtl : formatLtr); const canMerge = ( isRtl: boolean, - target: ContentModelBlock, + target: ShallowMutableContentModelBlock, current?: ContentModelFormatContainer ): target is ContentModelFormatContainer => canMergeQuote(target, current?.format || (isRtl ? formatRtl : formatLtr)); @@ -60,13 +63,13 @@ export function toggleModelBlockQuote( } function canMergeQuote( - target: ContentModelBlock, + target: ShallowMutableContentModelBlock, format: ContentModelFormatContainerFormat ): target is ContentModelFormatContainer { return isQuote(target) && areSameFormats(format, target.format); } -function isQuote(block: ContentModelBlock): block is ContentModelFormatContainer { +function isQuote(block: ReadonlyContentModelBlock): block is ReadonlyContentModelFormatContainer { return ( isBlockGroupOfType(block, 'FormatContainer') && block.tagName == 'blockquote' @@ -74,7 +77,9 @@ function isQuote(block: ContentModelBlock): block is ContentModelFormatContainer } function areAllBlockQuotes( - blockAndParents: OperationalBlocks[] + blockAndParents: ReadonlyOperationalBlocks< + ReadonlyContentModelFormatContainer | ReadonlyContentModelListItem + >[] ): blockAndParents is { block: ContentModelFormatContainer; parent: ContentModelBlockGroup; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts index 8fd34ad0af5..b3d12ebe3ac 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts @@ -4,28 +4,33 @@ import { createFormatContainer, getClosestAncestorBlockGroupIndex, iterateSelections, + mutateBlock, + mutateSegments, updateTableCellMetadata, updateTableMetadata, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelDocument, - ContentModelFormatContainer, - ContentModelListItem, ContentModelSegment, ContentModelSegmentFormat, ContentModelTable, - Selectable, - TableSelectionContext, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, + ReadonlyContentModelFormatContainer, + ReadonlyContentModelListItem, + ReadonlySelectable, + ReadonlyTableSelectionContext, + ShallowMutableContentModelBlock, + ShallowMutableContentModelFormatContainer, + ShallowMutableContentModelTable, } from 'roosterjs-content-model-types'; /** * @internal */ export function clearModelFormat( - model: ContentModelDocument, - blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][], + model: ReadonlyContentModelDocument, + blocksToClear: [ReadonlyContentModelBlockGroup[], ShallowMutableContentModelBlock][], segmentsToClear: ContentModelSegment[], tablesToClear: [ContentModelTable, boolean][] ) { @@ -33,11 +38,21 @@ export function clearModelFormat( model, (path, tableContext, block, segments) => { if (segments) { - segmentsToClear.push(...segments); + if (block?.blockType == 'Paragraph') { + const [, mutableSegments] = mutateSegments(block, segments); + + segmentsToClear.push(...mutableSegments); + } else if ( + path[0].blockGroupType == 'ListItem' && + segments.length == 1 && + path[0].formatHolder == segments[0] + ) { + segmentsToClear.push(mutateBlock(path[0]).formatHolder); + } } if (block) { - blocksToClear.push([path, block]); + blocksToClear.push([path, mutateBlock(block)]); } else if (tableContext) { clearTableCellFormat(tableContext, tablesToClear); } @@ -110,28 +125,32 @@ function clearSegmentsFormat( } function clearTableCellFormat( - tableContext: TableSelectionContext | undefined, - tablesToClear: [ContentModelTable, boolean][] + tableContext: ReadonlyTableSelectionContext | undefined, + tablesToClear: [ShallowMutableContentModelTable, boolean][] ) { if (tableContext) { const { table, colIndex, rowIndex, isWholeTableSelected } = tableContext; const cell = table.rows[rowIndex].cells[colIndex]; if (cell.isSelected) { - updateTableCellMetadata(cell, () => null); - cell.isHeader = false; - cell.format = { + const mutableCell = mutateBlock(cell); + updateTableCellMetadata(mutableCell, () => null); + mutableCell.isHeader = false; + mutableCell.format = { useBorderBox: cell.format.useBorderBox, }; } if (!tablesToClear.find(x => x[0] == table)) { - tablesToClear.push([table, isWholeTableSelected]); + tablesToClear.push([mutateBlock(table), isWholeTableSelected]); } } } -function clearContainerFormat(path: ContentModelBlockGroup[], block: ContentModelBlock) { +function clearContainerFormat( + path: ReadonlyContentModelBlockGroup[], + block: ShallowMutableContentModelBlock +) { const containerPathIndex = getClosestAncestorBlockGroupIndex( path, ['FormatContainer'], @@ -139,37 +158,50 @@ function clearContainerFormat(path: ContentModelBlockGroup[], block: ContentMode ); if (containerPathIndex >= 0 && containerPathIndex < path.length - 1) { - const container = path[containerPathIndex] as ContentModelFormatContainer; + const container = mutateBlock( + path[containerPathIndex] as ReadonlyContentModelFormatContainer + ); const containerIndex = path[containerPathIndex + 1].blocks.indexOf(container); const blockIndex = container.blocks.indexOf(block); if (blockIndex >= 0 && containerIndex >= 0) { - const newContainer = createFormatContainer(container.tagName, container.format); + const newContainer: ShallowMutableContentModelFormatContainer = createFormatContainer( + container.tagName, + container.format + ); container.blocks.splice(blockIndex, 1); newContainer.blocks = container.blocks.splice(blockIndex); - path[containerPathIndex + 1].blocks.splice(containerIndex + 1, 0, block, newContainer); + mutateBlock(path[containerPathIndex + 1]).blocks.splice( + containerIndex + 1, + 0, + block, + newContainer + ); } } } -function clearListFormat(path: ContentModelBlockGroup[]) { +function clearListFormat(path: ReadonlyContentModelBlockGroup[]) { const listItem = path[getClosestAncestorBlockGroupIndex(path, ['ListItem'], ['TableCell'])] as - | ContentModelListItem + | ReadonlyContentModelListItem | undefined; if (listItem) { - listItem.levels = []; + mutateBlock(listItem).levels = []; } } -function clearBlockFormat(path: ContentModelBlockGroup[], block: ContentModelBlock) { +function clearBlockFormat( + path: ReadonlyContentModelBlockGroup[], + block: ShallowMutableContentModelBlock +) { if (block.blockType == 'Divider') { const index = path[0].blocks.indexOf(block); if (index >= 0) { - path[0].blocks.splice(index, 1); + mutateBlock(path[0]).blocks.splice(index, 1); } } else if (block.blockType == 'Paragraph') { block.format = {}; @@ -177,15 +209,15 @@ function clearBlockFormat(path: ContentModelBlockGroup[], block: ContentModelBlo } } -function isOnlySelectionMarkerSelected(block: ContentModelBlock) { +function isOnlySelectionMarkerSelected(block: ReadonlyContentModelBlock) { const segments = block.blockType == 'Paragraph' ? block.segments.filter(x => x.isSelected) : []; return segments.length == 1 && segments[0].segmentType == 'SelectionMarker'; } -function isWholeBlockSelected(block: ContentModelBlock) { +function isWholeBlockSelected(block: ReadonlyContentModelBlock) { return ( - (block as Selectable).isSelected || + (block as ReadonlySelectable).isSelected || (block.blockType == 'Paragraph' && block.segments.every(x => x.isSelected)) ); } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/wrapBlock.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/wrapBlock.ts index 23d2aa89756..bc80044041b 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/wrapBlock.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/wrapBlock.ts @@ -1,11 +1,17 @@ -import { addBlock, setParagraphNotImplicit } from 'roosterjs-content-model-dom'; -import type { ContentModelBlock, ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import { addBlock, mutateBlock, setParagraphNotImplicit } from 'roosterjs-content-model-dom'; +import type { + ContentModelBlock, + ContentModelBlockGroup, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ShallowMutableContentModelBlock, +} from 'roosterjs-content-model-types'; /** * @internal */ export interface WrapBlockStep1Result { - parent: ContentModelBlockGroup; + parent: ReadonlyContentModelBlockGroup; wrapper: T; } @@ -14,21 +20,25 @@ export interface WrapBlockStep1Result( step1Result: WrapBlockStep1Result[], - parent: ContentModelBlockGroup | null, - blockToWrap: ContentModelBlock, + readonlyParent: ReadonlyContentModelBlockGroup | null, + readonlyBlockToWrap: ReadonlyContentModelBlock, creator: (isRtl: boolean) => T, - canMerge: (isRtl: boolean, target: ContentModelBlock) => target is T + canMerge: (isRtl: boolean, target: ShallowMutableContentModelBlock) => target is T ) { + const parent = readonlyParent ? mutateBlock(readonlyParent) : null; + const blockToWrap = mutateBlock(readonlyBlockToWrap); const index = parent?.blocks.indexOf(blockToWrap) ?? -1; if (parent && index >= 0) { parent.blocks.splice(index, 1); - const prevBlock: ContentModelBlock = parent.blocks[index - 1]; + const readonlyPrevBlock: ReadonlyContentModelBlock = parent.blocks[index - 1]; + const prevBlock = readonlyPrevBlock ? mutateBlock(readonlyPrevBlock) : null; const isRtl = blockToWrap.format.direction == 'rtl'; - const wrapper = canMerge(isRtl, prevBlock) - ? prevBlock - : createAndAdd(parent, index, creator, isRtl); + const wrapper = + prevBlock && canMerge(isRtl, prevBlock) + ? prevBlock + : createAndAdd(parent, index, creator, isRtl); setParagraphNotImplicit(blockToWrap); addBlock(wrapper, blockToWrap); @@ -43,29 +53,31 @@ export function wrapBlockStep1( step1Result: WrapBlockStep1Result[], - canMerge: (isRtl: boolean, target: ContentModelBlock, current: T) => target is T + canMerge: (isRtl: boolean, target: ShallowMutableContentModelBlock, current: T) => target is T ) { step1Result.forEach(({ parent, wrapper }) => { const index = parent.blocks.indexOf(wrapper); - const nextBlock = parent.blocks[index + 1]; + const readonlyNextBlock = parent.blocks[index + 1]; + const nextBlock = readonlyNextBlock ? mutateBlock(readonlyNextBlock) : null; const isRtl = wrapper.format.direction == 'rtl'; - if (index >= 0 && canMerge(isRtl, nextBlock, wrapper)) { + if (index >= 0 && nextBlock && canMerge(isRtl, nextBlock, wrapper)) { wrapper.blocks.forEach(setParagraphNotImplicit); wrapper.blocks.push(...nextBlock.blocks); - parent.blocks.splice(index + 1, 1); + mutateBlock(parent).blocks.splice(index + 1, 1); } }); } function createAndAdd( - parent: ContentModelBlockGroup, + parent: ReadonlyContentModelBlockGroup, index: number, creator: (isRtl: boolean) => T, isRtl: boolean ): T { const block = creator(isRtl); - parent.blocks.splice(index, 0, block); + mutateBlock(parent).blocks.splice(index, 0, block); + return block; } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts index 326f144bfa8..5fa9927b5dd 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts @@ -10,21 +10,21 @@ import { } from 'roosterjs-content-model-dom'; import type { ContentModelBlock, - ContentModelDocument, ContentModelEntity, ContentModelParagraph, FormatContentModelContext, InsertEntityPosition, InsertPoint, ReadonlyContentModelBlock, - ShallowMutableContentModelBlockGroup, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; /** * @internal */ export function insertEntityModel( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, entityModel: ContentModelEntity, position: InsertEntityPosition, isBlock: boolean, @@ -32,7 +32,7 @@ export function insertEntityModel( context?: FormatContentModelContext, insertPointOverride?: InsertPoint ) { - let blockParent: ShallowMutableContentModelBlockGroup | undefined; + let blockParent: ReadonlyContentModelBlockGroup | undefined; let blockIndex = -1; let insertPoint: InsertPoint | null; @@ -100,7 +100,7 @@ export function insertEntityModel( blocksToInsert.push(nextParagraph); } - blockParent.blocks.splice(blockIndex, 0, ...blocksToInsert); + mutateBlock(blockParent).blocks.splice(blockIndex, 0, ...blocksToInsert); if (focusAfterEntity && nextParagraph) { const marker = createSelectionMarker(nextParagraph.segments[0]?.format || model.format); @@ -113,7 +113,7 @@ export function insertEntityModel( } function getInsertPoint( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, insertPointOverride?: InsertPoint, context?: FormatContentModelContext ): InsertPoint | null { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/setListType.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/setListType.ts index f7b1a1b953d..977f1d17118 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/list/setListType.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/setListType.ts @@ -3,13 +3,16 @@ import { createListLevel, getOperationalBlocks, isBlockGroupOfType, + mutateBlock, normalizeContentModel, setParagraphNotImplicit, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, - ContentModelDocument, ContentModelListItem, + ReadonlyContentModelBlock, + ReadonlyContentModelDocument, + ReadonlyContentModelListItem, + ShallowMutableContentModelListItem, } from 'roosterjs-content-model-types'; /** @@ -19,7 +22,7 @@ import type { * @param removeMargins true to remove margins, false to keep margins @default false */ export function setListType( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, listType: 'OL' | 'UL', removeMargins: boolean = false ) { @@ -33,7 +36,7 @@ export function setListType( ? block.levels[block.levels.length - 1]?.listType == listType : shouldIgnoreBlock(block) ); - let existingListItems: ContentModelListItem[] = []; + let existingListItems: ReadonlyContentModelListItem[] = []; paragraphOrListItems.forEach(({ block, parent }, itemIndex) => { if (isBlockGroupOfType(block, 'ListItem')) { @@ -70,7 +73,7 @@ export function setListType( const prevBlock = parent.blocks[index - 1]; const segmentFormat = (block.blockType == 'Paragraph' && block.segments[0]?.format) || {}; - const newListItem = createListItem( + const newListItem: ShallowMutableContentModelListItem = createListItem( [ createListLevel(listType, { startNumberOverride: @@ -98,26 +101,30 @@ export function setListType( setParagraphNotImplicit(block); } - newListItem.blocks.push(block); + const mutableBlock = mutateBlock(block); - if (block.format.marginRight) { - newListItem.format.marginRight = block.format.marginRight; - block.format.marginRight = undefined; + newListItem.blocks.push(mutableBlock); + + if (mutableBlock.format.marginRight) { + newListItem.format.marginRight = mutableBlock.format.marginRight; + mutableBlock.format.marginRight = undefined; } - if (block.format.marginLeft) { - newListItem.format.marginLeft = block.format.marginLeft; - block.format.marginLeft = undefined; + if (mutableBlock.format.marginLeft) { + newListItem.format.marginLeft = mutableBlock.format.marginLeft; + mutableBlock.format.marginLeft = undefined; } - if (block.format.textAlign) { - newListItem.format.textAlign = block.format.textAlign; + if (mutableBlock.format.textAlign) { + newListItem.format.textAlign = mutableBlock.format.textAlign; } - parent.blocks.splice(index, 1, newListItem); + mutateBlock(parent).blocks.splice(index, 1, newListItem); existingListItems.push(newListItem); } else { - existingListItems.forEach(x => (x.levels[0].format.marginBottom = '0px')); + existingListItems.forEach( + x => (mutateBlock(x).levels[0].format.marginBottom = '0px') + ); existingListItems = []; } } @@ -129,7 +136,7 @@ export function setListType( return paragraphOrListItems.length > 0; } -function shouldIgnoreBlock(block: ContentModelBlock) { +function shouldIgnoreBlock(block: ReadonlyContentModelBlock) { switch (block.blockType) { case 'Table': return false; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStartNumber.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStartNumber.ts index 29847c4d026..f07a8ea5223 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStartNumber.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStartNumber.ts @@ -1,14 +1,14 @@ -import { getFirstSelectedListItem } from 'roosterjs-content-model-dom'; -import type { ContentModelDocument } from 'roosterjs-content-model-types'; +import { getFirstSelectedListItem, mutateBlock } from 'roosterjs-content-model-dom'; +import type { ReadonlyContentModelDocument } from 'roosterjs-content-model-types'; /** * Set start number of a list item * @param model The model document * @param value The number to set to, must be equal or greater than 1 */ -export function setModelListStartNumber(model: ContentModelDocument, value: number) { +export function setModelListStartNumber(model: ReadonlyContentModelDocument, value: number) { const listItem = getFirstSelectedListItem(model); - const level = listItem?.levels[listItem?.levels.length - 1]; + const level = listItem ? mutateBlock(listItem).levels[listItem?.levels.length - 1] : null; if (level) { level.format.startNumberOverride = value; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStyle.ts b/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStyle.ts index b3cbeaa32ff..989ed8528f6 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStyle.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/list/setModelListStyle.ts @@ -1,13 +1,20 @@ import { findListItemsInSameThread } from './findListItemsInSameThread'; -import { getFirstSelectedListItem, updateListMetadata } from 'roosterjs-content-model-dom'; -import type { ContentModelDocument, ListMetadataFormat } from 'roosterjs-content-model-types'; +import { + getFirstSelectedListItem, + mutateBlock, + updateListMetadata, +} from 'roosterjs-content-model-dom'; +import type { + ListMetadataFormat, + ReadonlyContentModelDocument, +} from 'roosterjs-content-model-types'; /** * Set style of list items with in same thread of current item * @param model The model document * @param style The style to set */ -export function setModelListStyle(model: ContentModelDocument, style: ListMetadataFormat) { +export function setModelListStyle(model: ReadonlyContentModelDocument, style: ListMetadataFormat) { const listItem = getFirstSelectedListItem(model); if (listItem) { @@ -15,7 +22,7 @@ export function setModelListStyle(model: ContentModelDocument, style: ListMetada const levelIndex = listItem.levels.length - 1; listItems.forEach(listItem => { - const level = listItem.levels[levelIndex]; + const level = mutateBlock(listItem).levels[levelIndex]; if (level) { updateListMetadata(level, metadata => Object.assign({}, metadata, style)); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts index 908fc5d6ea9..db904297d4f 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts @@ -1,28 +1,39 @@ import { getSelectedParagraphs, setSelection } from 'roosterjs-content-model-dom'; -import type { ContentModelDocument, ContentModelSegment } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelDocument, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, +} from 'roosterjs-content-model-types'; /** * @internal */ export function adjustSegmentSelection( - model: ContentModelDocument, - firstMatcher: (target: ContentModelSegment) => boolean, - siblingMatcher: (target: ContentModelSegment, ref: ContentModelSegment) => boolean + model: ReadonlyContentModelDocument, + firstMatcher: ( + target: ReadonlyContentModelSegment, + paragraph: ReadonlyContentModelParagraph + ) => boolean, + siblingMatcher: ( + target: ReadonlyContentModelSegment, + ref: ReadonlyContentModelSegment, + paragraph: ReadonlyContentModelParagraph + ) => boolean ): boolean { const paragraphs = getSelectedParagraphs(model); - let first: ContentModelSegment | undefined; - let last: ContentModelSegment | undefined; + let first: ReadonlyContentModelSegment | undefined; + let last: ReadonlyContentModelSegment | undefined; let changed = false; paragraphs.forEach(p => { - const index = first ? 0 : p.segments.findIndex(x => firstMatcher(x)); + const index = first ? 0 : p.segments.findIndex(x => firstMatcher(x, p)); const segments = p.segments; if (!first) { first = segments[index]; for (let i = index; i > 0; i--) { - if (siblingMatcher(segments[i - 1], first)) { + if (siblingMatcher(segments[i - 1], first, p)) { first = segments[i - 1]; changed = changed || !segments[i - 1].isSelected; } else { @@ -34,7 +45,7 @@ export function adjustSegmentSelection( if (first) { for (let i = index; i < p.segments.length; i++) { - if (i == index || siblingMatcher(segments[i], last || segments[index])) { + if (i == index || siblingMatcher(segments[i], last || segments[index], p)) { last = segments[i]; changed = changed || !segments[i].isSelected; } else { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts index f9c00da65cb..3fb7d47aca6 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts @@ -1,15 +1,15 @@ -import { createText, iterateSelections } from 'roosterjs-content-model-dom'; +import { createText, iterateSelections, mutateSegment } from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, - ContentModelParagraph, - ContentModelText, + ReadonlyContentModelDocument, + ReadonlyContentModelParagraph, + ReadonlyContentModelText, } from 'roosterjs-content-model-types'; /** * If a format cannot be applied to be applied to a trailing space, split the trailing space into a separate segment * @internal */ -export function adjustTrailingSpaceSelection(model: ContentModelDocument) { +export function adjustTrailingSpaceSelection(model: ReadonlyContentModelDocument) { iterateSelections(model, (_, __, block, segments) => { if (block?.blockType === 'Paragraph' && segments && segments.length > 0) { if ( @@ -37,7 +37,7 @@ export function adjustTrailingSpaceSelection(model: ContentModelDocument) { }); } -function shouldSplitTrailingSpace(segment: ContentModelText) { +function shouldSplitTrailingSpace(segment: ReadonlyContentModelText) { return segment.isSelected && hasTrailingSpace(segment.text) && !isTrailingSpace(segment.text); } @@ -49,27 +49,32 @@ function isTrailingSpace(text: string) { return text.trimRight().length == 0; } -function splitTextSegment(block: ContentModelParagraph, textSegment: Readonly) { - const text = textSegment.text.trimRight(); - const trailingSpace = textSegment.text.substring(text.length); - const newText = createText(text, textSegment.format, textSegment.link, textSegment.code); - newText.isSelected = true; - const trailingSpaceLink = textSegment.link - ? { - ...textSegment.link, - format: { - ...textSegment.link?.format, - underline: false, // Remove underline for trailing space link - }, - } - : undefined; - const trailingSpaceSegment = createText( - trailingSpace, - undefined, - trailingSpaceLink, - textSegment.code - ); - trailingSpaceSegment.isSelected = true; - const index = block.segments.indexOf(textSegment); - block.segments.splice(index, 1, newText, trailingSpaceSegment); +function splitTextSegment( + readonlyBlock: ReadonlyContentModelParagraph, + readonlyTextSegment: ReadonlyContentModelText +) { + mutateSegment(readonlyBlock, readonlyTextSegment, (textSegment, block) => { + const text = textSegment.text.trimRight(); + const trailingSpace = textSegment.text.substring(text.length); + const newText = createText(text, textSegment.format, textSegment.link, textSegment.code); + newText.isSelected = true; + const trailingSpaceLink = textSegment.link + ? { + ...textSegment.link, + format: { + ...textSegment.link?.format, + underline: false, // Remove underline for trailing space link + }, + } + : undefined; + const trailingSpaceSegment = createText( + trailingSpace, + undefined, + trailingSpaceLink, + textSegment.code + ); + trailingSpaceSegment.isSelected = true; + const index = block.segments.indexOf(textSegment); + block.segments.splice(index, 1, newText, trailingSpaceSegment); + }); } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts index 4c61c5aec23..cae71f3399e 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts @@ -1,16 +1,22 @@ -import { createText, isPunctuation, isSpace, iterateSelections } from 'roosterjs-content-model-dom'; +import { + createText, + isPunctuation, + isSpace, + iterateSelections, + mutateBlock, +} from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, ContentModelParagraph, ContentModelSegment, ContentModelText, + ReadonlyContentModelDocument, } from 'roosterjs-content-model-types'; /** * @internal */ export function adjustWordSelection( - model: ContentModelDocument, + model: ReadonlyContentModelDocument, marker: ContentModelSegment ): ContentModelSegment[] { let markerBlock: ContentModelParagraph | undefined; @@ -18,7 +24,7 @@ export function adjustWordSelection( iterateSelections(model, (_, __, block, segments) => { //Find the block with the selection marker if (block?.blockType == 'Paragraph' && segments?.length == 1 && segments[0] == marker) { - markerBlock = block; + markerBlock = mutateBlock(block); } return true; }); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/selection/collapseTableSelection.ts b/packages/roosterjs-content-model-api/lib/modelApi/selection/collapseTableSelection.ts index b9cd606a872..6710fe17dca 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/selection/collapseTableSelection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/selection/collapseTableSelection.ts @@ -1,6 +1,6 @@ -import { addSegment, createSelectionMarker } from 'roosterjs-content-model-dom'; +import { addSegment, createSelectionMarker, mutateBlock } from 'roosterjs-content-model-dom'; import type { - ContentModelTableRow, + ShallowMutableContentModelTableRow, TableSelectionCoordinates, } from 'roosterjs-content-model-types'; @@ -8,12 +8,12 @@ import type { * @internal */ export function collapseTableSelection( - rows: ContentModelTableRow[], + rows: ShallowMutableContentModelTableRow[], selection: TableSelectionCoordinates ) { const { firstColumn, firstRow } = selection; const cell = rows[firstRow]?.cells[firstColumn]; if (cell) { - addSegment(cell, createSelectionMarker()); + addSegment(mutateBlock(cell), createSelectionMarker()); } } 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 1d8be6d468e..4ec4c6f9772 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts @@ -1,9 +1,12 @@ -import type { ContentModelTable, TableAlignOperation } from 'roosterjs-content-model-types'; +import type { + ShallowMutableContentModelTable, + TableAlignOperation, +} from 'roosterjs-content-model-types'; /** * @internal */ -export function alignTable(table: ContentModelTable, operation: TableAlignOperation) { +export function alignTable(table: ShallowMutableContentModelTable, operation: TableAlignOperation) { table.format.marginLeft = operation == 'alignLeft' ? '' : 'auto'; table.format.marginRight = operation == 'alignRight' ? '' : 'auto'; 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 bf72e56a995..ebe4480f0af 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts @@ -1,7 +1,11 @@ -import { getSelectedCells, updateTableCellMetadata } from 'roosterjs-content-model-dom'; +import { + getSelectedCells, + mutateBlock, + updateTableCellMetadata, +} from 'roosterjs-content-model-dom'; import type { - ContentModelTable, - ContentModelTableCell, + ShallowMutableContentModelTable, + ShallowMutableContentModelTableCell, TableCellHorizontalAlignOperation, TableCellVerticalAlignOperation, } from 'roosterjs-content-model-types'; @@ -28,7 +32,7 @@ const VerticalAlignValueMap: Partial { @@ -40,7 +44,7 @@ export function alignTableCellHorizontally( * @internal */ export function alignTableCellVertically( - table: ContentModelTable, + table: ShallowMutableContentModelTable, operation: TableCellVerticalAlignOperation ) { alignTableCellInternal(table, cell => { @@ -55,8 +59,8 @@ export function alignTableCellVertically( } function alignTableCellInternal( - table: ContentModelTable, - callback: (cell: ContentModelTableCell) => void + table: ShallowMutableContentModelTable, + callback: (cell: ShallowMutableContentModelTableCell) => void ) { const sel = getSelectedCells(table); @@ -69,11 +73,11 @@ function alignTableCellInternal( if (format) { delete cell.cachedElement; - callback(cell); + callback(mutateBlock(cell)); cell.blocks.forEach(block => { - if (block.blockType === 'Paragraph') { - delete block.format.textAlign; + if (block.blockType === 'Paragraph' && block.format.textAlign) { + delete mutateBlock(block).format.textAlign; } }); } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/clearSelectedCells.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/clearSelectedCells.ts index 57eb8886bac..d2d2f5f2ead 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/clearSelectedCells.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/clearSelectedCells.ts @@ -1,12 +1,18 @@ -import { setSelection } from 'roosterjs-content-model-dom'; -import type { ContentModelTable, TableSelectionCoordinates } from 'roosterjs-content-model-types'; +import { mutateBlock, setSelection } from 'roosterjs-content-model-dom'; +import type { + ReadonlyContentModelTable, + TableSelectionCoordinates, +} from 'roosterjs-content-model-types'; /** * Clear selection of a table. * @param table The table model where the selection is to be cleared * @param sel The selection coordinates to be cleared */ -export function clearSelectedCells(table: ContentModelTable, sel: TableSelectionCoordinates) { +export function clearSelectedCells( + table: ReadonlyContentModelTable, + sel: TableSelectionCoordinates +) { if ( sel.firstColumn >= 0 && sel.firstRow >= 0 && @@ -15,9 +21,14 @@ export function clearSelectedCells(table: ContentModelTable, sel: TableSelection ) { for (let i = sel.firstRow; i <= sel.lastRow; i++) { const row = table.rows[i]; + for (let j = sel.firstColumn; j <= sel.lastColumn; j++) { const cell = row.cells[j]; - cell.isSelected = false; + + if (cell.isSelected) { + mutateBlock(cell).isSelected = false; + } + setSelection(cell); } } 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 bf930321835..63d85496fdb 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTable.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTable.ts @@ -1,9 +1,9 @@ -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-types'; /** * @internal */ -export function deleteTable(table: ContentModelTable) { +export function deleteTable(table: ShallowMutableContentModelTable) { table.rows = []; delete table.cachedElement; } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableColumn.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableColumn.ts index 68e6b0cbc0a..a1e28046bc5 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableColumn.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableColumn.ts @@ -1,11 +1,11 @@ import { collapseTableSelection } from '../selection/collapseTableSelection'; -import { getSelectedCells } from 'roosterjs-content-model-dom'; -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; +import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-types'; /** * @internal */ -export function deleteTableColumn(table: ContentModelTable) { +export function deleteTableColumn(table: ShallowMutableContentModelTable) { const sel = getSelectedCells(table); if (sel) { @@ -13,7 +13,7 @@ export function deleteTableColumn(table: ContentModelTable) { const cellInNextCol = table.rows[rowIndex].cells[sel.lastColumn + 1]; if (cellInNextCol) { - cellInNextCol.spanLeft = + mutateBlock(cellInNextCol).spanLeft = cellInNextCol.spanLeft && table.rows[rowIndex].cells[sel.firstColumn].spanLeft; } diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableRow.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableRow.ts index ba4b7d78de2..24cdf65e820 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableRow.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/deleteTableRow.ts @@ -1,19 +1,19 @@ import { collapseTableSelection } from '../selection/collapseTableSelection'; -import { getSelectedCells } from 'roosterjs-content-model-dom'; -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; +import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-types'; /** * @internal */ -export function deleteTableRow(table: ContentModelTable) { +export function deleteTableRow(table: ShallowMutableContentModelTable) { const sel = getSelectedCells(table); if (sel) { table.rows[sel.firstRow].cells.forEach((cell, colIndex) => { - const cellInNextRow = table.rows[sel.lastRow + 1]?.cells[colIndex]; + const cellInNextCell = table.rows[sel.lastRow + 1]?.cells[colIndex]; - if (cellInNextRow) { - cellInNextRow.spanAbove = cellInNextRow.spanAbove && cell.spanAbove; + if (cellInNextCell) { + mutateBlock(cellInNextCell).spanAbove = cellInNextCell.spanAbove && cell.spanAbove; } }); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/ensureFocusableParagraphForTable.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/ensureFocusableParagraphForTable.ts index b51358e86d2..347b3bcfbf0 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/ensureFocusableParagraphForTable.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/ensureFocusableParagraphForTable.ts @@ -1,10 +1,10 @@ -import { createBr, createParagraph } from 'roosterjs-content-model-dom'; +import { createBr, createParagraph, mutateBlock } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelDocument, ContentModelParagraph, - ContentModelTable, + ReadonlyContentModelBlock, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelDocument, + ReadonlyContentModelTable, } from 'roosterjs-content-model-types'; /** @@ -14,9 +14,9 @@ import type { * @returns a new paragraph that can but put focus in, or undefined if not needed */ export function ensureFocusableParagraphForTable( - model: ContentModelDocument, - path: ContentModelBlockGroup[], - table: ContentModelTable + model: ReadonlyContentModelDocument, + path: ReadonlyContentModelBlockGroup[], + table: ReadonlyContentModelTable ): ContentModelParagraph | undefined { let paragraph: ContentModelParagraph | undefined; const firstCell = table.rows.filter(row => row.cells.length > 0)[0]?.cells[0]; @@ -30,12 +30,12 @@ export function ensureFocusableParagraphForTable( if (!paragraph) { // If there is not a paragraph under this cell, create one paragraph = createEmptyParagraph(model); - firstCell.blocks.push(paragraph); + mutateBlock(firstCell).blocks.push(paragraph); } } else { // No table cell at all, which means the whole table is deleted. So we need to remove it from content model. - let block: ContentModelBlock = table; - let parent: ContentModelBlockGroup | undefined; + let block: ReadonlyContentModelBlock = table; + let parent: ReadonlyContentModelBlockGroup | undefined; paragraph = createEmptyParagraph(model); // If the table is the only block of its parent and parent is a FormatContainer, remove the parent as well. @@ -44,7 +44,7 @@ export function ensureFocusableParagraphForTable( const index = parent.blocks.indexOf(block) ?? -1; if (parent && index >= 0) { - parent.blocks.splice(index, 1, paragraph); + mutateBlock(parent).blocks.splice(index, 1, paragraph); } if ( @@ -64,7 +64,7 @@ export function ensureFocusableParagraphForTable( return paragraph; } -function createEmptyParagraph(model: ContentModelDocument) { +function createEmptyParagraph(model: ReadonlyContentModelDocument) { const newPara = createParagraph(false /*isImplicit*/, undefined /*blockFormat*/, model.format); const br = createBr(model.format); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableColumn.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableColumn.ts index 65723f3c981..e5384fcf7fb 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableColumn.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableColumn.ts @@ -1,7 +1,7 @@ import { clearSelectedCells } from './clearSelectedCells'; import { createTableCell, getSelectedCells } from 'roosterjs-content-model-dom'; import type { - ContentModelTable, + ShallowMutableContentModelTable, TableHorizontalInsertOperation, } from 'roosterjs-content-model-types'; @@ -11,7 +11,7 @@ import type { * @param operation The operation to be performed */ export function insertTableColumn( - table: ContentModelTable, + table: ShallowMutableContentModelTable, operation: TableHorizontalInsertOperation ) { const sel = getSelectedCells(table); diff --git a/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableRow.ts b/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableRow.ts index ad5b8ebaa8e..6192f750e0e 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableRow.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/insertTableRow.ts @@ -1,7 +1,7 @@ import { clearSelectedCells } from './clearSelectedCells'; import { createTableCell, getSelectedCells } from 'roosterjs-content-model-dom'; import type { - ContentModelTable, + ShallowMutableContentModelTable, TableVerticalInsertOperation, } from 'roosterjs-content-model-types'; @@ -10,12 +10,16 @@ import type { * @param table The table model where the row is to be inserted * @param operation The operation to be performed */ -export function insertTableRow(table: ContentModelTable, operation: TableVerticalInsertOperation) { +export function insertTableRow( + table: ShallowMutableContentModelTable, + operation: TableVerticalInsertOperation +) { const sel = getSelectedCells(table); const insertAbove = operation == 'insertAbove'; if (sel) { clearSelectedCells(table, sel); + for (let i = sel.firstRow; i <= sel.lastRow; i++) { const sourceRow = table.rows[insertAbove ? sel.firstRow : sel.lastRow]; 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 028b2a6dfe2..9f12c22fdf0 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableCells.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableCells.ts @@ -1,11 +1,11 @@ import { canMergeCells } from './canMergeCells'; -import { getSelectedCells } from 'roosterjs-content-model-dom'; -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; +import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-types'; /** * @internal */ -export function mergeTableCells(table: ContentModelTable) { +export function mergeTableCells(table: ShallowMutableContentModelTable) { const sel = getSelectedCells(table); if ( @@ -17,10 +17,10 @@ export function mergeTableCells(table: ContentModelTable) { const cell = table.rows[rowIndex].cells[colIndex]; if (cell) { - cell.spanLeft = colIndex > sel.firstColumn; - cell.spanAbove = rowIndex > sel.firstRow; + const mutableCell = mutateBlock(cell); - delete cell.cachedElement; + mutableCell.spanLeft = colIndex > sel.firstColumn; + mutableCell.spanAbove = rowIndex > sel.firstRow; } } 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 7e36425aad0..5a0dddc85aa 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableColumn.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableColumn.ts @@ -1,7 +1,7 @@ import { canMergeCells } from './canMergeCells'; -import { getSelectedCells } from 'roosterjs-content-model-dom'; +import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; import type { - ContentModelTable, + ShallowMutableContentModelTable, TableHorizontalMergeOperation, } from 'roosterjs-content-model-types'; @@ -9,7 +9,7 @@ import type { * @internal */ export function mergeTableColumn( - table: ContentModelTable, + table: ShallowMutableContentModelTable, operation: TableHorizontalMergeOperation ) { const sel = getSelectedCells(table); @@ -32,22 +32,19 @@ export function mergeTableColumn( mergingColIndex ) ) { - cell.spanLeft = true; + mutateBlock(cell).spanLeft = true; let newSelectedCol = mergingColIndex; while (table.rows[rowIndex]?.cells[newSelectedCol]?.spanLeft) { - delete table.rows[rowIndex].cells[newSelectedCol].cachedElement; + mutateBlock(table.rows[rowIndex].cells[newSelectedCol]); newSelectedCol--; } - if (table.rows[rowIndex]?.cells[newSelectedCol]) { - table.rows[rowIndex].cells[newSelectedCol].isSelected = true; - - delete table.rows[rowIndex].cells[newSelectedCol].cachedElement; + const newCell = table.rows[rowIndex]?.cells[newSelectedCol]; + if (newCell) { + mutateBlock(newCell).isSelected = true; } - - delete cell.cachedElement; } 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 7b4037312c6..bc382738982 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableRow.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/mergeTableRow.ts @@ -1,11 +1,17 @@ import { canMergeCells } from './canMergeCells'; -import { getSelectedCells } from 'roosterjs-content-model-dom'; -import type { ContentModelTable, TableVerticalMergeOperation } from 'roosterjs-content-model-types'; +import { getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; +import type { + ShallowMutableContentModelTable, + TableVerticalMergeOperation, +} from 'roosterjs-content-model-types'; /** * @internal */ -export function mergeTableRow(table: ContentModelTable, operation: TableVerticalMergeOperation) { +export function mergeTableRow( + table: ShallowMutableContentModelTable, + operation: TableVerticalMergeOperation +) { const sel = getSelectedCells(table); const mergeAbove = operation == 'mergeAbove'; @@ -26,7 +32,7 @@ export function mergeTableRow(table: ContentModelTable, operation: TableVertical colIndex ) ) { - cell.spanAbove = true; + mutateBlock(cell).spanAbove = true; let newSelectedRow = mergingRowIndex; @@ -36,14 +42,11 @@ export function mergeTableRow(table: ContentModelTable, operation: TableVertical newSelectedRow--; } - if (table.rows[newSelectedRow]?.cells[colIndex]) { - table.rows[newSelectedRow].cells[colIndex].isSelected = true; + const newCell = table.rows[newSelectedRow]?.cells[colIndex]; - delete table.rows[newSelectedRow].cells[colIndex].cachedElement; - delete table.rows[newSelectedRow].cachedElement; + if (newCell) { + mutateBlock(newCell).isSelected = true; } - - delete cell.cachedElement; } } } 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 ca46525f72c..937efffaae1 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellHorizontally.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellHorizontally.ts @@ -1,12 +1,12 @@ -import { createTableCell, getSelectedCells } from 'roosterjs-content-model-dom'; -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import { createTableCell, getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; +import type { ShallowMutableContentModelTable } from 'roosterjs-content-model-types'; const MIN_WIDTH = 30; /** * @internal */ -export function splitTableCellHorizontally(table: ContentModelTable) { +export function splitTableCellHorizontally(table: ShallowMutableContentModelTable) { const sel = getSelectedCells(table); if (sel) { @@ -20,11 +20,10 @@ export function splitTableCellHorizontally(table: ContentModelTable) { ) ) { table.rows.forEach((row, rowIndex) => { - delete row.cells[colIndex].cachedElement; + mutateBlock(row.cells[colIndex]); if (rowIndex >= sel.firstRow && rowIndex <= sel.lastRow) { - row.cells[colIndex + 1].spanLeft = false; - delete row.cells[colIndex + 1].cachedElement; + mutateBlock(row.cells[colIndex + 1]).spanLeft = false; } }); } else { 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 b8c51b2f25e..4cfce0e8a6c 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellVertically.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/table/splitTableCellVertically.ts @@ -1,12 +1,15 @@ -import { createTableCell, getSelectedCells } from 'roosterjs-content-model-dom'; -import type { ContentModelTable, ContentModelTableRow } from 'roosterjs-content-model-types'; +import { createTableCell, getSelectedCells, mutateBlock } from 'roosterjs-content-model-dom'; +import type { + ContentModelTableRow, + ShallowMutableContentModelTable, +} from 'roosterjs-content-model-types'; const MIN_HEIGHT = 22; /** * @internal */ -export function splitTableCellVertically(table: ContentModelTable) { +export function splitTableCellVertically(table: ShallowMutableContentModelTable) { const sel = getSelectedCells(table); if (sel) { @@ -30,8 +33,7 @@ export function splitTableCellVertically(table: ContentModelTable) { ) { belowRow.cells.forEach((belowCell, colIndex) => { if (colIndex >= sel.firstColumn && colIndex <= sel.lastColumn) { - belowCell.spanAbove = false; - delete belowCell.cachedElement; + mutateBlock(belowCell).spanAbove = false; } }); diff --git a/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts b/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts index 09e66661169..b9f410bf2e7 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts @@ -1,4 +1,5 @@ import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; +import { mutateSegment } from 'roosterjs-content-model-dom/lib'; import type { ContentModelImage, IEditor } from 'roosterjs-content-model-types'; /** @@ -12,9 +13,11 @@ export function adjustImageSelection(editor: IEditor): ContentModelImage | null model => adjustSegmentSelection( model, - target => { + (target, paragraph) => { if (target.isSelected && target.segmentType == 'Image') { - image = target; + mutateSegment(paragraph, target, segment => { + image = segment; + }); return true; } else { return false; diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/wrapBlockTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/wrapBlockTest.ts index 604c9ef6292..b568a0c1e3b 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/wrapBlockTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/wrapBlockTest.ts @@ -77,7 +77,60 @@ describe('wrapBlockStep1', () => { }, }, ]); + expect(canMerge).toHaveBeenCalledTimes(0); + expect(para.isImplicit).toBeFalse(); + }); + + it('Valid parent 2 - has sibling', () => { + const result: WrapBlockStep1Result[] = []; + const para: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [], + format: {}, + isImplicit: true, + }; + const para2 = createParagraph(); + const parent: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [para2, para], + }; + const canMerge = jasmine.createSpy().and.returnValue(false); + + wrapBlockStep1( + result, + parent, + para, + () => createFormatContainer('blockquote'), + canMerge as any + ); + + expect(parent).toEqual({ + blockGroupType: 'Document', + blocks: [ + para2, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + format: {}, + blocks: [para], + }, + ], + }); + expect(result).toEqual([ + { + parent: parent, + wrapper: { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + format: {}, + blocks: [para], + }, + }, + ]); expect(canMerge).toHaveBeenCalledTimes(1); + expect(canMerge).toHaveBeenCalledWith(false, para2); expect(para.isImplicit).toBeFalse(); }); @@ -190,7 +243,7 @@ describe('wrapBlockStep2', () => { }); }); - it('Has result, no block to merge', () => { + it('Has result, no block to merge 1 - no sibling', () => { const quote: ContentModelFormatContainer = createFormatContainer('blockquote'); const doc: ContentModelDocument = { blockGroupType: 'Document', @@ -224,8 +277,47 @@ describe('wrapBlockStep2', () => { }, ], }); + expect(canMerge).toHaveBeenCalledTimes(0); + }); + + it('Has result, no block to merge 2 - has sibling', () => { + const quote: ContentModelFormatContainer = createFormatContainer('blockquote'); + const para: ContentModelParagraph = createParagraph(); + const doc: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [quote, para], + }; + const result: WrapBlockStep1Result[] = [ + { + parent: doc, + wrapper: quote, + }, + ]; + const canMerge = jasmine.createSpy().and.returnValue(false); + + wrapBlockStep2(result, canMerge as any); + + expect(result).toEqual([ + { + parent: doc, + wrapper: quote, + }, + ]); + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [], + format: {}, + }, + para, + ], + }); expect(canMerge).toHaveBeenCalledTimes(1); - expect(canMerge).toHaveBeenCalledWith(false, undefined, quote); + expect(canMerge).toHaveBeenCalledWith(false, para, quote); }); it('Has results, can merge', () => { From 7bd493a2db34a26a7dabb3938aa7293a967ffa13 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 10:38:45 -0700 Subject: [PATCH 31/66] fix build --- .../lib/publicApi/image/adjustImageSelection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts b/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts index b9f410bf2e7..5f359355664 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts @@ -1,5 +1,5 @@ import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; -import { mutateSegment } from 'roosterjs-content-model-dom/lib'; +import { mutateSegment } from 'roosterjs-content-model-dom'; import type { ContentModelImage, IEditor } from 'roosterjs-content-model-types'; /** From b4ab1346a3b37a27bcf30650fc471ccd2b2c3a2b Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 11:37:53 -0700 Subject: [PATCH 32/66] improve --- .../contentModel/block/ContentModelBlock.ts | 8 +++++-- .../block/ContentModelParagraph.ts | 23 +++++++++++++++++++ .../segment/ContentModelSegment.ts | 12 ++++++++++ .../lib/index.ts | 2 ++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts index 3ae77cedd1c..7ec515f4e77 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelBlock.ts @@ -15,7 +15,11 @@ import type { ReadonlyContentModelListItem, ShallowMutableContentModelListItem, } from '../blockGroup/ContentModelListItem'; -import type { ContentModelParagraph, ReadonlyContentModelParagraph } from './ContentModelParagraph'; +import type { + ContentModelParagraph, + ReadonlyContentModelParagraph, + ShallowMutableContentModelParagraph, +} from './ContentModelParagraph'; import type { ContentModelTable, ReadonlyContentModelTable, @@ -54,6 +58,6 @@ export type ShallowMutableContentModelBlock = | ShallowMutableContentModelListItem | ShallowMutableContentModelGeneralBlock | ShallowMutableContentModelTable - | ContentModelParagraph + | ShallowMutableContentModelParagraph | ContentModelEntity | ContentModelDivider; diff --git a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts index 4db9b6ba111..94a46391352 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/block/ContentModelParagraph.ts @@ -6,6 +6,7 @@ import type { import type { ContentModelSegment, ReadonlyContentModelSegment, + ShallowMutableContentModelSegment, } from '../segment/ContentModelSegment'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -63,3 +64,25 @@ export interface ReadonlyContentModelParagraph */ readonly decorator?: ReadonlyContentModelParagraphDecorator; } + +/** + * Content Model of Paragraph (Shallow mutable) + */ +export interface ShallowMutableContentModelParagraph + extends ContentModelParagraphCommon, + ContentModelBlockBase<'Paragraph'> { + /** + * Segments within this paragraph + */ + segments: ShallowMutableContentModelSegment[]; + + /** + * Segment format on this paragraph. This is mostly used for default format + */ + segmentFormat?: ContentModelSegmentFormat; + + /** + * Decorator info for this paragraph, used by heading and P tags + */ + decorator?: ContentModelParagraphDecorator; +} diff --git a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts index 7fec3926408..4989fddc1c4 100644 --- a/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts +++ b/packages/roosterjs-content-model-types/lib/contentModel/segment/ContentModelSegment.ts @@ -3,6 +3,7 @@ import type { ContentModelEntity } from '../entity/ContentModelEntity'; import type { ContentModelGeneralSegment, ReadonlyContentModelGeneralSegment, + ShallowMutableContentModelGeneralSegment, } from './ContentModelGeneralSegment'; import type { ContentModelImage, ReadonlyContentModelImage } from './ContentModelImage'; import type { @@ -32,3 +33,14 @@ export type ReadonlyContentModelSegment = | ReadonlyContentModelGeneralSegment | ContentModelEntity | ReadonlyContentModelImage; + +/** + * Union type of Content Model Segment (Shallow mutable) + */ +export type ShallowMutableContentModelSegment = + | ContentModelSelectionMarker + | ContentModelText + | ContentModelBr + | ShallowMutableContentModelGeneralSegment + | ContentModelEntity + | ContentModelImage; diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index 242af3ef074..9f696e2c23b 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -105,6 +105,7 @@ export { ContentModelParagraph, ContentModelParagraphCommon, ReadonlyContentModelParagraph, + ShallowMutableContentModelParagraph, } from './contentModel/block/ContentModelParagraph'; export { ContentModelTable, @@ -202,6 +203,7 @@ export { export { ContentModelSegment, ReadonlyContentModelSegment, + ShallowMutableContentModelSegment, } from './contentModel/segment/ContentModelSegment'; export { From 806a1d3411347fc90ef44f29adaaaa12461ef387 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 14:15:04 -0700 Subject: [PATCH 33/66] Improve --- .../lib/modelApi/common/addBlock.ts | 7 +- .../lib/modelApi/common/addSegment.ts | 25 +++- .../lib/modelApi/common/ensureParagraph.ts | 25 +++- .../modelApi/selection/collectSelections.ts | 121 ++++++++++++++++-- .../modelApi/typeCheck/isGeneralSegment.ts | 10 ++ 5 files changed, 169 insertions(+), 19 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts index 3461d6baccb..4c331427d7b 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addBlock.ts @@ -1,10 +1,13 @@ -import type { ContentModelBlock, ContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { + ContentModelBlock, + ShallowMutableContentModelBlockGroup, +} from 'roosterjs-content-model-types'; /** * Add a given block to block group * @param group The block group to add block into * @param block The block to add */ -export function addBlock(group: ContentModelBlockGroup, block: ContentModelBlock) { +export function addBlock(group: ShallowMutableContentModelBlockGroup, block: ContentModelBlock) { group.blocks.push(block); } diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts index d564538490a..08535230a17 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts @@ -5,6 +5,8 @@ import type { ContentModelParagraph, ContentModelSegment, ContentModelSegmentFormat, + ShallowMutableContentModelBlockGroup, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -19,7 +21,28 @@ export function addSegment( newSegment: ContentModelSegment, blockFormat?: ContentModelBlockFormat, segmentFormat?: ContentModelSegmentFormat -): ContentModelParagraph { +): ContentModelParagraph; + +/** + * Add a given segment into a paragraph from its parent group. If the last block of the given group is not paragraph, create a new paragraph. (Shallow mutable) + * @param group The parent block group of the paragraph to add segment into + * @param newSegment The segment to add + * @param blockFormat The block format used for creating a new paragraph when need + * @returns The parent paragraph where the segment is added to + */ +export function addSegment( + group: ShallowMutableContentModelBlockGroup, + newSegment: ContentModelSegment, + blockFormat?: ContentModelBlockFormat, + segmentFormat?: ContentModelSegmentFormat +): ShallowMutableContentModelParagraph; + +export function addSegment( + group: ShallowMutableContentModelBlockGroup, + newSegment: ContentModelSegment, + blockFormat?: ContentModelBlockFormat, + segmentFormat?: ContentModelSegmentFormat +): ShallowMutableContentModelParagraph { const paragraph = ensureParagraph(group, blockFormat, segmentFormat); const lastSegment = paragraph.segments[paragraph.segments.length - 1]; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts index e638f1920ce..0714d6c8e0e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts @@ -1,10 +1,13 @@ import { addBlock } from './addBlock'; import { createParagraph } from '../creators/createParagraph'; +import { mutateBlock } from './mutate'; import type { ContentModelBlockFormat, ContentModelBlockGroup, ContentModelParagraph, ContentModelSegmentFormat, + ShallowMutableContentModelBlockGroup, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -17,11 +20,29 @@ export function ensureParagraph( group: ContentModelBlockGroup, blockFormat?: ContentModelBlockFormat, segmentFormat?: ContentModelSegmentFormat -): ContentModelParagraph { +): ContentModelParagraph; + +/** + * @internal + * Ensure there is a Paragraph that can insert segments in a Content Model Block Group (Shallow mutable) + * @param group The parent block group of the target paragraph + * @param blockFormat Format of the paragraph. This is only used if we need to create a new paragraph + */ +export function ensureParagraph( + group: ShallowMutableContentModelBlockGroup, + blockFormat?: ContentModelBlockFormat, + segmentFormat?: ContentModelSegmentFormat +): ShallowMutableContentModelParagraph; + +export function ensureParagraph( + group: ShallowMutableContentModelBlockGroup, + blockFormat?: ContentModelBlockFormat, + segmentFormat?: ContentModelSegmentFormat +): ShallowMutableContentModelParagraph { const lastBlock = group.blocks[group.blocks.length - 1]; if (lastBlock?.blockType == 'Paragraph') { - return lastBlock; + return mutateBlock(lastBlock); } else { const paragraph = createParagraph(true, blockFormat, segmentFormat); addBlock(group, paragraph); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts index e77bb56fb73..4d6c2c8e4f4 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/collectSelections.ts @@ -1,6 +1,7 @@ import { getClosestAncestorBlockGroupIndex } from '../editing/getClosestAncestorBlockGroupIndex'; import { isBlockGroupOfType } from '../typeCheck/isBlockGroupOfType'; import { iterateSelections } from './iterateSelections'; +import { mutateBlock } from '../common/mutate'; import type { ContentModelBlock, ContentModelBlockGroup, @@ -21,14 +22,18 @@ import type { ReadonlyContentModelTable, ReadonlyOperationalBlocks, ReadonlyTableSelectionContext, + ShallowMutableContentModelParagraph, + ShallowMutableContentModelSegment, TableSelectionContext, TypeOfBlockGroup, } from 'roosterjs-content-model-types'; +//#region getSelectedSegmentsAndParagraphs /** * Get an array of selected parent paragraph and child segment pair * @param model The Content Model to get selection from * @param includingFormatHolder True means also include format holder as segment from list item, in that case paragraph will be null + * @param includingEntity True to include entity in result as well */ export function getSelectedSegmentsAndParagraphs( model: ContentModelDocument, @@ -36,10 +41,29 @@ export function getSelectedSegmentsAndParagraphs( includingEntity?: boolean ): [ContentModelSegment, ContentModelParagraph | null, ContentModelBlockGroup[]][]; +/** + * Get an array of selected parent paragraph and child segment pair, return mutable paragraph and segments + * @param model The Content Model to get selection from + * @param includingFormatHolder True means also include format holder as segment from list item, in that case paragraph will be null + * @param includingEntity True to include entity in result as well + * @param mutate Set to true to indicate we will mutate the selected paragraphs + */ +export function getSelectedSegmentsAndParagraphs( + model: ReadonlyContentModelDocument, + includingFormatHolder: boolean, + includingEntity: boolean, + mutate: true +): [ + ShallowMutableContentModelSegment, + ContentModelParagraph | null, + ReadonlyContentModelBlockGroup[] +][]; + /** * Get an array of selected parent paragraph and child segment pair (Readonly) * @param model The Content Model to get selection from * @param includingFormatHolder True means also include format holder as segment from list item, in that case paragraph will be null + * @param includingEntity True to include entity in result as well */ export function getSelectedSegmentsAndParagraphs( model: ReadonlyContentModelDocument, @@ -54,7 +78,8 @@ export function getSelectedSegmentsAndParagraphs( export function getSelectedSegmentsAndParagraphs( model: ReadonlyContentModelDocument, includingFormatHolder: boolean, - includingEntity?: boolean + includingEntity?: boolean, + mutate?: boolean ): [ ReadonlyContentModelSegment, ReadonlyContentModelParagraph | null, @@ -70,22 +95,44 @@ export function getSelectedSegmentsAndParagraphs( ][] = []; selections.forEach(({ segments, block, path }) => { - if (segments && ((includingFormatHolder && !block) || block?.blockType == 'Paragraph')) { - segments.forEach(segment => { - if ( - includingEntity || - segment.segmentType != 'Entity' || - !segment.entityFormat.isReadonly - ) { - result.push([segment, block?.blockType == 'Paragraph' ? block : null, path]); + if (segments) { + if ( + includingFormatHolder && + !block && + segments.length == 1 && + path[0].blockGroupType == 'ListItem' && + segments[0] == path[0].formatHolder + ) { + const list = path[0]; + + if (mutate) { + mutateBlock(list); } - }); + + result.push([list.formatHolder, null, path]); + } else if (block?.blockType == 'Paragraph') { + if (mutate) { + mutateBlock(block); + } + + segments.forEach(segment => { + if ( + includingEntity || + segment.segmentType != 'Entity' || + !segment.entityFormat.isReadonly + ) { + result.push([segment, block, path]); + } + }); + } } }); return result; } +//#endregion +//#region getSelectedSegments /** * Get an array of selected segments from a content model * @param model The Content Model to get selection from @@ -96,6 +143,18 @@ export function getSelectedSegments( includingFormatHolder: boolean ): ContentModelSegment[]; +/** + * Get an array of selected segments from a content model, return mutable segments + * @param model The Content Model to get selection from + * @param includingFormatHolder True means also include format holder as segment from list item + * @param mutate Set to true to indicate we will mutate the selected paragraphs + */ +export function getSelectedSegments( + model: ReadonlyContentModelDocument, + includingFormatHolder: boolean, + mutate: true +): ShallowMutableContentModelSegment[]; + /** * Get an array of selected segments from a content model (Readonly) * @param model The Content Model to get selection from @@ -108,17 +167,39 @@ export function getSelectedSegments( export function getSelectedSegments( model: ReadonlyContentModelDocument, - includingFormatHolder: boolean + includingFormatHolder: boolean, + mutate?: boolean ): ReadonlyContentModelSegment[] { - return getSelectedSegmentsAndParagraphs(model, includingFormatHolder).map(x => x[0]); + const segments = mutate + ? getSelectedSegmentsAndParagraphs( + model, + includingFormatHolder, + false /*includeEntity*/, + true /*mutate*/ + ) + : getSelectedSegmentsAndParagraphs(model, includingFormatHolder); + + return segments.map(x => x[0]); } +//#endregion +//#region getSelectedParagraphs /** * Get any array of selected paragraphs from a content model * @param model The Content Model to get selection from */ export function getSelectedParagraphs(model: ContentModelDocument): ContentModelParagraph[]; +/** + * Get any array of selected paragraphs from a content model, return mutable paragraphs + * @param model The Content Model to get selection from + * @param mutate Set to true to indicate we will mutate the selected paragraphs + */ +export function getSelectedParagraphs( + model: ReadonlyContentModelDocument, + mutate: true +): ShallowMutableContentModelParagraph[]; + /** * Get any array of selected paragraphs from a content model (Readonly) * @param model The Content Model to get selection from @@ -128,7 +209,8 @@ export function getSelectedParagraphs( ): ReadonlyContentModelParagraph[]; export function getSelectedParagraphs( - model: ReadonlyContentModelDocument + model: ReadonlyContentModelDocument, + mutate?: boolean ): ReadonlyContentModelParagraph[] { const selections = collectSelections(model, { includeListFormatHolder: 'never' }); const result: ReadonlyContentModelParagraph[] = []; @@ -137,13 +219,15 @@ export function getSelectedParagraphs( selections.forEach(({ block }) => { if (block?.blockType == 'Paragraph') { - result.push(block); + result.push(mutate ? mutateBlock(block) : block); } }); return result; } +//#endregion +//#region getOperationalBlocks /** * Get an array of block group - block pair that is of the expected block group type from selection * @param group The root block group to search @@ -213,7 +297,9 @@ export function getOperationalBlocks( return result; } +//#endregion +//#region getFirstSelectedTable /** * Get the first selected table from content model * @param model The Content Model to get selection from @@ -258,7 +344,9 @@ export function getFirstSelectedTable( return [table, resultPath]; } +//#endregion +//#region getFirstSelectedListItem /** * Get the first selected list item from content model * @param model The Content Model to get selection from @@ -288,7 +376,9 @@ export function getFirstSelectedListItem( return listItem; } +//#endregion +//#region collectSelections interface SelectionInfo { path: ContentModelBlockGroup[]; segments?: ContentModelSegment[]; @@ -334,7 +424,9 @@ function collectSelections( return selections; } +//#endregion +//#region utils function removeUnmeaningfulSelections(selections: ReadonlySelectionInfo[]) { if ( selections.length > 1 && @@ -375,3 +467,4 @@ function isOnlySelectionMarkerSelected( return false; } } +//#endregion diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts index 67fc9d3a518..f7baf3d6e68 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/typeCheck/isGeneralSegment.ts @@ -3,6 +3,8 @@ import type { ContentModelGeneralSegment, ReadonlyContentModelBlockGroup, ReadonlyContentModelGeneralSegment, + ShallowMutableContentModelBlockGroup, + ShallowMutableContentModelGeneralSegment, } from 'roosterjs-content-model-types'; /** @@ -13,6 +15,14 @@ export function isGeneralSegment( group: ContentModelBlockGroup | ContentModelGeneralSegment ): group is ContentModelGeneralSegment; +/** + * Check if the given block group is a general segment (Shallow mutable) + * @param group The group to check + */ +export function isGeneralSegment( + group: ShallowMutableContentModelBlockGroup | ShallowMutableContentModelGeneralSegment +): group is ShallowMutableContentModelGeneralSegment; + /** * Check if the given block group is a general segment (Readonly) * @param group The group to check From de8ca0de28540e738c13805ab04b0be80a014bbc Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 14:18:39 -0700 Subject: [PATCH 34/66] improve --- .../lib/modelApi/common/addDecorators.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts index dd63bcbc423..6abccd578c9 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addDecorators.ts @@ -1,14 +1,17 @@ import type { - ContentModelSegment, DomToModelDecoratorContext, ReadonlyContentModelCode, ReadonlyContentModelLink, + ShallowMutableContentModelSegment, } from 'roosterjs-content-model-types'; /** * @internal */ -export function addLink(segment: ContentModelSegment, link: ReadonlyContentModelLink) { +export function addLink( + segment: ShallowMutableContentModelSegment, + link: ReadonlyContentModelLink +) { if (link.format.href) { segment.link = { format: { ...link.format }, @@ -22,7 +25,10 @@ export function addLink(segment: ContentModelSegment, link: ReadonlyContentModel * @param segment The segment to add decorator to * @param code The code decorator to add */ -export function addCode(segment: ContentModelSegment, code: ReadonlyContentModelCode) { +export function addCode( + segment: ShallowMutableContentModelSegment, + code: ReadonlyContentModelCode +) { if (code.format.fontFamily) { segment.code = { format: { ...code.format }, @@ -33,7 +39,10 @@ export function addCode(segment: ContentModelSegment, code: ReadonlyContentModel /** * @internal */ -export function addDecorators(segment: ContentModelSegment, context: DomToModelDecoratorContext) { +export function addDecorators( + segment: ShallowMutableContentModelSegment, + context: DomToModelDecoratorContext +) { addLink(segment, context.link); addCode(segment, context.code); } From 7a6b43fe52a6ec72587f7bd3129140471c2448ec Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 14:48:22 -0700 Subject: [PATCH 35/66] fix test --- .../test/modelApi/selection/collectSelectionsTest.ts | 5 +---- .../test/modelApi/selection/getSelectedSegmentsTest.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts index 1a3b5c23b47..51538c7552d 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/collectSelectionsTest.ts @@ -147,10 +147,7 @@ describe('getSelectedSegmentsAndParagraphs', () => { ], true, false, - [ - [s3, null, []], - [s4, null, []], - ] + [] ); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts index 2a42dadc893..aea8c2ee375 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/getSelectedSegmentsTest.ts @@ -120,7 +120,7 @@ describe('getSelectedSegments', () => { }, ], true, - [s3, s4] + [] ); }); From 62e491f444e7ec6e5a2ff505d98e4a06fbcda708 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 14:53:59 -0700 Subject: [PATCH 36/66] Improve --- .../lib/modelApi/common/addSegment.ts | 3 ++- .../lib/modelApi/common/ensureParagraph.ts | 3 ++- .../lib/modelApi/editing/deleteExpandedSelection.ts | 6 +++--- .../lib/modelApi/editing/mergeModel.ts | 3 ++- .../lib/parameter/DeleteSelectionStep.ts | 4 ++-- .../lib/selection/InsertPoint.ts | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts index 56812999ae7..08535230a17 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/addSegment.ts @@ -1,6 +1,7 @@ import { ensureParagraph } from './ensureParagraph'; import type { ContentModelBlockFormat, + ContentModelBlockGroup, ContentModelParagraph, ContentModelSegment, ContentModelSegmentFormat, @@ -16,7 +17,7 @@ import type { * @returns The parent paragraph where the segment is added to */ export function addSegment( - group: ShallowMutableContentModelBlockGroup, + group: ContentModelBlockGroup, newSegment: ContentModelSegment, blockFormat?: ContentModelBlockFormat, segmentFormat?: ContentModelSegmentFormat diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts index fad0d684443..0714d6c8e0e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/common/ensureParagraph.ts @@ -3,6 +3,7 @@ import { createParagraph } from '../creators/createParagraph'; import { mutateBlock } from './mutate'; import type { ContentModelBlockFormat, + ContentModelBlockGroup, ContentModelParagraph, ContentModelSegmentFormat, ShallowMutableContentModelBlockGroup, @@ -16,7 +17,7 @@ import type { * @param blockFormat Format of the paragraph. This is only used if we need to create a new paragraph */ export function ensureParagraph( - group: ShallowMutableContentModelBlockGroup, + group: ContentModelBlockGroup, blockFormat?: ContentModelBlockFormat, segmentFormat?: ContentModelSegmentFormat ): ContentModelParagraph; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts index 0954e6e7006..33ebafe00ca 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteExpandedSelection.ts @@ -8,7 +8,6 @@ import { iterateSelections } from '../selection/iterateSelections'; import { mutateBlock, mutateSegments } from '../common/mutate'; import { setParagraphNotImplicit } from '../block/setParagraphNotImplicit'; import type { - ContentModelParagraph, ContentModelSelectionMarker, DeleteSelectionContext, FormatContentModelContext, @@ -17,6 +16,7 @@ import type { ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, ReadonlyTableSelectionContext, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const DeleteSelectionIteratingOptions: IterateSelectionsOption = { @@ -45,7 +45,7 @@ export function deleteExpandedSelection( (path, tableContext, readonlyBlock, readonlySegments) => { // Set paragraph, format and index for default position where we will put cursor to. // Later we can overwrite these info when process the selections - let paragraph = createParagraph( + let paragraph: ShallowMutableContentModelParagraph = createParagraph( true /*implicit*/, undefined /*blockFormat*/, model.format @@ -131,7 +131,7 @@ export function deleteExpandedSelection( function createInsertPoint( marker: ContentModelSelectionMarker, - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, path: ReadonlyContentModelBlockGroup[], tableContext: ReadonlyTableSelectionContext | undefined ): InsertPoint { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index 47f7abcd0b1..b440afd1f69 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -24,6 +24,7 @@ import type { ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const HeadingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; @@ -261,7 +262,7 @@ function splitParagraph(markerPosition: InsertPoint, newParaFormat: ContentModel const { paragraph, marker, path } = markerPosition; const segmentIndex = paragraph.segments.indexOf(marker); const paraIndex = path[0].blocks.indexOf(paragraph); - const newParagraph = createParagraph( + const newParagraph: ShallowMutableContentModelParagraph = createParagraph( false /*isImplicit*/, { ...paragraph.format, ...newParaFormat }, paragraph.segmentFormat diff --git a/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts b/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts index f6a8f15188a..012f4987545 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts @@ -1,4 +1,4 @@ -import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ShallowMutableContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; import type { DeleteResult } from '../enum/DeleteResult'; import type { FormatContentModelContext } from './FormatContentModelContext'; import type { InsertPoint } from '../selection/InsertPoint'; @@ -26,7 +26,7 @@ export interface DeleteSelectionContext extends DeleteSelectionResult { /** * Last paragraph after previous step */ - lastParagraph?: ContentModelParagraph; + lastParagraph?: ShallowMutableContentModelParagraph; /** * Last table context after previous step diff --git a/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts b/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts index 2eab4b717e3..493d5073ee4 100644 --- a/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts +++ b/packages/roosterjs-content-model-types/lib/selection/InsertPoint.ts @@ -1,5 +1,5 @@ import type { ReadonlyContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; -import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ShallowMutableContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; import type { ContentModelSelectionMarker } from '../contentModel/segment/ContentModelSelectionMarker'; import type { ReadonlyTableSelectionContext } from './TableSelectionContext'; @@ -15,7 +15,7 @@ export interface InsertPoint { /** * The paragraph that contains this insert point */ - paragraph: ContentModelParagraph; + paragraph: ShallowMutableContentModelParagraph; /** * Block group path of this insert point, from direct parent group to the root group From 5f3027f38f2e19efb8309dcf623e571ad93f0a0f Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 15:04:47 -0700 Subject: [PATCH 37/66] Improve --- .../lib/modelApi/common/clearModelFormat.ts | 6 +- .../lib/modelApi/entity/insertEntityModel.ts | 8 +-- .../modelApi/selection/adjustWordSelection.ts | 14 ++-- .../lib/publicApi/link/adjustLinkSelection.ts | 6 +- .../lib/publicApi/link/insertLink.ts | 6 +- .../lib/publicApi/link/removeLink.ts | 6 +- .../lib/publicApi/segment/changeFontSize.ts | 4 +- .../publicApi/segment/setBackgroundColor.ts | 4 +- .../lib/publicApi/segment/setFontSize.ts | 4 +- .../utils/formatSegmentWithContentModel.ts | 24 ++++--- .../corePlugin/entity/entityDelimiterUtils.ts | 68 +++++++++++++------ .../deleteSteps/deleteCollapsedSelection.ts | 18 ++--- .../edit/deleteSteps/deleteWordSelection.ts | 7 +- .../lib/edit/inputSteps/handleEnterOnList.ts | 38 +++++++---- 14 files changed, 131 insertions(+), 82 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts index b3d12ebe3ac..8a6a69a9d9b 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts @@ -10,7 +10,6 @@ import { updateTableMetadata, } from 'roosterjs-content-model-dom'; import type { - ContentModelSegment, ContentModelSegmentFormat, ContentModelTable, ReadonlyContentModelBlock, @@ -22,6 +21,7 @@ import type { ReadonlyTableSelectionContext, ShallowMutableContentModelBlock, ShallowMutableContentModelFormatContainer, + ShallowMutableContentModelSegment, ShallowMutableContentModelTable, } from 'roosterjs-content-model-types'; @@ -31,7 +31,7 @@ import type { export function clearModelFormat( model: ReadonlyContentModelDocument, blocksToClear: [ReadonlyContentModelBlockGroup[], ShallowMutableContentModelBlock][], - segmentsToClear: ContentModelSegment[], + segmentsToClear: ShallowMutableContentModelSegment[], tablesToClear: [ContentModelTable, boolean][] ) { iterateSelections( @@ -110,7 +110,7 @@ function createTablesFormat(tablesToClear: [ContentModelTable, boolean][]) { } function clearSegmentsFormat( - segmentsToClear: ContentModelSegment[], + segmentsToClear: ShallowMutableContentModelSegment[], defaultSegmentFormat: ContentModelSegmentFormat | undefined ) { segmentsToClear.forEach(x => { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts index 5fa9927b5dd..09409b149cb 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts @@ -9,15 +9,15 @@ import { mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, ContentModelEntity, - ContentModelParagraph, FormatContentModelContext, InsertEntityPosition, InsertPoint, ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ShallowMutableContentModelBlock, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -74,8 +74,8 @@ export function insertEntityModel( } if (blockIndex >= 0 && blockParent) { - const blocksToInsert: ContentModelBlock[] = []; - let nextParagraph: ContentModelParagraph | undefined; + const blocksToInsert: ShallowMutableContentModelBlock[] = []; + let nextParagraph: ShallowMutableContentModelParagraph | undefined; if (isBlock) { const nextBlock = blockParent.blocks[blockIndex]; diff --git a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts index cae71f3399e..e26aefb19fd 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts @@ -6,10 +6,10 @@ import { mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelParagraph, - ContentModelSegment, ContentModelText, ReadonlyContentModelDocument, + ShallowMutableContentModelParagraph, + ShallowMutableContentModelSegment, } from 'roosterjs-content-model-types'; /** @@ -17,9 +17,9 @@ import type { */ export function adjustWordSelection( model: ReadonlyContentModelDocument, - marker: ContentModelSegment -): ContentModelSegment[] { - let markerBlock: ContentModelParagraph | undefined; + marker: ShallowMutableContentModelSegment +): ShallowMutableContentModelSegment[] { + let markerBlock: ShallowMutableContentModelParagraph | undefined; iterateSelections(model, (_, __, block, segments) => { //Find the block with the selection marker @@ -32,7 +32,7 @@ export function adjustWordSelection( const tempSegments = markerBlock ? [...markerBlock.segments] : undefined; if (tempSegments && markerBlock) { - const segments: ContentModelSegment[] = []; + const segments: ShallowMutableContentModelSegment[] = []; let markerSelectionIndex = tempSegments.indexOf(marker); for (let i = markerSelectionIndex - 1; i >= 0; i--) { const currentSegment = tempSegments[i]; @@ -133,7 +133,7 @@ function findDelimiter(segment: ContentModelText, moveRightward: boolean): numbe } function splitTextSegment( - segments: ContentModelSegment[], + segments: ShallowMutableContentModelSegment[], textSegment: Readonly, index: number, found: number diff --git a/packages/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts b/packages/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts index 198e6b3376f..2b18eba748d 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts @@ -18,7 +18,11 @@ export function adjustLinkSelection(editor: IEditor): [string, string | null] { target => !!target.isSelected && !!target.link, (target, ref) => !!target.link && target.link.format.href == ref.link!.format.href ); - let segments = getSelectedSegments(model, false /*includingFormatHolder*/); + let segments = getSelectedSegments( + model, + false /*includingFormatHolder*/, + true /*mutate*/ + ); const firstSegment = segments[0]; if (segments.length == 1 && firstSegment.segmentType == 'SelectionMarker') { diff --git a/packages/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts b/packages/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts index 789001d9999..523f92ed7fa 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts @@ -49,7 +49,11 @@ export function insertLink( editor.formatContentModel( (model, context) => { - const segments = getSelectedSegments(model, false /*includingFormatHolder*/); + const segments = getSelectedSegments( + model, + false /*includingFormatHolder*/, + true /*mutate*/ + ); const originalText = segments .map(x => (x.segmentType == 'Text' ? x.text : '')) .join(''); diff --git a/packages/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts b/packages/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts index 28433bf168a..5cf773373ca 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts @@ -21,7 +21,11 @@ export function removeLink(editor: IEditor) { (!!target.link && target.link.format.href == ref.link!.format.href) ); - const segments = getSelectedSegments(model, false /*includingFormatHolder*/); + const segments = getSelectedSegments( + model, + false /*includingFormatHolder*/, + true /*mutate*/ + ); let isChanged = false; segments.forEach(segment => { diff --git a/packages/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts b/packages/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts index 1bc3ede6e5a..cb97c07022d 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts @@ -2,9 +2,9 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContent import { parseValueWithUnit } from 'roosterjs-content-model-dom'; import { setFontSizeInternal } from './setFontSize'; import type { - ContentModelParagraph, ContentModelSegmentFormat, IEditor, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -36,7 +36,7 @@ export function changeFontSize(editor: IEditor, change: 'increase' | 'decrease') function changeFontSizeInternal( change: 'increase' | 'decrease', format: ContentModelSegmentFormat, - paragraph: ContentModelParagraph | null + paragraph: ShallowMutableContentModelParagraph | null ) { if (format.fontSize) { const sizeInPt = parseValueWithUnit(format.fontSize, undefined /*element*/, 'pt'); diff --git a/packages/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts b/packages/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts index 705185e4a55..a0e5774d5b3 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts @@ -1,6 +1,6 @@ import { createSelectionMarker, setSelection } from 'roosterjs-content-model-dom'; import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { ContentModelParagraph, IEditor } from 'roosterjs-content-model-types'; +import type { IEditor, ShallowMutableContentModelParagraph } from 'roosterjs-content-model-types'; /** * Set background color @@ -10,7 +10,7 @@ import type { ContentModelParagraph, IEditor } from 'roosterjs-content-model-typ export function setBackgroundColor(editor: IEditor, backgroundColor: string | null) { editor.focus(); - let lastParagraph: ContentModelParagraph | null = null; + let lastParagraph: ShallowMutableContentModelParagraph | null = null; let lastSegmentIndex: number = -1; formatSegmentWithContentModel( diff --git a/packages/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts b/packages/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts index d888e89752a..294652c398b 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts @@ -1,8 +1,8 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; import type { - ContentModelParagraph, ContentModelSegmentFormat, IEditor, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -29,7 +29,7 @@ export function setFontSize(editor: IEditor, fontSize: string) { export function setFontSizeInternal( fontSize: string, format: ContentModelSegmentFormat, - paragraph: ContentModelParagraph | null + paragraph: ShallowMutableContentModelParagraph | null ) { format.fontSize = fontSize; diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts index 8031a24b6d6..ad7306205ec 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts @@ -1,11 +1,11 @@ import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; import type { - ContentModelDocument, - ContentModelParagraph, - ContentModelSegment, ContentModelSegmentFormat, IEditor, + ReadonlyContentModelDocument, + ShallowMutableContentModelParagraph, + ShallowMutableContentModelSegment, } from 'roosterjs-content-model-types'; /** @@ -23,22 +23,24 @@ export function formatSegmentWithContentModel( toggleStyleCallback: ( format: ContentModelSegmentFormat, isTuringOn: boolean, - segment: ContentModelSegment | null, - paragraph: ContentModelParagraph | null + segment: ShallowMutableContentModelSegment | null, + paragraph: ShallowMutableContentModelParagraph | null ) => void, segmentHasStyleCallback?: ( format: ContentModelSegmentFormat, - segment: ContentModelSegment | null, - paragraph: ContentModelParagraph | null + segment: ShallowMutableContentModelSegment | null, + paragraph: ShallowMutableContentModelParagraph | null ) => boolean, includingFormatHolder?: boolean, - afterFormatCallback?: (model: ContentModelDocument) => void + afterFormatCallback?: (model: ReadonlyContentModelDocument) => void ) { editor.formatContentModel( (model, context) => { let segmentAndParagraphs = getSelectedSegmentsAndParagraphs( model, - !!includingFormatHolder + !!includingFormatHolder, + false /*includingEntity*/, + true /*mutate*/ ); let isCollapsedSelection = segmentAndParagraphs.length == 1 && @@ -60,8 +62,8 @@ export function formatSegmentWithContentModel( const formatsAndSegments: [ ContentModelSegmentFormat, - ContentModelSegment | null, - ContentModelParagraph | null + ShallowMutableContentModelSegment | null, + ShallowMutableContentModelParagraph | null ][] = segmentAndParagraphs.map(item => [item[0].format, item[0], item[1]]); const isTurningOff = segmentHasStyleCallback diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts b/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts index e86692c91a2..281aa62f707 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts @@ -13,16 +13,20 @@ import { iterateSelections, isCharacterValue, findClosestBlockEntityContainer, + mutateSegment, + setParagraphNotImplicit, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { CompositionEndEvent, - ContentModelBlockGroup, ContentModelFormatter, - ContentModelParagraph, ContentModelSegmentFormat, IEditor, KeyDownEvent, RangeSelection, + ReadonlyContentModelBlockGroup, + ReadonlyContentModelParagraph, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const DelimiterBefore = 'entityDelimiterBefore'; @@ -48,8 +52,13 @@ export function preventTypeInDelimiter(node: HTMLElement, editor: IEditor) { iterateSelections(model, (_path, _tableContext, block, _segments) => { if (block?.blockType == 'Paragraph') { block.segments.forEach(segment => { - if (segment.segmentType == 'Text') { - segment.text = segment.text.replace(ZeroWidthSpace, ''); + if ( + segment.segmentType == 'Text' && + segment.text.indexOf(ZeroWidthSpace) >= 0 + ) { + mutateSegment(block, segment, segment => { + segment.text = segment.text.replace(ZeroWidthSpace, ''); + }); } }); } @@ -297,15 +306,24 @@ function handleInputOnDelimiter( export const handleKeyDownInBlockDelimiter: ContentModelFormatter = (model, context) => { iterateSelections(model, (_path, _tableContext, block) => { if (block?.blockType == 'Paragraph') { - delete block.isImplicit; - const selectionMarker = block.segments.find(w => w.segmentType == 'SelectionMarker'); + const paragraph = mutateBlock(block); + const selectionMarker = paragraph.segments.find( + w => w.segmentType == 'SelectionMarker' + ); + + if (paragraph.isImplicit) { + setParagraphNotImplicit(paragraph); + } + if (selectionMarker?.segmentType == 'SelectionMarker') { - block.segmentFormat = { ...selectionMarker.format }; + paragraph.segmentFormat = { ...selectionMarker.format }; context.newPendingFormat = { ...selectionMarker.format }; } - block.segments.unshift(createBr()); + + paragraph.segments.unshift(createBr()); } }); + return true; }; @@ -314,8 +332,8 @@ export const handleKeyDownInBlockDelimiter: ContentModelFormatter = (model, cont * @returns */ export const handleEnterInlineEntity: ContentModelFormatter = model => { - let selectionBlock: ContentModelParagraph | undefined; - let selectionBlockParent: ContentModelBlockGroup | undefined; + let selectionBlock: ReadonlyContentModelParagraph | undefined; + let selectionBlockParent: ReadonlyContentModelBlockGroup | undefined; iterateSelections(model, (path, _tableContext, block) => { if (block?.blockType == 'Paragraph') { @@ -325,34 +343,40 @@ export const handleEnterInlineEntity: ContentModelFormatter = model => { }); if (selectionBlock && selectionBlockParent) { - const selectionMarker = selectionBlock.segments.find( + const markerIndex = selectionBlock.segments.findIndex( segment => segment.segmentType == 'SelectionMarker' ); - if (selectionMarker) { - const markerIndex = selectionBlock.segments.indexOf(selectionMarker); - const segmentsAfterMarker = selectionBlock.segments.splice(markerIndex); - const newPara = createParagraph( + if (markerIndex >= 0) { + const paragraph = mutateBlock(selectionBlock); + const segmentsAfterMarker = paragraph.segments.splice(markerIndex); + + const newPara: ShallowMutableContentModelParagraph = createParagraph( false, - selectionBlock.format, - selectionBlock.segmentFormat, - selectionBlock.decorator + paragraph.format, + paragraph.segmentFormat, + paragraph.decorator ); if ( - selectionBlock.segments.every( + paragraph.segments.every( x => x.segmentType == 'SelectionMarker' || x.segmentType == 'Br' ) || segmentsAfterMarker.every(x => x.segmentType == 'SelectionMarker') ) { - newPara.segments.push(createBr(selectionBlock.format)); + newPara.segments.push(createBr(paragraph.format)); } newPara.segments.push(...segmentsAfterMarker); - const selectionBlockIndex = selectionBlockParent.blocks.indexOf(selectionBlock); + const selectionBlockIndex = selectionBlockParent.blocks.indexOf(paragraph); + if (selectionBlockIndex >= 0) { - selectionBlockParent.blocks.splice(selectionBlockIndex + 1, 0, newPara); + mutateBlock(selectionBlockParent).blocks.splice( + selectionBlockIndex + 1, + 0, + newPara + ); } } } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts index 337c6d7c4d1..88c546b4be1 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts @@ -9,11 +9,11 @@ import { } from 'roosterjs-content-model-dom'; import type { ReadonlyBlockAndPath } from '../utils/getLeafSiblingBlock'; import type { - ContentModelParagraph, - ContentModelSegment, DeleteSelectionStep, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ReadonlyContentModelParagraph, + ReadonlyContentModelSegment, } from 'roosterjs-content-model-types'; function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteSelectionStep { @@ -26,7 +26,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS const { paragraph, marker, path, tableContext } = context.insertPoint; const segments = paragraph.segments; - fixupBr(segments); + fixupBr(paragraph); const index = segments.indexOf(marker) + (isForward ? 1 : -1); const segmentToDelete = segments[index]; @@ -63,7 +63,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS context.lastParagraph = block; } else { if (block.segments[block.segments.length - 1]?.segmentType == 'Br') { - block.segments.pop(); + mutateBlock(block).segments.pop(); } context.insertPoint = { @@ -109,8 +109,8 @@ function getRoot(path: ReadonlyContentModelBlockGroup[]): ReadonlyContentModelDo function shouldOutdentParagraph( isForward: boolean, - segments: ContentModelSegment[], - paragraph: ContentModelParagraph, + segments: ReadonlyContentModelSegment[], + paragraph: ReadonlyContentModelParagraph, path: ReadonlyContentModelBlockGroup[] ) { return ( @@ -127,12 +127,14 @@ function shouldOutdentParagraph( * If the last segment is BR, remove it for now. We may add it back later when normalize model. * So that if this is an empty paragraph, it will start to delete next block */ -function fixupBr(segments: ContentModelSegment[]) { +function fixupBr(paragraph: ReadonlyContentModelParagraph) { + const { segments } = paragraph; + if (segments[segments.length - 1]?.segmentType == 'Br') { const segmentsWithoutBr = segments.filter(x => x.segmentType != 'SelectionMarker'); if (segmentsWithoutBr[segmentsWithoutBr.length - 2]?.segmentType != 'Br') { - segments.pop(); + mutateBlock(paragraph).segments.pop(); } } } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts index 303dd449eae..89b675f1e6a 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts @@ -2,12 +2,13 @@ import { isPunctuation, isSpace, isWhiteSpacePreserved, + mutateBlock, normalizeText, } from 'roosterjs-content-model-dom'; import type { - ContentModelParagraph, DeleteSelectionContext, DeleteSelectionStep, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const enum DeleteWordState { @@ -35,7 +36,7 @@ function getDeleteWordSelection(direction: 'forward' | 'backward'): DeleteSelect const startIndex = paragraph.segments.indexOf(marker); const deleteNext = direction == 'forward'; - const iterator = iterateSegments(paragraph, startIndex, deleteNext, context); + const iterator = iterateSegments(mutateBlock(paragraph), startIndex, deleteNext, context); let curr = iterator.next(); for (let state = DeleteWordState.Start; state != DeleteWordState.End && !curr.done; ) { @@ -101,7 +102,7 @@ function getDeleteWordSelection(direction: 'forward' | 'backward'): DeleteSelect } function* iterateSegments( - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, markerIndex: number, forward: boolean, context: DeleteSelectionContext diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 38e5867b870..811a26b7527 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -11,6 +11,7 @@ import { getClosestAncestorBlockGroupIndex, isBlockGroupOfType, mutateBlock, + mutateSegment, } from 'roosterjs-content-model-dom'; import type { ContentModelListItem, @@ -18,6 +19,8 @@ import type { InsertPoint, ReadonlyContentModelBlockGroup, ReadonlyContentModelListItem, + ShallowMutableContentModelListItem, + ShallowMutableContentModelParagraph, ValidDeleteSelectionContext, } from 'roosterjs-content-model-types'; @@ -36,10 +39,11 @@ export const handleEnterOnList: DeleteSelectionStep = context => { const rawEvent = formatContext?.rawEvent; const index = getClosestAncestorBlockGroupIndex(path, ['ListItem'], ['TableCell']); - const listItem = path[index]; + const readonlyListItem = path[index]; const listParent = path[index + 1]; - if (listItem && listItem.blockGroupType === 'ListItem' && listParent) { + if (readonlyListItem && readonlyListItem.blockGroupType === 'ListItem' && listParent) { + const listItem = mutateBlock(readonlyListItem); const listIndex = listParent.blocks.indexOf(listItem); const nextBlock = listParent.blocks[listIndex + 1]; @@ -118,7 +122,10 @@ const createNewListItem = ( const newParagraph = createNewParagraph(insertPoint); const levels = createNewListLevel(listItem); - const newListItem = createListItem(levels, insertPoint.marker.format); + const newListItem: ShallowMutableContentModelListItem = createListItem( + levels, + insertPoint.marker.format + ); newListItem.blocks.push(newParagraph); insertPoint.paragraph = newParagraph; context.lastParagraph = newParagraph; @@ -143,27 +150,28 @@ const createNewListLevel = (listItem: ReadonlyContentModelListItem) => { const createNewParagraph = (insertPoint: InsertPoint) => { const { paragraph, marker } = insertPoint; - const newParagraph = createParagraph( + const newParagraph: ShallowMutableContentModelParagraph = createParagraph( false /*isImplicit*/, paragraph.format, paragraph.segmentFormat ); - const markerIndex = paragraph.segments.indexOf(marker); - const segments = paragraph.segments.splice( - markerIndex, - paragraph.segments.length - markerIndex - ); + mutateSegment(paragraph, marker, (marker, paragraph, markerIndex) => { + const segments = paragraph.segments.splice( + markerIndex, + paragraph.segments.length - markerIndex + ); - newParagraph.segments.push(...segments); + newParagraph.segments.push(...segments); - setParagraphNotImplicit(paragraph); + setParagraphNotImplicit(paragraph); - if (paragraph.segments.every(x => x.segmentType == 'SelectionMarker')) { - paragraph.segments.push(createBr(marker.format)); - } + if (paragraph.segments.every(x => x.segmentType == 'SelectionMarker')) { + paragraph.segments.push(createBr(marker.format)); + } - normalizeParagraph(newParagraph); + normalizeParagraph(newParagraph); + }); return newParagraph; }; From 829e6ca927a678465bd9d76c3a85c4037401a230 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 15:08:12 -0700 Subject: [PATCH 38/66] fix build --- .../lib/modelApi/entity/insertEntityModel.ts | 8 ++++---- .../lib/edit/deleteSteps/deleteCollapsedSelection.ts | 10 +++++----- .../lib/edit/deleteSteps/deleteWordSelection.ts | 4 ++-- .../lib/edit/inputSteps/handleEnterOnList.ts | 9 +++++++-- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts index 326f144bfa8..d7db2f0a7c2 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts @@ -9,15 +9,15 @@ import { mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelBlock, ContentModelDocument, ContentModelEntity, - ContentModelParagraph, FormatContentModelContext, InsertEntityPosition, InsertPoint, ReadonlyContentModelBlock, + ShallowMutableContentModelBlock, ShallowMutableContentModelBlockGroup, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -74,8 +74,8 @@ export function insertEntityModel( } if (blockIndex >= 0 && blockParent) { - const blocksToInsert: ContentModelBlock[] = []; - let nextParagraph: ContentModelParagraph | undefined; + const blocksToInsert: ShallowMutableContentModelBlock[] = []; + let nextParagraph: ShallowMutableContentModelParagraph | undefined; if (isBlock) { const nextBlock = blockParent.blocks[blockIndex]; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts index 337c6d7c4d1..06d6c066100 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts @@ -9,11 +9,11 @@ import { } from 'roosterjs-content-model-dom'; import type { ReadonlyBlockAndPath } from '../utils/getLeafSiblingBlock'; import type { - ContentModelParagraph, - ContentModelSegment, DeleteSelectionStep, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ShallowMutableContentModelParagraph, + ShallowMutableContentModelSegment, } from 'roosterjs-content-model-types'; function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteSelectionStep { @@ -109,8 +109,8 @@ function getRoot(path: ReadonlyContentModelBlockGroup[]): ReadonlyContentModelDo function shouldOutdentParagraph( isForward: boolean, - segments: ContentModelSegment[], - paragraph: ContentModelParagraph, + segments: ShallowMutableContentModelSegment[], + paragraph: ShallowMutableContentModelParagraph, path: ReadonlyContentModelBlockGroup[] ) { return ( @@ -127,7 +127,7 @@ function shouldOutdentParagraph( * If the last segment is BR, remove it for now. We may add it back later when normalize model. * So that if this is an empty paragraph, it will start to delete next block */ -function fixupBr(segments: ContentModelSegment[]) { +function fixupBr(segments: ShallowMutableContentModelSegment[]) { if (segments[segments.length - 1]?.segmentType == 'Br') { const segmentsWithoutBr = segments.filter(x => x.segmentType != 'SelectionMarker'); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts index 303dd449eae..b612d813a53 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts @@ -5,9 +5,9 @@ import { normalizeText, } from 'roosterjs-content-model-dom'; import type { - ContentModelParagraph, DeleteSelectionContext, DeleteSelectionStep, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; const enum DeleteWordState { @@ -101,7 +101,7 @@ function getDeleteWordSelection(direction: 'forward' | 'backward'): DeleteSelect } function* iterateSegments( - paragraph: ContentModelParagraph, + paragraph: ShallowMutableContentModelParagraph, markerIndex: number, forward: boolean, context: DeleteSelectionContext diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 38e5867b870..112cbb1a6c9 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -18,6 +18,8 @@ import type { InsertPoint, ReadonlyContentModelBlockGroup, ReadonlyContentModelListItem, + ShallowMutableContentModelListItem, + ShallowMutableContentModelParagraph, ValidDeleteSelectionContext, } from 'roosterjs-content-model-types'; @@ -118,7 +120,10 @@ const createNewListItem = ( const newParagraph = createNewParagraph(insertPoint); const levels = createNewListLevel(listItem); - const newListItem = createListItem(levels, insertPoint.marker.format); + const newListItem: ShallowMutableContentModelListItem = createListItem( + levels, + insertPoint.marker.format + ); newListItem.blocks.push(newParagraph); insertPoint.paragraph = newParagraph; context.lastParagraph = newParagraph; @@ -143,7 +148,7 @@ const createNewListLevel = (listItem: ReadonlyContentModelListItem) => { const createNewParagraph = (insertPoint: InsertPoint) => { const { paragraph, marker } = insertPoint; - const newParagraph = createParagraph( + const newParagraph: ShallowMutableContentModelParagraph = createParagraph( false /*isImplicit*/, paragraph.format, paragraph.segmentFormat From 2bc734bf333adab4bec752d2a3999ff6400e034d Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 15:22:37 -0700 Subject: [PATCH 39/66] improve --- .../lib/publicApi/entity/insertEntity.ts | 4 ++-- .../publicApi/table/applyTableBorderFormat.ts | 18 +++++++++--------- .../lib/publicApi/table/formatTable.ts | 3 ++- .../lib/publicApi/table/setTableCellShade.ts | 7 ++++++- .../entity/entityDelimiterUtilsTest.ts | 2 ++ 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts b/packages/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts index 0eb92d5cc0f..4332c3c2f7a 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts @@ -14,9 +14,9 @@ import type { EntityState, DOMInsertPoint, FormatContentModelOptions, - ContentModelDocument, FormatContentModelContext, InsertPoint, + ShallowMutableContentModelDocument, } from 'roosterjs-content-model-types'; const BlockEntityTag = 'div'; @@ -102,7 +102,7 @@ export function insertEntity( }; const callback = ( - model: ContentModelDocument, + model: ShallowMutableContentModelDocument, context: FormatContentModelContext, insertPoint?: InsertPoint ) => { 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 83a98fd84e7..38a7b0546fb 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts @@ -2,16 +2,17 @@ import { extractBorderValues, getFirstSelectedTable, getSelectedCells, + mutateBlock, parseValueWithUnit, updateTableCellMetadata, } from 'roosterjs-content-model-dom'; import type { IEditor, Border, - ContentModelTable, - ContentModelTableCell, BorderOperations, TableSelectionCoordinates, + ReadonlyContentModelTableCell, + ReadonlyContentModelTable, } from 'roosterjs-content-model-types'; /** @@ -385,22 +386,21 @@ export function applyTableBorderFormat( * @param positions The positions to apply */ function applyBorderFormat( - cell: ContentModelTableCell, + cell: ReadonlyContentModelTableCell, borderFormat: string, positions: BorderPositions[] ) { + const mutableCell = mutateBlock(cell); + positions.forEach(pos => { - cell.format[pos] = borderFormat; + mutableCell.format[pos] = borderFormat; }); - updateTableCellMetadata(cell, metadata => { + updateTableCellMetadata(mutableCell, metadata => { metadata = metadata || {}; metadata.borderOverride = true; return metadata; }); - - // Cell was modified, so delete cached element - delete cell.cachedElement; } /** @@ -413,7 +413,7 @@ function applyBorderFormat( * @param perimeter Where in the perimeter to apply */ function modifyPerimeter( - tableModel: ContentModelTable, + tableModel: ReadonlyContentModelTable, sel: TableSelectionCoordinates, borderFormat: string, perimeter: Perimeter, diff --git a/packages/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts b/packages/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts index e2a44af5277..07d0175cfda 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts @@ -1,6 +1,7 @@ import { applyTableFormat, getFirstSelectedTable, + mutateBlock, updateTableCellMetadata, } from 'roosterjs-content-model-dom'; import type { IEditor, TableMetadataFormat } from 'roosterjs-content-model-types'; @@ -22,7 +23,7 @@ export function formatTable(editor: IEditor, format: TableMetadataFormat, keepCe // Wipe border metadata tableModel.rows.forEach(row => { row.cells.forEach(cell => { - updateTableCellMetadata(cell, metadata => { + updateTableCellMetadata(mutateBlock(cell), metadata => { if (metadata) { delete metadata.borderOverride; } diff --git a/packages/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts b/packages/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts index fc00545ece3..cbab6522b2f 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts @@ -3,6 +3,7 @@ import { getFirstSelectedTable, normalizeTable, setTableCellBackgroundColor, + mutateBlock, } from 'roosterjs-content-model-dom'; import type { IEditor } from 'roosterjs-content-model-types'; @@ -24,7 +25,11 @@ export function setTableCellShade(editor: IEditor, color: string | null) { table.rows.forEach(row => row.cells.forEach(cell => { if (hasSelectionInBlockGroup(cell)) { - setTableCellBackgroundColor(cell, color, true /*isColorOverride*/); + setTableCellBackgroundColor( + mutateBlock(cell), + color, + true /*isColorOverride*/ + ); } }) ); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts index aa40486771e..2e0cd6a923b 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts @@ -985,6 +985,7 @@ describe('handleKeyDownInBlockDelimiter', () => { ], format: {}, segmentFormat: {}, + isImplicit: false, }, { blockType: 'Paragraph', @@ -1081,6 +1082,7 @@ describe('handleKeyDownInBlockDelimiter', () => { ], format: {}, segmentFormat: {}, + isImplicit: false, }, { segmentType: 'Entity', From 5e72761d6fcdfab1c20be8e3031f27a6f52904a4 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 15:24:07 -0700 Subject: [PATCH 40/66] improve --- .../sidePane/contentModel/ContentModelPanePlugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts b/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts index 4bb8825a43a..1f2651ae146 100644 --- a/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts +++ b/demo/scripts/controlsV2/sidePane/contentModel/ContentModelPanePlugin.ts @@ -1,3 +1,4 @@ +import { cloneModel } from 'roosterjs-content-model-dom'; import { ContentModelPane, ContentModelPaneProps } from './ContentModelPane'; import { createRibbonPlugin, RibbonButton, RibbonPlugin } from '../../roosterjsReact/ribbon'; import { getRefreshButton } from './buttons/refreshButton'; @@ -70,8 +71,9 @@ export class ContentModelPanePlugin extends SidePanePluginImpl< this.getComponent(component => { this.editor.formatContentModel( model => { - component.setContentModel(model); - setCurrentContentModel(model); + const clonedModel = cloneModel(model); + component.setContentModel(clonedModel); + setCurrentContentModel(clonedModel); return false; }, From 71f3ba83c72f7ca07208e7fbff8b7c32b2e9faef Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 15:34:02 -0700 Subject: [PATCH 41/66] Readonly types step 7: Port all other files --- .../menus/createImageEditMenuProvider.tsx | 4 +- .../emoji/plugin/createEmojiPlugin.ts | 19 +++++--- .../contentModel/buttons/exportButton.ts | 3 +- .../contentModel/buttons/importModelButton.ts | 7 ++- .../formatInsertPointWithContentModel.ts | 7 +-- .../utils/formatParagraphWithContentModel.ts | 12 +++-- .../utils/formatTableWithContentModel.ts | 15 +++++-- .../formatTextSegmentBeforeSelectionMarker.ts | 44 ++++++++++++------- .../corePlugin/copyPaste/CopyPastePlugin.ts | 4 +- .../entity/adjustSelectionAroundEntity.ts | 12 ++--- .../corePlugin/format/applyPendingFormat.ts | 29 +++++++----- .../lib/autoFormat/hyphen/transformHyphen.ts | 4 +- .../autoFormat/link/createLinkAfterSpace.ts | 4 +- .../autoFormat/list/keyboardListTrigger.ts | 10 ++--- .../autoFormat/numbers/transformFraction.ts | 4 +- .../autoFormat/numbers/transformOrdinals.ts | 4 +- .../lib/customReplace/CustomReplacePlugin.ts | 4 +- .../deleteSteps/deleteAllSegmentBefore.ts | 5 ++- .../lib/edit/deleteSteps/deleteEmptyQuote.ts | 2 +- .../lib/edit/handleKeyboardEventCommon.ts | 4 +- .../lib/edit/keyboardTab.ts | 4 +- .../lib/edit/tabUtils/handleTabOnList.ts | 12 ++--- .../lib/edit/tabUtils/handleTabOnParagraph.ts | 30 +++++++++---- .../lib/edit/tabUtils/handleTabOnTable.ts | 9 ++-- .../lib/edit/tabUtils/handleTabOnTableCell.ts | 22 +++++++--- .../lib/edit/utils/getLeafSiblingBlock.ts | 13 +++--- .../lib/picker/getQueryString.ts | 7 ++- .../lib/pluginUtils/splitTextSegment.ts | 7 ++- .../tableEdit/editors/features/TableMover.ts | 5 ++- .../lib/event/BeforePasteEvent.ts | 7 ++- 30 files changed, 193 insertions(+), 120 deletions(-) diff --git a/demo/scripts/controlsV2/roosterjsReact/contextMenu/menus/createImageEditMenuProvider.tsx b/demo/scripts/controlsV2/roosterjsReact/contextMenu/menus/createImageEditMenuProvider.tsx index 78e2b55d9fb..3f56d9e07a6 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/publicApi/utils/formatInsertPointWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel.ts index 462a9e00ac0..c35a0e516ca 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..62aff677029 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,9 @@ import { getSelectedParagraphs } from 'roosterjs-content-model-dom'; -import type { ContentModelParagraph, IEditor } from 'roosterjs-content-model-types'; +import type { + IEditor, + ReadonlyContentModelDocument, + ShallowMutableContentModelParagraph, +} from 'roosterjs-content-model-types'; /** * Invoke a callback to format the selected paragraph using Content Model @@ -10,11 +14,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); + (model: ReadonlyContentModelDocument, context) => { + 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-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-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/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/features/TableMover.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/features/TableMover.ts index 6af1d7482a0..2790800e7d7 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 @@ -10,6 +10,7 @@ import { getFirstSelectedTable, isNodeOfType, mergeModel, + mutateBlock, normalizeRect, setParagraphNotImplicit, setSelection, @@ -337,7 +338,7 @@ 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) { @@ -359,7 +360,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-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; From 5062a70dc1e8d864bd69393f5035ae92be5cc439 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 16:10:18 -0700 Subject: [PATCH 42/66] improve --- .../lib/modelApi/block/setModelIndentation.ts | 2 +- .../lib/modelApi/table/alignTable.ts | 2 -- .../lib/modelApi/table/alignTableCell.ts | 2 -- .../lib/modelApi/table/deleteTable.ts | 1 - .../lib/modelApi/table/mergeTableCells.ts | 2 -- .../lib/modelApi/table/mergeTableColumn.ts | 2 -- .../lib/modelApi/table/mergeTableRow.ts | 3 +-- .../table/splitTableCellHorizontally.ts | 2 +- .../table/splitTableCellVertically.ts | 8 +------- .../test/modelApi/table/alignTableTest.ts | 4 ---- .../test/modelApi/table/deleteTableTest.ts | 3 --- .../utils/formatTableWithContentModelTest.ts | 15 ++++++++++++++- .../lib/modelApi/editing/normalizeTable.ts | 1 - .../editing/setTableCellBackgroundColor.ts | 2 -- .../setTableCellBackgroundColorTest.ts | 19 +------------------ 15 files changed, 19 insertions(+), 49 deletions(-) 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 7abcf8e17f8..751ad0c5758 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/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-dom/lib/modelApi/editing/normalizeTable.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/normalizeTable.ts index 76a57ab0bcf..ac79add345a 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 && 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', () => { From 4bc44126b59814a4af7ffc984c5f65fc16bb1ec4 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 16:14:34 -0700 Subject: [PATCH 43/66] Readonly type steps 8: Finally enable readonly type --- .../lib/modelApi/selection/iterateSelections.ts | 16 +--------------- .../lib/parameter/FormatContentModelOptions.ts | 4 ++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts index 05db43d0348..220da2a8229 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts @@ -1,6 +1,5 @@ import type { ContentModelBlockGroup, - ContentModelBlockWithCache, IterateSelectionsCallback, IterateSelectionsOption, ReadonlyContentModelBlockGroup, @@ -38,20 +37,7 @@ export function iterateSelections( callback: ReadonlyIterateSelectionsCallback | IterateSelectionsCallback, option?: IterateSelectionsOption ): void { - const internalCallback: ReadonlyIterateSelectionsCallback = ( - path, - tableContext, - block, - segments - ) => { - if (!!(block as ContentModelBlockWithCache)?.cachedElement) { - delete (block as ContentModelBlockWithCache).cachedElement; - } - - return (callback as ReadonlyIterateSelectionsCallback)(path, tableContext, block, segments); - }; - - internalIterateSelections([group], internalCallback, option); + internalIterateSelections([group], callback as ReadonlyIterateSelectionsCallback, option); } function internalIterateSelections( diff --git a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts index ec43eae1ed1..c269e483078 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelOptions.ts @@ -1,4 +1,4 @@ -import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; +import type { ShallowMutableContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; import type { FormatContentModelContext } from './FormatContentModelContext'; import type { OnNodeCreated } from '../context/ModelToDomSettings'; @@ -52,6 +52,6 @@ export interface FormatContentModelOptions { * @returns True means the model is changed and need to write back to editor, otherwise false */ export type ContentModelFormatter = ( - model: ContentModelDocument, + model: ShallowMutableContentModelDocument, context: FormatContentModelContext ) => boolean; From c717e24f860bd832ecd4191e11275ba78cb85888 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 20 May 2024 16:38:44 -0700 Subject: [PATCH 44/66] fix test --- .../selection/iterateSelectionsTest.ts | 172 ++++-------------- 1 file changed, 40 insertions(+), 132 deletions(-) diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts index f1761b764a3..91bc8756a9b 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts @@ -1,7 +1,6 @@ import { iterateSelections } from '../../../lib/modelApi/selection/iterateSelections'; import { IterateSelectionsCallback } from 'roosterjs-content-model-types'; import { - addSegment, createContentModelDocument, createDivider, createEntity, @@ -251,17 +250,12 @@ describe('iterateSelections', () => { iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(3); - expect(callback).toHaveBeenCalledWith( - [group], - { - table: table, - colIndex: 0, - rowIndex: 0, - isWholeTableSelected: false, - }, - undefined, - undefined - ); + expect(callback).toHaveBeenCalledWith([group], { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }); expect(callback).toHaveBeenCalledWith( [cell1, group], { @@ -312,17 +306,12 @@ describe('iterateSelections', () => { }); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith( - [group], - { - table: table, - colIndex: 0, - rowIndex: 0, - isWholeTableSelected: false, - }, - undefined, - undefined - ); + expect(callback).toHaveBeenCalledWith([group], { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }); }); it('Group with table selection and ignore selected table content', () => { @@ -351,17 +340,12 @@ describe('iterateSelections', () => { }); expect(callback).toHaveBeenCalledTimes(3); - expect(callback).toHaveBeenCalledWith( - [group], - { - table: table, - colIndex: 0, - rowIndex: 0, - isWholeTableSelected: false, - }, - undefined, - undefined - ); + expect(callback).toHaveBeenCalledWith([group], { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }); expect(callback).toHaveBeenCalledWith( [cell1, group], { @@ -411,7 +395,7 @@ describe('iterateSelections', () => { iterateSelections(group, callback, { contentUnderSelectedTableCell: 'ignoreForTable' }); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], undefined, table, undefined); + expect(callback).toHaveBeenCalledWith([group], undefined, table); }); it('Select from the end of paragraph', () => { @@ -858,16 +842,16 @@ describe('iterateSelections', () => { iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); + expect(callback).toHaveBeenCalledWith([group], undefined, generalDiv); expect(callback1).toHaveBeenCalledTimes(1); - expect(callback1).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); + expect(callback1).toHaveBeenCalledWith([group], undefined, generalDiv); expect(callback2).toHaveBeenCalledTimes(1); - expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); + expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv); expect(callback3).toHaveBeenCalledTimes(1); - expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); + expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv); }); it('Get Selection from model that contains selected general block', () => { @@ -908,14 +892,14 @@ describe('iterateSelections', () => { ]); expect(callback2).toHaveBeenCalledTimes(1); - expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); + expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv); expect(callback3).toHaveBeenCalledTimes(2); expect(callback3).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [ text1, text2, ]); - expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); + expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv); }); it('Divider selection', () => { @@ -928,7 +912,7 @@ describe('iterateSelections', () => { iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], undefined, divider, undefined); + expect(callback).toHaveBeenCalledWith([group], undefined, divider); }); it('Return true from first selection', () => { @@ -984,7 +968,7 @@ describe('iterateSelections', () => { expect(newCallback).toHaveBeenCalledTimes(2); expect(newCallback).toHaveBeenCalledWith([group], undefined, para1, [text1]); - expect(newCallback).toHaveBeenCalledWith([group], undefined, divider, undefined); + expect(newCallback).toHaveBeenCalledWith([group], undefined, divider); }); it('Return true from first selection in nested block group', () => { @@ -1044,7 +1028,7 @@ describe('iterateSelections', () => { }); expect(newCallback).toHaveBeenCalledTimes(1); - expect(newCallback).toHaveBeenCalledWith([group], undefined, table, undefined); + expect(newCallback).toHaveBeenCalledWith([group], undefined, table); }); it('Return true from table cell selection', () => { @@ -1076,28 +1060,18 @@ describe('iterateSelections', () => { iterateSelections(group, newCallback); expect(newCallback).toHaveBeenCalledTimes(2); - expect(newCallback).toHaveBeenCalledWith( - [group], - { - table: table, - rowIndex: 0, - colIndex: 0, - isWholeTableSelected: true, - }, - undefined, - undefined - ); - expect(newCallback).toHaveBeenCalledWith( - [group], - { - table: table, - rowIndex: 0, - colIndex: 1, - isWholeTableSelected: true, - }, - undefined, - undefined - ); + expect(newCallback).toHaveBeenCalledWith([group], { + table: table, + rowIndex: 0, + colIndex: 0, + isWholeTableSelected: true, + }); + expect(newCallback).toHaveBeenCalledWith([group], { + table: table, + rowIndex: 0, + colIndex: 1, + isWholeTableSelected: true, + }); }); it('includeListFormatHolder=anySegment', () => { @@ -1207,70 +1181,4 @@ describe('iterateSelections', () => { expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([doc], undefined, para, [entity]); }); - - it('Check cachedElement is cleared', () => { - const quote1 = createFormatContainer('blockquote'); - const para1 = createParagraph(); - const divider1 = createDivider('hr'); - const quote2 = createFormatContainer('blockquote'); - const para2 = createParagraph(); - const divider2 = createDivider('hr'); - const marker1 = createSelectionMarker(); - const marker2 = createSelectionMarker(); - const cache = 'CACHE' as any; - - addSegment(quote1, marker1); - para1.segments.push(marker2); - divider1.isSelected = true; - - quote1.cachedElement = cache; - para1.cachedElement = cache; - divider1.cachedElement = cache; - quote2.cachedElement = cache; - para2.cachedElement = cache; - divider2.cachedElement = cache; - - const doc = createContentModelDocument(); - - doc.blocks.push(quote1, quote2, para1, para2, divider1, divider2); - - iterateSelections(doc, callback); - - expect(doc).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [ - { - blockType: 'Paragraph', - segments: [marker1], - format: {}, - isImplicit: true, - }, - ], - format: {}, - cachedElement: cache, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'FormatContainer', - tagName: 'blockquote', - blocks: [], - format: {}, - cachedElement: cache, - }, - { - blockType: 'Paragraph', - segments: [marker2], - format: {}, - }, - { blockType: 'Paragraph', segments: [], format: {}, cachedElement: cache }, - { blockType: 'Divider', tagName: 'hr', format: {}, isSelected: true }, - { blockType: 'Divider', tagName: 'hr', format: {}, cachedElement: cache }, - ], - }); - }); }); From 732d153d203d67d4445cd3148f597a72f72c6b3c Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 21 May 2024 16:38:32 -0700 Subject: [PATCH 45/66] improve --- .../lib/corePlugin/cache/domIndexerImpl.ts | 2 -- .../lib/modelToDom/handlers/handleDivider.ts | 2 +- .../lib/modelToDom/handlers/handleParagraph.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 5c42d7460ee..e26f992b512 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -281,8 +281,6 @@ function reconcileTextSelection( } onSegment(textNode, paragraph, textSegments); - - delete paragraph.cachedElement; } return selectable; diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts index 3e3e09aa4ea..08ca6e5b3ca 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts @@ -18,7 +18,7 @@ export const handleDivider: ContentModelBlockHandler = ( ) => { let element = context.allowCacheElement ? divider.cachedElement : undefined; - if (element) { + if (element && !divider.isSelected) { refNode = reuseCachedElement(parent, element, refNode); } else { element = doc.createElement(divider.tagName); diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts index 5b6441f1fe7..2ae14c63391 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts @@ -24,7 +24,7 @@ export const handleParagraph: ContentModelBlockHandler = ) => { let container = context.allowCacheElement ? paragraph.cachedElement : undefined; - if (container) { + if (container && paragraph.segments.every(x => x.segmentType != 'General' && !x.isSelected)) { refNode = reuseCachedElement(parent, container, refNode); } else { stackFormat(context, paragraph.decorator?.tagName || null, () => { From 37b5abe1c19580ef566ea1536346f529c522a8b9 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 24 May 2024 13:00:02 -0700 Subject: [PATCH 46/66] improve --- .../lib/modelApi/block/setModelDirection.ts | 14 +-- .../lib/modelApi/block/setModelIndentation.ts | 2 +- .../lib/modelApi/common/clearModelFormat.ts | 3 +- .../publicApi/segment/applySegmentFormat.ts | 5 +- .../modelApi/block/setModelAlignmentTest.ts | 15 ++- .../modelApi/block/setModelDirectionTest.ts | 13 +++ .../modelApi/common/clearModelFormatTest.ts | 102 ++++++++++++++++-- .../list/setModelListStartNumberTest.ts | 8 ++ .../modelApi/list/setModelListStyleTest.ts | 8 ++ .../formatSegmentWithContentModelTest.ts | 49 +++++++-- .../corePlugin/entity/entityDelimiterUtils.ts | 46 ++++---- .../entity/entityDelimiterUtilsTest.ts | 2 - .../lib/modelToDom/handlers/handleDivider.ts | 2 +- .../modelToDom/handlers/handleParagraph.ts | 2 +- 14 files changed, 211 insertions(+), 60 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts index 92dd2ac840e..4a77cca02b9 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts @@ -8,7 +8,6 @@ import { } from 'roosterjs-content-model-dom'; import type { BorderFormat, - ContentModelBlockFormat, ContentModelListItem, MarginFormat, PaddingFormat, @@ -37,25 +36,22 @@ export function setModelDirection(model: ReadonlyContentModelDocument, direction level.format.direction = direction; }); - item.blocks.forEach(block => internalSetDirection(block.format, direction)); + item.blocks.forEach(block => internalSetDirection(block, direction)); }); } else if (block) { - internalSetDirection(block.format, direction, block); + internalSetDirection(block, direction); } }); return paragraphOrListItemOrTable.length > 0; } -function internalSetDirection( - format: ContentModelBlockFormat, - direction: 'ltr' | 'rtl', - block?: ReadonlyContentModelBlock -) { - const wasRtl = format.direction == 'rtl'; +function internalSetDirection(block: ReadonlyContentModelBlock, direction: 'ltr' | 'rtl') { + const wasRtl = block.format.direction == 'rtl'; const isRtl = direction == 'rtl'; if (wasRtl != isRtl) { + const { format } = mutateBlock(block); format.direction = direction; // Adjust margin when change direction 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 7abcf8e17f8..2ff9d86662f 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -163,7 +163,7 @@ function isMultilevelSelection( } function calculateMarginValue( - format: ContentModelBlockFormat, + format: Readonly, isIndent: boolean, length: number = IndentStepInPixel ): number | null { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts index 8a6a69a9d9b..04d8cff9d3e 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts @@ -111,7 +111,7 @@ function createTablesFormat(tablesToClear: [ContentModelTable, boolean][]) { function clearSegmentsFormat( segmentsToClear: ShallowMutableContentModelSegment[], - defaultSegmentFormat: ContentModelSegmentFormat | undefined + defaultSegmentFormat: Readonly | undefined ) { segmentsToClear.forEach(x => { x.format = { ...(defaultSegmentFormat || {}) }; @@ -134,6 +134,7 @@ function clearTableCellFormat( if (cell.isSelected) { const mutableCell = mutateBlock(cell); + updateTableCellMetadata(mutableCell, () => null); mutableCell.isHeader = false; mutableCell.format = { diff --git a/packages/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts b/packages/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts index 15293f507bb..3a026e04b9a 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts @@ -6,7 +6,10 @@ import type { ContentModelSegmentFormat, IEditor } from 'roosterjs-content-model * @param editor The editor to operate on * @param newFormat The segment format to apply */ -export function applySegmentFormat(editor: IEditor, newFormat: ContentModelSegmentFormat) { +export function applySegmentFormat( + editor: IEditor, + newFormat: Readonly +) { formatSegmentWithContentModel( editor, 'applySegmentFormat', diff --git a/packages/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts b/packages/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts index cea3870af98..35a7648ff08 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts @@ -1,15 +1,26 @@ +import { ContentModelBlockFormat } from 'roosterjs-content-model-types'; import { setModelAlignment } from '../../../lib/modelApi/block/setModelAlignment'; import { createContentModelDocument, createListItem, createListLevel, - createParagraph, + createParagraph as originalCreateParagraph, createTable, createTableCell, createText, } from 'roosterjs-content-model-dom'; describe('align left', () => { + const mockedCachedElement = 'CACHE' as any; + + function createParagraph(isImplicit?: boolean, format?: ContentModelBlockFormat) { + const result = originalCreateParagraph(isImplicit, format); + + result.cachedElement = mockedCachedElement; + + return result; + } + it('Empty group', () => { const group = createContentModelDocument(); @@ -62,6 +73,7 @@ describe('align left', () => { blockType: 'Paragraph', format: {}, segments: [text1], + cachedElement: mockedCachedElement, }, { blockType: 'Paragraph', @@ -74,6 +86,7 @@ describe('align left', () => { blockType: 'Paragraph', format: {}, segments: [text3], + cachedElement: mockedCachedElement, }, ], }); diff --git a/packages/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts b/packages/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts index e262d80b6df..3f5b506478f 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts @@ -6,6 +6,7 @@ describe('setModelDirection', () => { const style = 'double'; const color = '#AABBCC'; const testBorderString = `${width} ${style} ${color}`; + const mockedCachedElement = 'CACHE' as any; function runTest( model: ContentModelDocument, @@ -42,6 +43,7 @@ describe('setModelDirection', () => { isSelected: true, }, ], + cachedElement: mockedCachedElement, }, ], }, @@ -80,6 +82,7 @@ describe('setModelDirection', () => { format: {}, isSelected: true, tagName: 'hr', + cachedElement: mockedCachedElement, }, ], }, @@ -134,6 +137,7 @@ describe('setModelDirection', () => { format: {}, }, ], + cachedElement: mockedCachedElement, }, { blockType: 'BlockGroup', @@ -164,6 +168,7 @@ describe('setModelDirection', () => { format: {}, }, ], + cachedElement: mockedCachedElement, }, ], }, @@ -265,6 +270,7 @@ describe('setModelDirection', () => { editingInfo: '{"borderOverride":true}', }, isSelected: true, + cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -279,13 +285,16 @@ describe('setModelDirection', () => { editingInfo: '{"borderOverride":true}', }, isSelected: true, + cachedElement: mockedCachedElement, }, ], + cachedElement: mockedCachedElement, }, ], format: {}, widths: [], dataset: {}, + cachedElement: mockedCachedElement, }, ], format: {}, @@ -371,6 +380,7 @@ describe('setModelDirection', () => { editingInfo: '{"borderOverride":true}', }, isSelected: true, + cachedElement: mockedCachedElement, }, { blockGroupType: 'TableCell', @@ -385,8 +395,10 @@ describe('setModelDirection', () => { editingInfo: '{"borderOverride":true}', }, isSelected: true, + cachedElement: mockedCachedElement, }, ], + cachedElement: mockedCachedElement, }, ], format: { @@ -394,6 +406,7 @@ describe('setModelDirection', () => { }, widths: [], dataset: {}, + cachedElement: mockedCachedElement, }, ], format: {}, diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts index dfeaf85a8fa..4eb36438c3e 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts @@ -1,18 +1,100 @@ import { clearModelFormat } from '../../../lib/modelApi/common/clearModelFormat'; +import { + ContentModelBlockFormat, + ContentModelFormatContainerFormat, + ContentModelListItemLevelFormat, + ContentModelSegmentFormat, + ContentModelTableCellFormat, + ContentModelTableFormat, + ReadonlyContentModelListLevel, + ReadonlyContentModelParagraphDecorator, + ReadonlyDatasetFormat, +} from 'roosterjs-content-model-types'; import { createContentModelDocument, - createDivider, - createFormatContainer, - createListItem, - createListLevel, - createParagraph, + createDivider as originalCreateDivider, + createFormatContainer as originalCreateFormatContainer, + createListItem as originalCreateListItem, + createListLevel as originalCreateListLevel, + createParagraph as originalCreateParagraph, createSelectionMarker, - createTable, - createTableCell, + createTable as originalCreateTable, + createTableCell as originalCreateTableCell, createText, } from 'roosterjs-content-model-dom'; describe('clearModelFormat', () => { + const mockedCachedElement = 'CACHE' as any; + + function createDivider(tagName: 'hr' | 'div', format?: Readonly) { + const result = originalCreateDivider(tagName, format); + result.cachedElement = mockedCachedElement; + return result; + } + + function createFormatContainer( + tag: Lowercase, + format?: Readonly + ) { + const result = originalCreateFormatContainer(tag, format); + result.cachedElement = mockedCachedElement; + return result; + } + + function createListItem( + levels: ReadonlyArray, + format?: Readonly + ) { + const result = originalCreateListItem(levels, format); + result.cachedElement = mockedCachedElement; + return result; + } + + function createListLevel( + listType: 'OL' | 'UL', + format?: Readonly, + dataset?: ReadonlyDatasetFormat + ) { + const result = originalCreateListLevel(listType, format, dataset); + result.cachedElement = mockedCachedElement; + return result; + } + + function createParagraph( + isImplicit?: boolean, + blockFormat?: Readonly, + segmentFormat?: Readonly, + decorator?: ReadonlyContentModelParagraphDecorator + ) { + const result = originalCreateParagraph(isImplicit, blockFormat, segmentFormat, decorator); + result.cachedElement = mockedCachedElement; + return result; + } + + function createTable(rowCount: number, format?: Readonly) { + const result = originalCreateTable(rowCount, format); + result.cachedElement = mockedCachedElement; + return result; + } + + function createTableCell( + spanLeftOrColSpan?: boolean | number, + spanAboveOrRowSpan?: boolean | number, + isHeader?: boolean, + format?: Readonly, + dataset?: ReadonlyDatasetFormat + ) { + const result = originalCreateTableCell( + spanLeftOrColSpan, + spanAboveOrRowSpan, + isHeader, + format, + dataset + ); + result.cachedElement = mockedCachedElement; + return result; + } + it('Empty model', () => { const model = createContentModelDocument(); @@ -55,6 +137,7 @@ describe('clearModelFormat', () => { text: 'test', }, ], + cachedElement: mockedCachedElement, }, ], }); @@ -450,6 +533,7 @@ describe('clearModelFormat', () => { text: 'test', }, ], + cachedElement: mockedCachedElement, }, ], }); @@ -613,6 +697,7 @@ describe('clearModelFormat', () => { blockType: 'Paragraph', format: { lineHeight: '10px' }, segments: [], + cachedElement: mockedCachedElement, }, { blockType: 'BlockGroup', @@ -624,6 +709,7 @@ describe('clearModelFormat', () => { blockType: 'Paragraph', format: { lineHeight: '20px' }, segments: [], + cachedElement: mockedCachedElement, }, ], }, @@ -670,6 +756,7 @@ describe('clearModelFormat', () => { blockType: 'Paragraph', format: { lineHeight: '50px' }, segments: [], + cachedElement: mockedCachedElement, }, ], }, @@ -677,6 +764,7 @@ describe('clearModelFormat', () => { blockType: 'Paragraph', format: { lineHeight: '60px' }, segments: [], + cachedElement: mockedCachedElement, }, ], }); diff --git a/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStartNumberTest.ts b/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStartNumberTest.ts index 5ec49dd766d..5eeae22240e 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStartNumberTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStartNumberTest.ts @@ -2,6 +2,8 @@ import { ContentModelDocument } from 'roosterjs-content-model-types'; import { setModelListStartNumber } from '../../../lib/modelApi/list/setModelListStartNumber'; describe('setModelListStartNumber', () => { + const mockedCachedElement = 'CACHE' as any; + function runTest(model: ContentModelDocument, value: number, expected: boolean) { // Act const actual = setModelListStartNumber(model, value); @@ -29,6 +31,7 @@ describe('setModelListStartNumber', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], levels: [ @@ -40,6 +43,7 @@ describe('setModelListStartNumber', () => { dataset: { editingInfo: '{"orderedStyleType":1}', }, + cachedElement: mockedCachedElement, }, ], formatHolder: { @@ -48,6 +52,7 @@ describe('setModelListStartNumber', () => { format: {}, }, format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'BlockGroup', @@ -68,6 +73,7 @@ describe('setModelListStartNumber', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], levels: [ @@ -79,6 +85,7 @@ describe('setModelListStartNumber', () => { dataset: { editingInfo: '{"orderedStyleType":1}', }, + cachedElement: mockedCachedElement, }, ], formatHolder: { @@ -87,6 +94,7 @@ describe('setModelListStartNumber', () => { format: {}, }, format: {}, + cachedElement: mockedCachedElement, }, ], format: {}, diff --git a/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStyleTest.ts b/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStyleTest.ts index ba55eae7dbf..a16432dcc1d 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStyleTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/list/setModelListStyleTest.ts @@ -2,6 +2,8 @@ import { ContentModelDocument, ListMetadataFormat } from 'roosterjs-content-mode import { setModelListStyle } from '../../../lib/modelApi/list/setModelListStyle'; describe('setModelListStyle', () => { + const mockedCachedElement = 'CACHE' as any; + function runTest(model: ContentModelDocument, style: ListMetadataFormat, expected: boolean) { // Act const actual = setModelListStyle(model, style); @@ -29,6 +31,7 @@ describe('setModelListStyle', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], levels: [ @@ -40,6 +43,7 @@ describe('setModelListStyle', () => { dataset: { editingInfo: '{"orderedStyleType":1}', }, + cachedElement: mockedCachedElement, }, ], formatHolder: { @@ -48,6 +52,7 @@ describe('setModelListStyle', () => { format: {}, }, format: {}, + cachedElement: mockedCachedElement, }, { blockType: 'BlockGroup', @@ -68,6 +73,7 @@ describe('setModelListStyle', () => { }, ], format: {}, + cachedElement: mockedCachedElement, }, ], levels: [ @@ -79,6 +85,7 @@ describe('setModelListStyle', () => { dataset: { editingInfo: '{"orderedStyleType":1}', }, + cachedElement: mockedCachedElement, }, ], formatHolder: { @@ -87,6 +94,7 @@ describe('setModelListStyle', () => { format: {}, }, format: {}, + cachedElement: mockedCachedElement, }, ], format: {}, diff --git a/packages/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts b/packages/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts index e6be395e9d3..31b52ffc0ad 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts @@ -1,5 +1,9 @@ import { formatSegmentWithContentModel } from '../../../lib/publicApi/utils/formatSegmentWithContentModel'; -import { IEditor } from 'roosterjs-content-model-types'; +import { + ContentModelBlockFormat, + IEditor, + ReadonlyContentModelParagraphDecorator, +} from 'roosterjs-content-model-types'; import { ContentModelDocument, ContentModelSegmentFormat, @@ -9,7 +13,7 @@ import { } from 'roosterjs-content-model-types'; import { createContentModelDocument, - createParagraph, + createParagraph as originalCreateParagraph, createSelectionMarker, createText, } from 'roosterjs-content-model-dom'; @@ -21,6 +25,18 @@ describe('formatSegment', () => { let formatContentModel: jasmine.Spy; let formatResult: boolean | undefined; let context: FormatContentModelContext | undefined; + const mockedCachedElement = 'CACHE' as any; + + function createParagraph( + isImplicit?: boolean, + blockFormat?: Readonly, + segmentFormat?: Readonly, + decorator?: ReadonlyContentModelParagraphDecorator + ) { + const result = originalCreateParagraph(isImplicit, blockFormat, segmentFormat, decorator); + result.cachedElement = mockedCachedElement; + return result; + } const apiName = 'mockedApi'; @@ -68,13 +84,16 @@ describe('formatSegment', () => { it('doc with selection', () => { model = createContentModelDocument(); - const para = createParagraph(); - const text = createText('test'); + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); - text.isSelected = true; + text1.isSelected = true; - para.segments.push(text); - model.blocks.push(para); + para1.segments.push(text1); + para2.segments.push(text2); + model.blocks.push(para1, para2); const callback = jasmine .createSpy('callback') @@ -93,7 +112,7 @@ describe('formatSegment', () => { segments: [ { segmentType: 'Text', - text: 'test', + text: 'test1', isSelected: true, format: { fontFamily: 'test', @@ -101,12 +120,24 @@ describe('formatSegment', () => { }, ], }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test2', + format: {}, + }, + ], + cachedElement: mockedCachedElement, + }, ], }); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith(text.format, true, text, para); + expect(callback).toHaveBeenCalledWith(text1.format, true, text1, para1); expect(context).toEqual({ newEntities: [], deletedEntities: [], diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts b/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts index 281aa62f707..58a2d74044b 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/entity/entityDelimiterUtils.ts @@ -14,7 +14,6 @@ import { isCharacterValue, findClosestBlockEntityContainer, mutateSegment, - setParagraphNotImplicit, mutateBlock, } from 'roosterjs-content-model-dom'; import type { @@ -304,23 +303,17 @@ function handleInputOnDelimiter( * @returns */ export const handleKeyDownInBlockDelimiter: ContentModelFormatter = (model, context) => { - iterateSelections(model, (_path, _tableContext, block) => { - if (block?.blockType == 'Paragraph') { - const paragraph = mutateBlock(block); - const selectionMarker = paragraph.segments.find( - w => w.segmentType == 'SelectionMarker' - ); - - if (paragraph.isImplicit) { - setParagraphNotImplicit(paragraph); - } + iterateSelections(model, (_path, _tableContext, readonlyBlock) => { + if (readonlyBlock?.blockType == 'Paragraph') { + const block = mutateBlock(readonlyBlock); + delete block.isImplicit; + const selectionMarker = block.segments.find(w => w.segmentType == 'SelectionMarker'); if (selectionMarker?.segmentType == 'SelectionMarker') { - paragraph.segmentFormat = { ...selectionMarker.format }; + block.segmentFormat = { ...selectionMarker.format }; context.newPendingFormat = { ...selectionMarker.format }; } - - paragraph.segments.unshift(createBr()); + block.segments.unshift(createBr()); } }); @@ -332,45 +325,44 @@ export const handleKeyDownInBlockDelimiter: ContentModelFormatter = (model, cont * @returns */ export const handleEnterInlineEntity: ContentModelFormatter = model => { - let selectionBlock: ReadonlyContentModelParagraph | undefined; + let readonlySelectionBlock: ReadonlyContentModelParagraph | undefined; let selectionBlockParent: ReadonlyContentModelBlockGroup | undefined; iterateSelections(model, (path, _tableContext, block) => { if (block?.blockType == 'Paragraph') { - selectionBlock = block; + readonlySelectionBlock = block; selectionBlockParent = path[path.length - 1]; } }); - if (selectionBlock && selectionBlockParent) { - const markerIndex = selectionBlock.segments.findIndex( + if (readonlySelectionBlock && selectionBlockParent) { + const markerIndex = readonlySelectionBlock.segments.findIndex( segment => segment.segmentType == 'SelectionMarker' ); if (markerIndex >= 0) { - const paragraph = mutateBlock(selectionBlock); - const segmentsAfterMarker = paragraph.segments.splice(markerIndex); + const selectionBlock = mutateBlock(readonlySelectionBlock); + const segmentsAfterMarker = selectionBlock.segments.splice(markerIndex); const newPara: ShallowMutableContentModelParagraph = createParagraph( false, - paragraph.format, - paragraph.segmentFormat, - paragraph.decorator + selectionBlock.format, + selectionBlock.segmentFormat, + selectionBlock.decorator ); if ( - paragraph.segments.every( + selectionBlock.segments.every( x => x.segmentType == 'SelectionMarker' || x.segmentType == 'Br' ) || segmentsAfterMarker.every(x => x.segmentType == 'SelectionMarker') ) { - newPara.segments.push(createBr(paragraph.format)); + newPara.segments.push(createBr(selectionBlock.format)); } newPara.segments.push(...segmentsAfterMarker); - const selectionBlockIndex = selectionBlockParent.blocks.indexOf(paragraph); - + const selectionBlockIndex = selectionBlockParent.blocks.indexOf(selectionBlock); if (selectionBlockIndex >= 0) { mutateBlock(selectionBlockParent).blocks.splice( selectionBlockIndex + 1, diff --git a/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts index 2e0cd6a923b..aa40486771e 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/entity/entityDelimiterUtilsTest.ts @@ -985,7 +985,6 @@ describe('handleKeyDownInBlockDelimiter', () => { ], format: {}, segmentFormat: {}, - isImplicit: false, }, { blockType: 'Paragraph', @@ -1082,7 +1081,6 @@ describe('handleKeyDownInBlockDelimiter', () => { ], format: {}, segmentFormat: {}, - isImplicit: false, }, { segmentType: 'Entity', diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts index 3e3e09aa4ea..08ca6e5b3ca 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleDivider.ts @@ -18,7 +18,7 @@ export const handleDivider: ContentModelBlockHandler = ( ) => { let element = context.allowCacheElement ? divider.cachedElement : undefined; - if (element) { + if (element && !divider.isSelected) { refNode = reuseCachedElement(parent, element, refNode); } else { element = doc.createElement(divider.tagName); diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts index 5b6441f1fe7..2ae14c63391 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts @@ -24,7 +24,7 @@ export const handleParagraph: ContentModelBlockHandler = ) => { let container = context.allowCacheElement ? paragraph.cachedElement : undefined; - if (container) { + if (container && paragraph.segments.every(x => x.segmentType != 'General' && !x.isSelected)) { refNode = reuseCachedElement(parent, container, refNode); } else { stackFormat(context, paragraph.decorator?.tagName || null, () => { From 96a4a48838b088bbf7192bec246d16b74b0d2cb2 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 28 May 2024 11:38:15 -0700 Subject: [PATCH 47/66] fix build --- .../lib/publicApi/table/applyTableBorderFormat.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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; From 87ab530736cd71287bdf67ab47ffc0b360067761 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 28 May 2024 11:38:59 -0700 Subject: [PATCH 48/66] fix build --- .../lib/tableEdit/editors/utils/getTableFromContentModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/utils/getTableFromContentModel.ts b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/utils/getTableFromContentModel.ts index cfc21aa6585..74cec91f209 100644 --- a/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/utils/getTableFromContentModel.ts +++ b/packages/roosterjs-content-model-plugins/lib/tableEdit/editors/utils/getTableFromContentModel.ts @@ -1,12 +1,12 @@ import { getFirstSelectedTable } from 'roosterjs-content-model-dom'; -import type { ContentModelTable, IEditor } from 'roosterjs-content-model-types'; +import type { IEditor, ReadonlyContentModelTable } from 'roosterjs-content-model-types'; /** * @internal * Get ContentModelTable from a table element if it is present in the content model */ export function getCMTableFromTable(editor: IEditor, table: HTMLTableElement) { - let cmTable: ContentModelTable | undefined; + let cmTable: ReadonlyContentModelTable | undefined; editor.formatContentModel( model => { From 2edbc0ae98fffa61e12b0f61f090058ad5485e30 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 28 May 2024 12:28:44 -0700 Subject: [PATCH 49/66] fix build --- .../tableEdit/editors/features/CellResizer.ts | 15 +++++++++------ .../tableEdit/editors/features/TableMover.ts | 12 +++++++----- .../tableEdit/editors/features/TableResizer.ts | 17 ++++++++++++----- 3 files changed, 28 insertions(+), 16 deletions(-) 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..99360f169d4 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; /** @@ -78,7 +79,7 @@ interface DragAndDropContext { } interface DragAndDropInitValue { - cmTable: ContentModelTable | undefined; + cmTable: ReadonlyContentModelTable | undefined; anchorColumn: number | undefined; anchorRow: number | undefined; anchorRowHeight: number; @@ -144,7 +145,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); @@ -173,13 +174,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 +193,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 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 5f8c9313425..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,6 +6,7 @@ import type { TableEditFeature } from './TableEditFeature'; import type { OnTableEditorCreatedCallback } from '../../OnTableEditorCreatedCallback'; import type { DragAndDropHandler } from '../../../pluginUtils/DragAndDrop/DragAndDropHandler'; import { + cloneModel, createContentModelDocument, createSelectionMarker, getFirstSelectedTable, @@ -17,11 +18,12 @@ import { 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; @@ -125,7 +127,7 @@ export interface TableMoverContext { * Exported for testing */ export interface TableMoverInitValue { - cmTable: ContentModelTable | undefined; + cmTable: ReadonlyContentModelTable | undefined; initialSelection: DOMSelection | null; tableRect: HTMLDivElement; } @@ -331,9 +333,9 @@ export function onDragEnd( 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, }); 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..5e8b8a815f1 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; @@ -117,7 +122,7 @@ interface DragAndDropInitValue { originalRect: DOMRect; originalHeights: number[]; originalWidths: number[]; - cmTable: ContentModelTable | undefined; + cmTable: ReadonlyContentModelTable | undefined; } function onDragStart(context: DragAndDropContext, event: MouseEvent) { @@ -168,16 +173,18 @@ function onDragging( // Assign new widths and heights to the CM table if (cmTable && cmTable.rows && (shouldResizeX || shouldResizeY)) { + const mutableTable = mutateBlock(cmTable); + // Modify the CM Table size for (let i = 0; i < cmTable.rows.length; i++) { for (let j = 0; j < cmTable.rows[i].cells.length; j++) { const cell = cmTable.rows[i].cells[j]; if (cell) { if (shouldResizeX && i == 0) { - cmTable.widths[j] = (originalWidths[j] ?? 0) * ratioX; + mutableTable.widths[j] = (originalWidths[j] ?? 0) * ratioX; } if (shouldResizeY && j == 0) { - cmTable.rows[i].height = (originalHeights[i] ?? 0) * ratioY; + mutableTable.rows[i].height = (originalHeights[i] ?? 0) * ratioY; } } } From a5ef96891fb1df16891e0777899d705f088ee183 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 29 May 2024 12:15:00 -0700 Subject: [PATCH 50/66] Fix build --- .../lib/edit/deleteSteps/deleteEmptyQuote.ts | 12 ++++++++---- .../lib/edit/inputSteps/handleEnterOnParagraph.ts | 3 ++- .../lib/edit/utils/splitParagraph.ts | 7 +++++-- 3 files changed, 15 insertions(+), 7 deletions(-) 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 40b2752af45..57abbb39218 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts @@ -7,11 +7,12 @@ import { } from 'roosterjs-content-model-dom'; import type { ContentModelFormatContainer, - ContentModelParagraph, DeleteSelectionStep, ReadonlyContentModelBlockGroup, ReadonlyContentModelFormatContainer, ReadonlyContentModelParagraph, + ShallowMutableContentModelFormatContainer, + ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; /** @@ -83,10 +84,10 @@ const isSelectionOnEmptyLine = ( }; const insertNewLine = ( - quote: ContentModelFormatContainer, + quote: ShallowMutableContentModelFormatContainer, parent: ReadonlyContentModelBlockGroup, quoteIndex: number, - paragraph: ContentModelParagraph + paragraph: ShallowMutableContentModelParagraph ) => { const paraIndex = quote.blocks.indexOf(paragraph); @@ -94,7 +95,10 @@ const insertNewLine = ( const mutableParent = mutateBlock(parent); if (paraIndex < quote.blocks.length - 1) { - const newQuote = createFormatContainer(quote.tagName, quote.format); + const newQuote: ShallowMutableContentModelFormatContainer = createFormatContainer( + quote.tagName, + quote.format + ); newQuote.blocks.push( ...quote.blocks.splice(paraIndex + 1, quote.blocks.length - paraIndex - 1) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts index 312966a5fa7..e70318cd375 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnParagraph.ts @@ -1,3 +1,4 @@ +import { mutateBlock } from 'roosterjs-content-model-dom'; import { splitParagraph } from '../utils/splitParagraph'; import type { DeleteSelectionStep } from 'roosterjs-content-model-types'; @@ -11,7 +12,7 @@ export const handleEnterOnParagraph: DeleteSelectionStep = context => { if (context.deleteResult == 'notDeleted' && paraIndex >= 0) { const newPara = splitParagraph(context.insertPoint); - path[0].blocks.splice(paraIndex + 1, 0, newPara); + mutateBlock(path[0]).blocks.splice(paraIndex + 1, 0, newPara); context.deleteResult = 'range'; context.lastParagraph = newPara; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts index 9fd83d3668a..963f9865f3e 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/utils/splitParagraph.ts @@ -4,7 +4,10 @@ import { normalizeParagraph, setParagraphNotImplicit, } from 'roosterjs-content-model-dom'; -import type { InsertPoint } from 'roosterjs-content-model-types'; +import type { + InsertPoint, + ShallowMutableContentModelParagraph, +} from 'roosterjs-content-model-types'; /** * @internal @@ -15,7 +18,7 @@ import type { InsertPoint } from 'roosterjs-content-model-types'; */ export function splitParagraph(insertPoint: InsertPoint) { const { paragraph, marker } = insertPoint; - const newParagraph = createParagraph( + const newParagraph: ShallowMutableContentModelParagraph = createParagraph( false /*isImplicit*/, paragraph.format, paragraph.segmentFormat From dd1171c106a2e9a3cf1e0a5d7d5a78b63c38b8ee Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 29 May 2024 12:27:17 -0700 Subject: [PATCH 51/66] fix build --- .../lib/corePlugin/cache/CachePlugin.ts | 4 ---- .../lib/corePlugin/cache/domIndexerImpl.ts | 2 +- .../lib/context/TextMutationObserver.ts | 2 +- .../lib/parameter/ReconcileChildListContext.ts | 4 ++-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index a36f27c6b0d..0e72ac454ac 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -171,13 +171,9 @@ class CachePlugin implements PluginWithState { this.invalidateCache(); } else { updateCachedSelection(this.state, newRangeEx); - - console.log('Successfully reconcile' + JSON.stringify(this.state.cachedModel)); } } else { this.state.cachedSelection = cachedSelection; - - console.log('Selection not changed'); } } } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index ba65d41b764..db3aea517d2 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -408,7 +408,7 @@ function indexNode( textNode: Text, format?: ContentModelSegmentFormat ) { - let copiedFormat = format ? { ...format } : undefined; + const copiedFormat = format ? { ...format } : undefined; if (copiedFormat) { getObjectKeys(copiedFormat).forEach(key => { diff --git a/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts b/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts index 88588d7b0c1..f24566d9a8f 100644 --- a/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts +++ b/packages/roosterjs-content-model-types/lib/context/TextMutationObserver.ts @@ -1,4 +1,4 @@ -import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; /** * A wrapper of MutationObserver to observe text change from editor diff --git a/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts b/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts index a95b71f74d1..ad75536ff55 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts @@ -1,5 +1,5 @@ -import type { ContentModelParagraph } from '../block/ContentModelParagraph'; -import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; +import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; /** * Context object used by DomIndexer when reconcile mutations with child list From ad9feeb9f6dfef51602cc3bd51f027bd4eb78daf Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 30 May 2024 16:41:53 -0700 Subject: [PATCH 52/66] improve --- .../lib/edit/keyboardEnter.ts | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index 6f5d17e1c3b..1e66b98ddb5 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -2,7 +2,11 @@ import { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote'; import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom'; import { handleEnterOnList } from './inputSteps/handleEnterOnList'; import { handleEnterOnParagraph } from './inputSteps/handleEnterOnParagraph'; -import type { DOMSelection, DeleteSelectionStep, IEditor } from 'roosterjs-content-model-types'; +import type { + DeleteSelectionContext, + IEditor, + ValidDeleteSelectionContext, +} from 'roosterjs-content-model-types'; /** * @internal @@ -12,7 +16,25 @@ export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { editor.formatContentModel( (model, context) => { - const result = deleteSelection(model, getInputSteps(selection, rawEvent), context); + // 1. delete the expanded selection if any, then merge paragraph + let result = deleteSelection(model, [], context); + + // 2. Add line break + if (selection && selection.type != 'table') { + // For ENTER key, although we may have deleted something, since we still need to split the line, we always treat it as not delete + // so further delete steps can keep working + result.deleteResult = 'notDeleted'; + + const steps = rawEvent.shiftKey + ? [handleEnterOnParagraph] + : [handleEnterOnList, deleteEmptyQuote, handleEnterOnParagraph]; + + steps.forEach(step => { + if (isValidDeleteSelectionContext(result)) { + step(result); + } + }); + } if (result.deleteResult == 'range') { // We have deleted something, next input should inherit the segment format from deleted content, so set pending format here @@ -33,22 +55,8 @@ export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { ); } -function getInputSteps(selection: DOMSelection | null, rawEvent: KeyboardEvent) { - const result: DeleteSelectionStep[] = [clearDeleteResult]; - - if (selection && selection.type != 'table') { - if (rawEvent.shiftKey) { - result.push(handleEnterOnParagraph); - } else { - result.push(handleEnterOnList, deleteEmptyQuote, handleEnterOnParagraph); - } - } - - return result; +function isValidDeleteSelectionContext( + context: DeleteSelectionContext +): context is ValidDeleteSelectionContext { + return !!context.insertPoint; } - -const clearDeleteResult: DeleteSelectionStep = context => { - // For ENTER key, although we may have deleted something, since we still need to split the line, we always treat it as not delete - // so further delete steps can keep working - context.deleteResult = 'notDeleted'; -}; From 3a7d92e14a5ea252980479f920c277f215dad3a5 Mon Sep 17 00:00:00 2001 From: JiuqingSong Date: Thu, 30 May 2024 17:20:10 -0700 Subject: [PATCH 53/66] fix build --- .../roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index 1e66b98ddb5..07ed60355ca 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -17,7 +17,7 @@ export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { editor.formatContentModel( (model, context) => { // 1. delete the expanded selection if any, then merge paragraph - let result = deleteSelection(model, [], context); + const result = deleteSelection(model, [], context); // 2. Add line break if (selection && selection.type != 'table') { From 6ae3bb0224a92256951e5d7f14069730327417fd Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 31 May 2024 12:29:35 -0700 Subject: [PATCH 54/66] Add experimental features --- demo/scripts/controlsV2/mainPane/MainPane.tsx | 3 + .../editorOptions/EditorOptionsPlugin.ts | 2 + .../editorOptions/ExperimentalFeatures.tsx | 42 ++ .../sidePane/editorOptions/OptionState.ts | 3 +- .../sidePane/editorOptions/OptionsPane.tsx | 8 + .../createDomToModelContextForSanitizing.ts | 1 + .../createEditorContext.ts | 1 + .../lib/corePlugin/cache/CachePlugin.ts | 7 +- .../lib/corePlugin/cache/domIndexerImpl.ts | 407 +++++++++--------- .../lib/editor/core/createEditorCore.ts | 1 + ...reateDomToModelContextForSanitizingTest.ts | 2 + .../createEditorContextTest.ts | 7 + .../test/corePlugin/cache/CachePluginTest.ts | 4 +- .../corePlugin/cache/domIndexerImplTest.ts | 18 +- .../test/editor/core/createEditorCoreTest.ts | 1 + .../lib/domToModel/domToContentModel.ts | 6 + .../modelApi/selection/iterateSelections.ts | 34 +- .../selection/iterateSelectionsTest.ts | 105 +++-- .../lib/context/EditorContext.ts | 5 + .../lib/editor/EditorCore.ts | 5 + .../lib/editor/EditorOptions.ts | 10 +- .../lib/editor/ExperimentalFeature.ts | 11 + .../lib/index.ts | 1 + .../lib/corePlugins/BridgePlugin.ts | 4 +- .../lib/publicTypes/EditorAdapterOptions.ts | 4 +- 25 files changed, 433 insertions(+), 259 deletions(-) create mode 100644 demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx create mode 100644 packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts diff --git a/demo/scripts/controlsV2/mainPane/MainPane.tsx b/demo/scripts/controlsV2/mainPane/MainPane.tsx index 9f3ec7f2316..a6923efd2be 100644 --- a/demo/scripts/controlsV2/mainPane/MainPane.tsx +++ b/demo/scripts/controlsV2/mainPane/MainPane.tsx @@ -370,6 +370,9 @@ export class MainPane extends React.Component<{}, MainPaneState> { knownColors={this.knownColors} disableCache={this.state.initState.disableCache} announcerStringGetter={getAnnouncingString} + experimentalFeatures={Array.from( + this.state.initState.experimentalFeatures + )} /> )} diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index 1bd0329e394..321ac281c8e 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -1,4 +1,5 @@ import { emojiReplacements } from './getReplacements'; +import { ExperimentalFeature } from 'roosterjs-content-model-types'; import { OptionPaneProps, OptionState, UrlPlaceholder } from './OptionState'; import { OptionsPane } from './OptionsPane'; import { SidePaneElementProps } from '../SidePaneElement'; @@ -55,6 +56,7 @@ const initialState: OptionState = { codeFormat: {}, }, customReplacements: emojiReplacements, + experimentalFeatures: new Set(['PersistCache']), }; export class EditorOptionsPlugin extends SidePanePluginImpl { diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx new file mode 100644 index 00000000000..da543da70c9 --- /dev/null +++ b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { ExperimentalFeature } from 'roosterjs-content-model-types'; +import { OptionState } from './OptionState'; + +export interface DefaultFormatProps { + state: OptionState; + resetState: (callback: (state: OptionState) => void, resetEditor: boolean) => void; +} + +export class ExperimentalFeatures extends React.Component { + render() { + return this.renderFeature('PersistCache'); + } + + private renderFeature(featureName: ExperimentalFeature): JSX.Element { + let checked = this.props.state.experimentalFeatures.has(featureName); + + return ( +
+ this.onFeatureClick(featureName)} + /> + +
+ ); + } + + private onFeatureClick = (featureName: ExperimentalFeature) => { + this.props.resetState(state => { + let checkbox = document.getElementById(featureName) as HTMLInputElement; + + if (checkbox.checked) { + state.experimentalFeatures.add(featureName); + } else { + state.experimentalFeatures.delete(featureName); + } + }, true); + }; +} diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts b/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts index 57bbd973492..d2eb00a73e7 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/OptionState.ts @@ -1,6 +1,6 @@ import { AutoFormatOptions, CustomReplace, MarkdownOptions } from 'roosterjs-content-model-plugins'; import type { SidePaneElementProps } from '../SidePaneElement'; -import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; +import type { ContentModelSegmentFormat, ExperimentalFeature } from 'roosterjs-content-model-types'; export interface LegacyPluginList { imageEdit: boolean; @@ -47,6 +47,7 @@ export interface OptionState { isRtl: boolean; disableCache: boolean; applyChangesOnMouseUp: boolean; + experimentalFeatures: Set; } export interface OptionPaneProps extends OptionState, SidePaneElementProps {} diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/OptionsPane.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/OptionsPane.tsx index c9cc0e0e7aa..369bce6fb77 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/OptionsPane.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/OptionsPane.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Code } from './Code'; import { DefaultFormatPane } from './DefaultFormatPane'; import { EditorCode } from './codes/EditorCode'; +import { ExperimentalFeatures } from './ExperimentalFeatures'; import { LegacyPlugins, Plugins } from './Plugins'; import { MainPane } from '../../mainPane/MainPane'; import { OptionPaneProps, OptionState } from './OptionState'; @@ -63,6 +64,12 @@ export class OptionsPane extends React.Component { +
+ + Experimental features + + +

@@ -140,6 +147,7 @@ export class OptionsPane extends React.Component { autoFormatOptions: { ...this.state.autoFormatOptions }, markdownOptions: { ...this.state.markdownOptions }, customReplacements: this.state.customReplacements, + experimentalFeatures: this.state.experimentalFeatures, }; if (callback) { diff --git a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts index 439bb3466e4..b88c24ae8af 100644 --- a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts +++ b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts @@ -41,6 +41,7 @@ export function createDomToModelContextForSanitizing( { defaultFormat, ...getRootComputedStyleForContext(document), + experimentalFeatures: [], }, defaultOption, { diff --git a/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts b/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts index 720c0645a71..ff671ba1259 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts @@ -19,6 +19,7 @@ export const createEditorContext: CreateEditorContext = (core, saveIndex) => { allowCacheElement: true, domIndexer: saveIndex ? cache.domIndexer : undefined, zoomScale: domHelper.calculateZoomScale(), + experimentalFeatures: core.experimentalFeatures ?? [], ...getRootComputedStyleForContext(logicalRoot.ownerDocument), }; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index 199209cf817..9bac8782375 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -1,6 +1,6 @@ import { areSameSelection } from './areSameSelection'; import { createTextMutationObserver } from './textMutationObserver'; -import { domIndexerImpl } from './domIndexerImpl'; +import { DomIndexerImpl } from './domIndexerImpl'; import { updateCachedSelection } from './updateCachedSelection'; import type { CachePluginState, @@ -26,7 +26,10 @@ class CachePlugin implements PluginWithState { this.state = option.disableCache ? {} : { - domIndexer: domIndexerImpl, + domIndexer: new DomIndexerImpl( + option.experimentalFeatures && + option.experimentalFeatures.indexOf('PersistCache') >= 0 + ), textMutationObserver: createTextMutationObserver(contentDiv, this.onMutation), }; } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index e26f992b512..1e0b09ad283 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -47,252 +47,251 @@ function isIndexedSegment(node: Node): node is IndexedSegmentNode { ); } -function onSegment( - segmentNode: Node, - paragraph: ContentModelParagraph, - segment: ContentModelSegment[] -) { - const indexedText = segmentNode as IndexedSegmentNode; - indexedText.__roosterjsContentModel = { - paragraph, - segments: segment, - }; -} - -function onParagraph(paragraphElement: HTMLElement) { - let previousText: Text | null = null; +/** + * @internal + * Implementation of DomIndexer + */ +export class DomIndexerImpl implements DomIndexer { + constructor(public readonly persistCache?: boolean) {} + + onSegment(segmentNode: Node, paragraph: ContentModelParagraph, segment: ContentModelSegment[]) { + const indexedText = segmentNode as IndexedSegmentNode; + indexedText.__roosterjsContentModel = { + paragraph, + segments: segment, + }; + } - for (let child = paragraphElement.firstChild; child; child = child.nextSibling) { - if (isNodeOfType(child, 'TEXT_NODE')) { - if (!previousText) { - previousText = child; - } else { - const item = isIndexedSegment(previousText) - ? previousText.__roosterjsContentModel - : undefined; + onParagraph(paragraphElement: HTMLElement) { + let previousText: Text | null = null; - if (item && isIndexedSegment(child)) { - item.segments = item.segments.concat(child.__roosterjsContentModel.segments); - child.__roosterjsContentModel.segments = []; + for (let child = paragraphElement.firstChild; child; child = child.nextSibling) { + if (isNodeOfType(child, 'TEXT_NODE')) { + if (!previousText) { + previousText = child; + } else { + const item = isIndexedSegment(previousText) + ? previousText.__roosterjsContentModel + : undefined; + + if (item && isIndexedSegment(child)) { + item.segments = item.segments.concat( + child.__roosterjsContentModel.segments + ); + child.__roosterjsContentModel.segments = []; + } } - } - } else if (isNodeOfType(child, 'ELEMENT_NODE')) { - previousText = null; + } else if (isNodeOfType(child, 'ELEMENT_NODE')) { + previousText = null; - onParagraph(child); - } else { - previousText = null; + this.onParagraph(child); + } else { + previousText = null; + } } } -} -function onTable(tableElement: HTMLTableElement, table: ContentModelTable) { - const indexedTable = tableElement as IndexedTableElement; - indexedTable.__roosterjsContentModel = { tableRows: table.rows }; -} + onTable(tableElement: HTMLTableElement, table: ContentModelTable) { + const indexedTable = tableElement as IndexedTableElement; + indexedTable.__roosterjsContentModel = { tableRows: table.rows }; + } -function reconcileSelection( - model: ContentModelDocument, - newSelection: DOMSelection, - oldSelection?: CacheSelection -): boolean { - if (oldSelection) { - if ( - oldSelection.type == 'range' && - isCollapsed(oldSelection) && - isNodeOfType(oldSelection.start.node, 'TEXT_NODE') - ) { - if (isIndexedSegment(oldSelection.start.node)) { - reconcileTextSelection(oldSelection.start.node); + reconcileSelection( + model: ContentModelDocument, + newSelection: DOMSelection, + oldSelection?: CacheSelection + ): boolean { + if (oldSelection) { + if ( + oldSelection.type == 'range' && + this.isCollapsed(oldSelection) && + isNodeOfType(oldSelection.start.node, 'TEXT_NODE') + ) { + if (isIndexedSegment(oldSelection.start.node)) { + this.reconcileTextSelection(oldSelection.start.node); + } + } else { + setSelection(model); } - } else { - setSelection(model); } - } - switch (newSelection.type) { - case 'image': - case 'table': - // For image and table selection, we just clear the cached model since during selecting the element id might be changed - return false; - - case 'range': - const newRange = newSelection.range; - if (newRange) { - const { - startContainer, - startOffset, - endContainer, - endOffset, - collapsed, - } = newRange; - - delete model.hasRevertedRangeSelection; - - if (collapsed) { - return !!reconcileNodeSelection(startContainer, startOffset); - } else if ( - startContainer == endContainer && - isNodeOfType(startContainer, 'TEXT_NODE') - ) { - if (newSelection.isReverted) { - model.hasRevertedRangeSelection = true; - } - - return ( - isIndexedSegment(startContainer) && - !!reconcileTextSelection(startContainer, startOffset, endOffset) - ); - } else { - const marker1 = reconcileNodeSelection(startContainer, startOffset); - const marker2 = reconcileNodeSelection(endContainer, endOffset); - - if (marker1 && marker2) { + switch (newSelection.type) { + case 'image': + case 'table': + // For image and table selection, we just clear the cached model since during selecting the element id might be changed + return false; + + case 'range': + const newRange = newSelection.range; + if (newRange) { + const { + startContainer, + startOffset, + endContainer, + endOffset, + collapsed, + } = newRange; + + delete model.hasRevertedRangeSelection; + + if (collapsed) { + return !!this.reconcileNodeSelection(startContainer, startOffset); + } else if ( + startContainer == endContainer && + isNodeOfType(startContainer, 'TEXT_NODE') + ) { if (newSelection.isReverted) { model.hasRevertedRangeSelection = true; } - setSelection(model, marker1, marker2); - return true; + return ( + isIndexedSegment(startContainer) && + !!this.reconcileTextSelection(startContainer, startOffset, endOffset) + ); } else { - return false; + const marker1 = this.reconcileNodeSelection(startContainer, startOffset); + const marker2 = this.reconcileNodeSelection(endContainer, endOffset); + + if (marker1 && marker2) { + if (newSelection.isReverted) { + model.hasRevertedRangeSelection = true; + } + + setSelection(model, marker1, marker2); + return true; + } else { + return false; + } } } - } - break; - } + break; + } - return false; -} + return false; + } -function isCollapsed(selection: RangeSelectionForCache): boolean { - const { start, end } = selection; + private isCollapsed(selection: RangeSelectionForCache): boolean { + const { start, end } = selection; - return start.node == end.node && start.offset == end.offset; -} + return start.node == end.node && start.offset == end.offset; + } -function reconcileNodeSelection(node: Node, offset: number): Selectable | undefined { - if (isNodeOfType(node, 'TEXT_NODE')) { - return isIndexedSegment(node) ? reconcileTextSelection(node, offset) : undefined; - } else if (offset >= node.childNodes.length) { - return insertMarker(node.lastChild, true /*isAfter*/); - } else { - return insertMarker(node.childNodes[offset], false /*isAfter*/); + private reconcileNodeSelection(node: Node, offset: number): Selectable | undefined { + if (isNodeOfType(node, 'TEXT_NODE')) { + return isIndexedSegment(node) ? this.reconcileTextSelection(node, offset) : undefined; + } else if (offset >= node.childNodes.length) { + return this.insertMarker(node.lastChild, true /*isAfter*/); + } else { + return this.insertMarker(node.childNodes[offset], false /*isAfter*/); + } } -} -function insertMarker(node: Node | null, isAfter: boolean): Selectable | undefined { - let marker: ContentModelSelectionMarker | undefined; + private insertMarker(node: Node | null, isAfter: boolean): Selectable | undefined { + let marker: ContentModelSelectionMarker | undefined; - if (node && isIndexedSegment(node)) { - const { paragraph, segments } = node.__roosterjsContentModel; - const index = paragraph.segments.indexOf(segments[0]); + if (node && isIndexedSegment(node)) { + const { paragraph, segments } = node.__roosterjsContentModel; + const index = paragraph.segments.indexOf(segments[0]); - if (index >= 0) { - const formatSegment = - (!isAfter && paragraph.segments[index - 1]) || paragraph.segments[index]; - marker = createSelectionMarker(formatSegment.format); + if (index >= 0) { + const formatSegment = + (!isAfter && paragraph.segments[index - 1]) || paragraph.segments[index]; + marker = createSelectionMarker(formatSegment.format); - paragraph.segments.splice(isAfter ? index + 1 : index, 0, marker); + paragraph.segments.splice(isAfter ? index + 1 : index, 0, marker); + } } - } - return marker; -} + return marker; + } -function reconcileTextSelection( - textNode: IndexedSegmentNode, - startOffset?: number, - endOffset?: number -) { - const { paragraph, segments } = textNode.__roosterjsContentModel; - const first = segments[0]; - const last = segments[segments.length - 1]; - let selectable: Selectable | undefined; - - if (first?.segmentType == 'Text' && last?.segmentType == 'Text') { - const newSegments: ContentModelSegment[] = []; - const txt = textNode.nodeValue || ''; - const textSegments: ContentModelText[] = []; - - if (startOffset === undefined) { - first.text = txt; - newSegments.push(first); - textSegments.push(first); - } else { - if (startOffset > 0) { - first.text = txt.substring(0, startOffset); + private reconcileTextSelection( + textNode: IndexedSegmentNode, + startOffset?: number, + endOffset?: number + ) { + const { paragraph, segments } = textNode.__roosterjsContentModel; + const first = segments[0]; + const last = segments[segments.length - 1]; + let selectable: Selectable | undefined; + + if (first?.segmentType == 'Text' && last?.segmentType == 'Text') { + const newSegments: ContentModelSegment[] = []; + const txt = textNode.nodeValue || ''; + const textSegments: ContentModelText[] = []; + + if (startOffset === undefined) { + first.text = txt; newSegments.push(first); textSegments.push(first); - } + } else { + if (startOffset > 0) { + first.text = txt.substring(0, startOffset); + newSegments.push(first); + textSegments.push(first); + } - if (endOffset === undefined) { - const marker = createSelectionMarker(first.format); - newSegments.push(marker); - - selectable = marker; - endOffset = startOffset; - } else if (endOffset > startOffset) { - const middle = createText( - txt.substring(startOffset, endOffset), - first.format, - first.link, - first.code - ); - - middle.isSelected = true; - newSegments.push(middle); - textSegments.push(middle); - selectable = middle; - } + if (endOffset === undefined) { + const marker = createSelectionMarker(first.format); + newSegments.push(marker); + + selectable = marker; + endOffset = startOffset; + } else if (endOffset > startOffset) { + const middle = createText( + txt.substring(startOffset, endOffset), + first.format, + first.link, + first.code + ); - if (endOffset < txt.length) { - const newLast = createText( - txt.substring(endOffset), - first.format, - first.link, - first.code - ); - newSegments.push(newLast); - textSegments.push(newLast); + middle.isSelected = true; + newSegments.push(middle); + textSegments.push(middle); + selectable = middle; + } + + if (endOffset < txt.length) { + const newLast = createText( + txt.substring(endOffset), + first.format, + first.link, + first.code + ); + newSegments.push(newLast); + textSegments.push(newLast); + } } - } - let firstIndex = paragraph.segments.indexOf(first); - let lastIndex = paragraph.segments.indexOf(last); + let firstIndex = paragraph.segments.indexOf(first); + let lastIndex = paragraph.segments.indexOf(last); - if (firstIndex >= 0 && lastIndex >= 0) { - while ( - firstIndex > 0 && - paragraph.segments[firstIndex - 1].segmentType == 'SelectionMarker' - ) { - firstIndex--; - } + if (firstIndex >= 0 && lastIndex >= 0) { + while ( + firstIndex > 0 && + paragraph.segments[firstIndex - 1].segmentType == 'SelectionMarker' + ) { + firstIndex--; + } - while ( - lastIndex < paragraph.segments.length - 1 && - paragraph.segments[lastIndex + 1].segmentType == 'SelectionMarker' - ) { - lastIndex++; + while ( + lastIndex < paragraph.segments.length - 1 && + paragraph.segments[lastIndex + 1].segmentType == 'SelectionMarker' + ) { + lastIndex++; + } + + paragraph.segments.splice(firstIndex, lastIndex - firstIndex + 1, ...newSegments); } - paragraph.segments.splice(firstIndex, lastIndex - firstIndex + 1, ...newSegments); + this.onSegment(textNode, paragraph, textSegments); + + if (!this.persistCache) { + delete paragraph.cachedElement; + } } - onSegment(textNode, paragraph, textSegments); + return selectable; } - - return selectable; } - -/** - * @internal - * Implementation of DomIndexer - */ -export const domIndexerImpl: DomIndexer = { - onSegment, - onParagraph, - onTable, - reconcileSelection, -}; diff --git a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts index 4d1d0001ef1..b1e05569c4c 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts @@ -46,6 +46,7 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti domHelper: createDOMHelper(contentDiv), ...getPluginState(corePlugins), disposeErrorHandler: options.disposeErrorHandler, + experimentalFeatures: options.experimentalFeatures ? [...options.experimentalFeatures] : [], }; } diff --git a/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createDomToModelContextForSanitizingTest.ts b/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createDomToModelContextForSanitizingTest.ts index d12ce404354..c98a5d0422e 100644 --- a/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createDomToModelContextForSanitizingTest.ts +++ b/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createDomToModelContextForSanitizingTest.ts @@ -50,6 +50,7 @@ describe('createDomToModelContextForSanitizing', () => { { defaultFormat: undefined, rootFontSize: 16, + experimentalFeatures: [], }, undefined, { @@ -94,6 +95,7 @@ describe('createDomToModelContextForSanitizing', () => { { defaultFormat: mockedDefaultFormat, rootFontSize: 16, + experimentalFeatures: [], }, mockedOption, { diff --git a/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts b/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts index 595081af099..acab20a1071 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts @@ -45,6 +45,7 @@ describe('createEditorContext', () => { pendingFormat: undefined, zoomScale: 1, rootFontSize: 16, + experimentalFeatures: [], }); }); @@ -91,6 +92,7 @@ describe('createEditorContext', () => { pendingFormat: undefined, zoomScale: 1, rootFontSize: 16, + experimentalFeatures: [], }); }); @@ -135,6 +137,7 @@ describe('createEditorContext', () => { pendingFormat: mockedPendingFormat, zoomScale: 1, rootFontSize: 16, + experimentalFeatures: [], }); }); @@ -181,6 +184,7 @@ describe('createEditorContext', () => { pendingFormat: mockedPendingFormat, zoomScale: 1, rootFontSize: 16, + experimentalFeatures: [], }); }); }); @@ -234,6 +238,7 @@ describe('createEditorContext - checkZoomScale', () => { domIndexer: undefined, pendingFormat: undefined, rootFontSize: 16, + experimentalFeatures: [], }); }); }); @@ -285,6 +290,7 @@ describe('createEditorContext - checkRootDir', () => { pendingFormat: undefined, zoomScale: 1, rootFontSize: 16, + experimentalFeatures: [], }); }); @@ -303,6 +309,7 @@ describe('createEditorContext - checkRootDir', () => { pendingFormat: undefined, zoomScale: 1, rootFontSize: 16, + experimentalFeatures: [], }); }); }); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts index 7cc0c643624..f924e9eb31d 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts @@ -1,6 +1,6 @@ import * as textMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; import { createCachePlugin } from '../../../lib/corePlugin/cache/CachePlugin'; -import { domIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { DomIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; import { CachePluginState, DomIndexer, @@ -75,7 +75,7 @@ describe('CachePlugin', () => { }); expect(addEventListenerSpy).toHaveBeenCalledWith('selectionchange', jasmine.anything()); expect(plugin.getState()).toEqual({ - domIndexer: domIndexerImpl, + domIndexer: new DomIndexerImpl(), textMutationObserver: mockedObserver, }); expect(startObservingSpy).toHaveBeenCalledTimes(1); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts index efe35870517..5905e3a26bb 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts @@ -1,6 +1,6 @@ import * as setSelection from 'roosterjs-content-model-dom/lib/modelApi/selection/setSelection'; import { createRange } from 'roosterjs-content-model-dom/test/testUtils'; -import { domIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { DomIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; import { CacheSelection, ContentModelDocument, @@ -24,7 +24,7 @@ describe('domIndexerImpl.onSegment', () => { const paragraph = 'Paragraph' as any; const segment = 'Segment' as any; - domIndexerImpl.onSegment(node, paragraph, [segment]); + new DomIndexerImpl().onSegment(node, paragraph, [segment]); expect(node).toEqual({ __roosterjsContentModel: { paragraph: 'Paragraph', segments: ['Segment'] }, @@ -33,6 +33,12 @@ describe('domIndexerImpl.onSegment', () => { }); describe('domIndexerImpl.onParagraph', () => { + let domIndexerImpl: DomIndexerImpl; + + beforeEach(() => { + domIndexerImpl = new DomIndexerImpl(); + }); + it('Paragraph, no child', () => { const node = document.createElement('div'); @@ -163,6 +169,12 @@ describe('domIndexerImpl.onParagraph', () => { }); describe('domIndexerImpl.onTable', () => { + let domIndexerImpl: DomIndexerImpl; + + beforeEach(() => { + domIndexerImpl = new DomIndexerImpl(); + }); + it('onTable', () => { const node = {} as any; const rows = 'ROWS' as any; @@ -181,10 +193,12 @@ describe('domIndexerImpl.onTable', () => { describe('domIndexerImpl.reconcileSelection', () => { let setSelectionSpy: jasmine.Spy; let model: ContentModelDocument; + let domIndexerImpl: DomIndexerImpl; beforeEach(() => { model = createContentModelDocument(); setSelectionSpy = spyOn(setSelection, 'setSelection').and.callThrough(); + domIndexerImpl = new DomIndexerImpl(); }); it('no old range, fake range', () => { diff --git a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts index 361612fe2aa..24a82bd0f19 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/createEditorCoreTest.ts @@ -100,6 +100,7 @@ describe('createEditorCore', () => { contextMenu: 'contextMenu' as any, domHelper: mockedDOMHelper, disposeErrorHandler: undefined, + experimentalFeatures: [], ...additionalResult, }); diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts b/packages/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts index eacb390f507..45929ed6360 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts @@ -1,5 +1,6 @@ import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument'; import { normalizeContentModel } from '../modelApi/common/normalizeContentModel'; +import type { ContentModelDocumentWithPersistedCache } from '../modelApi/selection/iterateSelections'; import type { ContentModelDocument, DomToModelContext } from 'roosterjs-content-model-types'; /** @@ -18,6 +19,11 @@ export function domToContentModel( model.hasRevertedRangeSelection = true; } + // When allowed, persist cached element and do not clear it if not changed + if (context.domIndexer && context.allowCacheElement) { + (model as ContentModelDocumentWithPersistedCache).persistCache = true; + } + context.elementProcessors.child(model, root, context); normalizeContentModel(model); diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts index 220da2a8229..5e86025cca6 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/iterateSelections.ts @@ -1,5 +1,7 @@ import type { ContentModelBlockGroup, + ContentModelBlockWithCache, + ContentModelDocument, IterateSelectionsCallback, IterateSelectionsOption, ReadonlyContentModelBlockGroup, @@ -8,6 +10,17 @@ import type { ReadonlyTableSelectionContext, } from 'roosterjs-content-model-types'; +/** + * @internal + * This is a temporary type to pass the information of whether element cache should be persisted when possible + */ +export interface ContentModelDocumentWithPersistedCache extends ContentModelDocument { + /** + * When set to + */ + persistCache?: boolean; +} + /** * Iterate all selected elements in a given model * @param group The given Content Model to iterate selection from @@ -37,7 +50,26 @@ export function iterateSelections( callback: ReadonlyIterateSelectionsCallback | IterateSelectionsCallback, option?: IterateSelectionsOption ): void { - internalIterateSelections([group], callback as ReadonlyIterateSelectionsCallback, option); + const persistCache = + group.blockGroupType == 'Document' + ? (group as ContentModelDocumentWithPersistedCache).persistCache + : false; + const internalCallback: ReadonlyIterateSelectionsCallback = persistCache + ? (callback as ReadonlyIterateSelectionsCallback) + : (path, tableContext, block, segments) => { + if (!!(block as ContentModelBlockWithCache)?.cachedElement) { + delete (block as ContentModelBlockWithCache).cachedElement; + } + + return (callback as ReadonlyIterateSelectionsCallback)( + path, + tableContext, + block, + segments + ); + }; + + internalIterateSelections([group], internalCallback, option); } function internalIterateSelections( diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts index 91bc8756a9b..762ab0eb22a 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/iterateSelectionsTest.ts @@ -250,12 +250,17 @@ describe('iterateSelections', () => { iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(3); - expect(callback).toHaveBeenCalledWith([group], { - table: table, - colIndex: 0, - rowIndex: 0, - isWholeTableSelected: false, - }); + expect(callback).toHaveBeenCalledWith( + [group], + { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }, + undefined, + undefined + ); expect(callback).toHaveBeenCalledWith( [cell1, group], { @@ -306,12 +311,17 @@ describe('iterateSelections', () => { }); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], { - table: table, - colIndex: 0, - rowIndex: 0, - isWholeTableSelected: false, - }); + expect(callback).toHaveBeenCalledWith( + [group], + { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }, + undefined, + undefined + ); }); it('Group with table selection and ignore selected table content', () => { @@ -340,12 +350,17 @@ describe('iterateSelections', () => { }); expect(callback).toHaveBeenCalledTimes(3); - expect(callback).toHaveBeenCalledWith([group], { - table: table, - colIndex: 0, - rowIndex: 0, - isWholeTableSelected: false, - }); + expect(callback).toHaveBeenCalledWith( + [group], + { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }, + undefined, + undefined + ); expect(callback).toHaveBeenCalledWith( [cell1, group], { @@ -395,7 +410,7 @@ describe('iterateSelections', () => { iterateSelections(group, callback, { contentUnderSelectedTableCell: 'ignoreForTable' }); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], undefined, table); + expect(callback).toHaveBeenCalledWith([group], undefined, table, undefined); }); it('Select from the end of paragraph', () => { @@ -842,16 +857,16 @@ describe('iterateSelections', () => { iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], undefined, generalDiv); + expect(callback).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); expect(callback1).toHaveBeenCalledTimes(1); - expect(callback1).toHaveBeenCalledWith([group], undefined, generalDiv); + expect(callback1).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); expect(callback2).toHaveBeenCalledTimes(1); - expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv); + expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); expect(callback3).toHaveBeenCalledTimes(1); - expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv); + expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); }); it('Get Selection from model that contains selected general block', () => { @@ -892,14 +907,14 @@ describe('iterateSelections', () => { ]); expect(callback2).toHaveBeenCalledTimes(1); - expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv); + expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); expect(callback3).toHaveBeenCalledTimes(2); expect(callback3).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [ text1, text2, ]); - expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv); + expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); }); it('Divider selection', () => { @@ -912,7 +927,7 @@ describe('iterateSelections', () => { iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([group], undefined, divider); + expect(callback).toHaveBeenCalledWith([group], undefined, divider, undefined); }); it('Return true from first selection', () => { @@ -968,7 +983,7 @@ describe('iterateSelections', () => { expect(newCallback).toHaveBeenCalledTimes(2); expect(newCallback).toHaveBeenCalledWith([group], undefined, para1, [text1]); - expect(newCallback).toHaveBeenCalledWith([group], undefined, divider); + expect(newCallback).toHaveBeenCalledWith([group], undefined, divider, undefined); }); it('Return true from first selection in nested block group', () => { @@ -1028,7 +1043,7 @@ describe('iterateSelections', () => { }); expect(newCallback).toHaveBeenCalledTimes(1); - expect(newCallback).toHaveBeenCalledWith([group], undefined, table); + expect(newCallback).toHaveBeenCalledWith([group], undefined, table, undefined); }); it('Return true from table cell selection', () => { @@ -1060,18 +1075,28 @@ describe('iterateSelections', () => { iterateSelections(group, newCallback); expect(newCallback).toHaveBeenCalledTimes(2); - expect(newCallback).toHaveBeenCalledWith([group], { - table: table, - rowIndex: 0, - colIndex: 0, - isWholeTableSelected: true, - }); - expect(newCallback).toHaveBeenCalledWith([group], { - table: table, - rowIndex: 0, - colIndex: 1, - isWholeTableSelected: true, - }); + expect(newCallback).toHaveBeenCalledWith( + [group], + { + table: table, + rowIndex: 0, + colIndex: 0, + isWholeTableSelected: true, + }, + undefined, + undefined + ); + expect(newCallback).toHaveBeenCalledWith( + [group], + { + table: table, + rowIndex: 0, + colIndex: 1, + isWholeTableSelected: true, + }, + undefined, + undefined + ); }); it('includeListFormatHolder=anySegment', () => { diff --git a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts index 27ff10f399e..937a72788f6 100644 --- a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts @@ -57,4 +57,9 @@ export interface EditorContext { * Root Font size in Px. */ rootFontSize?: number; + + /** + * Enabled experimental features + */ + experimentalFeatures?: ReadonlyArray; } diff --git a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts index 59f0600764c..46d00bd3733 100644 --- a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts +++ b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts @@ -369,4 +369,9 @@ export interface EditorCore extends PluginState { * @param error The error object we got */ readonly disposeErrorHandler?: (plugin: EditorPlugin, error: Error) => void; + + /** + * Enabled experimental features + */ + readonly experimentalFeatures: ReadonlyArray; } diff --git a/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts b/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts index 3288eb3c88f..6e421f6f8ee 100644 --- a/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts +++ b/packages/roosterjs-content-model-types/lib/editor/EditorOptions.ts @@ -1,3 +1,4 @@ +import type { ExperimentalFeature } from './ExperimentalFeature'; import type { KnownAnnounceStrings } from '../parameter/AnnounceData'; import type { PasteType } from '../enum/PasteType'; import type { Colors, ColorTransformFunction } from '../context/DarkColorHandler'; @@ -25,12 +26,15 @@ export interface EditorOptions { defaultModelToDomOptions?: ModelToDomOption; /** - * Whether content model should be cached in order to improve editing performance. - * Pass true to disable the cache. - * @default false + * @deprecated */ disableCache?: boolean; + /** + * Enabled experimental features + */ + experimentalFeatures?: (ExperimentalFeature | string)[]; + /** * List of plugins. * The order of plugins here determines in what order each event will be dispatched. diff --git a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts new file mode 100644 index 00000000000..bdd12a56d22 --- /dev/null +++ b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts @@ -0,0 +1,11 @@ +/** + * Predefined experiment features + * By default these features are not enabled. To enable them, pass the feature name into EditorOptions.experimentalFeatures + * when create editor + */ +export type ExperimentalFeature = + /** + * When this feature is enabled, we will persist a content model in memory as long as we can, + * and use cached element when write back if it is not changed. + */ + 'PersistCache'; diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index e20fc283675..f236306d258 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -326,6 +326,7 @@ export { export { DarkColorHandler, Colors, ColorTransformFunction } from './context/DarkColorHandler'; export { IEditor } from './editor/IEditor'; +export { ExperimentalFeature } from './editor/ExperimentalFeature'; export { EditorOptions } from './editor/EditorOptions'; export { CreateContentModel, diff --git a/packages/roosterjs-editor-adapter/lib/corePlugins/BridgePlugin.ts b/packages/roosterjs-editor-adapter/lib/corePlugins/BridgePlugin.ts index a4da134f5b0..0eb1d7a106a 100644 --- a/packages/roosterjs-editor-adapter/lib/corePlugins/BridgePlugin.ts +++ b/packages/roosterjs-editor-adapter/lib/corePlugins/BridgePlugin.ts @@ -68,7 +68,7 @@ export class BridgePlugin implements ContextMenuProvider { constructor( private onInitialize: (core: EditorAdapterCore) => ILegacyEditor, legacyPlugins: LegacyEditorPlugin[] = [], - private experimentalFeatures: ExperimentalFeatures[] = [] + private experimentalFeatures: string[] = [] ) { const editPlugin = createEditPlugin(); @@ -178,7 +178,7 @@ export class BridgePlugin implements ContextMenuProvider { private createEditorCore(editor: IEditor): EditorAdapterCore { return { customData: {}, - experimentalFeatures: this.experimentalFeatures ?? [], + experimentalFeatures: (this.experimentalFeatures as ExperimentalFeatures[]) ?? [], sizeTransformer: createSizeTransformer(editor), darkColorHandler: createDarkColorHandler(editor.getColorManager()), edit: this.edit, diff --git a/packages/roosterjs-editor-adapter/lib/publicTypes/EditorAdapterOptions.ts b/packages/roosterjs-editor-adapter/lib/publicTypes/EditorAdapterOptions.ts index 69621390e57..6386f05dbab 100644 --- a/packages/roosterjs-editor-adapter/lib/publicTypes/EditorAdapterOptions.ts +++ b/packages/roosterjs-editor-adapter/lib/publicTypes/EditorAdapterOptions.ts @@ -1,5 +1,5 @@ import type { EditorOptions } from 'roosterjs-content-model-types'; -import type { EditorPlugin, ExperimentalFeatures } from 'roosterjs-editor-types'; +import type { EditorPlugin } from 'roosterjs-editor-types'; /** * Options for editor adapter @@ -14,7 +14,7 @@ export interface EditorAdapterOptions extends EditorOptions { /** * Specify the enabled experimental features */ - experimentalFeatures?: ExperimentalFeatures[]; + experimentalFeatures?: string[]; /** * Legacy plugins using IEditor interface From 0698c39f1b6a32692f8cdd444251a89ca1a16cfc Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 3 Jun 2024 11:44:58 -0700 Subject: [PATCH 55/66] Improve --- .../roosterjs-content-model-core/lib/editor/Editor.ts | 9 +++++++++ .../lib/edit/EditPlugin.ts | 7 ++++++- .../roosterjs-content-model-types/lib/editor/IEditor.ts | 7 +++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-core/lib/editor/Editor.ts b/packages/roosterjs-content-model-core/lib/editor/Editor.ts index 3301ff252ee..6bad5976a24 100644 --- a/packages/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages/roosterjs-content-model-core/lib/editor/Editor.ts @@ -32,6 +32,7 @@ import type { CachedElementHandler, DomToModelOptionForCreateModel, AnnounceData, + ExperimentalFeature, } from 'roosterjs-content-model-types'; /** @@ -406,6 +407,14 @@ export class Editor implements IEditor { core.api.announce(core, announceData); } + /** + * Check if a given feature is enabled + * @param featureName The name of feature to check + */ + isExperimentalFeatureEnabled(featureName: ExperimentalFeature | string): boolean { + return this.getCore().experimentalFeatures.indexOf(featureName) >= 0; + } + /** * @returns the current EditorCore object * @throws a standard Error if there's no core object diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts index 91c23a861e6..d358fd9bec5 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts @@ -26,6 +26,7 @@ export class EditPlugin implements EditorPlugin { private disposer: (() => void) | null = null; private shouldHandleNextInputEvent = false; private selectionAfterDelete: DOMSelection | null = null; + private handleEnterKey = false; /** * Get name of this plugin @@ -42,6 +43,8 @@ export class EditPlugin implements EditorPlugin { */ initialize(editor: IEditor) { this.editor = editor; + this.handleEnterKey = this.editor.isExperimentalFeatureEnabled('PersistCache'); + if (editor.getEnvironment().isAndroid) { this.disposer = this.editor.attachDomEvent({ beforeinput: { @@ -154,7 +157,9 @@ export class EditPlugin implements EditorPlugin { break; case 'Enter': - keyboardEnter(editor, rawEvent); + if (this.handleEnterKey) { + keyboardEnter(editor, rawEvent); + } break; default: diff --git a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts index 2ff5e28bad0..39c5e08e7ae 100644 --- a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts +++ b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts @@ -18,6 +18,7 @@ import type { DarkColorHandler } from '../context/DarkColorHandler'; import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; import type { Rect } from '../parameter/Rect'; import type { EntityState } from '../parameter/FormatContentModelContext'; +import type { ExperimentalFeature } from './ExperimentalFeature'; /** * An interface of Editor, built on top of Content Model @@ -227,4 +228,10 @@ export interface IEditor { * @param announceData Data to announce */ announce(announceData: AnnounceData): void; + + /** + * Check if a given feature is enabled + * @param featureName The name of feature to check + */ + isExperimentalFeatureEnabled(featureName: ExperimentalFeature | string): boolean; } From f1dfe8014e62361a7cef475b69fb5b5156a9cd24 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 3 Jun 2024 11:52:20 -0700 Subject: [PATCH 56/66] fix test --- .../roosterjs-content-model-plugins/test/edit/EditPluginTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts index 4ca88a12128..016b1b1bf68 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts @@ -29,6 +29,7 @@ describe('EditPlugin', () => { ({ type: -1, } as any), // Force return invalid range to go through content model code + isExperimentalFeatureEnabled: () => true, } as any) as IEditor; }); From 37f6b4b3ec1ed70746859db464e97cd7faf57e19 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 3 Jun 2024 16:48:46 -0700 Subject: [PATCH 57/66] add test --- .../test/editor/EditorTest.ts | 32 +++ .../lib/edit/EditPlugin.ts | 2 + .../lib/edit/deleteSteps/deleteEmptyQuote.ts | 58 ++--- .../test/edit/EditPluginTest.ts | 65 +++++- .../edit/deleteSteps/deleteEmptyQuoteTest.ts | 214 +++++++++++++++++- .../inputSteps/handleEnterOnParagraphTest.ts | 102 +++++++++ 6 files changed, 435 insertions(+), 38 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnParagraphTest.ts diff --git a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts index cb415755e7c..3db0c62136f 100644 --- a/packages/roosterjs-content-model-core/test/editor/EditorTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/EditorTest.ts @@ -1116,4 +1116,36 @@ describe('Editor', () => { expect(resetSpy).toHaveBeenCalledWith(); expect(() => editor.announce(mockedData)).toThrow(); }); + + it('isExperimentalFeatureEnabled', () => { + const div = document.createElement('div'); + const resetSpy = jasmine.createSpy('reset'); + const mockedCore = { + plugins: [], + darkColorHandler: { + updateKnownColor: updateKnownColorSpy, + reset: resetSpy, + }, + api: { + setContentModel: setContentModelSpy, + }, + experimentalFeatures: ['Feature1', 'Feature2'], + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new Editor(div); + + const result1 = editor.isExperimentalFeatureEnabled('Feature1'); + const result2 = editor.isExperimentalFeatureEnabled('Feature2'); + const result3 = editor.isExperimentalFeatureEnabled('Feature3'); + + expect(result1).toBeTrue(); + expect(result2).toBeTrue(); + expect(result3).toBeFalse(); + + editor.dispose(); + expect(resetSpy).toHaveBeenCalledWith(); + expect(() => editor.isExperimentalFeatureEnabled('Feature4')).toThrow(); + }); }); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts index d358fd9bec5..5ee5b9232c6 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts @@ -159,6 +159,8 @@ export class EditPlugin implements EditorPlugin { case 'Enter': if (this.handleEnterKey) { keyboardEnter(editor, rawEvent); + } else { + keyboardInput(editor, rawEvent); } break; 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 57abbb39218..bb8ed065cc5 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts @@ -1,12 +1,10 @@ import { unwrapBlock, getClosestAncestorBlockGroupIndex, - isBlockGroupOfType, createFormatContainer, mutateBlock, } from 'roosterjs-content-model-dom'; import type { - ContentModelFormatContainer, DeleteSelectionStep, ReadonlyContentModelBlockGroup, ReadonlyContentModelFormatContainer, @@ -21,7 +19,11 @@ import type { export const deleteEmptyQuote: DeleteSelectionStep = context => { const { deleteResult } = context; - if (deleteResult == 'nothingToDelete' || deleteResult == 'notDeleted') { + if ( + deleteResult == 'nothingToDelete' || + deleteResult == 'notDeleted' || + deleteResult == 'range' + ) { const { insertPoint, formatContext } = context; const { path, paragraph } = insertPoint; const rawEvent = formatContext?.rawEvent as KeyboardEvent; @@ -35,52 +37,36 @@ export const deleteEmptyQuote: DeleteSelectionStep = context => { if (quote && quote.blockGroupType === 'FormatContainer' && quote.tagName == 'blockquote') { const parent = path[index + 1]; const quoteBlockIndex = parent.blocks.indexOf(quote); - const blockQuote = parent.blocks[quoteBlockIndex]; - if ( - isBlockGroupOfType(blockQuote, 'FormatContainer') && - blockQuote.tagName === 'blockquote' + if (isEmptyQuote(quote)) { + unwrapBlock(parent, quote); + rawEvent?.preventDefault(); + context.deleteResult = 'range'; + } else if ( + rawEvent?.key === 'Enter' && + quote.blocks.indexOf(paragraph) >= 0 && + isEmptyParagraph(paragraph) ) { - if (isEmptyQuote(blockQuote)) { - unwrapBlock(parent, blockQuote); - rawEvent?.preventDefault(); - context.deleteResult = 'range'; - } else if ( - isSelectionOnEmptyLine(blockQuote, paragraph) && - rawEvent?.key === 'Enter' - ) { - insertNewLine(blockQuote, parent, quoteBlockIndex, paragraph); - rawEvent?.preventDefault(); - context.deleteResult = 'range'; - } + insertNewLine(mutateBlock(quote), parent, quoteBlockIndex, paragraph); + rawEvent?.preventDefault(); + context.deleteResult = 'range'; } } } }; -const isEmptyQuote = (quote: ContentModelFormatContainer) => { +const isEmptyQuote = (quote: ReadonlyContentModelFormatContainer) => { return ( quote.blocks.length === 1 && quote.blocks[0].blockType === 'Paragraph' && - quote.blocks[0].segments.every( - s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br' - ) + isEmptyParagraph(quote.blocks[0]) ); }; -const isSelectionOnEmptyLine = ( - quote: ReadonlyContentModelFormatContainer, - paragraph: ReadonlyContentModelParagraph -) => { - const paraIndex = quote.blocks.indexOf(paragraph); - - if (paraIndex >= 0) { - return paragraph.segments.every( - s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br' - ); - } else { - return false; - } +const isEmptyParagraph = (paragraph: ReadonlyContentModelParagraph) => { + return paragraph.segments.every( + s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br' + ); }; const insertNewLine = ( diff --git a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts index 016b1b1bf68..0c895000cbb 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts @@ -1,4 +1,5 @@ import * as keyboardDelete from '../../lib/edit/keyboardDelete'; +import * as keyboardEnter from '../../lib/edit/keyboardEnter'; import * as keyboardInput from '../../lib/edit/keyboardInput'; import * as keyboardTab from '../../lib/edit/keyboardTab'; import { DOMEventRecord, IEditor } from 'roosterjs-content-model-types'; @@ -10,6 +11,7 @@ describe('EditPlugin', () => { let eventMap: Record; let attachDOMEventSpy: jasmine.Spy; let getEnvironmentSpy: jasmine.Spy; + let isExperimentalFeatureEnabledSpy: jasmine.Spy; beforeEach(() => { attachDOMEventSpy = jasmine @@ -21,6 +23,9 @@ describe('EditPlugin', () => { getEnvironmentSpy = jasmine.createSpy('getEnvironment').and.returnValue({ isAndroid: true, }); + isExperimentalFeatureEnabledSpy = jasmine + .createSpy('isExperimentalFeatureEnabled') + .and.returnValue(false); editor = ({ attachDomEvent: attachDOMEventSpy, @@ -29,7 +34,7 @@ describe('EditPlugin', () => { ({ type: -1, } as any), // Force return invalid range to go through content model code - isExperimentalFeatureEnabled: () => true, + isExperimentalFeatureEnabled: isExperimentalFeatureEnabledSpy, } as any) as IEditor; }); @@ -41,11 +46,13 @@ describe('EditPlugin', () => { let keyboardDeleteSpy: jasmine.Spy; let keyboardInputSpy: jasmine.Spy; let keyboardTabSpy: jasmine.Spy; + let keyboardEnterSpy: jasmine.Spy; beforeEach(() => { keyboardDeleteSpy = spyOn(keyboardDelete, 'keyboardDelete'); keyboardInputSpy = spyOn(keyboardInput, 'keyboardInput'); keyboardTabSpy = spyOn(keyboardTab, 'keyboardTab'); + keyboardEnterSpy = spyOn(keyboardEnter, 'keyboardEnter'); }); it('Backspace', () => { @@ -61,6 +68,8 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent); expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Delete', () => { @@ -76,6 +85,8 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent); expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Shift+Delete', () => { @@ -91,6 +102,8 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Tab', () => { @@ -107,6 +120,50 @@ describe('EditPlugin', () => { expect(keyboardTabSpy).toHaveBeenCalledWith(editor, rawEvent); expect(keyboardInputSpy).not.toHaveBeenCalled(); expect(keyboardDeleteSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + }); + + it('Enter, keyboardEnter not enabled', () => { + plugin = new EditPlugin(); + const rawEvent = { which: 13, key: 'Enter' } as any; + const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); + + editor.takeSnapshot = addUndoSnapshotSpy; + + plugin.initialize(editor); + + plugin.onPluginEvent({ + eventType: 'keyDown', + rawEvent, + }); + + expect(keyboardDeleteSpy).not.toHaveBeenCalled(); + expect(keyboardInputSpy).toHaveBeenCalledWith(editor, rawEvent); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); + }); + + it('Enter, keyboardEnter enabled', () => { + isExperimentalFeatureEnabledSpy.and.callFake( + (featureName: string) => featureName == 'PersistCache' + ); + plugin = new EditPlugin(); + const rawEvent = { which: 13, key: 'Enter' } as any; + const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); + + editor.takeSnapshot = addUndoSnapshotSpy; + + plugin.initialize(editor); + + plugin.onPluginEvent({ + eventType: 'keyDown', + rawEvent, + }); + + expect(keyboardDeleteSpy).not.toHaveBeenCalled(); + expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Other key', () => { @@ -125,6 +182,8 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).toHaveBeenCalledWith(editor, rawEvent); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Default prevented', () => { @@ -139,6 +198,8 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Trigger entity event first', () => { @@ -175,6 +236,8 @@ describe('EditPlugin', () => { key: 'Delete', } as any); expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardTabSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts b/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts index 3494db5d813..ab1f69fe948 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteEmptyQuoteTest.ts @@ -126,6 +126,218 @@ describe('deleteEmptyQuote', () => { ], format: {}, }; - runTest(model, model, 'notDeleted'); + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'rgb(102, 102, 102)' }, + }, + ], + format: { + marginTop: '1em', + marginRight: '40px', + marginBottom: '1em', + marginLeft: '40px', + paddingLeft: '10px', + borderLeft: '3px solid rgb(200, 200, 200)', + }, + }, + ], + format: {}, + }; + runTest(model, expectedModel, 'notDeleted'); + }); +}); + +describe('delete with Enter', () => { + it('Enter in empty paragraph in middle of quote', () => { + function runTest( + model: ContentModelDocument, + expectedModel: ContentModelDocument, + deleteResult: string + ) { + const result = deleteSelection(model, [deleteEmptyQuote], { + rawEvent: { + key: 'Enter', + preventDefault: () => {}, + } as any, + newEntities: [], + deletedEntities: [], + newImages: [], + }); + normalizeContentModel(model); + expect(result.deleteResult).toEqual(deleteResult); + expect(model).toEqual(expectedModel); + } + + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + }, + ], + format: { + marginTop: '1em', + marginRight: '40px', + marginBottom: '1em', + marginLeft: '40px', + paddingLeft: '10px', + borderLeft: '3px solid rgb(200, 200, 200)', + }, + }, + ], + format: {}, + }; + + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'rgb(102, 102, 102)' }, + }, + ], + format: { + marginTop: '1em', + marginRight: '40px', + marginBottom: '1em', + marginLeft: '40px', + paddingLeft: '10px', + borderLeft: '3px solid rgb(200, 200, 200)', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + { + segmentType: 'Br', + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'rgb(102, 102, 102)' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + tagName: 'blockquote', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'rgb(102, 102, 102)', + }, + }, + ], + format: {}, + segmentFormat: { textColor: 'rgb(102, 102, 102)' }, + }, + ], + format: { + marginTop: '1em', + marginRight: '40px', + marginBottom: '1em', + marginLeft: '40px', + paddingLeft: '10px', + borderLeft: '3px solid rgb(200, 200, 200)', + }, + }, + ], + format: {}, + }; + runTest(model, expectedModel, 'range'); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnParagraphTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnParagraphTest.ts new file mode 100644 index 00000000000..cbab594af4e --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnParagraphTest.ts @@ -0,0 +1,102 @@ +import { handleEnterOnParagraph } from '../../../lib/edit/inputSteps/handleEnterOnParagraph'; +import { ValidDeleteSelectionContext } from 'roosterjs-content-model-types'; +import { + createContentModelDocument, + createParagraph, + createSelectionMarker, + createText, +} from 'roosterjs-content-model-dom'; + +describe('handleEnterOnParagraph', () => { + it('Already deleted', () => { + const doc = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const mockedCache = {} as any; + + para.segments.push(marker); + doc.blocks.push(para); + doc.cachedElement = mockedCache; + + const context: ValidDeleteSelectionContext = { + deleteResult: 'range', + insertPoint: { + paragraph: para, + marker: marker, + path: [doc], + }, + }; + + handleEnterOnParagraph(context); + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [para], + cachedElement: mockedCache, + }); + }); + + it('Not deleted, split current paragraph', () => { + const doc = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const mockedCache = {} as any; + const text1 = createText('test1'); + const text2 = createText('test1'); + + para.segments.push(text1, marker, text2); + doc.blocks.push(para); + doc.cachedElement = mockedCache; + + const context: ValidDeleteSelectionContext = { + deleteResult: 'notDeleted', + insertPoint: { + paragraph: para, + marker: marker, + path: [doc], + }, + }; + + handleEnterOnParagraph(context); + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + text2, + ], + format: {}, + }, + ], + }); + + expect(context.insertPoint).toEqual({ + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + text2, + ], + format: {}, + }, + marker: marker, + path: [doc], + }); + }); +}); From 1f90c24ce154526cca4a669a8f3655102d3dde2c Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 4 Jun 2024 16:57:12 -0700 Subject: [PATCH 58/66] add test --- .../lib/edit/deleteSteps/deleteEmptyQuote.ts | 4 +- .../lib/edit/inputSteps/handleEnterOnList.ts | 17 +- .../edit/inputSteps/handleEnterOnListTest.ts | 218 +++ .../test/edit/keyboardEnterTest.ts | 1418 +++++++++++++++++ .../test/edit/utils/splitParagraphTest.ts | 99 ++ 5 files changed, 1753 insertions(+), 3 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts create mode 100644 packages/roosterjs-content-model-plugins/test/edit/utils/splitParagraphTest.ts 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 bb8ed065cc5..b3a21c69d40 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteEmptyQuote.ts @@ -29,8 +29,8 @@ export const deleteEmptyQuote: DeleteSelectionStep = context => { const rawEvent = formatContext?.rawEvent as KeyboardEvent; const index = getClosestAncestorBlockGroupIndex( path, - ['FormatContainer', 'ListItem'], - ['TableCell'] + ['FormatContainer'], + ['TableCell', 'ListItem'] ); const quote = path[index]; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index ab091efbab4..5a7205a2700 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -24,7 +24,11 @@ export const handleEnterOnList: DeleteSelectionStep = context => { if (deleteResult == 'notDeleted' || deleteResult == 'nothingToDelete') { const { path } = insertPoint; - const index = getClosestAncestorBlockGroupIndex(path, ['ListItem'], ['TableCell']); + const index = getClosestAncestorBlockGroupIndex( + path, + ['ListItem'], + ['TableCell', 'FormatContainer'] + ); const readonlyListItem = path[index]; const listParent = path[index + 1]; @@ -84,6 +88,7 @@ const createNewListItem = ( const { insertPoint } = context; const listIndex = listParent.blocks.indexOf(listItem); const currentPara = insertPoint.paragraph; + const paraIndex = listItem.blocks.indexOf(currentPara); const newParagraph = splitParagraph(insertPoint); const levels = createNewListLevel(listItem); @@ -91,7 +96,17 @@ const createNewListItem = ( levels, insertPoint.marker.format ); + newListItem.blocks.push(newParagraph); + + const remainingBlockCount = listItem.blocks.length - paraIndex - 1; + + if (paraIndex >= 0 && remainingBlockCount > 0) { + newListItem.blocks.push( + ...mutateBlock(listItem).blocks.splice(paraIndex + 1, remainingBlockCount) + ); + } + insertPoint.paragraph = newParagraph; mutateBlock(listParent).blocks.splice(listIndex + 1, 0, newListItem); diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index 69616999988..34a9ddd18d9 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -2438,4 +2438,222 @@ describe('handleEnterOnList - keyboardEnter', () => { }; runTest(input, true, expected, false, 1); }); + + it('List item contains multiple blocks', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test1', + segmentType: 'Text', + format: {}, + }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + { + text: 'test2', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: 'test3', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'test4', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ], + }; + + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test1', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + { + text: 'test2', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + { + segments: [ + { + text: 'test3', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'test4', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ], + }; + + runTest(model, false, expectedModel, false, 1); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts b/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts new file mode 100644 index 00000000000..b18a6d0b6f6 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts @@ -0,0 +1,1418 @@ +import { keyboardEnter } from '../../lib/edit/keyboardEnter'; +import { + createBr, + createFormatContainer, + createListItem, + createListLevel, + createParagraph, + createSelectionMarker, + createTable, + createTableCell, + createText, +} from 'roosterjs-content-model-dom'; +import { + ContentModelDocument, + ContentModelSegmentFormat, + FormatContentModelContext, + IEditor, +} from 'roosterjs-content-model-types'; + +describe('keyboardEnter', () => { + let getDOMSelectionSpy: jasmine.Spy; + let formatContentModelSpy: jasmine.Spy; + let preventDefaultSpy: jasmine.Spy; + let editor: IEditor; + + beforeEach(() => { + getDOMSelectionSpy = jasmine.createSpy('getDOMSelection').and.returnValue({ + type: 'range', + }); + formatContentModelSpy = jasmine.createSpy('formatContentModel'); + preventDefaultSpy = jasmine.createSpy('preventDefault'); + editor = { + getDOMSelection: getDOMSelectionSpy, + formatContentModel: formatContentModelSpy, + } as any; + }); + + function runTest( + input: ContentModelDocument, + shift: boolean, + output: ContentModelDocument, + isChanged: boolean, + pendingFormat: ContentModelSegmentFormat | undefined + ) { + const rawEvent: KeyboardEvent = { + key: 'Enter', + shiftKey: shift, + preventDefault: preventDefaultSpy, + } as any; + const context: FormatContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + rawEvent, + }; + formatContentModelSpy.and.callFake((callback: Function) => { + const result = callback(input, context); + + expect(result).toBe(isChanged); + expect(); + }); + + keyboardEnter(editor, rawEvent); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(input).toEqual(output); + expect(context.newPendingFormat).toEqual(pendingFormat); + } + + it('Empty model, no selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + false, + { + blockGroupType: 'Document', + blocks: [], + }, + false, + undefined + ); + }); + + it('Single paragraph, only have selection marker', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: '10pt' }, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Br', + format: { fontSize: '10pt' }, + }, + ], + segmentFormat: { fontSize: '10pt' }, + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: '10pt' }, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + }, + true, + { fontSize: '10pt' } + ); + }); + + it('Single paragraph, all text are selected', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + isSelected: true, + text: 'test', + format: { fontSize: '10pt' }, + }, + ], + }, + ], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Br', + format: { fontSize: '10pt' }, + }, + ], + segmentFormat: { fontSize: '10pt' }, + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: '10pt' }, + }, + { + segmentType: 'Br', + format: { fontSize: '10pt' }, + }, + ], + segmentFormat: { fontSize: '10pt' }, + }, + ], + }, + true, + { fontSize: '10pt' } + ); + }); + + it('Multiple paragraph, single selection', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); + + para1.segments.push(createBr()); + para2.segments.push(createBr()); + + runTest( + { + blockGroupType: 'Document', + blocks: [ + para1, + { + blockType: 'Paragraph', + format: {}, + segments: [ + text1, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + text2, + ], + }, + para2, + ], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + para1, + { + blockType: 'Paragraph', + format: {}, + segments: [text1], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + text2, + ], + }, + para2, + ], + }, + true, + {} + ); + }); + + it('Multiple paragraph, select from line end to line start', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const marker1 = createSelectionMarker(); + const marker2 = createSelectionMarker(); + + para1.segments.push(text1, marker1); + para2.segments.push(marker2, text2); + + runTest( + { + blockGroupType: 'Document', + blocks: [para1, para2], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [text1], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [marker1, text2], + }, + ], + }, + true, + {} + ); + }); + + it('Multiple paragraph, select text across lines', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + + para1.segments.push(text1, text2); + para2.segments.push(text3, text4); + + text2.isSelected = true; + text3.isSelected = true; + + runTest( + { + blockGroupType: 'Document', + blocks: [para1, para2], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [text1], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + text4, + ], + }, + ], + }, + true, + {} + ); + }); + + it('Empty paragraph in quote', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const quote = createFormatContainer('blockquote'); + const marker = createSelectionMarker(); + const br1 = createBr(); + const br2 = createBr(); + const br3 = createBr(); + + para1.segments.push(br1); + para2.segments.push(marker, br2); + para3.segments.push(br3); + quote.blocks.push(para2); + + runTest( + { + blockGroupType: 'Document', + blocks: [para1, quote, para3], + }, + false, + { + blockGroupType: 'Document', + blocks: [para1, para2, para3], + }, + true, + {} + ); + }); + + it('Empty paragraph in middle of quote', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const quote = createFormatContainer('blockquote'); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const br = createBr(); + const text3 = createText('test3'); + + para1.segments.push(text1); + para2.segments.push(marker, br); + para3.segments.push(text3); + quote.blocks.push(para1, para2, para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [quote], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockGroupType: 'FormatContainer', + blockType: 'BlockGroup', + blocks: [para1], + format: {}, + tagName: 'blockquote', + }, + para2, + { + blockGroupType: 'FormatContainer', + blockType: 'BlockGroup', + blocks: [para3], + format: {}, + tagName: 'blockquote', + }, + ], + }, + true, + {} + ); + }); + + it('Empty paragraph in middle of quote, not empty', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const quote = createFormatContainer('blockquote'); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('text2'); + const text3 = createText('test3'); + + para1.segments.push(text1); + para2.segments.push(marker, text2); + para3.segments.push(text3); + quote.blocks.push(para1, para2, para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [quote], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockGroupType: 'FormatContainer', + blockType: 'BlockGroup', + blocks: [ + para1, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [marker, text2], + }, + para3, + ], + format: {}, + tagName: 'blockquote', + }, + ], + }, + true, + {} + ); + }); + + it('Empty paragraph in middle of quote, shift', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const quote = createFormatContainer('blockquote'); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const br = createBr(); + const text3 = createText('test3'); + + para1.segments.push(text1); + para2.segments.push(marker, br); + para3.segments.push(text3); + quote.blocks.push(para1, para2, para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [quote], + }, + true, + { + blockGroupType: 'Document', + blocks: [ + { + blockGroupType: 'FormatContainer', + blockType: 'BlockGroup', + blocks: [ + para1, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [marker, br], + format: {}, + }, + para3, + ], + format: {}, + tagName: 'blockquote', + }, + ], + }, + true, + {} + ); + }); + + it('Single empty list item', () => { + const para1 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const marker = createSelectionMarker(); + const br = createBr(); + + para1.segments.push(marker, br); + list1.blocks.push(para1); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1], + }, + false, + { + blockGroupType: 'Document', + blocks: [para1], + }, + true, + {} + ); + }); + + it('Single empty list item, shift', () => { + const para1 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const marker = createSelectionMarker(); + const br = createBr(); + + para1.segments.push(marker, br); + list1.blocks.push(para1); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1], + }, + true, + { + blockGroupType: 'Document', + blocks: [ + { + blockGroupType: 'ListItem', + blockType: 'BlockGroup', + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + marker, + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + levels: [listLevel], + }, + ], + }, + true, + {} + ); + }); + + it('First empty list item', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const list2 = createListItem([listLevel]); + const list3 = createListItem([listLevel]); + const marker = createSelectionMarker(); + const br = createBr(); + const text2 = createText('test2'); + const text3 = createText('test3'); + + para1.segments.push(marker, br); + para2.segments.push(text2); + para3.segments.push(text3); + list1.blocks.push(para1); + list2.blocks.push(para2); + list3.blocks.push(para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1, list2, list3], + }, + false, + { + blockGroupType: 'Document', + blocks: [para1, list2, list3], + }, + true, + {} + ); + }); + + it('List item with text', () => { + const para1 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + + para1.segments.push(text1, marker, text2); + list1.blocks.push(para1); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + ], + levels: [listLevel], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [marker, text2], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + dataset: {}, + format: { + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true, + {} + ); + }); + + it('Selection across list items', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const list3 = createListItem([listLevel]); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + const text5 = createText('test5'); + + text2.isSelected = true; + text3.isSelected = true; + text4.isSelected = true; + + para1.segments.push(text1, text2); + para2.segments.push(text3); + para3.segments.push(text4, text5); + list1.blocks.push(para1); + list3.blocks.push(para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1, para2, list3], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + ], + levels: [listLevel], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + text5, + ], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true, + {} + ); + }); + + it('Selection across list items, shift', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const list3 = createListItem([listLevel]); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + const text5 = createText('test5'); + + text2.isSelected = true; + text3.isSelected = true; + text4.isSelected = true; + + para1.segments.push(text1, text2); + para2.segments.push(text3); + para3.segments.push(text4, text5); + list1.blocks.push(para1); + list3.blocks.push(para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1, para2, list3], + }, + true, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + text5, + ], + format: {}, + }, + ], + levels: [listLevel], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true, + {} + ); + }); + + it('multiple blocks under list item', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const listLevel = createListLevel('OL'); + const list1 = createListItem([listLevel]); + const list2 = createListItem([listLevel]); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + + para1.segments.push(text1, marker, text2); + para2.segments.push(text3); + para3.segments.push(text4); + list1.blocks.push(para1, para2); + list2.blocks.push(para3); + + runTest( + { + blockGroupType: 'Document', + blocks: [list1, list2], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + ], + levels: [listLevel], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [marker, text2], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [text3], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [text4], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true, + {} + ); + }); + + it('selection is in table', () => { + const para1 = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const td = createTableCell(); + const table = createTable(1); + + table.rows[0].cells.push(td); + td.blocks.push(para1); + para1.segments.push(text1, marker, text2); + + runTest( + { + blockGroupType: 'Document', + blocks: [table], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [marker, text2], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + }, + true, + {} + ); + }); + + it('selection is in table, under list', () => { + const para1 = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const td = createTableCell(); + const table = createTable(1); + + const listLevel = createListLevel('OL'); + const list = createListItem([listLevel]); + + table.rows[0].cells.push(td); + td.blocks.push(para1); + para1.segments.push(text1, marker, text2); + list.blocks.push(table); + + runTest( + { + blockGroupType: 'Document', + blocks: [list], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [marker, text2], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true, + {} + ); + }); + + it('selection is in table, under quote', () => { + const para1 = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const td = createTableCell(); + const table = createTable(1); + + const quote = createFormatContainer('blockquote'); + + table.rows[0].cells.push(td); + td.blocks.push(para1); + para1.segments.push(text1, marker, text2); + quote.blocks.push(table); + + runTest( + { + blockGroupType: 'Document', + blocks: [quote], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'FormatContainer', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [marker, text2], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + format: {}, + tagName: 'blockquote', + }, + ], + }, + true, + {} + ); + }); + + it('selection across table 1', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + const td = createTableCell(); + const table = createTable(1); + + table.rows[0].cells.push(td); + td.blocks.push(para2); + para1.segments.push(text1, text2); + para2.segments.push(text3, text4); + + text2.isSelected = true; + text3.isSelected = true; + + runTest( + { + blockGroupType: 'Document', + blocks: [para1, table], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [text4], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + }, + true, + {} + ); + }); + + it('selection across table 2', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + const td = createTableCell(); + const table = createTable(1); + + table.rows[0].cells.push(td); + td.blocks.push(para1); + para1.segments.push(text1, text2); + para2.segments.push(text3, text4); + + text2.isSelected = true; + text3.isSelected = true; + + runTest( + { + blockGroupType: 'Document', + blocks: [table, para2], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + { + blockType: 'Paragraph', + segments: [text4], + format: {}, + }, + ], + }, + true, + {} + ); + }); + + it('selection cover table', () => { + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + const text4 = createText('test4'); + const text5 = createText('test5'); + const td = createTableCell(); + const table = createTable(1); + + table.rows[0].cells.push(td); + td.blocks.push(para2); + para1.segments.push(text1, text2); + para2.segments.push(text3); + para3.segments.push(text4, text5); + + text2.isSelected = true; + text3.isSelected = true; + text4.isSelected = true; + td.isSelected = true; + + runTest( + { + blockGroupType: 'Document', + blocks: [para1, table, para3], + }, + false, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + text5, + ], + format: {}, + }, + ], + }, + true, + {} + ); + }); +}); diff --git a/packages/roosterjs-content-model-plugins/test/edit/utils/splitParagraphTest.ts b/packages/roosterjs-content-model-plugins/test/edit/utils/splitParagraphTest.ts new file mode 100644 index 00000000000..f810ee29904 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/edit/utils/splitParagraphTest.ts @@ -0,0 +1,99 @@ +import { ContentModelParagraph, InsertPoint } from 'roosterjs-content-model-types'; +import { splitParagraph } from '../../../lib/edit/utils/splitParagraph'; +import { + createBr, + createContentModelDocument, + createParagraph, + createSelectionMarker, + createText, +} from 'roosterjs-content-model-dom'; + +describe('splitParagraph', () => { + it('empty paragraph with selection marker and BR', () => { + const doc = createContentModelDocument(); + const marker = createSelectionMarker({ fontFamily: 'Arial' }); + const br = createBr(); + const para = createParagraph(false, { direction: 'ltr' }, { fontSize: '10pt' }); + const ip: InsertPoint = { + marker: marker, + paragraph: para, + path: [doc], + }; + + para.segments.push(marker, br); + doc.blocks.push(para); + + const result = splitParagraph(ip); + + const expectedResult: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker, br], + format: { direction: 'ltr' }, + segmentFormat: { fontSize: '10pt' }, + }; + + expect(result).toEqual(expectedResult); + expect(ip).toEqual({ + marker: marker, + paragraph: expectedResult, + path: [doc], + }); + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: { fontFamily: 'Arial' }, + }, + ], + format: { direction: 'ltr' }, + segmentFormat: { fontSize: '10pt', fontFamily: 'Arial' }, + }, + ], + }); + }); + + it('Paragraph with more segments', () => { + const doc = createContentModelDocument(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const para = createParagraph(false); + const ip: InsertPoint = { + marker: marker, + paragraph: para, + path: [doc], + }; + + para.segments.push(text1, marker, text2); + doc.blocks.push(para); + + const result = splitParagraph(ip); + + const expectedResult: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker, text2], + format: {}, + }; + + expect(result).toEqual(expectedResult); + expect(ip).toEqual({ + marker: marker, + paragraph: expectedResult, + path: [doc], + }); + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [text1], + format: {}, + }, + ], + }); + }); +}); From aaa58f674a1dd099d31cded9557cf29a0a6ad169 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 5 Jun 2024 11:01:46 -0700 Subject: [PATCH 59/66] improve --- .../roosterjs-content-model-dom/lib/index.ts | 4 +-- .../lib/modelApi/editing/deleteSelection.ts | 17 +++------- .../lib/modelApi/editing/runEditSteps.ts | 25 ++++++++++++++ .../lib/edit/EditPlugin.ts | 10 ++---- .../lib/edit/keyboardEnter.ts | 34 +++++++------------ .../test/edit/EditPluginTest.ts | 10 +++--- .../edit/inputSteps/handleEnterOnListTest.ts | 2 +- .../test/edit/keyboardEnterTest.ts | 2 +- 8 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 packages/roosterjs-content-model-dom/lib/modelApi/editing/runEditSteps.ts diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index f10c7245898..9aa77b83c05 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -102,8 +102,6 @@ export { cacheGetEventData } from './domUtils/event/cacheGetEventData'; export { isBlockGroupOfType } from './modelApi/typeCheck/isBlockGroupOfType'; -export { getClosestAncestorBlockGroupIndex } from './modelApi/editing/getClosestAncestorBlockGroupIndex'; - export { iterateSelections } from './modelApi/selection/iterateSelections'; export { getFirstSelectedListItem, @@ -134,6 +132,8 @@ export { setTableCellBackgroundColor } from './modelApi/editing/setTableCellBack export { retrieveModelFormatState } from './modelApi/editing/retrieveModelFormatState'; export { getListStyleTypeFromString } from './modelApi/editing/getListStyleTypeFromString'; export { getSegmentTextFormat } from './modelApi/editing/getSegmentTextFormat'; +export { getClosestAncestorBlockGroupIndex } from './modelApi/editing/getClosestAncestorBlockGroupIndex'; +export { runEditSteps } from './modelApi/editing/runEditSteps'; export { updateImageMetadata, getImageMetadata } from './modelApi/metadata/updateImageMetadata'; export { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts index adcd718f964..c97057efb06 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection.ts @@ -1,12 +1,12 @@ import { deleteExpandedSelection } from './deleteExpandedSelection'; import { mutateBlock } from '../common/mutate'; +import { runEditSteps } from './runEditSteps'; import type { DeleteSelectionContext, DeleteSelectionResult, DeleteSelectionStep, FormatContentModelContext, ReadonlyContentModelDocument, - ValidDeleteSelectionContext, } from 'roosterjs-content-model-types'; /** @@ -22,23 +22,16 @@ export function deleteSelection( formatContext?: FormatContentModelContext ): DeleteSelectionResult { const context = deleteExpandedSelection(model, formatContext); + const steps = additionalSteps.filter( + (x: DeleteSelectionStep | null): x is DeleteSelectionStep => !!x + ); - additionalSteps.forEach(step => { - if (step && isValidDeleteSelectionContext(context)) { - step(context); - } - }); + runEditSteps(steps, context); mergeParagraphAfterDelete(context); return context; } -function isValidDeleteSelectionContext( - context: DeleteSelectionContext -): context is ValidDeleteSelectionContext { - return !!context.insertPoint; -} - // If we end up with multiple paragraphs impacted, we need to merge them function mergeParagraphAfterDelete(context: DeleteSelectionContext) { const { insertPoint, deleteResult, lastParagraph, lastTableContext } = context; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/runEditSteps.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/runEditSteps.ts new file mode 100644 index 00000000000..368e97fcbd5 --- /dev/null +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/runEditSteps.ts @@ -0,0 +1,25 @@ +import type { + DeleteSelectionContext, + DeleteSelectionResult, + DeleteSelectionStep, + ValidDeleteSelectionContext, +} from 'roosterjs-content-model-types'; + +/** + * Run editing steps on top of a given context object which includes current insert point and previous editing result + * @param steps The editing steps to run + * @param context Context for the editing steps. + */ +export function runEditSteps(steps: DeleteSelectionStep[], context: DeleteSelectionResult) { + steps.forEach(step => { + if (step && isValidDeleteSelectionContext(context)) { + step(context); + } + }); +} + +function isValidDeleteSelectionContext( + context: DeleteSelectionContext +): context is ValidDeleteSelectionContext { + return !!context.insertPoint; +} diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts index 5ee5b9232c6..add783a2ab1 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts @@ -26,7 +26,7 @@ export class EditPlugin implements EditorPlugin { private disposer: (() => void) | null = null; private shouldHandleNextInputEvent = false; private selectionAfterDelete: DOMSelection | null = null; - private handleEnterKey = false; + private handleNormalEnter = false; /** * Get name of this plugin @@ -43,7 +43,7 @@ export class EditPlugin implements EditorPlugin { */ initialize(editor: IEditor) { this.editor = editor; - this.handleEnterKey = this.editor.isExperimentalFeatureEnabled('PersistCache'); + this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('PersistCache'); if (editor.getEnvironment().isAndroid) { this.disposer = this.editor.attachDomEvent({ @@ -157,11 +157,7 @@ export class EditPlugin implements EditorPlugin { break; case 'Enter': - if (this.handleEnterKey) { - keyboardEnter(editor, rawEvent); - } else { - keyboardInput(editor, rawEvent); - } + keyboardEnter(editor, rawEvent, this.handleNormalEnter); break; default: diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index 07ed60355ca..ef765e5e231 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -1,17 +1,17 @@ import { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote'; -import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom'; +import { deleteSelection, normalizeContentModel, runEditSteps } from 'roosterjs-content-model-dom'; import { handleEnterOnList } from './inputSteps/handleEnterOnList'; import { handleEnterOnParagraph } from './inputSteps/handleEnterOnParagraph'; -import type { - DeleteSelectionContext, - IEditor, - ValidDeleteSelectionContext, -} from 'roosterjs-content-model-types'; +import type { IEditor } from 'roosterjs-content-model-types'; /** * @internal */ -export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { +export function keyboardEnter( + editor: IEditor, + rawEvent: KeyboardEvent, + handleNormalEnter: boolean +) { const selection = editor.getDOMSelection(); editor.formatContentModel( @@ -25,15 +25,13 @@ export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { // so further delete steps can keep working result.deleteResult = 'notDeleted'; - const steps = rawEvent.shiftKey - ? [handleEnterOnParagraph] - : [handleEnterOnList, deleteEmptyQuote, handleEnterOnParagraph]; + const steps = rawEvent.shiftKey ? [] : [handleEnterOnList, deleteEmptyQuote]; - steps.forEach(step => { - if (isValidDeleteSelectionContext(result)) { - step(result); - } - }); + if (handleNormalEnter) { + steps.push(handleEnterOnParagraph); + } + + runEditSteps(steps, result); } if (result.deleteResult == 'range') { @@ -54,9 +52,3 @@ export function keyboardEnter(editor: IEditor, rawEvent: KeyboardEvent) { } ); } - -function isValidDeleteSelectionContext( - context: DeleteSelectionContext -): context is ValidDeleteSelectionContext { - return !!context.insertPoint; -} diff --git a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts index 0c895000cbb..9296ddbc17a 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts @@ -123,7 +123,7 @@ describe('EditPlugin', () => { expect(keyboardEnterSpy).not.toHaveBeenCalled(); }); - it('Enter, keyboardEnter not enabled', () => { + it('Enter, normal enter not enabled', () => { plugin = new EditPlugin(); const rawEvent = { which: 13, key: 'Enter' } as any; const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); @@ -138,12 +138,12 @@ describe('EditPlugin', () => { }); expect(keyboardDeleteSpy).not.toHaveBeenCalled(); - expect(keyboardInputSpy).toHaveBeenCalledWith(editor, rawEvent); - expect(keyboardEnterSpy).not.toHaveBeenCalled(); + expect(keyboardInputSpy).not.toHaveBeenCalled(); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, false); expect(keyboardTabSpy).not.toHaveBeenCalled(); }); - it('Enter, keyboardEnter enabled', () => { + it('Enter, normal enter enabled', () => { isExperimentalFeatureEnabledSpy.and.callFake( (featureName: string) => featureName == 'PersistCache' ); @@ -162,7 +162,7 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true); expect(keyboardTabSpy).not.toHaveBeenCalled(); }); diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index 34a9ddd18d9..81aeb2f74ca 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -1828,7 +1828,7 @@ describe('handleEnterOnList - keyboardEnter', () => { }, }); - keyboardEnter(editor, mockedEvent); + keyboardEnter(editor, mockedEvent, true); }, input, expectedResult, diff --git a/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts b/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts index b18a6d0b6f6..8bfe3820021 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts @@ -60,7 +60,7 @@ describe('keyboardEnter', () => { expect(); }); - keyboardEnter(editor, rawEvent); + keyboardEnter(editor, rawEvent, true); expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(input).toEqual(output); From 280345fa2e4f820bceb472e2b4c4c7e389699d5a Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 5 Jun 2024 11:10:46 -0700 Subject: [PATCH 60/66] do not scroll caret into view for now --- .../roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index ef765e5e231..d08dd37e948 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -48,7 +48,7 @@ export function keyboardEnter( }, { rawEvent, - scrollCaretIntoView: true, + scrollCaretIntoView: false, } ); } From a8e46cd0a7f969ebcce45eb6fd04dda9b51ecd7a Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 6 Jun 2024 12:17:07 -0700 Subject: [PATCH 61/66] improve --- .../lib/command/paste/mergePasteContent.ts | 2 +- .../lib/corePlugin/cache/domIndexerImpl.ts | 2 +- .../lib/constants}/EmptySegmentFormat.ts | 4 ++-- .../roosterjs-content-model-dom/lib/index.ts | 1 + .../lib/modelApi/editing/mergeModel.ts | 17 ++--------------- 5 files changed, 7 insertions(+), 19 deletions(-) rename packages/{roosterjs-content-model-core/lib/corePlugin/cache => roosterjs-content-model-dom/lib/constants}/EmptySegmentFormat.ts (67%) diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index c491851941b..1985248838c 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -1,7 +1,7 @@ import { createDomToModelContextForSanitizing } from '../createModelFromHtml/createDomToModelContextForSanitizing'; -import { EmptySegmentFormat } from '../../corePlugin/cache/EmptySegmentFormat'; import { ChangeSource, + EmptySegmentFormat, cloneModel, domToContentModel, getSegmentTextFormat, diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 35ef9fdf4c5..41783fb0666 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -1,5 +1,5 @@ -import { EmptySegmentFormat } from './EmptySegmentFormat'; import { + EmptySegmentFormat, createSelectionMarker, createText, getObjectKeys, diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts b/packages/roosterjs-content-model-dom/lib/constants/EmptySegmentFormat.ts similarity index 67% rename from packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts rename to packages/roosterjs-content-model-dom/lib/constants/EmptySegmentFormat.ts index 5408d42f816..4eb04cf14be 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/EmptySegmentFormat.ts +++ b/packages/roosterjs-content-model-dom/lib/constants/EmptySegmentFormat.ts @@ -1,9 +1,9 @@ import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; /** - * @internal + * Provide a default empty instance of segment format with all its properties */ -export const EmptySegmentFormat: Required = { +export const EmptySegmentFormat: Readonly> = { backgroundColor: '', fontFamily: '', fontSize: '', diff --git a/packages/roosterjs-content-model-dom/lib/index.ts b/packages/roosterjs-content-model-dom/lib/index.ts index d57626c233c..3b4dc813935 100644 --- a/packages/roosterjs-content-model-dom/lib/index.ts +++ b/packages/roosterjs-content-model-dom/lib/index.ts @@ -155,3 +155,4 @@ export { NumberingListType } from './constants/NumberingListType'; export { TableBorderFormat } from './constants/TableBorderFormat'; export { OrderedListStyleMap } from './constants/OrderedListStyleMap'; export { UnorderedListStyleMap } from './constants/UnorderedListStyleMap'; +export { EmptySegmentFormat } from './constants/EmptySegmentFormat'; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index 94ed4f65319..dfdc35ed0a6 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -5,6 +5,7 @@ import { createParagraph } from '../creators/createParagraph'; import { createSelectionMarker } from '../creators/createSelectionMarker'; import { createTableCell } from '../creators/createTableCell'; import { deleteSelection } from './deleteSelection'; +import { EmptySegmentFormat } from '../../constants/EmptySegmentFormat'; import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; import { getObjectKeys } from '../..//domUtils/getObjectKeys'; import { mutateBlock } from '../common/mutate'; @@ -29,21 +30,7 @@ import type { } from 'roosterjs-content-model-types'; const HeadingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; -// An object to provide keys of required properties of segment format, do NOT use any of its values -const RequiredEmptySegmentFormat: Required = { - backgroundColor: null!, - fontFamily: null!, - fontSize: null!, - fontWeight: null!, - italic: null!, - letterSpacing: null!, - lineHeight: null!, - strikethrough: null!, - superOrSubScriptSequence: null!, - textColor: null!, - underline: null!, -}; -const KeysOfSegmentFormat = getObjectKeys(RequiredEmptySegmentFormat); +const KeysOfSegmentFormat = getObjectKeys(EmptySegmentFormat); /** * Merge source model into target mode From ac47a4d3dae86ae0ebe82afcd86fe96ec5c3278a Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 6 Jun 2024 16:28:29 -0700 Subject: [PATCH 62/66] fix test --- .../utils/formatSegmentWithContentModel.ts | 6 +- .../lib/corePlugin/cache/CachePlugin.ts | 4 +- .../setContentModel/setContentModelTest.ts | 14 ++-- .../cache/textMutationObserverTest.ts | 71 ++++++++++++++++--- .../domToModel/processors/brProcessorTest.ts | 1 + .../processors/entityProcessorTest.ts | 1 + .../processors/generalProcessorTest.ts | 1 + .../processors/imageProcessorTest.ts | 1 + .../processors/tableProcessorTest.ts | 1 + .../processors/textProcessorTest.ts | 3 + .../handlers/handleParagraphTest.ts | 2 + .../modelToDom/handlers/handleTableTest.ts | 1 + 12 files changed, 87 insertions(+), 19 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts index ab2c099f0bd..3a8a2811f3b 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts @@ -42,9 +42,9 @@ export function formatSegmentWithContentModel( false /*includingEntity*/, true /*mutate*/ ); - let isCollapsedSelection = segmentAndParagraphs.every( - x => x[0].segmentType == 'SelectionMarker' - ); + let isCollapsedSelection = + segmentAndParagraphs.length >= 1 && + segmentAndParagraphs.every(x => x[0].segmentType == 'SelectionMarker'); if (isCollapsedSelection) { const para = segmentAndParagraphs[0][1]; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index 20ec9fc018d..e7723a17d27 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -149,9 +149,7 @@ class CachePlugin implements PluginWithState { }; private invalidateCache() { - if (!this.editor?.isInShadowEdit() && this.state.cachedModel) { - console.error('Clear cache'); - + if (!this.editor?.isInShadowEdit()) { this.state.cachedModel = undefined; this.state.cachedSelection = undefined; } diff --git a/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts index 234717bcdb7..7b86cb7c2ab 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts @@ -18,6 +18,7 @@ describe('setContentModel', () => { let createModelToDomContextWithConfigSpy: jasmine.Spy; let setDOMSelectionSpy: jasmine.Spy; let getDOMSelectionSpy: jasmine.Spy; + let flushMutationsSpy: jasmine.Spy; beforeEach(() => { contentModelToDomSpy = spyOn(contentModelToDom, 'contentModelToDom'); @@ -34,8 +35,9 @@ describe('setContentModel', () => { ).and.returnValue(mockedContext); setDOMSelectionSpy = jasmine.createSpy('setDOMSelection'); getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); + flushMutationsSpy = jasmine.createSpy('flushMutations'); - core = ({ + core = { physicalRoot: mockedDiv, logicalRoot: mockedDiv, api: { @@ -44,13 +46,17 @@ describe('setContentModel', () => { getDOMSelection: getDOMSelectionSpy, }, lifecycle: {}, - cache: {}, + cache: { + textMutationObserver: { + flushMutations: flushMutationsSpy, + }, + }, environment: { modelToDomSettings: { calculated: mockedConfig, }, }, - } as any) as EditorCore; + } as any; }); it('no default option, no shadow edit', () => { @@ -75,7 +81,7 @@ describe('setContentModel', () => { ); expect(setDOMSelectionSpy).toHaveBeenCalledWith(core, mockedRange); expect(core.cache.cachedSelection).toBe(mockedRange); - expect(core.cache.cachedModel).toBe(mockedModel); + expect(flushMutationsSpy).toHaveBeenCalledWith(mockedModel); }); it('with default option, no shadow edit', () => { diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts index 515ad8d418d..332142a38e3 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts @@ -1,10 +1,30 @@ -import * as TextMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; +import * as textMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; +import { DomIndexer, TextMutationObserver } from 'roosterjs-content-model-types'; +import { DomIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; describe('TextMutationObserverImpl', () => { + let domIndexer: DomIndexer; + let onSkipMutation: jasmine.Spy; + let observer: TextMutationObserver; + + beforeEach(() => { + domIndexer = new DomIndexerImpl(); + onSkipMutation = jasmine.createSpy('onSkipMutation'); + }); + + afterEach(() => { + observer?.stopObserving(); + }); + it('init', () => { const div = document.createElement('div'); const onMutation = jasmine.createSpy('onMutation'); - TextMutationObserver.createTextMutationObserver(div, onMutation); + textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); expect(onMutation).not.toHaveBeenCalled(); }); @@ -12,7 +32,13 @@ describe('TextMutationObserverImpl', () => { it('not text change', async () => { const div = document.createElement('div'); const onMutation = jasmine.createSpy('onMutation'); - const observer = TextMutationObserver.createTextMutationObserver(div, onMutation); + + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); observer.startObserving(); @@ -33,7 +59,12 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = TextMutationObserver.createTextMutationObserver(div, onMutation); + const observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); observer.startObserving(); @@ -56,7 +87,12 @@ describe('TextMutationObserverImpl', () => { div.appendChild(span); const onMutation = jasmine.createSpy('onMutation'); - const observer = TextMutationObserver.createTextMutationObserver(div, onMutation); + const observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); observer.startObserving(); @@ -77,7 +113,12 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = TextMutationObserver.createTextMutationObserver(div, onMutation); + const observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); observer.startObserving(); @@ -88,8 +129,10 @@ describe('TextMutationObserverImpl', () => { window.setTimeout(resolve, 10); }); - expect(onMutation).toHaveBeenCalledTimes(1); + expect(onMutation).toHaveBeenCalledTimes(2); expect(onMutation).toHaveBeenCalledWith(false); + + observer.stopObserving(); }); it('flush mutation', async () => { @@ -99,7 +142,12 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = TextMutationObserver.createTextMutationObserver(div, onMutation); + const observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); observer.startObserving(); @@ -120,7 +168,12 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = TextMutationObserver.createTextMutationObserver(div, onMutation); + const observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); observer.startObserving(); observer.flushMutations(); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts index 52367fcc9ba..ef681426f59 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts @@ -74,6 +74,7 @@ describe('brProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts index 06284b767ef..0d6feb24ac9 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts @@ -259,6 +259,7 @@ describe('entityProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts index b7fa18ff970..872c1fde02a 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts @@ -394,6 +394,7 @@ describe('generalProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts index b3fda940b4d..3a2be47d866 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts @@ -323,6 +323,7 @@ describe('imageProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts index 8b3563d4c5a..08cceeab06a 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts @@ -290,6 +290,7 @@ describe('tableProcessor', () => { onSegment: null!, onTable: onTableSpy, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts index f264ce94157..89ef5687a5d 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts @@ -577,6 +577,7 @@ describe('textProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; @@ -611,6 +612,7 @@ describe('textProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; @@ -656,6 +658,7 @@ describe('textProcessor', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts index a05f99d3398..8fec57ab9aa 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts @@ -581,6 +581,7 @@ describe('handleParagraph', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; @@ -624,6 +625,7 @@ describe('handleParagraph', () => { onSegment: onSegmentSpy, onTable: null!, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts index 4c437eaef13..a27afe8415e 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts @@ -601,6 +601,7 @@ describe('handleTable', () => { onSegment: null!, onTable: onTableSpy, reconcileSelection: null!, + reconcileChildList: null!, }; context.domIndexer = domIndexer; From 30455ffce8de6173d7e1bf9a6959cfde22bd4d5d Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 10 Jun 2024 14:44:00 -0700 Subject: [PATCH 63/66] Improve --- .../lib/corePlugin/cache/domIndexerImpl.ts | 42 +++++++++++++++---- .../corePlugin/cache/textMutationObserver.ts | 24 ++++++----- .../lib/context/DomIndexer.ts | 16 ++++--- .../lib/index.ts | 1 - .../parameter/ReconcileChildListContext.ts | 32 -------------- 5 files changed, 58 insertions(+), 57 deletions(-) delete mode 100644 packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 41783fb0666..4f7a035882d 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -19,7 +19,6 @@ import type { DomIndexer, DOMSelection, RangeSelectionForCache, - ReconcileChildListContext, Selectable, } from 'roosterjs-content-model-types'; @@ -40,6 +39,36 @@ interface IndexedTableElement extends HTMLTableElement { __roosterjsContentModel: TableItem; } +/** + * Context object used by DomIndexer when reconcile mutations with child list + */ +interface ReconcileChildListContext { + /** + * Index of segment in current paragraph + */ + segIndex: number; + + /** + * The current paragraph that we are handling + */ + paragraph?: ContentModelParagraph; + + /** + * Text node that is added from mutation but has not been handled. This can happen when we first see an added node then later we see a removed one. + * e.g. Type text in an empty paragraph (<div><br></div>), so a text node will be added and <BR> will be removed. + * Set to a valid text node means we need to handle it later. If it is finally not handled, that means we need to clear cache + * Set to undefined (initial value) means no pending text node is hit yet (valid case) + * Set to null means there was a pending text node which is already handled, so if we see another pending text node, + * we should clear cache since we don't know how to handle it + */ + pendingTextNode?: Text | null; + + /** + * Format of the removed segment, this will be used as the format for newly created segment + */ + format?: ContentModelSegmentFormat; +} + function isIndexedSegment(node: Node): node is IndexedSegmentNode { const { paragraph, segments } = (node as IndexedSegmentNode).__roosterjsContentModel ?? {}; @@ -302,12 +331,11 @@ export class DomIndexerImpl implements DomIndexer { return selectable; } - reconcileChildList( - addedNodes: ArrayLike, - removedNodes: ArrayLike, - context: ReconcileChildListContext - ): boolean { + reconcileChildList(addedNodes: ArrayLike, removedNodes: ArrayLike): boolean { let canHandle = true; + const context: ReconcileChildListContext = { + segIndex: -1, + }; // First process added nodes const addedNode = addedNodes[0]; @@ -327,7 +355,7 @@ export class DomIndexerImpl implements DomIndexer { canHandle = false; } - return canHandle; + return canHandle && !context.pendingTextNode; } private reconcileAddedNode(node: Text, context: ReconcileChildListContext): boolean { diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts index 4d215895188..bcaae8cc3e5 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts @@ -1,7 +1,6 @@ import type { ContentModelDocument, DomIndexer, - ReconcileChildListContext, TextMutationObserver, } from 'roosterjs-content-model-types'; @@ -41,12 +40,12 @@ class TextMutationObserverImpl implements TextMutationObserver { } private onMutationInternal = (mutations: MutationRecord[]) => { - const context: ReconcileChildListContext = { - segIndex: -1, - }; let canHandle = true; let firstTarget: Node | null = null; let lastTextChangeNode: Node | null = null; + let addedNodes: Node[] = []; + let removedNodes: Node[] = []; + let reconcileText = false; for (let i = 0; i < mutations.length && canHandle; i++) { const mutation = mutations[i]; @@ -65,7 +64,7 @@ class TextMutationObserverImpl implements TextMutationObserver { canHandle = false; } else { lastTextChangeNode = mutation.target; - this.onMutation(true /*textOnly*/); + reconcileText = true; } break; @@ -77,18 +76,21 @@ class TextMutationObserverImpl implements TextMutationObserver { } if (canHandle) { - canHandle = this.domIndexer.reconcileChildList( - mutation.addedNodes, - mutation.removedNodes, - context - ); + addedNodes = addedNodes.concat(Array.from(mutation.addedNodes)); + removedNodes = removedNodes.concat(Array.from(mutation.removedNodes)); } break; } } - if (!canHandle || context.pendingTextNode) { + if (canHandle && (addedNodes.length > 0 || removedNodes.length > 0)) { + canHandle = this.domIndexer.reconcileChildList(addedNodes, removedNodes); + } + + if (canHandle && reconcileText) { + this.onMutation(true /*textOnly*/); + } else if (!canHandle) { this.onMutation(false /*textOnly*/); } }; diff --git a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts index 3c1bbedfed2..f1d68f845a9 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts @@ -1,4 +1,3 @@ -import type { ReconcileChildListContext } from '../parameter/ReconcileChildListContext'; import type { CacheSelection } from '../pluginState/CachePluginState'; import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; @@ -49,9 +48,14 @@ export interface DomIndexer { oldSelection?: CacheSelection ) => boolean; - reconcileChildList: ( - addedNodes: ArrayLike, - removedNodes: ArrayLike, - context: ReconcileChildListContext - ) => boolean; + /** + * When child list of editor content is changed, we can use this method to do sync the change from editor into content model. + * This is mostly used when user start to type in an empty line. In that case browser will remove the existing BR node in the empty line if any, + * and create a new TEXT node for the typed text. Here we use these information to remove original Br segment and create a new Text segment + * in content model. But if we find anything that cannot be handled, return false so caller will invalidate the cached model + * @param addedNodes Nodes added by browser during mutation + * @param removedNodes Nodes removed by browser during mutation + * @returns True if the changed nodes are successfully reconciled, otherwise false + */ + reconcileChildList: (addedNodes: ArrayLike, removedNodes: ArrayLike) => boolean; } diff --git a/packages/roosterjs-content-model-types/lib/index.ts b/packages/roosterjs-content-model-types/lib/index.ts index dd07538cbc0..a0fbcc00d7f 100644 --- a/packages/roosterjs-content-model-types/lib/index.ts +++ b/packages/roosterjs-content-model-types/lib/index.ts @@ -438,7 +438,6 @@ export { NodeTypeMap } from './parameter/NodeTypeMap'; export { TypeOfBlockGroup } from './parameter/TypeOfBlockGroup'; export { OperationalBlocks, ReadonlyOperationalBlocks } from './parameter/OperationalBlocks'; export { ParsedTable, ParsedTableCell } from './parameter/ParsedTable'; -export { ReconcileChildListContext } from './parameter/ReconcileChildListContext'; export { ModelToTextCallback, ModelToTextCallbacks, diff --git a/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts b/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts deleted file mode 100644 index ad75536ff55..00000000000 --- a/packages/roosterjs-content-model-types/lib/parameter/ReconcileChildListContext.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; -import type { ContentModelSegmentFormat } from '../contentModel/format/ContentModelSegmentFormat'; - -/** - * Context object used by DomIndexer when reconcile mutations with child list - */ -export interface ReconcileChildListContext { - /** - * Index of segment in current paragraph - */ - segIndex: number; - - /** - * The current paragraph that we are handling - */ - paragraph?: ContentModelParagraph; - - /** - * Text node that is added from mutation but has not been handled. This can happen when we first see an added node then later we see a removed one. - * e.g. Type text in an empty paragraph (<div><br></div>), so a text node will be added and <BR> will be removed. - * Set to a valid text node means we need to handle it later. If it is finally not handled, that means we need to clear cache - * Set to undefined (initial value) means no pending text node is hit yet (valid case) - * Set to null means there was a pending text node which is already handled, so if we see another pending text node, - * we should clear cache since we don't know how to handle it - */ - pendingTextNode?: Text | null; - - /** - * Format of the removed segment, this will be used as the format for newly created segment - */ - format?: ContentModelSegmentFormat; -} From b296c39c6b650fad831df2edf37673a4333fd561 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 10 Jun 2024 14:44:58 -0700 Subject: [PATCH 64/66] fix test --- .../test/corePlugin/cache/textMutationObserverTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts index 332142a38e3..4187968b893 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts @@ -129,7 +129,7 @@ describe('TextMutationObserverImpl', () => { window.setTimeout(resolve, 10); }); - expect(onMutation).toHaveBeenCalledTimes(2); + expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith(false); observer.stopObserving(); From c46cd1887a0e5b521650748b745f2c1c6847389e Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 10 Jun 2024 16:08:04 -0700 Subject: [PATCH 65/66] add test cases --- .../lib/corePlugin/cache/domIndexerImpl.ts | 85 ++++--- .../corePlugin/cache/domIndexerImplTest.ts | 180 +++++++++++++- .../cache/textMutationObserverTest.ts | 222 +++++++++++++++++- 3 files changed, 447 insertions(+), 40 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 4f7a035882d..96bb781bb01 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -22,20 +22,32 @@ import type { Selectable, } from 'roosterjs-content-model-types'; -interface SegmentItem { +/** + * @internal Export for test only + */ +export interface SegmentItem { paragraph: ContentModelParagraph; segments: ContentModelSegment[]; } -interface TableItem { +/** + * @internal Export for test only + */ +export interface TableItem { tableRows: ContentModelTableRow[]; } -interface IndexedSegmentNode extends Node { +/** + * @internal Export for test only + */ +export interface IndexedSegmentNode extends Node { __roosterjsContentModel: SegmentItem; } -interface IndexedTableElement extends HTMLTableElement { +/** + * @internal Export for test only + */ +export interface IndexedTableElement extends HTMLTableElement { __roosterjsContentModel: TableItem; } @@ -89,7 +101,7 @@ function getIndexedSegmentItem(node: Node | null): SegmentItem | null { * Implementation of DomIndexer */ export class DomIndexerImpl implements DomIndexer { - constructor(public readonly persistCache?: boolean) {} + constructor(private readonly persistCache?: boolean) {} onSegment(segmentNode: Node, paragraph: ContentModelParagraph, segment: ContentModelSegment[]) { const indexedText = segmentNode as IndexedSegmentNode; @@ -206,6 +218,37 @@ export class DomIndexerImpl implements DomIndexer { return false; } + reconcileChildList(addedNodes: ArrayLike, removedNodes: ArrayLike): boolean { + if (!this.persistCache) { + return false; + } + + let canHandle = true; + const context: ReconcileChildListContext = { + segIndex: -1, + }; + + // First process added nodes + const addedNode = addedNodes[0]; + + if (addedNodes.length == 1 && isNodeOfType(addedNode, 'TEXT_NODE')) { + canHandle = this.reconcileAddedNode(addedNode, context); + } else if (addedNodes.length > 0) { + canHandle = false; + } + + // Second, process removed nodes + const removedNode = removedNodes[0]; + + if (canHandle && removedNodes.length == 1) { + canHandle = this.reconcileRemovedNode(removedNode, context); + } else if (removedNodes.length > 0) { + canHandle = false; + } + + return canHandle && !context.pendingTextNode; + } + private isCollapsed(selection: RangeSelectionForCache): boolean { const { start, end } = selection; @@ -331,33 +374,6 @@ export class DomIndexerImpl implements DomIndexer { return selectable; } - reconcileChildList(addedNodes: ArrayLike, removedNodes: ArrayLike): boolean { - let canHandle = true; - const context: ReconcileChildListContext = { - segIndex: -1, - }; - - // First process added nodes - const addedNode = addedNodes[0]; - - if (addedNodes.length == 1 && isNodeOfType(addedNode, 'TEXT_NODE')) { - canHandle = this.reconcileAddedNode(addedNode, context); - } else if (addedNodes.length > 0) { - canHandle = false; - } - - // Second, process removed nodes - const removedNode = removedNodes[0]; - - if (canHandle && removedNodes.length == 1) { - canHandle = this.reconcileRemovedNode(removedNode, context); - } else if (removedNodes.length > 0) { - canHandle = false; - } - - return canHandle && !context.pendingTextNode; - } - private reconcileAddedNode(node: Text, context: ReconcileChildListContext): boolean { let segmentItem: SegmentItem | null = null; let index = -1; @@ -409,6 +425,11 @@ export class DomIndexerImpl implements DomIndexer { context.paragraph = segmentItem.paragraph; context.segIndex = segmentItem.paragraph.segments.indexOf(segmentItem.segments[0]); + if (context.segIndex < 0) { + // Indexed segment is not under paragraph, something wrong happens, we cannot keep handling + return false; + } + for (let i = 0; i < segmentItem.segments.length; i++) { const index = segmentItem.paragraph.segments.indexOf(segmentItem.segments[i]); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts index 5905e3a26bb..698df5f7afc 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts @@ -1,6 +1,6 @@ import * as setSelection from 'roosterjs-content-model-dom/lib/modelApi/selection/setSelection'; import { createRange } from 'roosterjs-content-model-dom/test/testUtils'; -import { DomIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { DomIndexerImpl, IndexedSegmentNode } from '../../../lib/corePlugin/cache/domIndexerImpl'; import { CacheSelection, ContentModelDocument, @@ -737,3 +737,181 @@ describe('domIndexerImpl.reconcileSelection', () => { expect(model.hasRevertedRangeSelection).toBeFalsy(); }); }); + +describe('domIndexerImpl.reconcileChildList', () => { + it('Empty array', () => { + const domIndexer = new DomIndexerImpl(true); + const result = domIndexer.reconcileChildList([], []); + + expect(result).toBeTrue(); + }); + + it('Removed BR, not indexed', () => { + const domIndexer = new DomIndexerImpl(true); + const br = document.createElement('br'); + const result = domIndexer.reconcileChildList([], [br]); + + expect(result).toBeFalse(); + }); + + it('Removed BR, indexed, segment is not under paragraph', () => { + const domIndexer = new DomIndexerImpl(true); + const br: Node = document.createElement('br'); + + const paragraph = createParagraph(); + const segment = createBr(); + + (br as IndexedSegmentNode).__roosterjsContentModel = { + paragraph: paragraph, + segments: [segment], + }; + + const result = domIndexer.reconcileChildList([], [br]); + + expect(result).toBeFalse(); + expect(paragraph).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [], + }); + }); + + it('Removed BR, indexed, segment is under paragraph', () => { + const domIndexer = new DomIndexerImpl(true); + const br: Node = document.createElement('br'); + + const paragraph = createParagraph(); + const segment1 = createText('test1'); + const segment2 = createBr(); + const segment3 = createText('test3'); + + paragraph.segments.push(segment1, segment2, segment3); + + (br as IndexedSegmentNode).__roosterjsContentModel = { + paragraph: paragraph, + segments: [segment2], + }; + + const result = domIndexer.reconcileChildList([], [br]); + + expect(result).toBeTrue(); + expect(paragraph).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [segment1, segment3], + }); + }); + + it('Removed two BR, indexed', () => { + const domIndexer = new DomIndexerImpl(true); + const br1: Node = document.createElement('br'); + const br2: Node = document.createElement('br'); + + const paragraph = createParagraph(); + const segment1 = createBr(); + const segment2 = createBr(); + const segment3 = createText('test3'); + + paragraph.segments.push(segment1, segment2, segment3); + + (br1 as IndexedSegmentNode).__roosterjsContentModel = { + paragraph: paragraph, + segments: [segment1], + }; + + (br2 as IndexedSegmentNode).__roosterjsContentModel = { + paragraph: paragraph, + segments: [segment2], + }; + + const result = domIndexer.reconcileChildList([], [br1, br2]); + + expect(result).toBeFalse(); + expect(paragraph).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [segment1, segment2, segment3], + }); + }); + + it('Added BR', () => { + const domIndexer = new DomIndexerImpl(true); + const br: Node = document.createElement('br'); + + const result = domIndexer.reconcileChildList([br], []); + + expect(result).toBeFalse(); + }); + + it('Added Text', () => { + const domIndexer = new DomIndexerImpl(true); + const br: Text = document.createTextNode('test'); + + const result = domIndexer.reconcileChildList([], [br]); + + expect(result).toBeFalse(); + }); + + it('Added Text, remove BR', () => { + const domIndexer = new DomIndexerImpl(true); + const br: Node = document.createElement('br'); + const text: Text = document.createTextNode('test'); + + const paragraph = createParagraph(); + const segment = createBr({ + fontSize: '10pt', + }); + + paragraph.segments.push(segment); + + (br as IndexedSegmentNode).__roosterjsContentModel = { + paragraph: paragraph, + segments: [segment], + }; + + const result = domIndexer.reconcileChildList([text], [br]); + + expect(result).toBeTrue(); + expect(paragraph).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + fontSize: '10pt', + }, + }, + ], + }); + }); + + it('Added two Texts, remove BR', () => { + const domIndexer = new DomIndexerImpl(true); + const br: Node = document.createElement('br'); + const text1: Text = document.createTextNode('test1'); + const text2: Text = document.createTextNode('test2'); + + const paragraph = createParagraph(); + const segment = createBr({ + fontSize: '10pt', + }); + + paragraph.segments.push(segment); + + (br as IndexedSegmentNode).__roosterjsContentModel = { + paragraph: paragraph, + segments: [segment], + }; + + const result = domIndexer.reconcileChildList([text1, text2], [br]); + + expect(result).toBeFalse(); + expect(paragraph).toEqual({ + blockType: 'Paragraph', + format: {}, + segments: [segment], + }); + }); +}); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts index 4187968b893..d784fdd1435 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts @@ -27,6 +27,7 @@ describe('TextMutationObserverImpl', () => { ); expect(onMutation).not.toHaveBeenCalled(); + expect(onSkipMutation).not.toHaveBeenCalled(); }); it('not text change', async () => { @@ -50,6 +51,7 @@ describe('TextMutationObserverImpl', () => { expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith(false); + expect(onSkipMutation).not.toHaveBeenCalled(); }); it('text change', async () => { @@ -59,7 +61,7 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = textMutationObserver.createTextMutationObserver( + observer = textMutationObserver.createTextMutationObserver( div, domIndexer, onMutation, @@ -76,6 +78,7 @@ describe('TextMutationObserverImpl', () => { expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith(true); + expect(onSkipMutation).not.toHaveBeenCalled(); }); it('text change in deeper node', async () => { @@ -87,7 +90,8 @@ describe('TextMutationObserverImpl', () => { div.appendChild(span); const onMutation = jasmine.createSpy('onMutation'); - const observer = textMutationObserver.createTextMutationObserver( + + observer = textMutationObserver.createTextMutationObserver( div, domIndexer, onMutation, @@ -104,6 +108,7 @@ describe('TextMutationObserverImpl', () => { expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith(true); + expect(onSkipMutation).not.toHaveBeenCalled(); }); it('text and non-text change', async () => { @@ -113,7 +118,8 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = textMutationObserver.createTextMutationObserver( + + observer = textMutationObserver.createTextMutationObserver( div, domIndexer, onMutation, @@ -131,8 +137,7 @@ describe('TextMutationObserverImpl', () => { expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith(false); - - observer.stopObserving(); + expect(onSkipMutation).not.toHaveBeenCalled(); }); it('flush mutation', async () => { @@ -142,7 +147,7 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = textMutationObserver.createTextMutationObserver( + observer = textMutationObserver.createTextMutationObserver( div, domIndexer, onMutation, @@ -159,6 +164,7 @@ describe('TextMutationObserverImpl', () => { }); expect(onMutation).toHaveBeenCalledWith(true); + expect(onSkipMutation).not.toHaveBeenCalled(); }); it('flush mutation without change', async () => { @@ -168,7 +174,62 @@ describe('TextMutationObserverImpl', () => { div.appendChild(text); const onMutation = jasmine.createSpy('onMutation'); - const observer = textMutationObserver.createTextMutationObserver( + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); + + observer.startObserving(); + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(onMutation).not.toHaveBeenCalled(); + expect(onSkipMutation).not.toHaveBeenCalled(); + }); + + it('flush mutation with a new model', async () => { + const div = document.createElement('div'); + const text = document.createTextNode('test'); + + div.appendChild(text); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); + + observer.startObserving(); + + text.nodeValue = '1'; + + const newModel = 'MODEL' as any; + observer.flushMutations(newModel); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(onMutation).not.toHaveBeenCalled(); + expect(onSkipMutation).toHaveBeenCalledWith(newModel); + }); + + it('flush mutation when type in new line - 1', async () => { + const div = document.createElement('div'); + const br = document.createElement('br'); + const text = document.createTextNode('test'); + + div.appendChild(br); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver( div, domIndexer, onMutation, @@ -176,12 +237,159 @@ describe('TextMutationObserverImpl', () => { ); observer.startObserving(); + + div.replaceChild(text, br); + + const reconcileChildListSpy = spyOn(domIndexer, 'reconcileChildList').and.returnValue(true); + observer.flushMutations(); await new Promise(resolve => { window.setTimeout(resolve, 10); }); + expect(reconcileChildListSpy).toHaveBeenCalledWith([text], [br]); expect(onMutation).not.toHaveBeenCalled(); + expect(onSkipMutation).not.toHaveBeenCalled(); + }); + + it('flush mutation when type in new line - 2', async () => { + const div = document.createElement('div'); + const br = document.createElement('br'); + const text = document.createTextNode(''); + + div.appendChild(br); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); + + observer.startObserving(); + + div.insertBefore(text, br); + div.removeChild(br); + text.nodeValue = 'test'; + + const reconcileChildListSpy = spyOn(domIndexer, 'reconcileChildList').and.returnValue(true); + + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(reconcileChildListSpy).toHaveBeenCalledWith([text], [br]); + expect(onMutation).toHaveBeenCalledWith(true); + expect(onSkipMutation).not.toHaveBeenCalled(); + }); + + it('flush mutation when type in new line, fail to reconcile', async () => { + const div = document.createElement('div'); + const br = document.createElement('br'); + const text = document.createTextNode('test'); + + div.appendChild(br); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); + + observer.startObserving(); + + div.replaceChild(text, br); + + const reconcileChildListSpy = spyOn(domIndexer, 'reconcileChildList').and.returnValue( + false + ); + + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(reconcileChildListSpy).toHaveBeenCalledWith([text], [br]); + expect(onMutation).toHaveBeenCalledWith(false); + expect(onSkipMutation).not.toHaveBeenCalled(); + }); + + it('mutation happens in different root', async () => { + const div = document.createElement('div'); + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const br = document.createElement('br'); + const text = document.createTextNode('test'); + + div1.appendChild(br); + div.appendChild(div1); + div.appendChild(div2); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); + + observer.startObserving(); + + div1.removeChild(br); + div2.appendChild(text); + + const reconcileChildListSpy = spyOn(domIndexer, 'reconcileChildList').and.returnValue( + false + ); + + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(reconcileChildListSpy).not.toHaveBeenCalled(); + expect(onMutation).toHaveBeenCalledWith(false); + expect(onSkipMutation).not.toHaveBeenCalled(); + }); + + it('attribute change', async () => { + const div = document.createElement('div'); + const div1 = document.createElement('div'); + + div.appendChild(div1); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver( + div, + domIndexer, + onMutation, + onSkipMutation + ); + + observer.startObserving(); + + div1.id = 'div1'; + + const reconcileChildListSpy = spyOn(domIndexer, 'reconcileChildList').and.returnValue( + false + ); + + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(reconcileChildListSpy).not.toHaveBeenCalled(); + expect(onMutation).toHaveBeenCalledWith(false); + expect(onSkipMutation).not.toHaveBeenCalled(); }); }); From b8ca862eb76ecead72930db942018bd1b21530d9 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 11 Jun 2024 16:52:27 -0700 Subject: [PATCH 66/66] Handle mutation in entity --- .../lib/corePlugin/cache/CachePlugin.ts | 42 +++++++++++++++---- .../corePlugin/cache/textMutationObserver.ts | 20 +++++++-- .../selection/getDOMInsertPointRect.ts | 27 ++++++------ 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index e7723a17d27..0d0a4ec838d 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -1,6 +1,7 @@ import { areSameSelection } from './areSameSelection'; import { createTextMutationObserver } from './textMutationObserver'; import { DomIndexerImpl } from './domIndexerImpl'; +import { findClosestEntityWrapper, getSelectionRootNode } from 'roosterjs-content-model-dom'; import { updateCachedSelection } from './updateCachedSelection'; import type { CachePluginState, @@ -9,6 +10,7 @@ import type { PluginWithState, EditorOptions, ContentModelDocument, + DOMHelper, } from 'roosterjs-content-model-types'; /** @@ -17,6 +19,7 @@ import type { class CachePlugin implements PluginWithState { private editor: IEditor | null = null; private state: CachePluginState; + private logicalRoot: HTMLElement | null = null; /** * Construct a new instance of CachePlugin class @@ -38,7 +41,8 @@ class CachePlugin implements PluginWithState { contentDiv, domIndexer, this.onMutation, - this.onSkipMutation + this.onSkipMutation, + this.areNodesUnderEntity ), }; } @@ -71,6 +75,7 @@ class CachePlugin implements PluginWithState { */ dispose() { this.state.textMutationObserver?.stopObserving(); + this.logicalRoot = null; if (this.editor) { this.editor @@ -99,6 +104,10 @@ class CachePlugin implements PluginWithState { } switch (event.eventType) { + case 'logicalRootChanged': + this.logicalRoot = event.logicalRoot; + break; + case 'keyDown': case 'input': if (!this.state.textMutationObserver) { @@ -159,28 +168,47 @@ class CachePlugin implements PluginWithState { const cachedSelection = this.state.cachedSelection; this.state.cachedSelection = undefined; // Clear it to force getDOMSelection() retrieve the latest selection range - const newRangeEx = editor.getDOMSelection() || undefined; + const selection = editor.getDOMSelection() || undefined; const model = this.state.cachedModel; const isSelectionChanged = forceUpdate || !cachedSelection || - !newRangeEx || - !areSameSelection(newRangeEx, cachedSelection); + !selection || + !areSameSelection(selection, cachedSelection); if (isSelectionChanged) { if ( !model || - !newRangeEx || - !this.state.domIndexer?.reconcileSelection(model, newRangeEx, cachedSelection) + !selection || + (!this.state.domIndexer?.reconcileSelection(model, selection, cachedSelection) && + !this.isNodeUnderEntity(editor.getDOMHelper(), getSelectionRootNode(selection))) ) { this.invalidateCache(); } else { - updateCachedSelection(this.state, newRangeEx); + updateCachedSelection(this.state, selection); } } else { this.state.cachedSelection = cachedSelection; } } + + private areNodesUnderEntity = (nodes: Node[]) => { + const domHelper = this.editor?.getDOMHelper(); + + return !!domHelper && nodes.every(node => this.isNodeUnderEntity(domHelper, node)); + }; + + private isNodeUnderEntity(domHelper: DOMHelper, node: Node | undefined) { + const entity = node && findClosestEntityWrapper(node, domHelper); + + if (!entity) { + return false; + } else if (this.logicalRoot) { + return this.logicalRoot.contains(node); + } else { + return domHelper.isNodeInEditor(node); + } + } } /** diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts index bcaae8cc3e5..5b5696ea777 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts @@ -11,7 +11,8 @@ class TextMutationObserverImpl implements TextMutationObserver { private contentDiv: HTMLDivElement, private domIndexer: DomIndexer, private onMutation: (isTextChangeOnly: boolean) => void, - private onSkipMutation: (newModel: ContentModelDocument) => void + private onSkipMutation: (newModel: ContentModelDocument) => void, + private areNodesUnderEntity: (nodes: Node[]) => boolean ) { this.observer = new MutationObserver(this.onMutationInternal); } @@ -47,6 +48,12 @@ class TextMutationObserverImpl implements TextMutationObserver { let removedNodes: Node[] = []; let reconcileText = false; + const nodeSet = new Set(mutations.map(x => x.target)); + + if (this.areNodesUnderEntity(Array.from(nodeSet))) { + return; + } + for (let i = 0; i < mutations.length && canHandle; i++) { const mutation = mutations[i]; @@ -103,7 +110,14 @@ export function createTextMutationObserver( contentDiv: HTMLDivElement, domIndexer: DomIndexer, onMutation: (isTextChangeOnly: boolean) => void, - onSkipMutation: (newModel: ContentModelDocument) => void + onSkipMutation: (newModel: ContentModelDocument) => void, + areNodesUnderEntity: (nodes: Node[]) => boolean ): TextMutationObserver { - return new TextMutationObserverImpl(contentDiv, domIndexer, onMutation, onSkipMutation); + return new TextMutationObserverImpl( + contentDiv, + domIndexer, + onMutation, + onSkipMutation, + areNodesUnderEntity + ); } diff --git a/packages/roosterjs-content-model-dom/lib/domUtils/selection/getDOMInsertPointRect.ts b/packages/roosterjs-content-model-dom/lib/domUtils/selection/getDOMInsertPointRect.ts index b4fb3795f55..f7fa4427362 100644 --- a/packages/roosterjs-content-model-dom/lib/domUtils/selection/getDOMInsertPointRect.ts +++ b/packages/roosterjs-content-model-dom/lib/domUtils/selection/getDOMInsertPointRect.ts @@ -20,7 +20,9 @@ export function getDOMInsertPointRect(doc: Document, pos: DOMInsertPoint): Rect return rect; } - // 2) try to get rect using range.getClientRects + // 2) Normalize this selection and try again + // If selection is at beginning of a TEXT node, we will get node=text.parentNode and offset=0 + // This will move it down to the real text node while (node.lastChild) { if (offset == node.childNodes.length) { node = node.lastChild; @@ -31,24 +33,19 @@ export function getDOMInsertPointRect(doc: Document, pos: DOMInsertPoint): Rect } } - const rects = range.getClientRects && range.getClientRects(); - rect = rects && rects.length == 1 ? normalizeRect(rects[0]) : null; + range.setStart(node, offset); + range.setEnd(node, offset); + rect = normalizeRect(range.getBoundingClientRect()); + if (rect) { return rect; } - // 3) if node is text node, try inserting a SPAN and get the rect of SPAN for others - if (isNodeOfType(node, 'TEXT_NODE')) { - const span = node.ownerDocument.createElement('span'); - - span.textContent = '\u200b'; - range.insertNode(span); - rect = normalizeRect(span.getBoundingClientRect()); - span.parentNode?.removeChild(span); - - if (rect) { - return rect; - } + // 3) try to get rect using range.getClientRects + const rects = range.getClientRects && range.getClientRects(); + rect = rects && rects.length == 1 ? normalizeRect(rects[0]) : null; + if (rect) { + return rect; } // 4) try getBoundingClientRect on element