From d537a2e3a3fbca31a4437e5b35128559b719a8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 7 Dec 2023 18:50:29 -0300 Subject: [PATCH 01/64] change image --- .../lib/plugins/ImageEdit/ImageEdit.ts | 17 --------- .../editInfoUtils/tryToConvertGifToPng.ts | 35 ------------------- 2 files changed, 52 deletions(-) delete mode 100644 packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/tryToConvertGifToPng.ts diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index eb3caa489ea..62f3e636212 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -7,7 +7,6 @@ import { deleteEditInfo, getEditInfoFromImage } from './editInfoUtils/editInfo'; import { getRotateHTML, Rotator, updateRotateHandleState } from './imageEditors/Rotator'; import { ImageEditElementClass } from './types/ImageEditElementClass'; import { MIN_HEIGHT_WIDTH } from './constants/constants'; -import { tryToConvertGifToPng } from './editInfoUtils/tryToConvertGifToPng'; import type { DNDDirectionX, DnDDirectionY } from './types/DragAndDropContext'; import type DragAndDropContext from './types/DragAndDropContext'; import type DragAndDropHandler from '../../pluginUtils/DragAndDropHandler'; @@ -135,11 +134,6 @@ export default class ImageEdit implements EditorPlugin { */ private isCropping: boolean = false; - /** - * If the image is a gif, this is the png source of the gif image - */ - private pngSource: string | null = null; - /** * Create a new instance of ImageEdit * @param options Image editing options @@ -301,12 +295,6 @@ export default class ImageEdit implements EditorPlugin { // When there is image in editing, clean up any cached objects and elements this.clearDndHelpers(); - // If the image is a gif we change the editing image to a new png image, then we need to change the - // image source to the original gif image - if (this.pngSource) { - this.clonedImage.src = this.editInfo.src; - } - // Apply the changes, and add undo snapshot if necessary applyChange( this.editor, @@ -326,7 +314,6 @@ export default class ImageEdit implements EditorPlugin { this.editor.select(this.image); } - this.pngSource = null; this.image = null; this.editInfo = null; this.lastSrc = null; @@ -342,9 +329,6 @@ export default class ImageEdit implements EditorPlugin { // Get initial edit info this.editInfo = getEditInfoFromImage(image); - //Check if the image is a gif and convert it to a png - this.pngSource = tryToConvertGifToPng(this.editInfo); - //Check if the image was resized by the user this.wasResized = checkIfImageWasResized(this.image); @@ -444,7 +428,6 @@ export default class ImageEdit implements EditorPlugin { // Set image src to original src to help show editing UI, also it will be used when regenerate image dataURL after editing if (this.clonedImage) { - this.clonedImage.src = this.pngSource ?? this.editInfo.src; this.clonedImage.style.position = 'absolute'; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/tryToConvertGifToPng.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/tryToConvertGifToPng.ts deleted file mode 100644 index 68eb410d8bd..00000000000 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/editInfoUtils/tryToConvertGifToPng.ts +++ /dev/null @@ -1,35 +0,0 @@ -import generateDataURL from './generateDataURL'; -import type ImageEditInfo from '../types/ImageEditInfo'; - -/** - * @internal - * Check if the image is a gif, if true, use canvas to convert it to a png. - * If the image is not a gif, return null. - * @param image to be converted - * @returns the converted image data url or null, if the image is not a gif - */ -export function tryToConvertGifToPng(editInfo: ImageEditInfo) { - const { src, widthPx, heightPx, naturalHeight, naturalWidth } = editInfo; - if (src.indexOf('.gif') > -1 || src.indexOf('image/gif') > -1) { - try { - const image = document.createElement('img'); - image.src = src; - const newEditInfo = { - src: src, - widthPx: widthPx, - heightPx: heightPx, - naturalWidth: naturalWidth, - naturalHeight: naturalHeight, - leftPercent: 0, - rightPercent: 0, - topPercent: 0, - bottomPercent: 0, - angleRad: 0, - }; - return generateDataURL(image, newEditInfo); - } catch { - return null; - } - } - return null; -} From e22b0ebcb3f7f6445c683942ff7808dffda7a25c Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 18 Dec 2023 09:11:10 -0800 Subject: [PATCH 02/64] Content Model: Better hide cursor for table and image selection (#2270) * Standalone Editor: CreateStandaloneEditorCore * Standalone Editor: Port LifecyclePlugin * fix build * fix test * improve * fix test * Standalone Editor: Support keyboard input (init step) * Standalone Editor: Port EntityPlugin * improve * Add test * improve * port selection api * improve * improve * fix build * fix build * fix build * improve * Improve * improve * improve * fix test * improve * add test * remove unused code * Standalone Editor: port ImageSelection plugin * add test * Standalone Editor: Port UndoPlugin * improve * Port undo api * fix test * improve * improve * fix build * Improve * Improve * Improve * fix build * Improve * Add test * fix test * Add undo/redo API * Standalone Editor: Port event core API * fix build * fix build * Standalone Editor: Port transformColor API * Improve * Content Model: Better hide cursor for table and image selection * fix build --------- Co-authored-by: Bryan Valverde U --- .../lib/coreApi/setDOMSelection.ts | 23 ++++++--- .../test/coreApi/setDOMSelectionTest.ts | 47 +++++++++++++------ 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts index 50c413b9ae4..37e969891bd 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts @@ -12,7 +12,8 @@ const IMAGE_ID = 'image'; const TABLE_ID = 'table'; const CONTENT_DIV_ID = 'contentDiv'; const DEFAULT_SELECTION_BORDER_COLOR = '#DB626C'; -const TABLE_CSS_RULE = '{background-color: rgb(198,198,198) !important; caret-color: transparent}'; +const TABLE_CSS_RULE = '{background-color: rgb(198,198,198) !important;}'; +const CARET_CSS_RULE = '{caret-color: transparent}'; const MAX_RULE_SELECTOR_LENGTH = 9000; /** @@ -37,7 +38,8 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC const image = selection.image; selectionRules = buildImageCSS( - rootSelector + ' #' + addUniqueId(image, IMAGE_ID), + rootSelector, + addUniqueId(image, IMAGE_ID), core.selection.imageSelectionBorderColor ); core.selection.selection = selection; @@ -48,7 +50,8 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC const { table, firstColumn, firstRow } = selection; selectionRules = buildTableCss( - rootSelector + ' #' + addUniqueId(table, TABLE_ID), + rootSelector, + addUniqueId(table, TABLE_ID), selection ); core.selection.selection = selection; @@ -92,15 +95,20 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC } }; -function buildImageCSS(rootSelector: string, borderColor?: string): string[] { +function buildImageCSS(editorSelector: string, imageId: string, borderColor?: string): string[] { const color = borderColor || DEFAULT_SELECTION_BORDER_COLOR; return [ - `${rootSelector} {outline-style:auto!important;outline-color:${color}!important;caret-color:transparent;}`, + `${editorSelector} #${imageId} {outline-style:auto!important;outline-color:${color}!important;}`, + `${editorSelector} ${CARET_CSS_RULE}`, ]; } -function buildTableCss(rootSelector: string, selection: TableSelection): string[] { +function buildTableCss( + editorSelector: string, + tableId: string, + selection: TableSelection +): string[] { const { firstColumn, firstRow, lastColumn, lastRow } = selection; const cells = parseTableCells(selection.table); const isAllTableSelected = @@ -108,11 +116,12 @@ function buildTableCss(rootSelector: string, selection: TableSelection): string[ firstColumn == 0 && lastRow == cells.length - 1 && lastColumn == (cells[lastRow]?.length ?? 0) - 1; + const rootSelector = editorSelector + ' #' + tableId; const selectors = isAllTableSelected ? [rootSelector, `${rootSelector} *`] : handleTableSelected(rootSelector, selection, cells); - const cssRules: string[] = []; + const cssRules: string[] = [`${editorSelector} ${CARET_CSS_RULE}`]; let currentRules: string = ''; for (let i = 0; i < selectors.length; i++) { diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts index 358a777722e..2f86441a183 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts @@ -391,8 +391,10 @@ describe('setDOMSelection', () => { expect(contentDiv.id).toBe('contentDiv_0'); expect(mockedImage.id).toBe('image_0'); expect(deleteRuleSpy).not.toHaveBeenCalled(); + expect(insertRuleSpy).toHaveBeenCalledTimes(2); + expect(insertRuleSpy).toHaveBeenCalledWith('#contentDiv_0 {caret-color: transparent}'); expect(insertRuleSpy).toHaveBeenCalledWith( - '#contentDiv_0 #image_0 {outline-style:auto!important;outline-color:#DB626C!important;caret-color:transparent;}' + '#contentDiv_0 #image_0 {outline-style:auto!important;outline-color:#DB626C!important;}' ); }); @@ -438,8 +440,10 @@ describe('setDOMSelection', () => { expect(contentDiv.id).toBe('contentDiv_0'); expect(mockedImage.id).toBe('image_0_0'); expect(deleteRuleSpy).not.toHaveBeenCalled(); + expect(insertRuleSpy).toHaveBeenCalledTimes(2); + expect(insertRuleSpy).toHaveBeenCalledWith('#contentDiv_0 {caret-color: transparent}'); expect(insertRuleSpy).toHaveBeenCalledWith( - '#contentDiv_0 #image_0_0 {outline-style:auto!important;outline-color:#DB626C!important;caret-color:transparent;}' + '#contentDiv_0 #image_0_0 {outline-style:auto!important;outline-color:#DB626C!important;}' ); }); @@ -485,8 +489,10 @@ describe('setDOMSelection', () => { expect(contentDiv.id).toBe('contentDiv_0'); expect(mockedImage.id).toBe('image_0'); expect(deleteRuleSpy).not.toHaveBeenCalled(); + expect(insertRuleSpy).toHaveBeenCalledTimes(2); + expect(insertRuleSpy).toHaveBeenCalledWith('#contentDiv_0 {caret-color: transparent}'); expect(insertRuleSpy).toHaveBeenCalledWith( - '#contentDiv_0 #image_0 {outline-style:auto!important;outline-color:red!important;caret-color:transparent;}' + '#contentDiv_0 #image_0 {outline-style:auto!important;outline-color:red!important;}' ); }); }); @@ -541,7 +547,8 @@ describe('setDOMSelection', () => { expect(contentDiv.id).toBe('contentDiv_0'); expect(mockedTable.id).toBe('table_0'); expect(deleteRuleSpy).not.toHaveBeenCalled(); - expect(insertRuleSpy).not.toHaveBeenCalled(); + expect(insertRuleSpy).toHaveBeenCalledTimes(1); + expect(insertRuleSpy).toHaveBeenCalledWith('#contentDiv_0 {caret-color: transparent}'); }); function runTest( @@ -550,7 +557,7 @@ describe('setDOMSelection', () => { firstRow: number, lastColumn: number, lastRow: number, - result: string + ...result: string[] ) { const mockedSelection = { type: 'table', @@ -591,7 +598,11 @@ describe('setDOMSelection', () => { expect(contentDiv.id).toBe('contentDiv_0'); expect(mockedTable.id).toBe('table_0'); expect(deleteRuleSpy).not.toHaveBeenCalled(); - expect(insertRuleSpy).toHaveBeenCalledWith(result); + expect(insertRuleSpy).toHaveBeenCalledTimes(result.length); + + result.forEach(rule => { + expect(insertRuleSpy).toHaveBeenCalledWith(rule); + }); } it('Select Table Cells TR under Table Tag', () => { @@ -601,7 +612,8 @@ describe('setDOMSelection', () => { 0, 1, 1, - '#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -612,7 +624,8 @@ describe('setDOMSelection', () => { 0, 0, 1, - '#contentDiv_0 #table_0> tr:nth-child(1)>TD:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(1)>TD:nth-child(1) *,#contentDiv_0 #table_0> tr:nth-child(2)>TD:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(2)>TD:nth-child(1) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0> tr:nth-child(1)>TD:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(1)>TD:nth-child(1) *,#contentDiv_0 #table_0> tr:nth-child(2)>TD:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(2)>TD:nth-child(1) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -656,7 +669,8 @@ describe('setDOMSelection', () => { 0, 0, 1, - '#contentDiv_0 #table_0> tr:nth-child(1)>TH:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(1)>TH:nth-child(1) *,#contentDiv_0 #table_0> tr:nth-child(2)>TH:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(2)>TH:nth-child(1) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0> tr:nth-child(1)>TH:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(1)>TH:nth-child(1) *,#contentDiv_0 #table_0> tr:nth-child(2)>TH:nth-child(1),#contentDiv_0 #table_0> tr:nth-child(2)>TH:nth-child(1) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -667,7 +681,8 @@ describe('setDOMSelection', () => { 1, 2, 2, - '#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -678,7 +693,8 @@ describe('setDOMSelection', () => { 1, 2, 2, - '#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -689,7 +705,8 @@ describe('setDOMSelection', () => { 1, 1, 4, - '#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(1)>TD:nth-child(2) *,#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>TBODY> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -700,7 +717,8 @@ describe('setDOMSelection', () => { 1, 1, 2, - '#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2),#contentDiv_0 #table_0>THEAD> tr:nth-child(2)>TD:nth-child(2) *,#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2),#contentDiv_0 #table_0>TFOOT> tr:nth-child(1)>TD:nth-child(2) * {background-color: rgb(198,198,198) !important;}' ); }); @@ -711,7 +729,8 @@ describe('setDOMSelection', () => { 0, 1, 1, - '#contentDiv_0 #table_0,#contentDiv_0 #table_0 * {background-color: rgb(198,198,198) !important; caret-color: transparent}' + '#contentDiv_0 {caret-color: transparent}', + '#contentDiv_0 #table_0,#contentDiv_0 #table_0 * {background-color: rgb(198,198,198) !important;}' ); }); }); From 725550c6d9e4c631949546c4a3f1896595ca1024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Mon, 18 Dec 2023 14:55:55 -0300 Subject: [PATCH 03/64] gif treatment --- .../roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index 62f3e636212..875761bcae3 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -428,6 +428,7 @@ export default class ImageEdit implements EditorPlugin { // Set image src to original src to help show editing UI, also it will be used when regenerate image dataURL after editing if (this.clonedImage) { + this.clonedImage.src = this.editInfo.src; this.clonedImage.style.position = 'absolute'; } From 4aa91a73b95b6f3716ba3db35b244a092e05d5b6 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Mon, 18 Dec 2023 15:11:37 -0600 Subject: [PATCH 04/64] Move Paste from publicApi to coreApi to leverage the same domToModelOptions #2275 --- .../lib/{publicApi/model => coreApi}/paste.ts | 76 +++++---- .../corePlugin/ContentModelCopyPastePlugin.ts | 3 +- .../lib/editor/standaloneCoreApiMap.ts | 2 + .../roosterjs-content-model-core/lib/index.ts | 1 - .../{publicApi/model => coreApi}/pasteTest.ts | 154 +++++++----------- .../ContentModelCopyPastePluginTest.ts | 5 +- .../lib/editor/ContentModelEditor.ts | 7 +- .../paste/e2e/cmPasteFromExcelOnlineTest.ts | 5 +- .../test/paste/e2e/cmPasteFromExcelTest.ts | 7 +- .../test/paste/e2e/cmPasteFromWacTest.ts | 5 +- .../test/paste/e2e/cmPasteFromWordTest.ts | 6 +- .../test/paste/e2e/cmPasteTest.ts | 3 +- .../lib/editor/StandaloneEditorCore.ts | 22 +++ .../lib/index.ts | 3 +- 14 files changed, 148 insertions(+), 151 deletions(-) rename packages-content-model/roosterjs-content-model-core/lib/{publicApi/model => coreApi}/paste.ts (82%) rename packages-content-model/roosterjs-content-model-core/test/{publicApi/model => coreApi}/pasteTest.ts (85%) diff --git a/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/paste.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts similarity index 82% rename from packages-content-model/roosterjs-content-model-core/lib/publicApi/model/paste.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts index 770f2a6a79e..405b6df5f09 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/paste.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts @@ -1,7 +1,8 @@ -import { ChangeSource } from '../../constants/ChangeSource'; +import { ChangeSource } from '../constants/ChangeSource'; +import { getSelectedSegments } from '../publicApi/selection/collectSelections'; +import { mergeModel } from '../publicApi/model/mergeModel'; import { GetContentMode, PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; -import { getSelectedSegments } from '../selection/collectSelections'; -import { mergeModel } from './mergeModel'; +import type { BeforePasteEvent } from 'roosterjs-editor-types'; import type { ContentModelDocument, ContentModelSegmentFormat, @@ -10,16 +11,17 @@ import type { PasteType, ContentModelBeforePasteEventData, ContentModelBeforePasteEvent, - IStandaloneEditor, ClipboardData, + Paste, + StandaloneEditorCore, } from 'roosterjs-content-model-types'; -import type { IEditor } from 'roosterjs-editor-types'; import { AllowedEntityClasses, applySegmentFormatToElement, createDomToModelContext, domToContentModel, moveChildNodes, + tableProcessor, } from 'roosterjs-content-model-dom'; import { createDefaultHtmlSanitizerOptions, @@ -52,29 +54,38 @@ const EmptySegmentFormat: Required = { }; /** + * @internal * Paste into editor using a clipboardData object - * @param editor The editor to paste content into + * @param core The StandaloneEditorCore object. * @param clipboardData Clipboard data retrieved from clipboard * @param pasteType Type of content to paste. @default normal */ -export function paste( - editor: IStandaloneEditor & IEditor, +export const paste: Paste = ( + core: StandaloneEditorCore, clipboardData: ClipboardData, pasteType: PasteType = 'normal' -) { +) => { if (clipboardData.snapshotBeforePaste) { // Restore original content before paste a new one - editor.setContent(clipboardData.snapshotBeforePaste); + core.api.setContent( + core, + clipboardData.snapshotBeforePaste, + true /* triggerContentChangedEvent */ + ); } else { - clipboardData.snapshotBeforePaste = editor.getContent(GetContentMode.RawHTMLWithSelection); + clipboardData.snapshotBeforePaste = core.api.getContent( + core, + GetContentMode.RawHTMLWithSelection + ); } - editor.focus(); + core.api.focus(core); let originalFormat: ContentModelSegmentFormat | undefined; - editor.formatContentModel( + core.api.formatContentModel( + core, (model, context) => { - const eventData = createBeforePasteEventData(editor, clipboardData, pasteType); + const eventData = createBeforePasteEventData(core, clipboardData, pasteType); const currentSegment = getSelectedSegments(model, true /*includingFormatHolder*/)[0]; const { fontFamily, fontSize, textColor, backgroundColor, letterSpacing, lineHeight } = currentSegment?.format ?? {}; @@ -83,7 +94,7 @@ export function paste( fragment, customizedMerge, } = triggerPluginEventAndCreatePasteFragment( - editor, + core, clipboardData, pasteType, eventData, @@ -117,14 +128,13 @@ export function paste( return true; }, - { changeSource: ChangeSource.Paste, getChangeData: () => clipboardData, apiName: 'paste', } ); -} +}; /** * @internal @@ -163,7 +173,7 @@ function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined } function createBeforePasteEventData( - editor: IEditor, + core: StandaloneEditorCore, clipboardData: ClipboardData, pasteType: PasteType ): ContentModelBeforePasteEventData { @@ -176,12 +186,22 @@ function createBeforePasteEventData( return { clipboardData, - fragment: editor.getDocument().createDocumentFragment(), + fragment: core.contentDiv.ownerDocument.createDocumentFragment(), sanitizingOption: options, htmlBefore: '', htmlAfter: '', htmlAttributes: {}, - domToModelOption: {}, + domToModelOption: Object.assign( + {}, + ...[ + core.defaultDomToModelOptions, + { + processorOverride: { + table: tableProcessor, + }, + }, + ] + ), pasteType: PasteTypeMap[pasteType], }; } @@ -191,7 +211,7 @@ function createBeforePasteEventData( * This function will also create a DocumentFragment for paste. */ function triggerPluginEventAndCreatePasteFragment( - editor: IEditor, + core: StandaloneEditorCore, clipboardData: ClipboardData, pasteType: PasteType, eventData: ContentModelBeforePasteEventData, @@ -204,14 +224,14 @@ function triggerPluginEventAndCreatePasteFragment( const { fragment } = event; const { rawHtml, text, imageDataUri } = clipboardData; - const trustedHTMLHandler = editor.getTrustedHTMLHandler(); + const trustedHTMLHandler = core.trustedHTMLHandler; const doc: Document | undefined = rawHtml ? new DOMParser().parseFromString(trustedHTMLHandler(rawHtml), 'text/html') : undefined; // Step 2: Retrieve Metadata from Html and the Html that was copied. - retrieveMetadataFromClipboard(doc, event, trustedHTMLHandler); + retrieveMetadataFromClipboard(doc, event as BeforePasteEvent, trustedHTMLHandler); // Step 3: Fill the BeforePasteEvent object, especially the fragment for paste if ( @@ -234,19 +254,13 @@ function triggerPluginEventAndCreatePasteFragment( applySegmentFormatToElement(formatContainer, currentFormat); - let pluginEvent: ContentModelBeforePasteEvent = event; - // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste, when the type of paste is different than Plain Text if (pasteType !== 'asPlainText') { - pluginEvent = editor.triggerPluginEvent( - PluginEventType.BeforePaste, - event, - true /* broadcast */ - ) as ContentModelBeforePasteEvent; + core.api.triggerEvent(core, event, true /* broadcast */); } // Step 5. Sanitize the fragment before paste to make sure the content is safe sanitizePasteContent(event, null /*position*/); - return pluginEvent; + return event; } diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts index ef3684725c1..ed9e2d1dde6 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts @@ -6,7 +6,6 @@ import { deleteSelection } from '../publicApi/selection/deleteSelection'; import { extractClipboardItems } from '../utils/extractClipboardItems'; import { getSelectedCells } from '../publicApi/table/getSelectedCells'; import { iterateSelections } from '../publicApi/selection/iterateSelections'; -import { paste } from '../publicApi/model/paste'; import { PluginEventType } from 'roosterjs-editor-types'; import { transformColor } from '../publicApi/color/transformColor'; import { @@ -194,7 +193,7 @@ class ContentModelCopyPastePlugin implements PluginWithState { if (!editor.isDisposed()) { - paste(editor, clipboardData); + editor.paste(clipboardData); } }); } diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts index 0f53a6121fc..e5bd01e83d3 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts @@ -7,6 +7,7 @@ import { formatContentModel } from '../coreApi/formatContentModel'; import { getDOMSelection } from '../coreApi/getDOMSelection'; import { getVisibleViewport } from '../coreApi/getVisibleViewport'; import { hasFocus } from '../coreApi/hasFocus'; +import { paste } from '../coreApi/paste'; import { restoreUndoSnapshot } from '../coreApi/restoreUndoSnapshot'; import { setContentModel } from '../coreApi/setContentModel'; import { setDOMSelection } from '../coreApi/setDOMSelection'; @@ -33,4 +34,5 @@ export const standaloneCoreApiMap: PortedCoreApiMap = { restoreUndoSnapshot: restoreUndoSnapshot, attachDomEvent: attachDomEvent, triggerEvent: triggerEvent, + paste: paste, }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/index.ts b/packages-content-model/roosterjs-content-model-core/lib/index.ts index 509c3e5c256..54569e3f00d 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/index.ts @@ -1,5 +1,4 @@ export { CachedElementHandler, CloneModelOptions, cloneModel } from './publicApi/model/cloneModel'; -export { paste } from './publicApi/model/paste'; export { mergeModel, MergeModelOption } from './publicApi/model/mergeModel'; export { isBlockGroupOfType } from './publicApi/model/isBlockGroupOfType'; export { diff --git a/packages-content-model/roosterjs-content-model-core/test/publicApi/model/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts similarity index 85% rename from packages-content-model/roosterjs-content-model-core/test/publicApi/model/pasteTest.ts rename to packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index d3389adbef3..afd9ce356a8 100644 --- a/packages-content-model/roosterjs-content-model-core/test/publicApi/model/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -1,17 +1,18 @@ -import * as addParserF from '../../../../roosterjs-content-model-plugins/lib/paste/utils/addParser'; +import * as addParserF from '../../../roosterjs-content-model-plugins/lib/paste/utils/addParser'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; -import * as ExcelF from '../../../../roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; -import * as getPasteSourceF from '../../../../roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; -import * as getSelectedSegmentsF from '../../../lib/publicApi/selection/collectSelections'; -import * as mergeModelFile from '../../../lib/publicApi/model/mergeModel'; -import * as pasteF from '../../../lib/publicApi/model/paste'; -import * as PPT from '../../../../roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; -import * as setProcessorF from '../../../../roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; -import * as WacComponents from '../../../../roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; -import * as WordDesktopFile from '../../../../roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; +import * as ExcelF from '../../../roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; +import * as getPasteSourceF from '../../../roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; +import * as getSelectedSegmentsF from '../../lib/publicApi/selection/collectSelections'; +import * as mergeModelFile from '../../lib/publicApi/model/mergeModel'; +import * as PPT from '../../../roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; +import * as setProcessorF from '../../../roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; +import * as WacComponents from '../../../roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; +import * as WordDesktopFile from '../../../roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; +import { BeforePasteEvent, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; -import { ContentModelPastePlugin } from '../../../../roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; +import { ContentModelPastePlugin } from '../../../roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; import { createContentModelDocument, tableProcessor } from 'roosterjs-content-model-dom'; +import { mergePasteContent } from '../../lib/coreApi/paste'; import { ClipboardData, ContentModelDocument, @@ -24,14 +25,7 @@ import { import { expectEqual, initEditor, -} from '../../../../roosterjs-content-model-plugins/test/paste/e2e/testUtils'; -import { - BeforePasteEvent, - IEditor, - PasteType, - PluginEvent, - PluginEventType, -} from 'roosterjs-editor-types'; +} from '../../../roosterjs-content-model-plugins/test/paste/e2e/testUtils'; let clipboardData: ClipboardData; @@ -45,10 +39,6 @@ describe('Paste ', () => { let mockedMergeModel: ContentModelDocument; let getFocusedPosition: jasmine.Spy; let getContent: jasmine.Spy; - let getSelectionRange: jasmine.Spy; - let getDocument: jasmine.Spy; - let getTrustedHTMLHandler: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; let getVisibleViewport: jasmine.Spy; let mergeModelSpy: jasmine.Spy; let formatResult: boolean | undefined; @@ -70,40 +60,17 @@ describe('Paste ', () => { }; div = document.createElement('div'); document.body.appendChild(div); - mockedModel = ({} as any) as ContentModelDocument; + mockedModel = { + blockGroupType: 'Document', + blocks: [], + } as ContentModelDocument; + mockedMergeModel = ({} as any) as ContentModelDocument; createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); focus = jasmine.createSpy('focus'); getFocusedPosition = jasmine.createSpy('getFocusedPosition').and.returnValue(mockedPos); getContent = jasmine.createSpy('getContent'); - getDocument = jasmine.createSpy('getDocument').and.returnValue(document); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent').and.returnValue({ - clipboardData, - fragment: document.createDocumentFragment(), - sanitizingOption: { - elementCallbacks: {}, - attributeCallbacks: {}, - cssStyleCallbacks: {}, - additionalTagReplacements: {}, - additionalAllowedAttributes: [], - additionalAllowedCssClasses: [], - additionalDefaultStyleValues: {}, - additionalGlobalStyleNodes: [], - additionalPredefinedCssForElement: {}, - preserveHtmlComments: false, - unknownTagReplacement: null, - }, - htmlBefore: '', - htmlAfter: '', - htmlAttributes: {}, - domToModelOption: {}, - pasteType: PasteType.Normal, - }); - getTrustedHTMLHandler = jasmine - .createSpy('getTrustedHTMLHandler') - .and.returnValue((html: string) => html); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); mergeModelSpy = spyOn(mergeModelFile, 'mergeModel').and.callFake(() => { mockedModel = mockedMergeModel; @@ -120,7 +87,11 @@ describe('Paste ', () => { const formatContentModel = jasmine .createSpy('formatContentModel') .and.callFake( - (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + ( + core: any, + callback: ContentModelFormatter, + options: FormatWithContentModelOptions + ) => { context = { newEntities: [], deletedEntities: [], @@ -133,19 +104,19 @@ describe('Paste ', () => { formatResult = undefined; context = undefined; - editor = ({ - focus, - createContentModel, - getFocusedPosition, - getContent, - getSelectionRange, - getDocument, - getTrustedHTMLHandler, - triggerPluginEvent, - getVisibleViewport, - isDarkMode: () => false, - formatContentModel, - } as any) as IStandaloneEditor & IEditor; + editor = new ContentModelEditor(div, { + plugins: [new ContentModelPastePlugin()], + coreApiOverride: { + focus, + createContentModel, + getContent, + getVisibleViewport, + formatContentModel, + }, + }); + + spyOn(editor, 'getDocument').and.callThrough(); + spyOn(editor, 'triggerPluginEvent').and.callThrough(); }); afterEach(() => { @@ -154,25 +125,20 @@ describe('Paste ', () => { }); it('Execute', () => { - pasteF.paste(editor, clipboardData); + try { + editor.paste(clipboardData); + } catch (e) { + console.log(e); + } expect(formatResult).toBeTrue(); - expect(focus).toHaveBeenCalled(); - expect(getContent).toHaveBeenCalled(); - expect(triggerPluginEvent).toHaveBeenCalled(); - expect(getDocument).toHaveBeenCalled(); - expect(getTrustedHTMLHandler).toHaveBeenCalled(); expect(mockedModel).toEqual(mockedMergeModel); }); it('Execute | As plain text', () => { - pasteF.paste(editor, clipboardData, 'asPlainText'); + editor.paste(clipboardData, true /* asText */); expect(formatResult).toBeTrue(); - expect(focus).toHaveBeenCalled(); - expect(getContent).toHaveBeenCalled(); - expect(getDocument).toHaveBeenCalled(); - expect(getTrustedHTMLHandler).toHaveBeenCalled(); expect(mockedModel).toEqual(mockedMergeModel); }); @@ -194,7 +160,7 @@ describe('Paste ', () => { }, }); - pasteF.paste(editor, clipboardData); + editor.paste(clipboardData); editor.createContentModel({ processorOverride: { @@ -256,7 +222,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); - pasteF.paste(editor!, clipboardData); + editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); @@ -267,7 +233,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); - pasteF.paste(editor!, clipboardData); + editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(4); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); @@ -278,7 +244,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.paste(editor!, clipboardData); + editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); @@ -289,7 +255,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.paste(editor!, clipboardData); + editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); @@ -300,7 +266,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); - pasteF.paste(editor!, clipboardData); + editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); @@ -312,7 +278,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); - pasteF.paste(editor!, clipboardData, 'asPlainText'); + editor?.paste(clipboardData, true /* pasteAsText */); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -323,7 +289,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); - pasteF.paste(editor!, clipboardData, 'asPlainText'); + editor?.paste(clipboardData, true /* pasteAsText */); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -334,7 +300,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.paste(editor!, clipboardData, 'asPlainText'); + editor?.paste(clipboardData, true /* pasteAsText */); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -345,7 +311,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.paste(editor!, clipboardData, 'asPlainText'); + editor?.paste(clipboardData, true /* pasteAsText */); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -356,7 +322,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); - pasteF.paste(editor!, clipboardData, 'asPlainText'); + editor?.paste(clipboardData, true /* pasteAsText */); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -392,7 +358,7 @@ describe('paste with content model & paste plugin', () => { ], }); - pasteF.paste(editor!, clipboardData); + editor?.paste(clipboardData); expect(eventChecker?.clipboardData).toEqual(clipboardData); expect(eventChecker?.htmlBefore).toBeTruthy(); @@ -502,7 +468,7 @@ describe('mergePasteContent', () => { spyOn(mergeModelFile, 'mergeModel').and.callThrough(); - pasteF.mergePasteContent( + mergePasteContent( sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, pasteModel, @@ -590,7 +556,7 @@ describe('mergePasteContent', () => { spyOn(mergeModelFile, 'mergeModel').and.callThrough(); - pasteF.mergePasteContent( + mergePasteContent( sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, pasteModel, @@ -608,7 +574,7 @@ describe('mergePasteContent', () => { spyOn(mergeModelFile, 'mergeModel').and.callThrough(); - pasteF.mergePasteContent( + mergePasteContent( sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, pasteModel, @@ -655,7 +621,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = '

Test

'; - pasteF.paste(editor, clipboardData); + editor.paste(clipboardData); const model = editor.createContentModel({ processorOverride: { @@ -698,7 +664,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = 'Link'; - pasteF.paste(editor, clipboardData); + editor.paste(clipboardData); const model = editor.createContentModel({ processorOverride: { @@ -730,7 +696,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = 'Link'; - pasteF.paste(editor, clipboardData); + editor.paste(clipboardData); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index aed2982e7a9..c5fc8d6afe6 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -5,7 +5,6 @@ import * as deleteSelectionsFile from '../../lib/publicApi/selection/deleteSelec import * as extractClipboardItemsFile from '../../lib/utils/extractClipboardItems'; import * as iterateSelectionsFile from '../../lib/publicApi/selection/iterateSelections'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; -import * as PasteFile from '../../lib/publicApi/model/paste'; import * as transformColor from '../../lib/publicApi/color/transformColor'; import { createModelToDomContext, createTable, createTableCell } from 'roosterjs-content-model-dom'; import { createRange } from 'roosterjs-editor-dom'; @@ -539,7 +538,6 @@ describe('ContentModelCopyPastePlugin |', () => { let clipboardData = {}; it('Handle', () => { - spyOn(PasteFile, 'paste').and.callFake(() => {}); const preventDefaultSpy = jasmine.createSpy('preventDefaultPaste'); let clipboardEvent = { clipboardData: ({ @@ -560,8 +558,7 @@ describe('ContentModelCopyPastePlugin |', () => { domEvents.paste.beforeDispatch?.(clipboardEvent); - expect(pasteSpy).not.toHaveBeenCalledWith(clipboardData); - expect(PasteFile.paste).toHaveBeenCalled(); + expect(pasteSpy).toHaveBeenCalledWith(clipboardData); expect(extractClipboardItemsFile.extractClipboardItems).toHaveBeenCalledWith( Array.from(clipboardEvent.clipboardData!.items), allowedCustomPasteType diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index 495a8b5ee18..eb630d5f06f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -2,7 +2,7 @@ import { buildRangeEx } from './utils/buildRangeEx'; import { createEditorCore } from './createEditorCore'; import { getObjectKeys } from 'roosterjs-content-model-dom'; import { getPendableFormatState } from './utils/getPendableFormatState'; -import { isBold, paste, redo, transformColor, undo } from 'roosterjs-content-model-core'; +import { isBold, redo, transformColor, undo } from 'roosterjs-content-model-core'; import { ChangeSource, ColorTransformDirection, @@ -448,8 +448,9 @@ export class ContentModelEditor implements IContentModelEditor { applyCurrentFormat: boolean = false, pasteAsImage: boolean = false ) { - paste( - this, + const core = this.getCore(); + core.api.paste( + core, clipboardData, pasteAsText ? 'asPlainText' diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts index b27b1305ce7..d0f1148bff4 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts @@ -2,7 +2,6 @@ import * as processPastedContentFromExcel from '../../../lib/paste/Excel/process import { expectEqual, initEditor } from './testUtils'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; -import { paste } from 'roosterjs-content-model-core'; import { tableProcessor } from 'roosterjs-content-model-dom'; import type { ClipboardData } from 'roosterjs-content-model-types'; @@ -35,7 +34,7 @@ describe(ID, () => { it('E2E', () => { spyOn(processPastedContentFromExcel, 'processPastedContentFromExcel').and.callThrough(); - paste(editor, clipboardData); + editor.paste(clipboardData); editor.createContentModel({ processorOverride: { table: tableProcessor, @@ -59,7 +58,7 @@ describe(ID, () => { snapshotBeforePaste: '

', }); - paste(editor, CD); + editor.paste(CD); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index f5b76bc4990..057fb0e3874 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -3,7 +3,6 @@ import { Browser } from 'roosterjs-editor-dom'; import { expectEqual, initEditor } from './testUtils'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; -import { paste } from 'roosterjs-content-model-core'; import { tableProcessor } from 'roosterjs-content-model-dom'; import type { ClipboardData } from 'roosterjs-content-model-types'; @@ -39,7 +38,7 @@ describe(ID, () => { } spyOn(processPastedContentFromExcel, 'processPastedContentFromExcel').and.callThrough(); - paste(editor, clipboardData); + editor.paste(clipboardData); editor.createContentModel({}); expect(processPastedContentFromExcel.processPastedContentFromExcel).toHaveBeenCalled(); @@ -51,7 +50,7 @@ describe(ID, () => { } spyOn(processPastedContentFromExcel, 'processPastedContentFromExcel').and.callThrough(); - paste(editor, clipboardData, 'asImage'); + editor.paste(clipboardData, false, false, true); const model = editor.createContentModel({ processorOverride: { @@ -102,7 +101,7 @@ describe(ID, () => { snapshotBeforePaste: '
', }); - paste(editor, CD); + editor.paste(CD); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts index 302b3be33f9..516c80cb152 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts @@ -2,7 +2,6 @@ import * as processPastedContentWacComponents from '../../../lib/paste/WacCompon import { ClipboardData, DomToModelOption } from 'roosterjs-content-model-types'; import { expectEqual, initEditor } from './testUtils'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; -import { paste } from 'roosterjs-content-model-core'; import { tableProcessor } from 'roosterjs-content-model-dom'; const ID = 'CM_Paste_From_WORD_Online_E2E'; @@ -37,7 +36,7 @@ describe(ID, () => { 'processPastedContentWacComponents' ).and.callThrough(); - paste(editor, clipboardData); + editor.paste(clipboardData); editor.createContentModel({ processorOverride: { table: tableProcessor, @@ -53,7 +52,7 @@ describe(ID, () => { clipboardData.rawHtml = '

Test Table 

Test Table 

 

'; - paste(editor, clipboardData); + editor.paste(clipboardData); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index 06eec17884d..3c6fe02b923 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -1,6 +1,6 @@ import * as wordFile from '../../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { ClipboardData, DomToModelOption } from 'roosterjs-content-model-types'; -import { cloneModel, paste } from 'roosterjs-content-model-core'; +import { cloneModel } from 'roosterjs-content-model-core'; import { expectEqual, initEditor } from './testUtils'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; @@ -35,7 +35,7 @@ describe(ID, () => { itChromeOnly('E2E', () => { clipboardData.rawHtml = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n

Test

\r\n\r\n

asdsad

\r\n\r\n\r\n\r\n'; - paste(editor, clipboardData); + editor.paste(clipboardData); const model = cloneModel( editor.createContentModel({ @@ -103,7 +103,7 @@ describe(ID, () => { clipboardData.rawHtml = '

Asdasdsad

asdadasd

 

asdsadasdasdsadasdsadsad

 

'; - paste(editor, clipboardData); + editor.paste(clipboardData); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts index 75bc4459657..496f15deb61 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts @@ -3,7 +3,6 @@ import { ClipboardData, DomToModelOption } from 'roosterjs-content-model-types'; import { expectEqual, initEditor } from './testUtils'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; -import { paste } from 'roosterjs-content-model-core'; import { tableProcessor } from 'roosterjs-content-model-dom'; const ID = 'CM_Paste_E2E'; @@ -36,7 +35,7 @@ describe(ID, () => { '
No.
ID
Work Item Type

', }); - paste(editor, clipboardData); + editor.paste(clipboardData); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index d7a547875b7..fb025a8d856 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -1,3 +1,5 @@ +import type { ClipboardData } from '../parameter/ClipboardData'; +import type { PasteType } from '../enum/PasteType'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { Snapshot } from '../parameter/Snapshot'; import type { EntityState } from '../parameter/FormatWithContentModelContext'; @@ -225,6 +227,18 @@ export type EnsureTypeInContainer = ( deprecated?: boolean ) => void; +/** + * Paste into editor using a clipboardData object + * @param core The StandaloneEditorCore object. + * @param clipboardData Clipboard data retrieved from clipboard + * @param pasteType Type of content to paste. @default normal + */ +export type Paste = ( + core: StandaloneEditorCore, + clipboardData: ClipboardData, + pasteType: PasteType +) => void; + /** * Temp interface * TODO: Port other core API @@ -333,6 +347,14 @@ export interface PortedCoreApiMap { * @param broadcast Set to true to skip the shouldHandleEventExclusively check */ triggerEvent: TriggerEvent; + + /** + * Paste into editor using a clipboardData object + * @param editor The editor to paste content into + * @param clipboardData Clipboard data retrieved from clipboard + * @param pasteType Type of content to paste. @default normal + */ + paste: Paste; } /** diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index fdf5d62948a..ae0606da2cd 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -221,6 +221,7 @@ export { RestoreUndoSnapshot, EnsureTypeInContainer, GetVisibleViewport, + Paste, } from './editor/StandaloneEditorCore'; export { StandaloneEditorCorePlugins } from './editor/StandaloneEditorCorePlugins'; @@ -233,12 +234,12 @@ export { ContentModelFormatPluginState, PendingFormat, } from './pluginState/ContentModelFormatPluginState'; +export { CopyPastePluginState } from './pluginState/CopyPastePluginState'; export { DOMEventPluginState } from './pluginState/DOMEventPluginState'; export { LifecyclePluginState } from './pluginState/LifecyclePluginState'; export { EntityPluginState, KnownEntityItem } from './pluginState/EntityPluginState'; export { SelectionPluginState } from './pluginState/SelectionPluginState'; export { UndoPluginState } from './pluginState/UndoPluginState'; -export { CopyPastePluginState } from './pluginState/CopyPastePluginState'; export { EditorEnvironment } from './parameter/EditorEnvironment'; export { From 5e0fc110d6e4084661e143be2a8a245d266586de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 19 Dec 2023 10:26:53 -0300 Subject: [PATCH 05/64] selection --- .../lib/corePlugin/SelectionPlugin.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index bc844d0b49e..791c2cbf132 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -105,8 +105,18 @@ class SelectionPlugin implements PluginWithState { case PluginEventType.MouseDown: selection = this.editor.getDOMSelection(); - - if (selection?.type == 'image' && selection.image !== event.rawEvent.target) { + const env = this.editor.getEnvironment(); + if ( + env.isMac && + event.rawEvent.button === 2 && + (image = this.getClickingImage(event.rawEvent)) + ) { + this.selectImage(this.editor, image); + } else if ( + !env.isMac && + selection?.type == 'image' && + selection.image !== event.rawEvent.target + ) { this.selectBeforeImage(this.editor, selection.image); } break; @@ -130,16 +140,6 @@ class SelectionPlugin implements PluginWithState { } } break; - - case PluginEventType.ContextMenu: - selection = this.editor.getDOMSelection(); - - if ( - (image = this.getClickingImage(event.rawEvent)) && - (selection?.type != 'image' || selection.image != image) - ) { - this.selectImage(this.editor, image); - } } } From 7cca68d83be8e42ab180fa2fadc2ead106e52e09 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Tue, 19 Dec 2023 11:25:39 -0300 Subject: [PATCH 06/64] WIP --- .../lib/corePlugin/SelectionPlugin.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index 791c2cbf132..f5ee66ffd0c 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -105,15 +105,12 @@ class SelectionPlugin implements PluginWithState { case PluginEventType.MouseDown: selection = this.editor.getDOMSelection(); - const env = this.editor.getEnvironment(); if ( - env.isMac && event.rawEvent.button === 2 && (image = this.getClickingImage(event.rawEvent)) ) { this.selectImage(this.editor, image); } else if ( - !env.isMac && selection?.type == 'image' && selection.image !== event.rawEvent.target ) { From 473161c9a7116732c4d64f618a5adbb15abd12b8 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Tue, 19 Dec 2023 13:41:00 -0300 Subject: [PATCH 07/64] fix mac image selection --- .../lib/corePlugin/SelectionPlugin.ts | 3 +- .../test/corePlugin/SelectionPluginTest.ts | 117 ++++++++---------- 2 files changed, 52 insertions(+), 68 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index f5ee66ffd0c..2e636473ca3 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -107,7 +107,8 @@ class SelectionPlugin implements PluginWithState { selection = this.editor.getDOMSelection(); if ( event.rawEvent.button === 2 && - (image = this.getClickingImage(event.rawEvent)) + (image = this.getClickingImage(event.rawEvent)) && + image.isContentEditable ) { this.selectImage(this.editor, image); } else if ( diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts index 2a7eaf85ae3..44347183609 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts @@ -293,6 +293,56 @@ describe('SelectionPlugin handle image selection', () => { expect(setDOMSelectionSpy).toHaveBeenCalledTimes(0); }); + it('Image selection, mouse down to same image right click', () => { + const mockedImage = document.createElement('img'); + + mockedImage.contentEditable = 'true'; + + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + + plugin.onPluginEvent({ + eventType: PluginEventType.MouseDown, + rawEvent: { + target: mockedImage, + button: 2, + } as any, + }); + + expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); + }); + + it('Image selection, mouse down to image right click', () => { + const mockedImage = document.createElement('img'); + + mockedImage.contentEditable = 'true'; + plugin.onPluginEvent({ + eventType: PluginEventType.MouseDown, + rawEvent: { + target: mockedImage, + button: 2, + } as any, + }); + + expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); + }); + + it('Image selection, mouse down to div right click', () => { + const node = document.createElement('div'); + + plugin.onPluginEvent({ + eventType: PluginEventType.MouseDown, + rawEvent: { + target: node, + button: 2, + } as any, + }); + + expect(setDOMSelectionSpy).toHaveBeenCalledTimes(0); + }); + it('no selection, mouse up to image, is clicking, isEditable', () => { const mockedImage = document.createElement('img'); @@ -514,71 +564,4 @@ describe('SelectionPlugin handle image selection', () => { expect(stopPropagationSpy).not.toHaveBeenCalled(); expect(setDOMSelectionSpy).not.toHaveBeenCalled(); }); - - it('context menu, no selection, click on image', () => { - const mockedImage1 = document.createElement('img'); - - const rawEvent = { - target: mockedImage1, - } as any; - - plugin.onPluginEvent({ - eventType: PluginEventType.ContextMenu, - rawEvent: rawEvent, - } as any); - - expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); - expect(setDOMSelectionSpy).toHaveBeenCalledWith({ - type: 'image', - image: mockedImage1, - }); - }); - - it('context menu, image selection, click on same image', () => { - const mockedImage1 = document.createElement('img'); - - const rawEvent = { - target: mockedImage1, - } as any; - - getDOMSelectionSpy.and.returnValue({ - type: 'image', - image: mockedImage1, - }); - - plugin.onPluginEvent({ - eventType: PluginEventType.ContextMenu, - rawEvent: rawEvent, - } as any); - - expect(setDOMSelectionSpy).not.toHaveBeenCalled(); - }); - - it('context menu, image selection, click on different image', () => { - const mockedImage1 = document.createElement('img'); - const mockedImage2 = document.createElement('img'); - - mockedImage1.id = 'image1'; - mockedImage2.id = 'image2'; - - const rawEvent = { - target: mockedImage1, - } as any; - - getDOMSelectionSpy.and.returnValue({ - type: 'image', - image: mockedImage2, - }); - - plugin.onPluginEvent({ - eventType: PluginEventType.ContextMenu, - rawEvent: rawEvent, - } as any); - - expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); - expect(setDOMSelectionSpy).toHaveBeenCalledWith({ - type: 'image', - image: mockedImage1, - }); - }); }); From bb24aaac12ad4d4c776e2ff3a9f18c4f0bfba448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 19 Dec 2023 13:55:26 -0300 Subject: [PATCH 08/64] constant --- .../lib/corePlugin/SelectionPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index 2e636473ca3..e3792491c42 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -10,6 +10,7 @@ import type { } from 'roosterjs-content-model-types'; const MouseMiddleButton = 1; +const MouseRightButton = 2; class SelectionPlugin implements PluginWithState { private editor: (IStandaloneEditor & IEditor) | null = null; @@ -106,7 +107,7 @@ class SelectionPlugin implements PluginWithState { case PluginEventType.MouseDown: selection = this.editor.getDOMSelection(); if ( - event.rawEvent.button === 2 && + event.rawEvent.button === MouseRightButton && (image = this.getClickingImage(event.rawEvent)) && image.isContentEditable ) { From d375dd36484ebd51e0f2819d978402b4136b0c74 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 19 Dec 2023 09:39:00 -0800 Subject: [PATCH 09/64] Standalone Editor: Port paste API step 1 (#2279) --- .../test/coreApi/pasteTest.ts | 21 +- .../block/paddingFormatHandler.ts | 11 +- .../block/whiteSpaceFormatHandler.ts | 3 +- .../common/backgroundColorFormatHandler.ts | 10 +- .../common/borderFormatHandler.ts | 27 ++- .../segment/boldFormatHandler.ts | 3 +- .../segment/letterSpacingFormatHandler.ts | 14 +- .../formatHandlers/utils/shouldSetValue.ts | 15 ++ .../processors/knownElementProcessorTest.ts | 2 - .../block/paddingFormatHandlerTest.ts | 78 +++++++ .../block/whiteSpaceFormatHandlerTest.ts | 6 + .../backgroundColorFormatHandlerTest.ts | 7 + .../common/borderFormatHandlerTest.ts | 18 ++ .../segment/boldFormatHandlerTest.ts | 15 ++ .../segment/letterSpacingFormatHandlerTest.ts | 7 + .../utils/shouldSetValueTest.ts | 45 ++++ .../paste/e2e/cmPasteFromExcelOnlineTest.ts | 23 -- .../test/paste/e2e/cmPasteFromExcelTest.ts | 23 -- .../test/paste/e2e/cmPasteFromWacTest.ts | 39 ++-- .../test/paste/e2e/cmPasteTest.ts | 6 - .../paste/processPastedContentFromWacTest.ts | 206 ------------------ 21 files changed, 268 insertions(+), 311 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/shouldSetValue.ts create mode 100644 packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/paddingFormatHandlerTest.ts create mode 100644 packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/shouldSetValueTest.ts diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index afd9ce356a8..f224b10d8d3 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -1,16 +1,16 @@ -import * as addParserF from '../../../roosterjs-content-model-plugins/lib/paste/utils/addParser'; +import * as addParserF from 'roosterjs-content-model-plugins/lib/paste/utils/addParser'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; -import * as ExcelF from '../../../roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; -import * as getPasteSourceF from '../../../roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; +import * as ExcelF from 'roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; +import * as getPasteSourceF from 'roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; import * as getSelectedSegmentsF from '../../lib/publicApi/selection/collectSelections'; import * as mergeModelFile from '../../lib/publicApi/model/mergeModel'; -import * as PPT from '../../../roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; -import * as setProcessorF from '../../../roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; -import * as WacComponents from '../../../roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; -import * as WordDesktopFile from '../../../roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; +import * as PPT from 'roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; +import * as setProcessorF from 'roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; +import * as WacComponents from 'roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; +import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { BeforePasteEvent, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; -import { ContentModelPastePlugin } from '../../../roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; +import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; import { createContentModelDocument, tableProcessor } from 'roosterjs-content-model-dom'; import { mergePasteContent } from '../../lib/coreApi/paste'; import { @@ -22,10 +22,7 @@ import { FormatWithContentModelOptions, IStandaloneEditor, } from 'roosterjs-content-model-types'; -import { - expectEqual, - initEditor, -} from '../../../roosterjs-content-model-plugins/test/paste/e2e/testUtils'; +import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; let clipboardData: ClipboardData; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/paddingFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/paddingFormatHandler.ts index bbf96aa91ef..33338ca3638 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/paddingFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/paddingFormatHandler.ts @@ -12,11 +12,16 @@ const PaddingKeys: (keyof PaddingFormat & keyof CSSStyleDeclaration)[] = [ * @internal */ export const paddingFormatHandler: FormatHandler = { - parse: (format, element) => { + parse: (format, element, _, defaultStyle) => { PaddingKeys.forEach(key => { - const value = element.style[key]; + let value = element.style[key]; + const defaultValue = defaultStyle[key] ?? '0px'; - if (value) { + if (value == '0') { + value = '0px'; + } + + if (value && value != defaultValue) { format[key] = value; } }); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/whiteSpaceFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/whiteSpaceFormatHandler.ts index bc3934869b1..03cb6cf5791 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/whiteSpaceFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/whiteSpaceFormatHandler.ts @@ -1,3 +1,4 @@ +import { shouldSetValue } from '../utils/shouldSetValue'; import type { FormatHandler } from '../FormatHandler'; import type { WhiteSpaceFormat } from 'roosterjs-content-model-types'; @@ -8,7 +9,7 @@ export const whiteSpaceFormatHandler: FormatHandler = { parse: (format, element, _, defaultStyle) => { const whiteSpace = element.style.whiteSpace || defaultStyle.whiteSpace; - if (whiteSpace) { + if (shouldSetValue(whiteSpace, 'normal', format.whiteSpace, defaultStyle.whiteSpace)) { format.whiteSpace = whiteSpace; } }, diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/backgroundColorFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/backgroundColorFormatHandler.ts index 408bca73edd..47e1cb439e7 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/backgroundColorFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/backgroundColorFormatHandler.ts @@ -1,4 +1,5 @@ import { getColor, setColor } from '../utils/color'; +import { shouldSetValue } from '../utils/shouldSetValue'; import type { BackgroundColorFormat } from 'roosterjs-content-model-types'; import type { FormatHandler } from '../FormatHandler'; @@ -15,7 +16,14 @@ export const backgroundColorFormatHandler: FormatHandler !!context.isDarkMode ) || defaultStyle.backgroundColor; - if (backgroundColor) { + if ( + shouldSetValue( + backgroundColor, + 'transparent', + undefined /*existingValue*/, + defaultStyle.backgroundColor + ) + ) { format.backgroundColor = backgroundColor; } }, diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts index 5301c31c358..d03028de2cd 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts @@ -9,21 +9,40 @@ export const BorderKeys: (keyof BorderFormat & keyof CSSStyleDeclaration)[] = [ 'borderRight', 'borderBottom', 'borderLeft', - 'borderRadius', +]; + +// This array needs to match BorderKeys array +const BorderWidthKeys: (keyof CSSStyleDeclaration)[] = [ + 'borderTopWidth', + 'borderRightWidth', + 'borderBottomWidth', + 'borderLeftWidth', ]; /** * @internal */ export const borderFormatHandler: FormatHandler = { - parse: (format, element) => { - BorderKeys.forEach(key => { + parse: (format, element, _, defaultStyle) => { + BorderKeys.forEach((key, i) => { const value = element.style[key]; + const defaultWidth = defaultStyle[BorderWidthKeys[i]] ?? '0px'; + let width = element.style[BorderWidthKeys[i]]; - if (value) { + if (width == '0') { + width = '0px'; + } + + if (value && width != defaultWidth) { format[key] = value == 'none' ? '' : value; } }); + + const borderRadius = element.style.borderRadius; + + if (borderRadius) { + format.borderRadius = borderRadius; + } }, apply: (format, element) => { BorderKeys.forEach(key => { diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/boldFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/boldFormatHandler.ts index ecabe2b8705..54341feeade 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/boldFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/boldFormatHandler.ts @@ -1,3 +1,4 @@ +import { shouldSetValue } from '../utils/shouldSetValue'; import { wrapAllChildNodes } from '../../domUtils/moveChildNodes'; import type { BoldFormat } from 'roosterjs-content-model-types'; import type { FormatHandler } from '../FormatHandler'; @@ -9,7 +10,7 @@ export const boldFormatHandler: FormatHandler = { parse: (format, element, context, defaultStyle) => { const fontWeight = element.style.fontWeight || defaultStyle.fontWeight; - if (fontWeight) { + if (shouldSetValue(fontWeight, '400', format.fontWeight, defaultStyle.fontWeight)) { format.fontWeight = fontWeight; } }, diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/letterSpacingFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/letterSpacingFormatHandler.ts index 314da333469..f1a451d4499 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/letterSpacingFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/segment/letterSpacingFormatHandler.ts @@ -1,3 +1,4 @@ +import { shouldSetValue } from '../utils/shouldSetValue'; import type { FormatHandler } from '../FormatHandler'; import type { LetterSpacingFormat } from 'roosterjs-content-model-types'; @@ -5,14 +6,21 @@ import type { LetterSpacingFormat } from 'roosterjs-content-model-types'; * @internal */ export const letterSpacingFormatHandler: FormatHandler = { - parse: (format, element, context, defaultStyle) => { + parse: (format, element, _, defaultStyle) => { const letterSpacing = element.style.letterSpacing || defaultStyle.letterSpacing; - if (letterSpacing) { + if ( + shouldSetValue( + letterSpacing, + 'normal', + format.letterSpacing, + defaultStyle.letterSpacing + ) + ) { format.letterSpacing = letterSpacing; } }, - apply: (format, element, context) => { + apply: (format, element) => { if (format.letterSpacing) { element.style.letterSpacing = format.letterSpacing; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/shouldSetValue.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/shouldSetValue.ts new file mode 100644 index 00000000000..bc26ad21ca0 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/shouldSetValue.ts @@ -0,0 +1,15 @@ +/** + * @internal + */ +export function shouldSetValue( + value: string | undefined, + normalValue: string, + existingValue: string | undefined, + defaultValue: string | undefined +): boolean { + return ( + !!value && + value != 'inherit' && + !!(value != normalValue || existingValue || (defaultValue && value != defaultValue)) + ); +} diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/knownElementProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/knownElementProcessorTest.ts index ea61ed51624..1dd5486e2ac 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/knownElementProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/knownElementProcessorTest.ts @@ -313,8 +313,6 @@ describe('knownElementProcessor', () => { blockType: 'BlockGroup', blockGroupType: 'FormatContainer', format: { - paddingLeft: '0px', - paddingRight: '0px', paddingTop: '20px', paddingBottom: '40px', }, diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/paddingFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/paddingFormatHandlerTest.ts new file mode 100644 index 00000000000..501a476b8cd --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/paddingFormatHandlerTest.ts @@ -0,0 +1,78 @@ +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext, ModelToDomContext, PaddingFormat } from 'roosterjs-content-model-types'; +import { paddingFormatHandler } from '../../../lib/formatHandlers/block/paddingFormatHandler'; + +describe('paddingFormatHandler.parse', () => { + let div: HTMLElement; + let format: PaddingFormat; + let context: DomToModelContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createDomToModelContext(); + }); + + it('No padding', () => { + paddingFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({}); + }); + + it('Has padding in CSS', () => { + div.style.padding = '1px 2px 3px 4px'; + paddingFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + paddingTop: '1px', + paddingRight: '2px', + paddingBottom: '3px', + paddingLeft: '4px', + }); + }); + + it('Overwrite padding values', () => { + div.style.paddingLeft = '15pt'; + format.paddingLeft = '30px'; + paddingFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + paddingLeft: '15pt', + }); + }); + + it('0 padding', () => { + div.style.padding = '0 10px 20px 0'; + paddingFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + paddingRight: '10px', + paddingBottom: '20px', + }); + }); +}); + +describe('paddingFormatHandler.apply', () => { + let div: HTMLElement; + let format: PaddingFormat; + let context: ModelToDomContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createModelToDomContext(); + }); + + it('No padding', () => { + paddingFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Has padding', () => { + format.paddingTop = '1px'; + format.paddingRight = '2px'; + format.paddingBottom = '3px'; + format.paddingLeft = '4px'; + + paddingFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toBe('
'); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts index a187c4c06b2..92723a16806 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts @@ -49,6 +49,12 @@ describe('whiteSpaceFormatHandler.parse', () => { whiteSpace: 'pre', }); }); + + it('White space = normal', () => { + div.style.whiteSpace = 'normal'; + whiteSpaceFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({}); + }); }); describe('whiteSpaceFormatHandler.apply', () => { diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts index 959beb060f1..210c944beda 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/backgroundColorFormatHandlerTest.ts @@ -37,6 +37,13 @@ describe('backgroundColorFormatHandler.parse', () => { div.style.backgroundColor = 'transparent'; backgroundColorFormatHandler.parse(format, div, context, {}); + expect(format.backgroundColor).toBeUndefined(); + }); + + it('Transparent, different with default value', () => { + div.style.backgroundColor = 'transparent'; + backgroundColorFormatHandler.parse(format, div, context, { backgroundColor: 'red' }); + expect(format.backgroundColor).toBe('transparent'); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts index c0d0fe7d493..17fb9bb5e9b 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts @@ -73,6 +73,24 @@ describe('borderFormatHandler.parse', () => { expect(format).toEqual({}); }); + + it('Has 0 width border', () => { + div.style.border = '0px sold black'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({}); + }); + + it('Has border radius', () => { + div.style.borderRadius = '10px'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderRadius: '10px', + }); + }); }); describe('borderFormatHandler.apply', () => { diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/boldFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/boldFormatHandlerTest.ts index f1facbeaf35..9daaf858fd5 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/boldFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/boldFormatHandlerTest.ts @@ -45,6 +45,21 @@ describe('boldFormatHandler.parse', () => { expect(format.fontWeight).toBe('600'); }); + it('bold 400', () => { + div.style.fontWeight = '400'; + boldFormatHandler.parse(format, div, context, {}); + + expect(format.fontWeight).toBeUndefined(); + }); + + it('bold 400 when it is already in 600', () => { + div.style.fontWeight = '400'; + format.fontWeight = '600'; + boldFormatHandler.parse(format, div, context, {}); + + expect(format.fontWeight).toBe('400'); + }); + it('default style to bold', () => { ['bold', 'bolder', '600', '700'].forEach(value => { boldFormatHandler.parse(format, div, context, { fontWeight: value }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/letterSpacingFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/letterSpacingFormatHandlerTest.ts index c052389033c..3a60e5d8920 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/letterSpacingFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/segment/letterSpacingFormatHandlerTest.ts @@ -30,6 +30,13 @@ describe('letterSpacingFormatHandler.parse', () => { expect(format.letterSpacing).toBe('1em'); }); + + it('Normal', () => { + div.style.letterSpacing = 'normal'; + letterSpacingFormatHandler.parse(format, div, context, {}); + + expect(format.letterSpacing).toBeUndefined(); + }); }); describe('letterSpacingFormatHandler.apply', () => { diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/shouldSetValueTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/shouldSetValueTest.ts new file mode 100644 index 00000000000..ee708efed2b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/shouldSetValueTest.ts @@ -0,0 +1,45 @@ +import { shouldSetValue } from '../../../lib/formatHandlers/utils/shouldSetValue'; + +describe('shouldSetValue', () => { + it('no value', () => { + const result = shouldSetValue(undefined, '', 'existing', ''); + + expect(result).toBeFalsy(); + }); + + it('Empty string value', () => { + const result = shouldSetValue('', '', 'existing', ''); + + expect(result).toBeFalsy(); + }); + + it('Has value, is inherit', () => { + const result = shouldSetValue('inherit', '', 'existing', ''); + + expect(result).toBeFalsy(); + }); + + it('value equals normal value', () => { + const result = shouldSetValue('test', 'test', '', ''); + + expect(result).toBeFalsy(); + }); + + it('Has value, no existing value', () => { + const result = shouldSetValue('test', 'test2', '', ''); + + expect(result).toBeTruthy(); + }); + + it('Has value, value equal to default value', () => { + const result = shouldSetValue('test', 'test2', '', 'test'); + + expect(result).toBeTruthy(); + }); + + it('Has value, no normal value, no existing value, no default value', () => { + const result = shouldSetValue('test', '', '', ''); + + expect(result).toBeTruthy(); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts index d0f1148bff4..4fd539d34a5 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts @@ -182,13 +182,11 @@ describe(ID, () => { blockType: 'Paragraph', format: { textAlign: 'center', - whiteSpace: 'normal', }, }, ], format: { textAlign: 'center', - whiteSpace: 'normal', borderTop: '0.5pt solid', borderRight: '0.5pt solid', borderBottom: '0.5pt solid', @@ -222,7 +220,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -264,7 +261,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -313,7 +309,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -360,7 +355,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -402,7 +396,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -451,7 +444,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -498,7 +490,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -540,7 +531,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -589,7 +579,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -636,7 +625,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -678,7 +666,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -727,7 +714,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -774,7 +760,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -816,7 +801,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -865,7 +849,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -912,7 +895,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -954,7 +936,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -1003,7 +984,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -1050,7 +1030,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -1092,7 +1071,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -1141,7 +1119,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index 057fb0e3874..a6b8cb2035c 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -224,13 +224,11 @@ describe(ID, () => { blockType: 'Paragraph', format: { textAlign: 'center', - whiteSpace: 'normal', }, }, ], format: { textAlign: 'center', - whiteSpace: 'normal', borderTop: '0.5pt solid', borderRight: '0.5pt solid', borderBottom: '0.5pt solid', @@ -263,7 +261,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -304,7 +301,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -351,7 +347,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -396,7 +391,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -437,7 +431,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -484,7 +477,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -529,7 +521,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -570,7 +561,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -617,7 +607,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -662,7 +651,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -703,7 +691,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -750,7 +737,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -795,7 +781,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -836,7 +821,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -883,7 +867,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -928,7 +911,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -969,7 +951,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -1016,7 +997,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, @@ -1061,7 +1041,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'black', }, }, @@ -1102,7 +1081,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(5, 99, 193)', underline: true, }, @@ -1149,7 +1127,6 @@ describe(ID, () => { format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt', - fontWeight: '400', textColor: 'rgb(219, 219, 219)', }, }, diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts index 516c80cb152..5736f9d17b2 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts @@ -5,23 +5,25 @@ import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { tableProcessor } from 'roosterjs-content-model-dom'; const ID = 'CM_Paste_From_WORD_Online_E2E'; -const clipboardData = ({ - types: ['text/plain', 'text/html'], - text: 'asd\r\n\r\nTest ', - image: null, - files: [], - customValues: {}, - snapshotBeforePaste: '

', - htmlFirstLevelChildTags: ['DIV', 'DIV'], - html: - '

asd 

  • Test 

', -}); describe(ID, () => { let editor: IContentModelEditor = undefined!; + let clipboardData: ClipboardData; beforeEach(() => { editor = initEditor(ID); + + clipboardData = ({ + types: ['text/plain', 'text/html'], + text: 'asd\r\n\r\nTest ', + image: null, + files: [], + customValues: {}, + snapshotBeforePaste: '

', + htmlFirstLevelChildTags: ['DIV', 'DIV'], + html: + '

asd 

  • Test 

', + }); }); afterEach(() => { @@ -95,8 +97,6 @@ describe(ID, () => { segmentType: 'Text', text: 'Test Table ', format: { - letterSpacing: - 'normal', fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', fontSize: '11pt', @@ -125,14 +125,11 @@ describe(ID, () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', paddingRight: '7px', - paddingBottom: '0px', paddingLeft: '7px', }, }, @@ -140,7 +137,6 @@ describe(ID, () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', borderTop: '1px solid', borderRight: '1px solid', borderBottom: '1px solid', @@ -170,8 +166,6 @@ describe(ID, () => { segmentType: 'Text', text: 'Test Table ', format: { - letterSpacing: - 'normal', fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', fontSize: '11pt', @@ -200,14 +194,11 @@ describe(ID, () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', paddingRight: '7px', - paddingBottom: '0px', paddingLeft: '7px', }, }, @@ -215,7 +206,6 @@ describe(ID, () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', borderTop: '1px solid', borderRight: '1px solid', borderBottom: '1px solid', @@ -237,8 +227,6 @@ describe(ID, () => { useBorderBox: true, direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', - backgroundColor: 'transparent', marginTop: '0px', marginRight: '0px', marginBottom: '0px', @@ -281,7 +269,6 @@ describe(ID, () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: 'normal', fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', fontSize: '12pt', diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts index 496f15deb61..fe25a51a933 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts @@ -63,7 +63,6 @@ describe(ID, () => { segmentType: 'Text', text: 'No.', format: { - letterSpacing: 'normal', fontFamily: 'Calibri, sans-serif', fontSize: '11pt', fontWeight: '700', @@ -113,7 +112,6 @@ describe(ID, () => { segmentType: 'Text', text: 'ID', format: { - letterSpacing: 'normal', fontFamily: 'Calibri, sans-serif', fontSize: '11pt', fontWeight: '700', @@ -161,7 +159,6 @@ describe(ID, () => { segmentType: 'Text', text: 'Work Item Type', format: { - letterSpacing: 'normal', fontFamily: 'Calibri, sans-serif', fontSize: '11pt', fontWeight: '700', @@ -171,7 +168,6 @@ describe(ID, () => { ], format: { textAlign: 'center', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', @@ -181,7 +177,6 @@ describe(ID, () => { ], format: { textAlign: 'center', - whiteSpace: 'normal', borderTop: '0.5pt solid', borderRight: '0.5pt solid', borderBottom: '0.5pt solid', @@ -204,7 +199,6 @@ describe(ID, () => { ], format: { textAlign: 'start', - whiteSpace: 'normal', backgroundColor: 'rgb(255, 255, 255)', width: '170pt', useBorderBox: true, diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index f5ce08c4c4f..c4958d39c11 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -1357,12 +1357,10 @@ describe('wordOnlineHandler', () => { segmentType: 'Image', src: 'http://www.microsoft.com', format: { - letterSpacing: 'normal', fontFamily: '"Segoe UI", "Segoe UI Web", Arial, Verdana, sans-serif', fontSize: '12px', italic: false, - fontWeight: '400', textColor: 'rgb(0, 0, 0)', backgroundColor: 'rgb(255, 255, 255)', width: '264px', @@ -1371,10 +1369,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', borderTop: Browser.isFirefox ? 'medium none' : '', borderRight: Browser.isFirefox ? 'medium none' : '', borderBottom: Browser.isFirefox ? 'medium none' : '', @@ -1662,8 +1656,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: 'ODSP', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', @@ -1679,8 +1671,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', @@ -1696,8 +1686,6 @@ describe('wordOnlineHandler', () => { { segmentType: 'Br', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', @@ -1714,8 +1702,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: 'xFun', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', @@ -1731,8 +1717,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', @@ -1754,12 +1738,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -1774,14 +1752,11 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', paddingRight: '6px', - paddingBottom: '0px', paddingLeft: '6px', }, }, @@ -1789,17 +1764,11 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', backgroundColor: 'rgb(21, 96, 130)', width: '312px', borderTop: '1px solid', - borderRight: '0px none', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', verticalAlign: 'middle', }, spanLeft: false, @@ -1825,8 +1794,6 @@ describe('wordOnlineHandler', () => { text: 'Title of Announcement', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '21.5pt', @@ -1842,8 +1809,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '21.5pt', @@ -1865,12 +1830,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -1885,14 +1844,11 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', paddingRight: '6px', - paddingBottom: '0px', paddingLeft: '6px', }, }, @@ -1900,17 +1856,11 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', backgroundColor: 'rgb(21, 96, 130)', width: '312px', borderTop: '1px solid', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', - borderLeft: '0px none', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', verticalAlign: 'middle', }, spanLeft: false, @@ -1941,8 +1891,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: 'Announcement ', format: { - letterSpacing: - 'normal', fontFamily: 'Aptos_MSFontService, Aptos_MSFontService_EmbeddedFont, Aptos_MSFontService_MSFontService, sans-serif', fontSize: '14pt', @@ -1958,8 +1906,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: 'Aptos_MSFontService, Aptos_MSFontService_EmbeddedFont, Aptos_MSFontService_MSFontService, sans-serif', fontSize: '14pt', @@ -1981,12 +1927,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2001,14 +1941,11 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', paddingRight: '6px', - paddingBottom: '0px', paddingLeft: '6px', }, }, @@ -2016,17 +1953,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', backgroundColor: 'rgb(0, 0, 0)', width: '624px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', verticalAlign: 'middle', }, spanLeft: false, @@ -2042,17 +1974,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', backgroundColor: 'rgb(0, 0, 0)', width: '624px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', verticalAlign: 'middle', }, spanLeft: true, @@ -2083,8 +2010,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: 'Hello ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2101,8 +2026,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2124,12 +2047,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2147,8 +2064,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2170,12 +2085,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2194,8 +2103,6 @@ describe('wordOnlineHandler', () => { text: '[Brief description of change]', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2212,8 +2119,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2229,8 +2134,6 @@ describe('wordOnlineHandler', () => { { segmentType: 'Br', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2247,8 +2150,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2270,12 +2171,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2294,8 +2189,6 @@ describe('wordOnlineHandler', () => { text: '[What changed and how it ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2313,8 +2206,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: 'benefits', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2331,8 +2222,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2349,8 +2238,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: 'devs', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2367,8 +2254,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ']', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2385,8 +2270,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2408,12 +2291,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2431,8 +2308,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2453,12 +2328,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2477,8 +2346,6 @@ describe('wordOnlineHandler', () => { text: '[Any action needed by devs]', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2494,8 +2361,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2516,12 +2381,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2539,8 +2398,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2561,12 +2418,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2585,8 +2436,6 @@ describe('wordOnlineHandler', () => { text: '[Link to Documentation ]', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2602,8 +2451,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2618,8 +2465,6 @@ describe('wordOnlineHandler', () => { { segmentType: 'Br', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2635,8 +2480,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2652,8 +2495,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2674,12 +2515,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2698,8 +2533,6 @@ describe('wordOnlineHandler', () => { text: '[What comes next if something comes next]', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2716,8 +2549,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2733,8 +2564,6 @@ describe('wordOnlineHandler', () => { { segmentType: 'Br', format: { - letterSpacing: - 'normal', fontFamily: 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2751,8 +2580,6 @@ describe('wordOnlineHandler', () => { segmentType: 'Text', text: ' ', format: { - letterSpacing: - 'normal', fontFamily: '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', @@ -2774,12 +2601,6 @@ describe('wordOnlineHandler', () => { marginRight: '0px', marginTop: '0px', marginBottom: '0px', - paddingLeft: '0px', - paddingRight: '0px', - backgroundColor: - 'transparent', - paddingTop: '0px', - paddingBottom: '0px', }, segmentFormat: { fontWeight: 'normal', @@ -2794,14 +2615,11 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', paddingRight: '6px', - paddingBottom: '0px', paddingLeft: '6px', }, }, @@ -2809,18 +2627,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid rgb(0, 0, 0)', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid rgb(0, 0, 0)', verticalAlign: 'top', width: '624px', - backgroundColor: 'transparent', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', }, spanLeft: false, spanAbove: false, @@ -2835,18 +2647,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid rgb(0, 0, 0)', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid rgb(0, 0, 0)', verticalAlign: 'top', width: '624px', - backgroundColor: 'transparent', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', }, spanLeft: true, spanAbove: false, @@ -2861,8 +2667,6 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', - backgroundColor: 'transparent', marginTop: '0px', marginRight: '0px', marginBottom: '0px', @@ -2881,31 +2685,21 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', marginTop: '2px', marginRight: '0px', marginBottom: '2px', display: 'flex', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', }, }, ], format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'normal', backgroundColor: 'rgb(255, 255, 255)', marginTop: '0px', marginRight: '0px', marginBottom: '0px', marginLeft: '0px', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', }, }, ], From 9aafcb65241bf936cc0cf12dbbb1f4ab5c827547 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 19 Dec 2023 15:25:14 -0800 Subject: [PATCH 10/64] Standalone Editor: Port paste API step 2 (#2280) * Standalone Editor: Port paste API step 1 * Standalone Editor: Port paste API step 2 * improve * improve --- .../lib/coreApi/createContentModel.ts | 9 ++- .../lib/coreApi/paste.ts | 15 +---- .../lib/coreApi/setContentModel.ts | 9 ++- .../lib/editor/createStandaloneEditorCore.ts | 8 ++- .../createStandaloneEditorDefaultSettings.ts | 59 +++++++++++-------- .../test/coreApi/createContentModelTest.ts | 2 + .../test/coreApi/setContentModelTest.ts | 18 ++++-- .../test/editor/createEditorCoreTest.ts | 32 ++++++---- .../lib/editor/StandaloneEditorCore.ts | 36 ++++++----- .../lib/index.ts | 2 +- 10 files changed, 115 insertions(+), 75 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts index 8d160466b20..3d01c9aeaa5 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts @@ -48,8 +48,13 @@ function internalCreateContentModel( ) { const editorContext = core.api.createEditorContext(core); const domToModelContext = option - ? createDomToModelContext(editorContext, ...(core.defaultDomToModelOptions || []), option) - : createDomToModelContextWithConfig(core.defaultDomToModelConfig, editorContext); + ? createDomToModelContext( + editorContext, + core.domToModelSettings.builtIn, + core.domToModelSettings.customized, + option + ) + : createDomToModelContextWithConfig(core.domToModelSettings.calculated, editorContext); return domToContentModel(core.contentDiv, domToModelContext, selection); } diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts index 405b6df5f09..d0e3080f7dc 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts @@ -1,7 +1,7 @@ import { ChangeSource } from '../constants/ChangeSource'; +import { GetContentMode, PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; import { getSelectedSegments } from '../publicApi/selection/collectSelections'; import { mergeModel } from '../publicApi/model/mergeModel'; -import { GetContentMode, PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; import type { BeforePasteEvent } from 'roosterjs-editor-types'; import type { ContentModelDocument, @@ -21,7 +21,6 @@ import { createDomToModelContext, domToContentModel, moveChildNodes, - tableProcessor, } from 'roosterjs-content-model-dom'; import { createDefaultHtmlSanitizerOptions, @@ -191,17 +190,7 @@ function createBeforePasteEventData( htmlBefore: '', htmlAfter: '', htmlAttributes: {}, - domToModelOption: Object.assign( - {}, - ...[ - core.defaultDomToModelOptions, - { - processorOverride: { - table: tableProcessor, - }, - }, - ] - ), + domToModelOption: Object.assign({}, core.domToModelSettings.customized), pasteType: PasteTypeMap[pasteType], }; } diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts index 68b4be7037c..ef396794bfc 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts @@ -15,8 +15,13 @@ import type { SetContentModel } from 'roosterjs-content-model-types'; export const setContentModel: SetContentModel = (core, model, option, onNodeCreated) => { const editorContext = core.api.createEditorContext(core); const modelToDomContext = option - ? createModelToDomContext(editorContext, ...(core.defaultModelToDomOptions || []), option) - : createModelToDomContextWithConfig(core.defaultModelToDomConfig, editorContext); + ? createModelToDomContext( + editorContext, + core.modelToDomSettings.builtIn, + core.modelToDomSettings.customized, + option + ) + : createModelToDomContextWithConfig(core.modelToDomSettings.calculated, editorContext); const selection = contentModelToDom( core.contentDiv.ownerDocument, diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts index 103b22a98d5..5ff1f87ec93 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts @@ -1,7 +1,10 @@ import { createStandaloneEditorCorePlugins } from '../corePlugin/createStandaloneEditorCorePlugins'; -import { createStandaloneEditorDefaultSettings } from './createStandaloneEditorDefaultSettings'; import { DarkColorHandlerImpl } from './DarkColorHandlerImpl'; import { standaloneCoreApiMap } from './standaloneCoreApiMap'; +import { + createDomToModelSettings, + createModelToDomSettings, +} from './createStandaloneEditorDefaultSettings'; import type { EditorPlugin } from 'roosterjs-editor-types'; import type { EditorEnvironment, @@ -48,7 +51,8 @@ export function createStandaloneEditorCore( options.getDarkColor ?? getDarkColorFallback ), trustedHTMLHandler: options.trustedHTMLHandler || defaultTrustHtmlHandler, - ...createStandaloneEditorDefaultSettings(options), + domToModelSettings: createDomToModelSettings(options), + modelToDomSettings: createModelToDomSettings(options), ...getPluginState(corePlugins), ...unportedCorePluginState, }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts index e7a600fd4bd..bbba2c45b6a 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings.ts @@ -2,42 +2,55 @@ import { createDomToModelConfig, createModelToDomConfig } from 'roosterjs-conten import { listItemMetadataApplier, listLevelMetadataApplier } from '../metadata/updateListMetadata'; import { tablePreProcessor } from '../override/tablePreProcessor'; import type { + ContentModelSettings, DomToModelOption, + DomToModelSettings, ModelToDomOption, - StandaloneEditorDefaultSettings, + ModelToDomSettings, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; /** * @internal - * Create default DOM and Content Model conversion settings for a standalone editor + * Create default DOM to Content Model conversion settings for a standalone editor * @param options The editor options */ -export function createStandaloneEditorDefaultSettings( +export function createDomToModelSettings( options: StandaloneEditorOptions -): StandaloneEditorDefaultSettings { - const defaultDomToModelOptions: (DomToModelOption | undefined)[] = [ - { - processorOverride: { - table: tablePreProcessor, - }, +): ContentModelSettings { + const builtIn: DomToModelOption = { + processorOverride: { + table: tablePreProcessor, }, - options.defaultDomToModelOptions, - ]; - const defaultModelToDomOptions: (ModelToDomOption | undefined)[] = [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, + }; + const customized: DomToModelOption = options.defaultDomToModelOptions ?? {}; + + return { + builtIn, + customized, + calculated: createDomToModelConfig([builtIn, customized]), + }; +} + +/** + * @internal + * Create default Content Model to DOM conversion settings for a standalone editor + * @param options The editor options + */ +export function createModelToDomSettings( + options: StandaloneEditorOptions +): ContentModelSettings { + const builtIn: ModelToDomOption = { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, }, - options.defaultModelToDomOptions, - ]; + }; + const customized: ModelToDomOption = options.defaultModelToDomOptions ?? {}; return { - defaultDomToModelOptions, - defaultModelToDomOptions, - defaultDomToModelConfig: createDomToModelConfig(defaultDomToModelOptions), - defaultModelToDomConfig: createModelToDomConfig(defaultModelToDomOptions), + builtIn, + customized, + calculated: createModelToDomConfig([builtIn, customized]), }; } diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts index 16e33c508a5..e9810252e3a 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts @@ -44,6 +44,7 @@ describe('createContentModel', () => { cachedModel: mockedCachedMode, }, lifecycle: {}, + domToModelSettings: {}, } as any) as StandaloneEditorCore & EditorCore; }); @@ -105,6 +106,7 @@ describe('createContentModel with selection', () => { createEditorContext: createEditorContextSpy, }, cache: {}, + domToModelSettings: {}, }; }); diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts index cf53f6b2df5..0940f354c93 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts @@ -49,8 +49,10 @@ describe('setContentModel', () => { getDOMSelection: getDOMSelectionSpy, }, lifecycle: {}, - defaultModelToDomConfig: mockedConfig, cache: {}, + modelToDomSettings: { + calculated: mockedConfig, + }, } as any) as StandaloneEditorCore & EditorCore; }); @@ -95,12 +97,13 @@ describe('setContentModel', () => { const defaultOption = { o: 'OPTION' } as any; const additionalOption = { o: 'OPTION1', o2: 'OPTION2' } as any; - core.defaultModelToDomOptions = [defaultOption]; + core.modelToDomSettings.builtIn = defaultOption; setContentModel(core, mockedModel, additionalOption); expect(createModelToDomContextSpy).toHaveBeenCalledWith( mockedEditorContext, defaultOption, + undefined, additionalOption ); expect(contentModelToDomSpy).toHaveBeenCalledWith( @@ -141,9 +144,14 @@ describe('setContentModel', () => { ignoreSelection: true, }); - expect(createModelToDomContextSpy).toHaveBeenCalledWith(mockedEditorContext, { - ignoreSelection: true, - }); + expect(createModelToDomContextSpy).toHaveBeenCalledWith( + mockedEditorContext, + undefined, + undefined, + { + ignoreSelection: true, + } + ); expect(contentModelToDomSpy).toHaveBeenCalledWith( mockedDoc, mockedDiv, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts index ffa4589a29c..5d1496aff1e 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts @@ -54,8 +54,11 @@ const mockedLifecyclePlugin = { getState: () => mockedLifecycleState, } as any; const mockedEventTranslatePlugin = 'EventTranslate' as any; -const mockedDefaultSettings = { - settings: 'SETTINGS', +const mockedDefaultDomToModelSettings = { + settings: 'DOMTOMODELSETTINGS', +} as any; +const mockedDefaultModelToDomSettings = { + settings: 'MODELTODOMSETTINGS', } as any; describe('createEditorCore', () => { @@ -87,10 +90,12 @@ describe('createEditorCore', () => { spyOn(EventTranslate, 'createEventTypeTranslatePlugin').and.returnValue( mockedEventTranslatePlugin ); - spyOn( - createStandaloneEditorDefaultSettings, - 'createStandaloneEditorDefaultSettings' - ).and.returnValue(mockedDefaultSettings); + spyOn(createStandaloneEditorDefaultSettings, 'createDomToModelSettings').and.returnValue( + mockedDefaultDomToModelSettings + ); + spyOn(createStandaloneEditorDefaultSettings, 'createModelToDomSettings').and.returnValue( + mockedDefaultModelToDomSettings + ); }); it('No additional option', () => { @@ -126,7 +131,8 @@ describe('createEditorCore', () => { sizeTransformer: jasmine.anything(), darkColorHandler: jasmine.anything(), disposeErrorHandler: undefined, - ...mockedDefaultSettings, + domToModelSettings: mockedDefaultDomToModelSettings, + modelToDomSettings: mockedDefaultModelToDomSettings, environment: { isMac: false, isAndroid: false, @@ -147,9 +153,12 @@ describe('createEditorCore', () => { }; const core = createEditorCore(contentDiv, options); - expect( - createStandaloneEditorDefaultSettings.createStandaloneEditorDefaultSettings - ).toHaveBeenCalledWith(options); + expect(createStandaloneEditorDefaultSettings.createDomToModelSettings).toHaveBeenCalledWith( + options + ); + expect(createStandaloneEditorDefaultSettings.createModelToDomSettings).toHaveBeenCalledWith( + options + ); expect(core).toEqual({ contentDiv, @@ -182,7 +191,8 @@ describe('createEditorCore', () => { sizeTransformer: jasmine.anything(), darkColorHandler: jasmine.anything(), disposeErrorHandler: undefined, - ...mockedDefaultSettings, + domToModelSettings: mockedDefaultDomToModelSettings, + modelToDomSettings: mockedDefaultModelToDomSettings, environment: { isMac: false, isAndroid: false, diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index fb025a8d856..84a635b241a 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -414,8 +414,7 @@ export interface StandaloneCoreApiMap extends PortedCoreApiMap, UnportedCoreApiM */ export interface StandaloneEditorCore extends StandaloneEditorCorePluginState, - UnportedCorePluginState, - StandaloneEditorDefaultSettings { + UnportedCorePluginState { /** * The content DIV element of this editor */ @@ -436,6 +435,16 @@ export interface StandaloneEditorCore */ readonly plugins: EditorPlugin[]; + /** + * Settings used by DOM to Content Model conversion + */ + readonly domToModelSettings: ContentModelSettings; + + /** + * Settings used by Content Model to DOM conversion + */ + readonly modelToDomSettings: ContentModelSettings; + /** * Editor running environment */ @@ -458,26 +467,21 @@ export interface StandaloneEditorCore /** * Default DOM and Content Model conversion settings for an editor */ -export interface StandaloneEditorDefaultSettings { - /** - * Default DOM to Content Model options - */ - defaultDomToModelOptions: (DomToModelOption | undefined)[]; - +export interface ContentModelSettings { /** - * Default Content Model to DOM options + * Built in options used by editor */ - defaultModelToDomOptions: (ModelToDomOption | undefined)[]; + builtIn: OptionType; /** - * Default DOM to Content Model config, calculated from defaultDomToModelOptions, - * will be used for creating content model if there is no other customized options + * Customize options passed in from Editor Options, used for overwrite default option. + * This will also be used by copy/paste */ - defaultDomToModelConfig: DomToModelSettings; + customized: OptionType; /** - * Default Content Model to DOM config, calculated from defaultModelToDomOptions, - * will be used for setting content model if there is no other customized options + * Configuration calculated from default and customized options. + * This is a cached object so that we don't need to cache it every time when we use Content Model */ - defaultModelToDomConfig: ModelToDomSettings; + calculated: ConfigType; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index ae0606da2cd..d6722c1658a 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -205,7 +205,7 @@ export { FormatContentModel, StandaloneCoreApiMap, StandaloneEditorCore, - StandaloneEditorDefaultSettings, + ContentModelSettings, SwitchShadowEdit, TriggerEvent, AddUndoSnapshot, From 8d78b9786daf3ede822c2c02fa785695d7877d33 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 20 Dec 2023 12:05:36 -0600 Subject: [PATCH 11/64] Table Fidelity improvement: Width Attribute and Cellpadding attribute (#2284) * init * add test * update name of test --- .../lib/formatHandlers/common/sizeFormatHandler.ts | 2 +- .../formatHandlers/table/tableSpacingFormatHandler.ts | 6 ++++++ .../formatHandlers/common/sizeFormatHandlerTest.ts | 11 +++++++++++ .../table/tableSpacingFormatHandlerTest.ts | 6 ++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/sizeFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/sizeFormatHandler.ts index 3a43767b51e..c4e4e256ad1 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/sizeFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/sizeFormatHandler.ts @@ -62,7 +62,7 @@ function tryParseSize(element: HTMLElement, attrName: 'width' | 'height'): strin return attrValue && PercentageRegex.test(attrValue) ? attrValue - : Number.isNaN(value) + : Number.isNaN(value) || value == 0 ? undefined : value + 'px'; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts index 37ee6c87d6e..df66ef61867 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts @@ -2,6 +2,7 @@ import type { FormatHandler } from '../FormatHandler'; import type { SpacingFormat } from 'roosterjs-content-model-types'; const BorderCollapsed = 'collapse'; +const CellPadding = 'cellPadding'; /** * @internal @@ -10,6 +11,11 @@ export const tableSpacingFormatHandler: FormatHandler = { parse: (format, element) => { if (element.style.borderCollapse == BorderCollapsed) { format.borderCollapse = true; + } else { + const cellPadding = element.getAttribute(CellPadding); + if (cellPadding) { + format.borderCollapse = true; + } } }, apply: (format, element) => { diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/sizeFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/sizeFormatHandlerTest.ts index ae7ff75f41b..3c6591a3124 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/sizeFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/sizeFormatHandlerTest.ts @@ -56,6 +56,17 @@ describe('sizeFormatHandler.parse', () => { expect(format).toEqual({ width: '10px', height: '20px' }); }); + it('Element with width and height attributes equal to 0', () => { + const element = document.createElement('div'); + + element.setAttribute('width', '0'); + element.setAttribute('height', '0'); + + sizeFormatHandler.parse(format, element, context, {}); + + expect(format).toEqual({}); + }); + it('Element with width and height in attribute in percentage', () => { const element = document.createElement('div'); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts index 3b15c9c4777..4af7f3d2985 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts @@ -30,6 +30,12 @@ describe('tableSpacingFormatHandler.parse', () => { tableSpacingFormatHandler.parse(format, div, context, {}); expect(format).toEqual({}); }); + + it('Set border collapsed if element contains cellpadding attribute', () => { + div.setAttribute('cellPadding', '0'); + tableSpacingFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ borderCollapse: true }); + }); }); describe('tableSpacingFormatHandler.apply', () => { From b04155e2cffbed6316ce23b01a8f9a61f1c81500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 20 Dec 2023 16:43:47 -0300 Subject: [PATCH 12/64] adjust link selection --- .../selection/adjustTrailingSpaceSelection.ts | 65 +++++++++++++++++++ .../lib/publicApi/link/insertLink.ts | 2 + .../adjustTrailingSpaceSelectionTest.ts | 49 ++++++++++++++ .../test/publicApi/link/insertLinkTest.ts | 50 ++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts create mode 100644 packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts new file mode 100644 index 00000000000..a19fb05038b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts @@ -0,0 +1,65 @@ +import { createText } from 'roosterjs-content-model-dom'; +import { iterateSelections } from 'roosterjs-content-model-core'; +import type { + ContentModelDocument, + ContentModelSegment, + ContentModelText, +} from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function adjustTrailingSpaceSelection(model: ContentModelDocument) { + iterateSelections(model, (_, __, block, segments) => { + if (block?.blockType == 'Paragraph') { + const tempSegments = [...block.segments]; + tempSegments?.forEach((segment, index) => { + if ( + segment.isSelected && + segment.segmentType == 'Text' && + hasTrailingSpace(segment.text) && + !isTrailingSpace(segment.text) + ) { + splitTextSegment(block.segments, segment, index); + } + }); + } + return true; + }); +} + +function hasTrailingSpace(text: string) { + return text.length > 0 && text.trimRight().length < text.length; +} + +function isTrailingSpace(text: string) { + return text.length > 0 && text.trimRight().length == 0; +} + +function splitTextSegment( + segments: ContentModelSegment[], + textSegment: Readonly, + index: number +) { + 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, + }, + } + : undefined; + const trailingSpaceSegment = createText( + trailingSpace, + undefined, + trailingSpaceLink, + textSegment.code + ); + trailingSpaceSegment.isSelected = true; + segments.splice(index, 1, newText, trailingSpaceSegment); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts index 3d29e419aae..64b68247a7b 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts @@ -1,3 +1,4 @@ +import { adjustTrailingSpaceSelection } from '../../modelApi/selection/adjustTrailingSpaceSelection'; import { ChangeSource, getSelectedSegments, mergeModel } from 'roosterjs-content-model-core'; import { HtmlSanitizer, matchLink } from 'roosterjs-editor-dom'; import type { ContentModelLink, IStandaloneEditor } from 'roosterjs-content-model-types'; @@ -91,6 +92,7 @@ export default function insertLink( }); } + adjustTrailingSpaceSelection(model); return segments.length > 0; }, { diff --git a/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts new file mode 100644 index 00000000000..4c29b94d71f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts @@ -0,0 +1,49 @@ +import { addSegment, createContentModelDocument, createText } from 'roosterjs-content-model-dom'; +import { adjustTrailingSpaceSelection } from '../../../lib/modelApi/selection/adjustTrailingSpaceSelection'; +import { ContentModelDocument } from 'roosterjs-content-model-types'; + +describe('adjustTrailingSpaceSelection', () => { + function runTest(model: ContentModelDocument, modelResult: ContentModelDocument) { + adjustTrailingSpaceSelection(model); + expect(model).toEqual(modelResult); + } + + it('no trailing space', () => { + const model = createContentModelDocument(); + const text = createText('text'); + text.isSelected = true; + addSegment(model, text); + runTest(model, model); + }); + + it('trailing space', () => { + const model = createContentModelDocument(); + const text = createText('text '); + text.isSelected = true; + addSegment(model, text); + runTest(model, { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'text', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: ' ', + isSelected: true, + }, + ], + isImplicit: true, + }, + ], + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts index b3c4f48538f..dc478caac08 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts @@ -357,4 +357,54 @@ describe('insertLink', () => { document.body.removeChild(div); }); + + it('Valid url with trailing space', () => { + const doc = createContentModelDocument(); + const text = createText('test '); + text.isSelected = true; + addSegment(doc, text); + + runTest(doc, 'http://test.com', { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'test', + link: { + dataset: {}, + format: { + href: 'http://test.com', + anchorTitle: undefined, + target: undefined, + underline: true, + }, + }, + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: ' ', + link: { + dataset: {}, + format: { + href: 'http://test.com', + anchorTitle: undefined, + target: undefined, + underline: false, + }, + }, + isSelected: true, + }, + ], + }, + ], + }); + }); }); From b05732ff738b729a8b4989f6678c3b4310f71fda Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 20 Dec 2023 12:59:25 -0800 Subject: [PATCH 13/64] Standalone Editor: Port paste API step 3 (#2281) * Standalone Editor: Port paste API step 1 * Standalone Editor: Port paste API step 2 * Standalone Editor: Port paste API step 3 * improve * improve * Improve * Improve --- .../lib/coreApi/paste.ts | 255 +++--------- .../lib/utils/paste/convertInlineCss.ts | 26 ++ .../lib/utils/paste/createPasteFragment.ts | 84 ++++ .../paste/generatePasteOptionFromPlugins.ts | 67 ++++ .../lib/utils/paste/mergePasteContent.ts | 80 ++++ .../lib/utils/paste/retrieveHtmlInfo.ts | 128 ++++++ .../test/coreApi/pasteTest.ts | 230 +---------- .../ContentModelCopyPastePluginTest.ts | 55 ++- .../test/utils/paste/convertInlineCssTest.ts | 123 ++++++ .../utils/paste/createPasteFragmentTest.ts | 293 ++++++++++++++ .../generatePasteOptionFromPluginsTest.ts | 257 ++++++++++++ .../test/utils/paste/mergePasteContentTest.ts | 373 ++++++++++++++++++ .../test/utils/paste/retrieveHtmlInfoTest.ts | 223 +++++++++++ .../roosterjs-content-model-dom/lib/index.ts | 1 - .../common/applySegmentFormatToElement.ts | 16 - .../test/paste/e2e/cmPasteFromWordTest.ts | 2 +- .../lib/event/ContentModelBeforePasteEvent.ts | 1 + .../lib/parameter/ClipboardData.ts | 3 +- 18 files changed, 1761 insertions(+), 456 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/utils/paste/convertInlineCss.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/utils/paste/retrieveHtmlInfo.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/utils/paste/convertInlineCssTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/utils/paste/retrieveHtmlInfoTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/applySegmentFormatToElement.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts index d0e3080f7dc..8dd60207aca 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts @@ -1,55 +1,22 @@ import { ChangeSource } from '../constants/ChangeSource'; -import { GetContentMode, PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; -import { getSelectedSegments } from '../publicApi/selection/collectSelections'; -import { mergeModel } from '../publicApi/model/mergeModel'; -import type { BeforePasteEvent } from 'roosterjs-editor-types'; +import { cloneModel } from '../publicApi/model/cloneModel'; +import { convertInlineCss } from '../utils/paste/convertInlineCss'; +import { createPasteFragment } from '../utils/paste/createPasteFragment'; +import { generatePasteOptionFromPlugins } from '../utils/paste/generatePasteOptionFromPlugins'; +import { mergePasteContent } from '../utils/paste/mergePasteContent'; +import { retrieveHtmlInfo } from '../utils/paste/retrieveHtmlInfo'; +import { sanitizePasteContent } from 'roosterjs-editor-dom'; +import type { CloneModelOptions } from '../publicApi/model/cloneModel'; import type { - ContentModelDocument, - ContentModelSegmentFormat, - FormatWithContentModelContext, - InsertPoint, PasteType, - ContentModelBeforePasteEventData, - ContentModelBeforePasteEvent, ClipboardData, Paste, StandaloneEditorCore, } from 'roosterjs-content-model-types'; -import { - AllowedEntityClasses, - applySegmentFormatToElement, - createDomToModelContext, - domToContentModel, - moveChildNodes, -} from 'roosterjs-content-model-dom'; -import { - createDefaultHtmlSanitizerOptions, - handleImagePaste, - handleTextPaste, - retrieveMetadataFromClipboard, - sanitizePasteContent, -} from 'roosterjs-editor-dom'; +import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; -// Map new PasteType to old PasteType -// TODO: We can remove this once we have standalone editor -const PasteTypeMap: Record = { - asImage: OldPasteType.AsImage, - asPlainText: OldPasteType.AsPlainText, - mergeFormat: OldPasteType.MergeFormat, - normal: OldPasteType.Normal, -}; -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), }; /** @@ -64,66 +31,52 @@ export const paste: Paste = ( clipboardData: ClipboardData, pasteType: PasteType = 'normal' ) => { - if (clipboardData.snapshotBeforePaste) { - // Restore original content before paste a new one - core.api.setContent( - core, - clipboardData.snapshotBeforePaste, - true /* triggerContentChangedEvent */ - ); + core.api.focus(core); + + if (clipboardData.modelBeforePaste) { + core.api.setContentModel(core, cloneModel(clipboardData.modelBeforePaste, CloneOption)); } else { - clipboardData.snapshotBeforePaste = core.api.getContent( - core, - GetContentMode.RawHTMLWithSelection - ); + clipboardData.modelBeforePaste = cloneModel(core.api.createContentModel(core), CloneOption); } - core.api.focus(core); - let originalFormat: ContentModelSegmentFormat | undefined; - core.api.formatContentModel( core, (model, context) => { - const eventData = createBeforePasteEventData(core, clipboardData, pasteType); - const currentSegment = getSelectedSegments(model, true /*includingFormatHolder*/)[0]; - const { fontFamily, fontSize, textColor, backgroundColor, letterSpacing, lineHeight } = - currentSegment?.format ?? {}; - const { - domToModelOption, - fragment, - customizedMerge, - } = triggerPluginEventAndCreatePasteFragment( - core, + // 1. Prepare variables + const doc = createDOMFromHtml(clipboardData.rawHtml, core.trustedHTMLHandler); + + // 2. Handle HTML from clipboard + const htmlFromClipboard = retrieveHtmlInfo(doc, clipboardData); + + // 3. Create target fragment + const sourceFragment = createPasteFragment( + core.contentDiv.ownerDocument, clipboardData, pasteType, - eventData, - { fontFamily, fontSize, textColor, backgroundColor, letterSpacing, lineHeight } + (clipboardData.rawHtml == clipboardData.html + ? doc + : createDOMFromHtml(clipboardData.html, core.trustedHTMLHandler) + )?.body ); - const pasteModel = domToContentModel( - fragment, - createDomToModelContext(undefined /*editorContext*/, domToModelOption) + // 4. Trigger BeforePaste event to allow plugins modify the fragment + const eventResult = generatePasteOptionFromPlugins( + core, + clipboardData, + sourceFragment, + htmlFromClipboard, + pasteType ); - const insertPoint = mergePasteContent( - model, - context, - pasteModel, - pasteType == 'mergeFormat', - customizedMerge - ); + // 5. Convert global CSS to inline CSS + convertInlineCss(eventResult.fragment, htmlFromClipboard.globalCssRules); - if (insertPoint) { - originalFormat = insertPoint.marker.format; - } + // Sanitize the fragment before paste to make sure the content is safe + // TODO: remove this part + sanitizePasteContent(eventResult, null /*position*/); - if (originalFormat) { - context.newPendingFormat = { - ...EmptySegmentFormat, - ...model.format, - ...originalFormat, - }; // Use empty format as initial value to clear any other format inherits from pasted content - } + // 6. Merge pasted content into main Content Model + mergePasteContent(model, context, eventResult, core.domToModelSettings.customized); return true; }, @@ -135,121 +88,9 @@ export const paste: Paste = ( ); }; -/** - * @internal - * Export only for unit test - */ -export function mergePasteContent( - model: ContentModelDocument, - context: FormatWithContentModelContext, - pasteModel: ContentModelDocument, - applyCurrentFormat: boolean, - customizedMerge: - | undefined - | ((source: ContentModelDocument, target: ContentModelDocument) => InsertPoint | null) -): InsertPoint | null { - return customizedMerge - ? customizedMerge(model, pasteModel) - : mergeModel(model, pasteModel, context, { - mergeFormat: applyCurrentFormat ? 'keepSourceEmphasisFormat' : 'none', - mergeTable: shouldMergeTable(pasteModel), - }); -} - -function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined { - // If model contains a table and a paragraph element after the table with a single BR segment, remove the Paragraph after the table - if ( - pasteModel.blocks.length == 2 && - pasteModel.blocks[0].blockType === 'Table' && - pasteModel.blocks[1].blockType === 'Paragraph' && - pasteModel.blocks[1].segments.length === 1 && - pasteModel.blocks[1].segments[0].segmentType === 'Br' - ) { - pasteModel.blocks.splice(1); - } - // Only merge table when the document contain a single table. - return pasteModel.blocks.length === 1 && pasteModel.blocks[0].blockType === 'Table'; -} - -function createBeforePasteEventData( - core: StandaloneEditorCore, - clipboardData: ClipboardData, - pasteType: PasteType -): ContentModelBeforePasteEventData { - const options = createDefaultHtmlSanitizerOptions(); - - options.additionalAllowedCssClasses.push(...AllowedEntityClasses); - - // Remove "caret-color" style generated by Safari to make sure caret shows in right color after paste - options.cssStyleCallbacks['caret-color'] = () => false; - - return { - clipboardData, - fragment: core.contentDiv.ownerDocument.createDocumentFragment(), - sanitizingOption: options, - htmlBefore: '', - htmlAfter: '', - htmlAttributes: {}, - domToModelOption: Object.assign({}, core.domToModelSettings.customized), - pasteType: PasteTypeMap[pasteType], - }; -} - -/** - * This function is used to create a BeforePasteEvent object after trigger the event, so other plugins can modify the event object - * This function will also create a DocumentFragment for paste. - */ -function triggerPluginEventAndCreatePasteFragment( - core: StandaloneEditorCore, - clipboardData: ClipboardData, - pasteType: PasteType, - eventData: ContentModelBeforePasteEventData, - currentFormat: ContentModelSegmentFormat -): ContentModelBeforePasteEventData { - const event = { - eventType: PluginEventType.BeforePaste, - ...eventData, - } as ContentModelBeforePasteEvent; - - const { fragment } = event; - const { rawHtml, text, imageDataUri } = clipboardData; - const trustedHTMLHandler = core.trustedHTMLHandler; - - const doc: Document | undefined = rawHtml - ? new DOMParser().parseFromString(trustedHTMLHandler(rawHtml), 'text/html') - : undefined; - - // Step 2: Retrieve Metadata from Html and the Html that was copied. - retrieveMetadataFromClipboard(doc, event as BeforePasteEvent, trustedHTMLHandler); - - // Step 3: Fill the BeforePasteEvent object, especially the fragment for paste - if ( - (pasteType == 'asImage' && imageDataUri) || - (pasteType != 'asPlainText' && !text && imageDataUri) - ) { - // Paste image - handleImagePaste(imageDataUri, fragment); - } else if (pasteType != 'asPlainText' && rawHtml && doc ? doc.body : false) { - moveChildNodes(fragment, doc?.body); - } else if (text) { - // Paste text - handleTextPaste(text, null /*position*/, fragment); - } - - const formatContainer = fragment.ownerDocument.createElement('span'); - - moveChildNodes(formatContainer, fragment); - fragment.appendChild(formatContainer); - - applySegmentFormatToElement(formatContainer, currentFormat); - - // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste, when the type of paste is different than Plain Text - if (pasteType !== 'asPlainText') { - core.api.triggerEvent(core, event, true /* broadcast */); - } - - // Step 5. Sanitize the fragment before paste to make sure the content is safe - sanitizePasteContent(event, null /*position*/); - - return event; +function createDOMFromHtml( + html: string | null | undefined, + trustedHTMLHandler: TrustedHTMLHandler +): Document | null { + return html ? new DOMParser().parseFromString(trustedHTMLHandler(html), 'text/html') : null; } diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/convertInlineCss.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/convertInlineCss.ts new file mode 100644 index 00000000000..1ec7973f0ae --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/convertInlineCss.ts @@ -0,0 +1,26 @@ +import { toArray } from 'roosterjs-content-model-dom'; +import type { CssRule } from './retrieveHtmlInfo'; + +/** + * @internal + */ +export function convertInlineCss(root: ParentNode, cssRules: CssRule[]) { + for (let i = cssRules.length - 1; i >= 0; i--) { + const { selectors, text } = cssRules[i]; + + for (const selector of selectors) { + if (!selector || !selector.trim() || selector.indexOf(':') >= 0) { + continue; + } + + const nodes = toArray(root.querySelectorAll(selector)); + + // Always put existing styles after so that they have higher priority + // Which means if both global style and inline style apply to the same element, + // inline style will have higher priority + nodes.forEach(node => + node.setAttribute('style', text + (node.getAttribute('style') || '')) + ); + } + } +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts new file mode 100644 index 00000000000..4c95cd4c570 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts @@ -0,0 +1,84 @@ +import { moveChildNodes, wrap } from 'roosterjs-content-model-dom'; +import type { ClipboardData, PasteType } from 'roosterjs-content-model-types'; + +const NBSP_HTML = '\u00A0'; +const ENSP_HTML = '\u2002'; +const TAB_SPACES = 6; + +/** + * @internal + */ +export function createPasteFragment( + document: Document, + clipboardData: ClipboardData, + pasteType: PasteType, + root: HTMLElement | undefined +): DocumentFragment { + const { imageDataUri, text } = clipboardData; + const fragment = document.createDocumentFragment(); + + if ( + (pasteType == 'asImage' && imageDataUri) || + (pasteType != 'asPlainText' && !text && imageDataUri) + ) { + // Paste image + const img = document.createElement('img'); + img.style.maxWidth = '100%'; + img.src = imageDataUri; + fragment.appendChild(img); + } else if (pasteType != 'asPlainText' && root) { + moveChildNodes(fragment, root); + } else if (text) { + text.split('\n').forEach((line, index, lines) => { + line = line + .replace(/^ /g, NBSP_HTML) + .replace(/ $/g, NBSP_HTML) + .replace(/\r/g, '') + .replace(/ {2}/g, ' ' + NBSP_HTML); + + if (line.includes('\t')) { + line = transformTabCharacters(line); + } + + const textNode = document.createTextNode(line); + + // There are 3 scenarios: + // 1. Single line: Paste as it is + // 2. Two lines: Add
between the lines + // 3. 3 or More lines, For first and last line, paste as it is. For middle lines, wrap with DIV, and add BR if it is empty line + if (lines.length == 2 && index == 0) { + // 1 of 2 lines scenario, add BR + fragment.appendChild(textNode); + fragment.appendChild(document.createElement('br')); + } else if (index > 0 && index < lines.length - 1) { + // Middle line of >=3 lines scenario, wrap with DIV + fragment.appendChild( + wrap(document, line == '' ? document.createElement('br') : textNode, 'div') + ); + } else { + // All others, paste as it is + fragment.appendChild(textNode); + } + }); + } + + return fragment; +} + +/** + * Transform \t characters into EN SPACE characters + * @param input string NOT containing \n characters + * @example t("\thello", 2) => "    hello" + */ +function transformTabCharacters(input: string, initialOffset: number = 0) { + let line = input; + let tIndex: number; + while ((tIndex = line.indexOf('\t')) != -1) { + const lineBefore = line.slice(0, tIndex); + const lineAfter = line.slice(tIndex + 1); + const tabCount = TAB_SPACES - ((lineBefore.length + initialOffset) % TAB_SPACES); + const tabStr = Array(tabCount).fill(ENSP_HTML).join(''); + line = lineBefore + tabStr + lineAfter; + } + return line; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts new file mode 100644 index 00000000000..6654303d8c6 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts @@ -0,0 +1,67 @@ +import { PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; +import type { HtmlFromClipboard } from './retrieveHtmlInfo'; +import type { + ClipboardData, + ContentModelBeforePasteEvent, + DomToModelOption, + PasteType, + StandaloneEditorCore, +} from 'roosterjs-content-model-types'; + +// Map new PasteType to old PasteType +// TODO: We can remove this once we have standalone editor +const PasteTypeMap: Record = { + asImage: OldPasteType.AsImage, + asPlainText: OldPasteType.AsPlainText, + mergeFormat: OldPasteType.MergeFormat, + normal: OldPasteType.Normal, +}; + +/** + * @internal + */ +export function generatePasteOptionFromPlugins( + core: StandaloneEditorCore, + clipboardData: ClipboardData, + fragment: DocumentFragment, + htmlFromClipboard: HtmlFromClipboard, + pasteType: PasteType +): ContentModelBeforePasteEvent { + const domToModelOption: DomToModelOption = { + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + }; + + const event: ContentModelBeforePasteEvent = { + eventType: PluginEventType.BeforePaste, + clipboardData, + fragment, + htmlBefore: htmlFromClipboard.htmlBefore ?? '', + htmlAfter: htmlFromClipboard.htmlAfter ?? '', + htmlAttributes: htmlFromClipboard.metadata, + pasteType: PasteTypeMap[pasteType], + domToModelOption, + + // Deprecated + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + }; + + if (pasteType !== 'asPlainText') { + core.api.triggerEvent(core, event, true /* broadcast */); + } + + return event; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts new file mode 100644 index 00000000000..e9dd72004ec --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts @@ -0,0 +1,80 @@ +import { createDomToModelContext, domToContentModel } from 'roosterjs-content-model-dom'; +import { getSegmentTextFormat } from '../../publicApi/domUtils/getSegmentTextFormat'; +import { getSelectedSegments } from '../../publicApi/selection/collectSelections'; +import { mergeModel } from '../../publicApi/model/mergeModel'; +import { PasteType } from 'roosterjs-editor-types'; +import type { MergeModelOption } from '../../publicApi/model/mergeModel'; +import type { + ContentModelBeforePasteEvent, + ContentModelDocument, + ContentModelSegmentFormat, + DomToModelOption, + FormatWithContentModelContext, +} from 'roosterjs-content-model-types'; + +const EmptySegmentFormat: Required = { + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, +}; + +/** + * @internal + */ +export function mergePasteContent( + model: ContentModelDocument, + context: FormatWithContentModelContext, + eventResult: ContentModelBeforePasteEvent, + defaultDomToModelOptions: DomToModelOption +) { + const { fragment, domToModelOption, customizedMerge, pasteType } = eventResult; + const selectedSegment = getSelectedSegments(model, true /*includeFormatHolder*/)[0]; + const domToModelContext = createDomToModelContext( + undefined /*editorContext*/, + defaultDomToModelOptions, + domToModelOption + ); + + domToModelContext.segmentFormat = selectedSegment ? getSegmentTextFormat(selectedSegment) : {}; + + const pasteModel = domToContentModel(fragment, domToModelContext); + const mergeOption: MergeModelOption = { + mergeFormat: pasteType == PasteType.MergeFormat ? 'keepSourceEmphasisFormat' : 'none', + mergeTable: shouldMergeTable(pasteModel), + }; + + const insertPoint = customizedMerge + ? customizedMerge(model, pasteModel) + : mergeModel(model, pasteModel, context, mergeOption); + + if (insertPoint) { + context.newPendingFormat = { + ...EmptySegmentFormat, + ...model.format, + ...insertPoint.marker.format, + }; + } +} + +function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined { + // If model contains a table and a paragraph element after the table with a single BR segment, remove the Paragraph after the table + if ( + pasteModel.blocks.length == 2 && + pasteModel.blocks[0].blockType === 'Table' && + pasteModel.blocks[1].blockType === 'Paragraph' && + pasteModel.blocks[1].segments.length === 1 && + pasteModel.blocks[1].segments[0].segmentType === 'Br' + ) { + pasteModel.blocks.splice(1); + } + // Only merge table when the document contain a single table. + return pasteModel.blocks.length === 1 && pasteModel.blocks[0].blockType === 'Table'; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/retrieveHtmlInfo.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/retrieveHtmlInfo.ts new file mode 100644 index 00000000000..e0a1aefe773 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/retrieveHtmlInfo.ts @@ -0,0 +1,128 @@ +import { isNodeOfType, toArray } from 'roosterjs-content-model-dom'; +import type { ClipboardData } from 'roosterjs-content-model-types'; + +const START_FRAGMENT = ''; +const END_FRAGMENT = ''; + +/** + * @internal + */ +export interface CssRule { + selectors: string[]; + text: string; +} + +/** + * @internal + */ +export interface HtmlFromClipboard { + metadata: Record; + globalCssRules: CssRule[]; + htmlBefore?: string; + htmlAfter?: string; +} + +/** + * @internal + */ +export function retrieveHtmlInfo( + doc: Document | null, + clipboardData: Partial +): HtmlFromClipboard { + let result: HtmlFromClipboard = { + metadata: {}, + globalCssRules: [], + }; + + if (doc) { + result = { + ...retrieveHtmlStrings(clipboardData), + globalCssRules: retrieveCssRules(doc), + metadata: retrieveMetadata(doc), + }; + + clipboardData.htmlFirstLevelChildTags = retrieveTopLevelTags(doc); + } + + return result; +} + +function retrieveTopLevelTags(doc: Document): string[] { + const topLevelTags: string[] = []; + + for (let child = doc.body.firstChild; child; child = child.nextSibling) { + if (isNodeOfType(child, 'TEXT_NODE')) { + const trimmedString = child.nodeValue?.replace(/(\r\n|\r|\n)/gm, '').trim(); + + if (trimmedString) { + topLevelTags.push(''); // Push an empty string as tag for text node + } + } else if (isNodeOfType(child, 'ELEMENT_NODE')) { + topLevelTags.push(child.tagName); + } + } + + return topLevelTags; +} + +function retrieveMetadata(doc: Document): Record { + const result: Record = {}; + const attributes = doc.querySelector('html')?.attributes; + + (attributes ? toArray(attributes) : []).forEach(attr => { + result[attr.name] = attr.value; + }); + + toArray(doc.querySelectorAll('meta')).forEach(meta => { + result[meta.name] = meta.content; + }); + + return result; +} + +function retrieveCssRules(doc: Document): CssRule[] { + const styles = toArray(doc.querySelectorAll('style')); + const result: CssRule[] = []; + + styles.forEach(styleNode => { + const sheet = styleNode.sheet as CSSStyleSheet; + + for (let ruleIndex = 0; ruleIndex < sheet.cssRules.length; ruleIndex++) { + const rule = sheet.cssRules[ruleIndex] as CSSStyleRule; + + if (rule.type == CSSRule.STYLE_RULE && rule.selectorText) { + result.push({ + selectors: rule.selectorText.split(','), + text: rule.style.cssText, + }); + } + } + + styleNode.parentNode?.removeChild(styleNode); + }); + + return result; +} + +function retrieveHtmlStrings( + clipboardData: Partial +): { + htmlBefore: string; + htmlAfter: string; +} { + const rawHtml = clipboardData.rawHtml ?? ''; + const startIndex = rawHtml.indexOf(START_FRAGMENT); + const endIndex = rawHtml.lastIndexOf(END_FRAGMENT); + let htmlBefore = ''; + let htmlAfter = ''; + + if (startIndex >= 0 && endIndex >= startIndex + START_FRAGMENT.length) { + htmlBefore = rawHtml.substring(0, startIndex); + htmlAfter = rawHtml.substring(endIndex + END_FRAGMENT.length); + clipboardData.html = rawHtml.substring(startIndex + START_FRAGMENT.length, endIndex); + } else { + clipboardData.html = rawHtml; + } + + return { htmlBefore, htmlAfter }; +} diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index f224b10d8d3..1a775827442 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -11,8 +11,7 @@ import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/Word import { BeforePasteEvent, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; -import { createContentModelDocument, tableProcessor } from 'roosterjs-content-model-dom'; -import { mergePasteContent } from '../../lib/coreApi/paste'; +import { tableProcessor } from 'roosterjs-content-model-dom'; import { ClipboardData, ContentModelDocument, @@ -364,233 +363,6 @@ describe('paste with content model & paste plugin', () => { }); }); -describe('mergePasteContent', () => { - it('merge table', () => { - // A doc with only one table in content - const pasteModel: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - rows: [ - { - height: 0, - format: {}, - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'FromPaste', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - ], - }, - ], - format: { useBorderBox: true, borderCollapse: true }, - widths: [], - dataset: { - editingInfo: '', - }, - }, - ], - }; - - // A doc with a table, and selection marker inside of it. - const sourceModel: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - rows: [ - { - height: 22, - format: {}, - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - { segmentType: 'Br', format: {} }, - ], - format: {}, - isImplicit: true, - }, - ], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - ], - }, - ], - format: { useBorderBox: true, borderCollapse: true }, - widths: [120, 120], - dataset: { - editingInfo: '', - }, - }, - { - blockType: 'Paragraph', - segments: [{ segmentType: 'Br', format: {} }], - format: {}, - }, - ], - format: {}, - }; - - spyOn(mergeModelFile, 'mergeModel').and.callThrough(); - - mergePasteContent( - sourceModel, - { newEntities: [], deletedEntities: [], newImages: [] }, - pasteModel, - false /* applyCurrentFormat */, - undefined /* customizedMerge */ - ); - - expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( - sourceModel, - pasteModel, - { newEntities: [], deletedEntities: [], newImages: [] }, - { - mergeFormat: 'none', - mergeTable: true, - } - ); - expect(sourceModel).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - rows: [ - { - height: 22, - format: {}, - cells: [ - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'FromPaste', - format: {}, - }, - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - format: { - useBorderBox: true, - borderTop: '1px solid #ABABAB', - borderRight: '1px solid #ABABAB', - borderBottom: '1px solid #ABABAB', - borderLeft: '1px solid #ABABAB', - }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - ], - }, - ], - format: { useBorderBox: true, borderCollapse: true }, - widths: [120, 120], - dataset: { - editingInfo: - '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":null}', - }, - }, - { - blockType: 'Paragraph', - segments: [{ segmentType: 'Br', format: {} }], - format: {}, - }, - ], - format: {}, - }); - }); - - it('customized merge', () => { - const pasteModel: ContentModelDocument = createContentModelDocument(); - const sourceModel: ContentModelDocument = createContentModelDocument(); - - const customizedMerge = jasmine.createSpy('customizedMerge'); - - spyOn(mergeModelFile, 'mergeModel').and.callThrough(); - - mergePasteContent( - sourceModel, - { newEntities: [], deletedEntities: [], newImages: [] }, - pasteModel, - false /* applyCurrentFormat */, - customizedMerge /* customizedMerge */ - ); - - expect(mergeModelFile.mergeModel).not.toHaveBeenCalled(); - expect(customizedMerge).toHaveBeenCalledWith(sourceModel, pasteModel); - }); - - it('Apply current format', () => { - const pasteModel: ContentModelDocument = createContentModelDocument(); - const sourceModel: ContentModelDocument = createContentModelDocument(); - - spyOn(mergeModelFile, 'mergeModel').and.callThrough(); - - mergePasteContent( - sourceModel, - { newEntities: [], deletedEntities: [], newImages: [] }, - pasteModel, - true /* applyCurrentFormat */, - undefined /* customizedMerge */ - ); - - expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( - sourceModel, - pasteModel, - { newEntities: [], deletedEntities: [], newImages: [] }, - { - mergeFormat: 'keepSourceEmphasisFormat', - mergeTable: false, - } - ); - }); -}); - describe('Paste with clipboardData', () => { let editor: IEditor & IStandaloneEditor = undefined!; const ID = 'EDITOR_ID'; diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index c5fc8d6afe6..13feb5b58a5 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -33,8 +33,32 @@ const deleteResultValue = 'deleteResult' as any; const allowedCustomPasteType = ['Test']; +describe('ContentModelCopyPastePlugin.Ctor', () => { + it('Ctor without options', () => { + const plugin = createContentModelCopyPastePlugin({}); + const state = plugin.getState(); + + expect(state).toEqual({ + allowedCustomPasteType: [], + tempDiv: null, + }); + }); + + it('Ctor with options', () => { + const plugin = createContentModelCopyPastePlugin({ + allowedCustomPasteType, + }); + const state = plugin.getState(); + + expect(state).toEqual({ + allowedCustomPasteType: allowedCustomPasteType, + tempDiv: null, + }); + }); +}); + describe('ContentModelCopyPastePlugin |', () => { - let editor: IEditor = null!; + let editor: IEditor & IStandaloneEditor = null!; let plugin: PluginWithState; let domEvents: Record = {}; let div: HTMLDivElement; @@ -566,6 +590,35 @@ describe('ContentModelCopyPastePlugin |', () => { expect(preventDefaultSpy).toHaveBeenCalledTimes(1); }); + it('Handle with domToModelOptions', () => { + const preventDefaultSpy = jasmine.createSpy('preventDefaultPaste'); + let clipboardEvent = { + clipboardData: ({ + items: [{}], + }), + preventDefault() { + preventDefaultSpy(); + }, + }; + spyOn(extractClipboardItemsFile, 'extractClipboardItems').and.returnValue(< + Promise + >{ + then: (cb: (value: ClipboardData) => void | PromiseLike) => { + cb(clipboardData); + }, + }); + isDisposed.and.returnValue(false); + + domEvents.paste.beforeDispatch?.(clipboardEvent); + + expect(pasteSpy).toHaveBeenCalledWith(clipboardData); + expect(extractClipboardItemsFile.extractClipboardItems).toHaveBeenCalledWith( + Array.from(clipboardEvent.clipboardData!.items), + allowedCustomPasteType + ); + expect(preventDefaultSpy).toHaveBeenCalledTimes(1); + }); + it('Handle, editor is disposed', () => { const preventDefaultSpy = jasmine.createSpy('preventDefaultPaste'); let clipboardEvent = { diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/convertInlineCssTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/convertInlineCssTest.ts new file mode 100644 index 00000000000..89fba0c41c8 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/convertInlineCssTest.ts @@ -0,0 +1,123 @@ +import { convertInlineCss } from '../../../lib/utils/paste/convertInlineCss'; +import { CssRule } from '../../../lib/utils/paste/retrieveHtmlInfo'; + +describe('convertInlineCss', () => { + it('Empty DOM, empty CSS', () => { + const root = document.createElement('div'); + const cssRules: CssRule[] = []; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual(''); + }); + + it('Empty DOM, has CSS', () => { + const root = document.createElement('div'); + const cssRules: CssRule[] = [ + { + selectors: ['div'], + text: '{color:red;}', + }, + ]; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual(''); + }); + + it('Has CSS, has node match selector', () => { + const root = document.createElement('div'); + + root.innerHTML = 'test
test2
test3'; + + const cssRules: CssRule[] = [ + { + selectors: ['div'], + text: 'color:red;', + }, + ]; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual('test
test2
test3'); + }); + + it('Has CSS, has node match selector with existing CSS', () => { + const root = document.createElement('div'); + + root.innerHTML = 'test
test2
test3'; + + const cssRules: CssRule[] = [ + { + selectors: ['div'], + text: 'color:red;', + }, + ]; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual( + 'test
test2
test3' + ); + }); + + it('Has CSS, has node match selector with conflict CSS', () => { + const root = document.createElement('div'); + + root.innerHTML = 'test
test2
test3'; + + const cssRules: CssRule[] = [ + { + selectors: ['div'], + text: 'color:red;', + }, + ]; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual('test
test2
test3'); + }); + + it('Has multiple CSS, has node match selector with conflict CSS', () => { + const root = document.createElement('div'); + + root.innerHTML = + 'test
test2
test3test4'; + + const cssRules: CssRule[] = [ + { + selectors: ['div', '.test'], + text: 'color:red;', + }, + { + selectors: ['div'], + text: 'color:blue;', + }, + ]; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual( + 'test
test2
test3test4' + ); + }); + + it('Has multiple CSS with complex selector, has node match selector', () => { + const root = document.createElement('div'); + + root.innerHTML = '
test
test2'; + + const cssRules: CssRule[] = [ + { + selectors: ['#div1 span'], + text: 'color:red;', + }, + ]; + + convertInlineCss(root, cssRules); + + expect(root.innerHTML).toEqual( + '
test
test2' + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts new file mode 100644 index 00000000000..0d66cd3ffa7 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts @@ -0,0 +1,293 @@ +import * as moveChildNodes from 'roosterjs-content-model-dom/lib/domUtils/moveChildNodes'; +import { ClipboardData, PasteType } from 'roosterjs-content-model-types'; +import { createPasteFragment } from '../../../lib/utils/paste/createPasteFragment'; +import { expectHtml } from 'roosterjs-editor-dom/test/DomTestHelper'; + +describe('createPasteFragment', () => { + let moveChildNodesSpy: jasmine.Spy; + + beforeEach(() => { + moveChildNodesSpy = spyOn(moveChildNodes, 'moveChildNodes').and.callThrough(); + }); + + function runTest( + root: HTMLElement, + clipboardData: ClipboardData, + pasteType: PasteType, + expectedHtml: string | string[], + isMoveChildNodesCalled: boolean + ) { + const tempDiv = document.createElement('div'); + + const fragment = createPasteFragment(document, clipboardData, pasteType, root); + + tempDiv.appendChild(fragment); + + expectHtml(tempDiv.innerHTML, expectedHtml); + + if (isMoveChildNodesCalled) { + expect(moveChildNodesSpy).toHaveBeenCalledWith(fragment, root); + } else { + expect(moveChildNodesSpy).not.toHaveBeenCalled(); + } + } + + it('Empty source, paste image', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: '', + rawHtml: '', + image: null, + customValues: {}, + }, + 'asImage', + 'HTML', + true + ); + }); + + it('Has url, paste image', () => { + runTest( + document.createElement('div'), + { + types: [], + text: '', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asImage', + [ + '', + '', + ], + false + ); + }); + + it('Has url, paste normal, no text', () => { + runTest( + document.createElement('div'), + { + types: [], + text: '', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'normal', + [ + '', + '', + ], + false + ); + }); + + it('Has url, paste normal, has text', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: 'text', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'normal', + 'HTML', + true + ); + }); + + it('Has url, paste plain text, no text', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: '', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + '', + false + ); + }); + + it('Has text, paste normal', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: 'text', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'normal', + 'HTML', + true + ); + }); + + it('Has text, paste text, single line', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: 'text', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + 'text', + false + ); + }); + + it('Has text, paste text, 2 lines', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: 'line1\r\nline2', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + 'line1
line2', + false + ); + }); + + it('Has text, paste text, 3 lines', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: 'line1\r\nline2\r\nline3', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + 'line1
line2
line3', + false + ); + }); + + it('Has text, paste text, 4 lines', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: 'line1\r\nline2\r\nline3\r\nline4', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + 'line1
line2
line3
line4', + false + ); + }); + + it('Has text, paste text, 1 line, has space', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: ' line 1 ', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + '  line    1   ', + false + ); + }); + + it('Has text, paste text, 2 line, has tab', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: '\tline 1\r\n line\t2', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + '      line 1
  line      2', + false + ); + }); + + it('Has text, paste text, 2 line, has 2 tabs', () => { + const root = document.createElement('div'); + + root.innerHTML = 'HTML'; + runTest( + root, + { + types: [], + text: '1\t234\t5', + rawHtml: '', + image: null, + customValues: {}, + imageDataUri: 'test', + }, + 'asPlainText', + '1     234   5', + false + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts new file mode 100644 index 00000000000..539a8f88a23 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts @@ -0,0 +1,257 @@ +import { generatePasteOptionFromPlugins } from '../../../lib/utils/paste/generatePasteOptionFromPlugins'; +import { PasteType, PluginEventType } from 'roosterjs-editor-types'; +import { StandaloneEditorCore } from 'roosterjs-content-model-types'; + +describe('generatePasteOptionFromPlugins', () => { + let core: StandaloneEditorCore; + let triggerPluginEventSpy: jasmine.Spy; + + const mockedClipboardData = 'CLIPBOARDDATA' as any; + const mockedFragment = 'FRAGMENT' as any; + const htmlBefore = 'HTMLBEFORE'; + const htmlAfter = 'HTMLAFTER'; + const mockedMetadata = 'METADATA' as any; + const mockedCssRule = 'CSSRULE' as any; + const mockedResult = { + fragment: 'FragmentResult', + domToModelOption: 'OptionResult', + pasteType: 'TypeResult', + } as any; + const sanitizingOption: any = { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }; + + beforeEach(() => { + triggerPluginEventSpy = jasmine.createSpy('triggerPluginEvent'); + core = { + api: { + triggerEvent: triggerPluginEventSpy, + }, + } as any; + }); + + it('PasteType=Normal', () => { + let originalEvent: any; + + triggerPluginEventSpy.and.callFake((core, event) => { + originalEvent = { ...event }; + Object.assign(event, mockedResult); + }); + const result = generatePasteOptionFromPlugins( + core, + mockedClipboardData, + mockedFragment, + { + htmlAfter, + htmlBefore, + metadata: mockedMetadata, + globalCssRules: mockedCssRule, + }, + 'normal' + ); + + expect(result).toEqual({ + fragment: 'FragmentResult', + domToModelOption: 'OptionResult', + pasteType: 'TypeResult', + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + htmlBefore, + htmlAfter, + htmlAttributes: mockedMetadata, + sanitizingOption, + } as any); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); + expect(originalEvent).toEqual({ + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + fragment: mockedFragment, + htmlBefore: htmlBefore, + htmlAfter: htmlAfter, + htmlAttributes: mockedMetadata, + pasteType: PasteType.Normal, + domToModelOption: { + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + }, + sanitizingOption, + }); + expect(triggerPluginEventSpy).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + fragment: 'FragmentResult', + htmlBefore: htmlBefore, + htmlAfter: htmlAfter, + htmlAttributes: mockedMetadata, + pasteType: 'TypeResult', + domToModelOption: 'OptionResult', + sanitizingOption, + }, + true + ); + }); + + it('PasteType=asImage, return customizedMerge', () => { + const mockedCustomizedMerge = 'MERGE' as any; + + triggerPluginEventSpy.and.callFake((core, event) => { + event.fragment = 'FragmentResult'; + event.domToModelOption = 'OptionResult'; + event.pasteType = 'TypeResult'; + event.customizedMerge = mockedCustomizedMerge; + }); + + const result = generatePasteOptionFromPlugins( + core, + mockedClipboardData, + mockedFragment, + { + htmlAfter, + htmlBefore, + metadata: mockedMetadata, + globalCssRules: mockedCssRule, + }, + 'asImage' + ); + + expect(result).toEqual({ + fragment: 'FragmentResult', + domToModelOption: 'OptionResult', + pasteType: 'TypeResult', + customizedMerge: mockedCustomizedMerge, + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + htmlBefore, + htmlAfter, + htmlAttributes: mockedMetadata, + sanitizingOption, + } as any); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); + expect(triggerPluginEventSpy).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + fragment: 'FragmentResult', + htmlBefore: htmlBefore, + htmlAfter: htmlAfter, + htmlAttributes: mockedMetadata, + pasteType: 'TypeResult', + domToModelOption: 'OptionResult', + sanitizingOption, + customizedMerge: mockedCustomizedMerge, + }, + true + ); + }); + + it('PasteType=mergeFormat, no htmlBefore and htmlAfter', () => { + let originalEvent: any; + + triggerPluginEventSpy.and.callFake((core, event) => { + originalEvent = { ...event }; + Object.assign(event, mockedResult); + }); + const result = generatePasteOptionFromPlugins( + core, + mockedClipboardData, + mockedFragment, + { + metadata: mockedMetadata, + globalCssRules: mockedCssRule, + }, + 'mergeFormat' + ); + + expect(result).toEqual({ + fragment: 'FragmentResult', + domToModelOption: 'OptionResult', + pasteType: 'TypeResult', + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: mockedMetadata, + sanitizingOption, + } as any); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); + expect(triggerPluginEventSpy).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + fragment: 'FragmentResult', + htmlBefore: '', + htmlAfter: '', + htmlAttributes: mockedMetadata, + pasteType: 'TypeResult', + domToModelOption: 'OptionResult', + sanitizingOption, + }, + true + ); + expect(originalEvent).toEqual({ + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + fragment: mockedFragment, + htmlBefore: '', + htmlAfter: '', + htmlAttributes: mockedMetadata, + pasteType: PasteType.MergeFormat, + domToModelOption: { + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + }, + sanitizingOption, + }); + }); + + it('PasteType=asPlainText', () => { + triggerPluginEventSpy.and.callFake((core, event) => { + Object.assign(event, mockedResult); + }); + const result = generatePasteOptionFromPlugins( + core, + mockedClipboardData, + mockedFragment, + { + htmlAfter, + htmlBefore, + metadata: mockedMetadata, + globalCssRules: mockedCssRule, + }, + 'asPlainText' + ); + + expect(result).toEqual({ + fragment: mockedFragment, + domToModelOption: { + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + }, + pasteType: PasteType.AsPlainText, + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + htmlBefore, + htmlAfter, + htmlAttributes: mockedMetadata, + sanitizingOption, + }); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts new file mode 100644 index 00000000000..faeef5847f2 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts @@ -0,0 +1,373 @@ +import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext'; +import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; +import * as mergeModelFile from '../../../lib/publicApi/model/mergeModel'; +import { createContentModelDocument } from 'roosterjs-content-model-dom'; +import { mergePasteContent } from '../../../lib/utils/paste/mergePasteContent'; +import { PasteType } from 'roosterjs-editor-types'; +import { + ContentModelDocument, + ContentModelSegmentFormat, + FormatWithContentModelContext, + InsertPoint, +} from 'roosterjs-content-model-types'; + +describe('mergePasteContent', () => { + it('merge table', () => { + // A doc with only one table in content + const pasteModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'FromPaste', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { useBorderBox: true, borderCollapse: true }, + widths: [], + dataset: { + editingInfo: '', + }, + }, + ], + }; + + // A doc with a table, and selection marker inside of it. + const sourceModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { useBorderBox: true, borderCollapse: true }, + widths: [120, 120], + dataset: { + editingInfo: '', + }, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + }, + ], + format: {}, + }; + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(domToContentModel, 'domToContentModel').and.returnValue(pasteModel); + + const eventResult = { + pasteType: PasteType.Normal, + domToModelOption: {}, + } as any; + + const context: FormatWithContentModelContext = { + newEntities: [], + deletedEntities: [], + newImages: [], + }; + + mergePasteContent(sourceModel, context, eventResult, {}); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith(sourceModel, pasteModel, context, { + mergeFormat: 'none', + mergeTable: true, + }); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: '', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, + }, + }); + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'FromPaste', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + useBorderBox: true, + borderTop: '1px solid #ABABAB', + borderRight: '1px solid #ABABAB', + borderBottom: '1px solid #ABABAB', + borderLeft: '1px solid #ABABAB', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { useBorderBox: true, borderCollapse: true }, + widths: [120, 120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":null}', + }, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + }, + ], + format: {}, + }); + }); + + it('customized merge', () => { + const pasteModel: ContentModelDocument = createContentModelDocument(); + const sourceModel: ContentModelDocument = createContentModelDocument(); + + const customizedMerge = jasmine.createSpy('customizedMerge'); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(domToContentModel, 'domToContentModel').and.returnValue(pasteModel); + + const eventResult = { + pasteType: PasteType.Normal, + domToModelOption: {}, + customizedMerge, + } as any; + + mergePasteContent( + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + eventResult, + {} + ); + + expect(mergeModelFile.mergeModel).not.toHaveBeenCalled(); + expect(customizedMerge).toHaveBeenCalledWith(sourceModel, pasteModel); + }); + + it('Apply current format', () => { + const pasteModel: ContentModelDocument = createContentModelDocument(); + const sourceModel: ContentModelDocument = createContentModelDocument(); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(domToContentModel, 'domToContentModel').and.returnValue(pasteModel); + + const eventResult = { + pasteType: PasteType.MergeFormat, + domToModelOption: {}, + } as any; + + mergePasteContent( + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + eventResult, + {} + ); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + pasteModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeFormat: 'keepSourceEmphasisFormat', + mergeTable: false, + } + ); + }); + + it('Set pending format after merge', () => { + const pasteModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [], + }; + const targetModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: { + italic: true, + lineHeight: '1pt', + }, + text: 'test', + isSelected: true, + }, + ], + }, + ], + format: { + fontFamily: 'Tahoma', + fontSize: '11pt', + }, + }; + const insertPointFormat: ContentModelSegmentFormat = { + fontFamily: 'Arial', + }; + const insertPoint: InsertPoint = { + marker: { + format: insertPointFormat, + isSelected: true, + segmentType: 'SelectionMarker', + }, + paragraph: null!, + path: [], + }; + const mockedDomToModelContext = { + name: 'DOMTOMODELCONTEXT', + } as any; + + const domToContentModelSpy = spyOn(domToContentModel, 'domToContentModel').and.returnValue( + pasteModel + ); + const mergeModelSpy = spyOn(mergeModelFile, 'mergeModel').and.returnValue(insertPoint); + const createDomToModelContextSpy = spyOn( + createDomToModelContext, + 'createDomToModelContext' + ).and.returnValue(mockedDomToModelContext); + + const context: FormatWithContentModelContext = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; + const mockedDomToModelOptions = 'OPTION1' as any; + const mockedDefaultDomToModelOptions = 'OPTIONS3' as any; + const mockedFragment = 'FRAGMENT' as any; + + mergePasteContent( + targetModel, + context, + { + fragment: mockedFragment, + domToModelOption: mockedDefaultDomToModelOptions, + } as any, + mockedDomToModelOptions + ); + + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Arial', + fontSize: '11pt', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: '', + underline: false, + }, + }); + expect(domToContentModelSpy).toHaveBeenCalledWith(mockedFragment, mockedDomToModelContext); + expect(mergeModelSpy).toHaveBeenCalledWith(targetModel, pasteModel, context, { + mergeFormat: 'none', + mergeTable: false, + }); + expect(createDomToModelContextSpy).toHaveBeenCalledWith( + undefined, + mockedDomToModelOptions, + mockedDefaultDomToModelOptions + ); + expect(mockedDomToModelContext.segmentFormat).toEqual({ lineHeight: '1pt' }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/retrieveHtmlInfoTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/retrieveHtmlInfoTest.ts new file mode 100644 index 00000000000..dafa01821e1 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/retrieveHtmlInfoTest.ts @@ -0,0 +1,223 @@ +import { ClipboardData } from 'roosterjs-content-model-types'; +import { HtmlFromClipboard, retrieveHtmlInfo } from '../../../lib/utils/paste/retrieveHtmlInfo'; +import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; + +describe('retrieveHtmlInfo', () => { + function runTest( + rawHtml: string | null, + expectedResult: HtmlFromClipboard, + expectedClipboard: Partial, + expectedHtml: string | undefined + ) { + const doc = rawHtml === null ? null : new DOMParser().parseFromString(rawHtml, 'text/html'); + const clipboardData: Partial = { + rawHtml, + }; + + const result = retrieveHtmlInfo(doc, clipboardData); + + expect(result).toEqual(expectedResult); + expect(clipboardData).toEqual({ + rawHtml, + ...expectedClipboard, + }); + expect(doc?.body.innerHTML).toEqual(expectedHtml); + } + + it('Null doc', () => { + runTest( + null, + { + metadata: {}, + globalCssRules: [], + }, + {}, + undefined + ); + }); + + it('Empty doc', () => { + runTest( + '', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: [], + html: '', + }, + '' + ); + }); + + it('Text node only', () => { + runTest( + 'test', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: [''], + html: 'test', + }, + 'test' + ); + }); + + it('DIV and text node only', () => { + runTest( + '
test
', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: '
test
', + }, + '
test
' + ); + }); + + it('text, DIV, SPAN and comment node only', () => { + runTest( + 'test1
test2
\r\ntest4test5', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['', 'DIV', 'SPAN', ''], + html: 'test1
test2
\r\ntest4test5', + }, + 'test1
test2
\ntest4test5' + ); + }); + + it('Has start fragment only', () => { + runTest( + '
test
', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: '
test
', + }, + '
test
' + ); + }); + + it('Has end fragment only', () => { + runTest( + '
test
', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: '
test
', + }, + '
test
' + ); + }); + + it('Has fragment comments', () => { + runTest( + '
test
', + { + htmlBefore: '
', + htmlAfter: '
', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: 'test', + }, + '
test
' + ); + }); + + it('Has metadata', () => { + runTest( + '
test
', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: { a: 'b', 'c:d': 'e', f: 'g', h: 'i' }, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: + '
test
', + }, + '
test
' + ); + }); + + it('Has empty global CSS nodes', () => { + runTest( + '
test
', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: '
test
', + }, + '
test
' + ); + }); + + itChromeOnly('Has global CSS rule', () => { + runTest( + '
test
', + { + htmlBefore: '', + htmlAfter: '', + globalCssRules: [ + { + selectors: ['.a'], + text: 'color: red;', + }, + { + selectors: ['.b div', ' .c'], + text: 'font-size: 10pt;', + }, + { + selectors: ['test'], + text: 'border: none;', + }, + ], + metadata: {}, + }, + { + htmlFirstLevelChildTags: ['DIV'], + html: + '
test
', + }, + '
test
' + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/index.ts b/packages-content-model/roosterjs-content-model-dom/lib/index.ts index 44de6d5e129..d3927395b34 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/index.ts @@ -57,7 +57,6 @@ export { unwrapBlock } from './modelApi/common/unwrapBlock'; export { addSegment } from './modelApi/common/addSegment'; export { isWhiteSpacePreserved } from './modelApi/common/isWhiteSpacePreserved'; export { normalizeSingleSegment } from './modelApi/common/normalizeSegment'; -export { applySegmentFormatToElement } from './modelApi/common/applySegmentFormatToElement'; export { setParagraphNotImplicit } from './modelApi/block/setParagraphNotImplicit'; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/applySegmentFormatToElement.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/applySegmentFormatToElement.ts deleted file mode 100644 index 7a736fc6a5e..00000000000 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/applySegmentFormatToElement.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { applyFormat } from '../../modelToDom/utils/applyFormat'; -import { createModelToDomContext } from '../../modelToDom/context/createModelToDomContext'; -import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; - -/** - * Format an existing HTML element using Segment Format - * @param element The element to format - * @param format The format to apply - */ -export function applySegmentFormatToElement( - element: HTMLElement, - format: ContentModelSegmentFormat -) { - const context = createModelToDomContext(); - applyFormat(element, context.formatAppliers.segment, format, context); -} diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index 3c6fe02b923..4d32527f7c7 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -25,7 +25,7 @@ describe(ID, () => { beforeEach(() => { editor = initEditor(ID); spyOn(wordFile, 'processPastedContentFromWordDesktop').and.callThrough(); - delete clipboardData.snapshotBeforePaste; + delete clipboardData.modelBeforePaste; }); afterEach(() => { diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts index 5e28cc48be8..09b7a83e835 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts @@ -15,6 +15,7 @@ export interface ContentModelBeforePasteEventData extends BeforePasteEventData { * domToModel Options to use when creating the content model from the paste fragment */ domToModelOption: Partial; + /** * customizedMerge Customized merge function to use when merging the paste fragment into the editor */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/ClipboardData.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/ClipboardData.ts index cf5bf15ae2b..a81bfe39f26 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/parameter/ClipboardData.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/ClipboardData.ts @@ -1,3 +1,4 @@ +import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { EdgeLinkPreview } from './EdgeLinkPreview'; /** @@ -44,7 +45,7 @@ export interface ClipboardData { /** * An editor content snapshot before pasting happens. This is used for changing paste format */ - snapshotBeforePaste?: string; + modelBeforePaste?: ContentModelDocument; /** * BASE64 encoded data uri of the image if any From 2df5113c2c4e394f1b70f3cef8b600131c847e21 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 20 Dec 2023 15:08:04 -0600 Subject: [PATCH 14/64] Adjust Selection on Cut/Copy first table cell (#2287) * init * fix * address comments * address additional scenario --- .../corePlugin/ContentModelCopyPastePlugin.ts | 37 +- .../ContentModelCopyPastePluginTest.ts | 510 +++++++++++++++++- 2 files changed, 544 insertions(+), 3 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts index ed9e2d1dde6..180b3f1f15f 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts @@ -26,6 +26,10 @@ import type { IStandaloneEditor, OnNodeCreated, StandaloneEditorOptions, + ContentModelDocument, + ContentModelParagraph, + TableSelectionContext, + ContentModelSegment, } from 'roosterjs-content-model-types'; import type { IEditor, PluginWithState } from 'roosterjs-editor-types'; @@ -106,7 +110,7 @@ class ContentModelCopyPastePlugin implements PluginWithState { + if (selectionMarker) { + if (tableCtxt != tableContext && firstBlock?.segments.includes(selectionMarker)) { + firstBlock.segments.splice(firstBlock.segments.indexOf(selectionMarker), 1); + } + return true; + } + + const marker = segments?.find(segment => segment.segmentType == 'SelectionMarker'); + if (!selectionMarker && marker) { + tableContext = tableCtxt; + firstBlock = block?.blockType == 'Paragraph' ? block : undefined; + selectionMarker = marker; + } + + return false; + }); +} + function cleanUpAndRestoreSelection(tempDiv: HTMLDivElement) { tempDiv.style.backgroundColor = ''; tempDiv.style.color = ''; diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index 13feb5b58a5..ad027127cde 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -21,6 +21,7 @@ import { CopyPastePluginState, } from 'roosterjs-content-model-types'; import { + adjustSelectionForCopyCut, createContentModelCopyPastePlugin, onNodeCreated, preprocessTable, @@ -209,7 +210,7 @@ describe('ContentModelCopyPastePlugin |', () => { ); expect(createContentModelSpy).toHaveBeenCalled(); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(iterateSelectionsFile.iterateSelections).not.toHaveBeenCalled(); + expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalled(); expect(focusSpy).toHaveBeenCalled(); expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); @@ -380,7 +381,7 @@ describe('ContentModelCopyPastePlugin |', () => { // On Cut Spy expect(formatContentModelSpy).not.toHaveBeenCalled(); expect(formatResult).toBeFalsy(); - expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalledTimes(0); + expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalledTimes(1); }); }); @@ -426,6 +427,7 @@ describe('ContentModelCopyPastePlugin |', () => { }; } ); + spyOn(iterateSelectionsFile, 'iterateSelections').and.returnValue(undefined); spyOn(contentModelToDomFile, 'contentModelToDom').and.returnValue(selectionValue); triggerPluginEventSpy.and.callThrough(); @@ -746,4 +748,508 @@ describe('ContentModelCopyPastePlugin |', () => { }); }); }); + + describe('adjustSelectionForCopyCut', () => { + it('adjust the selection when selecting first cell of table', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + isImplicit: true, + }, + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asd', + format: {}, + isSelected: true, + }, + ], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [120], + dataset: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + adjustSelectionForCopyCut(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [], + format: {}, + segmentFormat: {}, + isImplicit: true, + }, + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asd', + format: {}, + isSelected: true, + }, + ], + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [120], + dataset: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }); + }); + + it('adjust the selection when selecting first cell of a table nested in another table', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + isImplicit: true, + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + ], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + widths: [120], + rows: [ + { + height: 44, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + widths: [116.4000015258789], + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'Test', + segmentType: 'Text', + isSelected: true, + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: {}, + dataset: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + adjustSelectionForCopyCut(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + isImplicit: true, + segments: [], + segmentFormat: {}, + blockType: 'Paragraph', + format: {}, + }, + { + widths: [120], + rows: [ + { + height: 44, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + widths: jasmine.anything() as any, + rows: [ + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'Test', + segmentType: 'Text', + isSelected: true, + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: {}, + dataset: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: {}, + }, + { + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }); + }); + + it('Adjust selection starting at last cell with no text and finishing on text after table', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + widths: [120.00000762939453], + rows: [ + { + height: 22.000001907348633, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'asd', + segmentType: 'Text', + format: {}, + }, + { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: {}, + dataset: {}, + }, + { + segments: [ + { + text: 'asd', + segmentType: 'Text', + format: {}, + isSelected: true, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }; + + adjustSelectionForCopyCut(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + widths: [120.00000762939453], + rows: [ + { + height: 22.000001907348633, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + text: 'asd', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: {}, + dataset: {}, + }, + { + segments: [ + { + text: 'asd', + segmentType: 'Text', + format: {}, + isSelected: true, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }); + }); + + it('Do not adjust when it is not needed', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdsadsada', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'sdsad', + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + + adjustSelectionForCopyCut(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdsadsada', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'sdsad', + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }); + }); + }); }); From 7c91c0d129a3bfbc78a6275fdacd4b6a4648dd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 20 Dec 2023 18:30:09 -0300 Subject: [PATCH 15/64] adjust space for underline --- .../selection/adjustTrailingSpaceSelection.ts | 32 ++++++------ .../lib/publicApi/segment/toggleUnderline.ts | 5 +- .../publicApi/segment/toggleUnderlineTest.ts | 50 +++++++++++++++++++ 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts index a19fb05038b..47d0eafce2d 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts @@ -10,19 +10,17 @@ import type { * @internal */ export function adjustTrailingSpaceSelection(model: ContentModelDocument) { - iterateSelections(model, (_, __, block, segments) => { - if (block?.blockType == 'Paragraph') { - const tempSegments = [...block.segments]; - tempSegments?.forEach((segment, index) => { - if ( - segment.isSelected && - segment.segmentType == 'Text' && - hasTrailingSpace(segment.text) && - !isTrailingSpace(segment.text) - ) { - splitTextSegment(block.segments, segment, index); - } - }); + iterateSelections(model, (_, __, block) => { + if (block?.blockType == 'Paragraph' && block.segments.length == 1) { + const segment = block.segments[0]; + if ( + segment.isSelected && + segment.segmentType == 'Text' && + hasTrailingSpace(segment.text) && + !isTrailingSpace(segment.text) + ) { + splitTextSegment(block.segments, segment); + } } return true; }); @@ -33,13 +31,12 @@ function hasTrailingSpace(text: string) { } function isTrailingSpace(text: string) { - return text.length > 0 && text.trimRight().length == 0; + return text.trimRight().length == 0; } function splitTextSegment( segments: ContentModelSegment[], - textSegment: Readonly, - index: number + textSegment: Readonly ) { const text = textSegment.text.trimRight(); const trailingSpace = textSegment.text.substring(text.length); @@ -60,6 +57,5 @@ function splitTextSegment( trailingSpaceLink, textSegment.code ); - trailingSpaceSegment.isSelected = true; - segments.splice(index, 1, newText, trailingSpaceSegment); + segments.splice(0, 1, newText, trailingSpaceSegment); } diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts index 8614fdd7b1d..b58ef453783 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts @@ -1,3 +1,4 @@ +import { adjustTrailingSpaceSelection } from '../../modelApi/selection/adjustTrailingSpaceSelection'; import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; import type { IStandaloneEditor } from 'roosterjs-content-model-types'; @@ -18,6 +19,8 @@ export default function toggleUnderline(editor: IStandaloneEditor) { segment.link.format.underline = !!isTurningOn; } }, - (format, segment) => !!format.underline || !!segment?.link?.format?.underline + (format, segment) => !!format.underline || !!segment?.link?.format?.underline, + false /*includingFormatHolder*/, + adjustTrailingSpaceSelection ); } diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleUnderlineTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleUnderlineTest.ts index ca0b7e65c77..210971c9eb3 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleUnderlineTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleUnderlineTest.ts @@ -470,4 +470,54 @@ describe('toggleUnderline', () => { 1 ); }); + + it('Turn on underline with trailing space', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test ', + format: {}, + isSelected: true, + }, + ], + format: {}, + }, + ], + format: {}, + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: { + underline: true, + }, + isSelected: true, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + isSelected: true, + }, + ], + format: {}, + }, + ], + format: {}, + }, + 1 + ); + }); }); From f6845de308d90b5ca868720f032f79c0df4e0e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 20 Dec 2023 18:31:55 -0300 Subject: [PATCH 16/64] adjust space for underline --- .../lib/modelApi/selection/adjustTrailingSpaceSelection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts index 47d0eafce2d..e3873234181 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts @@ -57,5 +57,6 @@ function splitTextSegment( trailingSpaceLink, textSegment.code ); + trailingSpaceSegment.isSelected = true; segments.splice(0, 1, newText, trailingSpaceSegment); } From f80a03c6f816b479a89903fa3284e1e7e4f8f48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 20 Dec 2023 19:04:36 -0300 Subject: [PATCH 17/64] fix multiple blocks --- .../selection/adjustTrailingSpaceSelection.ts | 3 +- .../adjustTrailingSpaceSelectionTest.ts | 62 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts index e3873234181..a1f761b89b3 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts @@ -22,7 +22,8 @@ export function adjustTrailingSpaceSelection(model: ContentModelDocument) { splitTextSegment(block.segments, segment); } } - return true; + + return false; }); } diff --git a/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts index 4c29b94d71f..72f65062dc4 100644 --- a/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts @@ -1,6 +1,11 @@ -import { addSegment, createContentModelDocument, createText } from 'roosterjs-content-model-dom'; import { adjustTrailingSpaceSelection } from '../../../lib/modelApi/selection/adjustTrailingSpaceSelection'; import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { + addSegment, + createContentModelDocument, + createParagraph, + createText, +} from 'roosterjs-content-model-dom'; describe('adjustTrailingSpaceSelection', () => { function runTest(model: ContentModelDocument, modelResult: ContentModelDocument) { @@ -46,4 +51,59 @@ describe('adjustTrailingSpaceSelection', () => { ], }); }); + + it('trailing space multiple blocks', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + const text = createText('text '); + text.isSelected = true; + paragraph.segments.push(text); + const paragraph2 = createParagraph(); + const text2 = createText('text2 '); + text2.isSelected = true; + paragraph2.segments.push(text2); + model.blocks.push(paragraph, paragraph2); + + runTest(model, { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'text', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: ' ', + isSelected: true, + }, + ], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'text2', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: ' ', + isSelected: true, + }, + ], + }, + ], + }); + }); }); From 6bf01190cd8d61b67509bf6cc1fba27a549a5722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 21 Dec 2023 11:42:57 -0300 Subject: [PATCH 18/64] fixes --- .../selection/adjustTrailingSpaceSelection.ts | 45 +++++---- .../adjustTrailingSpaceSelectionTest.ts | 91 +++++++++++++++++++ 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts index a1f761b89b3..f6d93201843 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustTrailingSpaceSelection.ts @@ -2,24 +2,35 @@ import { createText } from 'roosterjs-content-model-dom'; import { iterateSelections } from 'roosterjs-content-model-core'; import type { ContentModelDocument, - ContentModelSegment, + ContentModelParagraph, ContentModelText, } 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) { - iterateSelections(model, (_, __, block) => { - if (block?.blockType == 'Paragraph' && block.segments.length == 1) { - const segment = block.segments[0]; + iterateSelections(model, (_, __, block, segments) => { + if (block?.blockType === 'Paragraph' && segments && segments.length > 0) { if ( - segment.isSelected && - segment.segmentType == 'Text' && - hasTrailingSpace(segment.text) && - !isTrailingSpace(segment.text) + segments.length === 1 && + segments[0].segmentType === 'Text' && + shouldSplitTrailingSpace(segments[0]) ) { - splitTextSegment(block.segments, segment); + splitTextSegment(block, segments[0]); + } else { + const lastTextSegment = + segments[segments.length - 1].segmentType === 'SelectionMarker' + ? segments[segments.length - 2] + : segments[segments.length - 1]; + if ( + lastTextSegment && + lastTextSegment.segmentType === 'Text' && + shouldSplitTrailingSpace(lastTextSegment) + ) { + splitTextSegment(block, lastTextSegment); + } } } @@ -27,18 +38,19 @@ export function adjustTrailingSpaceSelection(model: ContentModelDocument) { }); } +function shouldSplitTrailingSpace(segment: ContentModelText) { + return segment.isSelected && hasTrailingSpace(segment.text) && !isTrailingSpace(segment.text); +} + function hasTrailingSpace(text: string) { - return text.length > 0 && text.trimRight().length < text.length; + return text.trimRight() !== text; } function isTrailingSpace(text: string) { return text.trimRight().length == 0; } -function splitTextSegment( - segments: ContentModelSegment[], - textSegment: Readonly -) { +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); @@ -48,7 +60,7 @@ function splitTextSegment( ...textSegment.link, format: { ...textSegment.link?.format, - underline: false, + underline: false, // Remove underline for trailing space link }, } : undefined; @@ -59,5 +71,6 @@ function splitTextSegment( textSegment.code ); trailingSpaceSegment.isSelected = true; - segments.splice(0, 1, newText, trailingSpaceSegment); + const index = block.segments.indexOf(textSegment); + block.segments.splice(index, 1, newText, trailingSpaceSegment); } diff --git a/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts index 72f65062dc4..2b090a60fd1 100644 --- a/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustTrailingSpaceSelectionTest.ts @@ -4,6 +4,7 @@ import { addSegment, createContentModelDocument, createParagraph, + createSelectionMarker, createText, } from 'roosterjs-content-model-dom'; @@ -106,4 +107,94 @@ describe('adjustTrailingSpaceSelection', () => { ], }); }); + + it('trailing space multiple segments', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + const text = createText('text '); + text.isSelected = true; + const text2 = createText('text2 '); + text2.isSelected = true; + paragraph.segments.push(text, text2); + model.blocks.push(paragraph); + + runTest(model, { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'text ', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: 'text2', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: ' ', + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('trailing space multiple segments and selection marker', () => { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + const text = createText('text '); + text.isSelected = true; + const text2 = createText('text2 '); + text2.isSelected = true; + const marker = createSelectionMarker(); + marker.isSelected = true; + paragraph.segments.push(text, text2, marker); + model.blocks.push(paragraph); + + runTest(model, { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'text ', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: 'text2', + isSelected: true, + }, + { + segmentType: 'Text', + format: {}, + text: ' ', + isSelected: true, + }, + { + segmentType: 'SelectionMarker', + format: {}, + + isSelected: true, + }, + ], + }, + ], + }); + }); }); From 462c62602794d294226d40a388d9e4399b64ac96 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 21 Dec 2023 12:26:05 -0800 Subject: [PATCH 19/64] Standalone Editor: Port paste API step 4 (#2282) * Standalone Editor: Port paste API step 1 * Standalone Editor: Port paste API step 2 * Standalone Editor: Port paste API step 3 * Standalone Editor: Port paste API step 4 * improve * improve * fix test --- .../lib/coreApi/paste.ts | 5 - .../lib/override/pasteDisplayFormatParser.ts | 12 + .../lib/override/pasteEntityProcessor.ts | 27 ++ .../lib/override/pasteGeneralProcessor.ts | 46 ++ .../paste/generatePasteOptionFromPlugins.ts | 6 +- .../lib/utils/paste/mergePasteContent.ts | 12 + .../lib/utils/sanitizeElement.ts | 398 ++++++++++++++++++ .../overrides/pasteDisplayFormatParserTest.ts | 57 +++ .../overrides/pasteEntityProcessorTest.ts | 80 ++++ .../overrides/pasteGeneralProcessorTest.ts | 145 +++++++ .../generatePasteOptionFromPluginsTest.ts | 6 + .../test/utils/paste/mergePasteContentTest.ts | 30 +- .../test/utils/sanitizeElementTest.ts | 260 ++++++++++++ .../lib/domUtils/entityUtils.ts | 13 - .../roosterjs-content-model-dom/lib/index.ts | 1 - .../lib/paste/ContentModelPastePlugin.ts | 26 +- .../lib/paste/WacComponents/constants.ts | 19 - .../processPastedContentWacComponents.ts | 2 - .../processPastedContentFromWordDesktop.ts | 18 - .../test/paste/ContentModelPastePluginTest.ts | 23 +- .../test/paste/e2e/cmPasteFromWordTest.ts | 8 +- .../paste/processPastedContentFromWacTest.ts | 12 +- ...processPastedContentFromWordDesktopTest.ts | 2 +- .../lib/event/ContentModelBeforePasteEvent.ts | 33 +- .../lib/index.ts | 2 + 25 files changed, 1128 insertions(+), 115 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/override/pasteDisplayFormatParser.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/pasteDisplayFormatParserTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts index 8dd60207aca..1d0505b1bd0 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts @@ -5,7 +5,6 @@ import { createPasteFragment } from '../utils/paste/createPasteFragment'; import { generatePasteOptionFromPlugins } from '../utils/paste/generatePasteOptionFromPlugins'; import { mergePasteContent } from '../utils/paste/mergePasteContent'; import { retrieveHtmlInfo } from '../utils/paste/retrieveHtmlInfo'; -import { sanitizePasteContent } from 'roosterjs-editor-dom'; import type { CloneModelOptions } from '../publicApi/model/cloneModel'; import type { PasteType, @@ -71,10 +70,6 @@ export const paste: Paste = ( // 5. Convert global CSS to inline CSS convertInlineCss(eventResult.fragment, htmlFromClipboard.globalCssRules); - // Sanitize the fragment before paste to make sure the content is safe - // TODO: remove this part - sanitizePasteContent(eventResult, null /*position*/); - // 6. Merge pasted content into main Content Model mergePasteContent(model, context, eventResult, core.domToModelSettings.customized); diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/pasteDisplayFormatParser.ts b/packages-content-model/roosterjs-content-model-core/lib/override/pasteDisplayFormatParser.ts new file mode 100644 index 00000000000..aae54d4a164 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/override/pasteDisplayFormatParser.ts @@ -0,0 +1,12 @@ +import type { DisplayFormat, FormatParser } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export const pasteDisplayFormatParser: FormatParser = (format, element) => { + const display = element.style.display; + + if (display && display != 'flex') { + format.display = display; + } +}; diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts b/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts new file mode 100644 index 00000000000..67df4b70e71 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts @@ -0,0 +1,27 @@ +import { + AllowedTags, + DisallowedTags, + removeStyle, + sanitizeElement, +} from '../utils/sanitizeElement'; +import type { DomToModelOptionForPaste, ElementProcessor } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function createPasteEntityProcessor( + options: DomToModelOptionForPaste +): ElementProcessor { + const allowedTags = AllowedTags.concat(options.additionalAllowedTags); + const disallowedTags = DisallowedTags.concat(options.additionalDisallowedTags); + + return (group, element, context) => { + const sanitizedElement = sanitizeElement(element, allowedTags, disallowedTags, { + position: removeStyle, + }); + + if (sanitizedElement) { + context.defaultElementProcessors.entity(group, sanitizedElement, context); + } + }; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts b/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts new file mode 100644 index 00000000000..071726bd940 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts @@ -0,0 +1,46 @@ +import { moveChildNodes } from 'roosterjs-content-model-dom'; +import { + AllowedTags, + createSanitizedElement, + DisallowedTags, + removeDisplayFlex, + removeStyle, +} from '../utils/sanitizeElement'; +import type { DomToModelOptionForPaste, ElementProcessor } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function createPasteGeneralProcessor( + options: DomToModelOptionForPaste +): ElementProcessor { + const allowedTags = AllowedTags.concat(options.additionalAllowedTags); + const disallowedTags = DisallowedTags.concat(options.additionalDisallowedTags); + + return (group, element, context) => { + const tag = element.tagName.toLowerCase(); + const processor = + allowedTags.indexOf(tag) >= 0 + ? internalGeneralProcessor + : disallowedTags.indexOf(tag) >= 0 + ? undefined // Ignore those disallowed tags + : context.defaultElementProcessors.span; // For other unknown tags, treat them as SPAN + + processor?.(group, element, context); + }; +} + +const internalGeneralProcessor: ElementProcessor = (group, element, context) => { + const sanitizedElement = createSanitizedElement( + element.ownerDocument, + element.tagName, + element.attributes, + { + position: removeStyle, + display: removeDisplayFlex, + } + ); + + moveChildNodes(sanitizedElement, element); + context.defaultElementProcessors['*']?.(group, sanitizedElement, context); +}; diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts index 6654303d8c6..ec156e42079 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts @@ -3,7 +3,7 @@ import type { HtmlFromClipboard } from './retrieveHtmlInfo'; import type { ClipboardData, ContentModelBeforePasteEvent, - DomToModelOption, + DomToModelOptionForPaste, PasteType, StandaloneEditorCore, } from 'roosterjs-content-model-types'; @@ -27,7 +27,9 @@ export function generatePasteOptionFromPlugins( htmlFromClipboard: HtmlFromClipboard, pasteType: PasteType ): ContentModelBeforePasteEvent { - const domToModelOption: DomToModelOption = { + const domToModelOption: DomToModelOptionForPaste = { + additionalAllowedTags: [], + additionalDisallowedTags: [], additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts index e9dd72004ec..ba1b6c5cde1 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts @@ -1,7 +1,10 @@ import { createDomToModelContext, domToContentModel } from 'roosterjs-content-model-dom'; +import { createPasteEntityProcessor } from '../../override/pasteEntityProcessor'; +import { createPasteGeneralProcessor } from '../../override/pasteGeneralProcessor'; import { getSegmentTextFormat } from '../../publicApi/domUtils/getSegmentTextFormat'; import { getSelectedSegments } from '../../publicApi/selection/collectSelections'; import { mergeModel } from '../../publicApi/model/mergeModel'; +import { pasteDisplayFormatParser } from '../../override/pasteDisplayFormatParser'; import { PasteType } from 'roosterjs-editor-types'; import type { MergeModelOption } from '../../publicApi/model/mergeModel'; import type { @@ -40,6 +43,15 @@ export function mergePasteContent( const domToModelContext = createDomToModelContext( undefined /*editorContext*/, defaultDomToModelOptions, + { + processorOverride: { + entity: createPasteEntityProcessor(domToModelOption), + '*': createPasteGeneralProcessor(domToModelOption), + }, + formatParserOverride: { + display: pasteDisplayFormatParser, + }, + }, domToModelOption ); diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts new file mode 100644 index 00000000000..a99dc177b53 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts @@ -0,0 +1,398 @@ +import { isNodeOfType } from 'roosterjs-content-model-dom'; + +/** + * @internal + */ +export const AllowedTags: ReadonlyArray = [ + 'a', + 'abbr', + 'address', + 'area', + 'article', + 'aside', + 'b', + 'bdi', + 'bdo', + 'blockquote', + 'body', + 'br', + 'button', + 'canvas', + 'caption', + 'center', + 'cite', + 'code', + 'col', + 'colgroup', + 'data', + 'datalist', + 'dd', + 'del', + 'details', + 'dfn', + 'dialog', + 'dir', + 'div', + 'dl', + 'dt', + 'em', + 'fieldset', + 'figcaption', + 'figure', + 'font', + 'footer', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'header', + 'hgroup', + 'hr', + 'html', + 'i', + 'img', + 'input', + 'ins', + 'kbd', + 'label', + 'legend', + 'li', + 'main', + 'map', + 'mark', + 'menu', + 'menuitem', + 'meter', + 'nav', + 'ol', + 'optgroup', + 'option', + 'output', + 'p', + 'picture', + 'pre', + 'progress', + 'q', + 'rp', + 'rt', + 'ruby', + 's', + 'samp', + 'section', + 'select', + 'small', + 'span', + 'strike', + 'strong', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'time', + 'tr', + 'tt', + 'u', + 'ul', + 'var', + 'wbr', + 'xmp', +]; + +/** + * @internal + */ +export const DisallowedTags: ReadonlyArray = [ + 'applet', + 'audio', + 'base', + 'basefont', + 'embed', + 'frame', + 'frameset', + 'iframe', + 'link', + 'meta', + 'noscript', + 'object', + 'param', + 'script', + 'slot', + 'source', + 'style', + 'template', + 'title', + 'track', + 'video', +]; + +const VARIABLE_REGEX = /^\s*var\(\s*[a-zA-Z0-9-_]+\s*(,\s*(.*))?\)\s*$/; +const VARIABLE_PREFIX = 'var('; + +const AllowedAttributes = [ + 'accept', + 'align', + 'alt', + 'checked', + 'cite', + 'class', + 'color', + 'cols', + 'colspan', + 'contextmenu', + 'coords', + 'datetime', + 'default', + 'dir', + 'dirname', + 'disabled', + 'download', + 'face', + 'headers', + 'height', + 'hidden', + 'high', + 'href', + 'hreflang', + 'ismap', + 'kind', + 'label', + 'lang', + 'list', + 'low', + 'max', + 'maxlength', + 'media', + 'min', + 'multiple', + 'open', + 'optimum', + 'pattern', + 'placeholder', + 'readonly', + 'rel', + 'required', + 'reversed', + 'rows', + 'rowspan', + 'scope', + 'selected', + 'shape', + 'size', + 'sizes', + 'span', + 'spellcheck', + 'src', + 'srclang', + 'srcset', + 'start', + 'step', + 'style', + 'tabindex', + 'target', + 'title', + 'translate', + 'type', + 'usemap', + 'valign', + 'value', + 'width', + 'wrap', + 'bgColor', +]; + +const DefaultStyleValue: { [name: string]: string } = { + 'background-color': 'transparent', + 'border-bottom-color': 'rgb(0, 0, 0)', + 'border-bottom-style': 'none', + 'border-bottom-width': '0px', + 'border-image-outset': '0', + 'border-image-repeat': 'stretch', + 'border-image-slice': '100%', + 'border-image-source': 'none', + 'border-image-width': '1', + 'border-left-color': 'rgb(0, 0, 0)', + 'border-left-style': 'none', + 'border-left-width': '0px', + 'border-right-color': 'rgb(0, 0, 0)', + 'border-right-style': 'none', + 'border-right-width': '0px', + 'border-top-color': 'rgb(0, 0, 0)', + 'border-top-style': 'none', + 'border-top-width': '0px', + 'outline-color': 'transparent', + 'outline-style': 'none', + 'outline-width': '0px', + overflow: 'visible', + '-webkit-text-stroke-width': '0px', + 'word-wrap': 'break-word', + 'margin-left': '0px', + 'margin-right': '0px', + padding: '0px', + 'padding-top': '0px', + 'padding-left': '0px', + 'padding-right': '0px', + 'padding-bottom': '0px', + border: '0px', + 'border-top': '0px', + 'border-left': '0px', + 'border-right': '0px', + 'border-bottom': '0px', + 'vertical-align': 'baseline', + float: 'none', + 'font-style': 'normal', + 'font-variant-ligatures': 'normal', + 'font-variant-caps': 'normal', + 'font-weight': '400', + 'letter-spacing': 'normal', + orphans: '2', + 'text-align': 'start', + 'text-indent': '0px', + 'text-transform': 'none', + widows: '2', + 'word-spacing': '0px', + 'white-space': 'normal', +}; + +/** + * @internal + */ +export function sanitizeElement( + element: HTMLElement, + allowedTags: ReadonlyArray, + disallowedTags: ReadonlyArray, + styleCallbacks?: Record string | null> +): HTMLElement | null { + const tag = element.tagName.toLowerCase(); + const sanitizedElement = + disallowedTags.indexOf(tag) >= 0 + ? null + : createSanitizedElement( + element.ownerDocument, + allowedTags.indexOf(tag) >= 0 ? tag : 'span', + element.attributes, + styleCallbacks + ); + + if (sanitizedElement) { + for (let child = element.firstChild; child; child = child.nextSibling) { + const newChild = isNodeOfType(child, 'ELEMENT_NODE') + ? sanitizeElement(child, allowedTags, disallowedTags, styleCallbacks) + : isNodeOfType(child, 'TEXT_NODE') + ? child.cloneNode() + : null; + + if (newChild) { + sanitizedElement?.appendChild(newChild); + } + } + } + + return sanitizedElement; +} + +/** + * @internal + */ +export function createSanitizedElement( + doc: Document, + tag: string, + attributes: NamedNodeMap, + styleCallbacks?: Record string | null> +): HTMLElement { + const element = doc.createElement(tag); + + for (let i = 0; i < attributes.length; i++) { + const attribute = attributes[i]; + const name = attribute.name.toLowerCase().trim(); + const value = attribute.value; + + const newValue = + name == 'style' + ? processStyles(tag, value, styleCallbacks) + : AllowedAttributes.indexOf(name) >= 0 || name.indexOf('data-') == 0 + ? value + : null; + + if ( + newValue !== null && + newValue !== undefined && + !newValue.match(/s\n*c\n*r\n*i\n*p\n*t\n*:/i) // match script: with any NewLine inside. Browser will ignore those NewLine char and still treat it as script prefix + ) { + element.setAttribute(name, newValue); + } + } + + return element; +} + +/** + * @internal + */ +export function removeStyle(): string | null { + return null; +} + +/** + * @internal + */ +export function removeDisplayFlex(value: string) { + return value == 'flex' ? null : value; +} + +function processStyles( + tagName: string, + value: string, + styleCallbacks?: Record string | null> +) { + const pairs = value.split(';'); + const result: string[] = []; + + pairs.forEach(pair => { + const valueIndex = pair.indexOf(':'); + const name = pair.slice(0, valueIndex).trim(); + let value: string | null = pair.slice(valueIndex + 1).trim(); + + if (name && value) { + if (isCssVariable(value)) { + value = processCssVariable(value); + } + + const callback = styleCallbacks?.[name]; + + if (callback) { + value = callback(value, tagName); + } + + if ( + !!value && + value != 'inherit' && + value != 'initial' && + value.indexOf('expression') < 0 && + !name.startsWith('-') && + DefaultStyleValue[name] != value + ) { + result.push(`${name}:${value}`); + } + } + }); + + return result.join(';'); +} + +function processCssVariable(value: string): string { + const match = VARIABLE_REGEX.exec(value); + return match?.[2] || ''; // Without fallback value, we don't know what does the original value mean, so ignore it +} + +function isCssVariable(value: string): boolean { + return value.indexOf(VARIABLE_PREFIX) == 0; +} diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteDisplayFormatParserTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteDisplayFormatParserTest.ts new file mode 100644 index 00000000000..6fc53b53f57 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteDisplayFormatParserTest.ts @@ -0,0 +1,57 @@ +import { DisplayFormat } from 'roosterjs-content-model-types'; +import { pasteDisplayFormatParser } from '../../lib/override/pasteDisplayFormatParser'; + +describe('pasteDisplayFormatParser', () => { + it('no display', () => { + const div = document.createElement('div'); + const format: DisplayFormat = {}; + const context: any = {}; + + pasteDisplayFormatParser(format, div, context, {}); + + expect(format).toEqual({}); + }); + + it('display: block', () => { + const div = document.createElement('div'); + + div.style.display = 'block'; + + const format: DisplayFormat = {}; + const context: any = {}; + + pasteDisplayFormatParser(format, div, context, {}); + + expect(format).toEqual({ + display: 'block', + }); + }); + + it('display: inline', () => { + const div = document.createElement('div'); + + div.style.display = 'inline'; + + const format: DisplayFormat = {}; + const context: any = {}; + + pasteDisplayFormatParser(format, div, context, {}); + + expect(format).toEqual({ + display: 'inline', + }); + }); + + it('display: flex', () => { + const div = document.createElement('div'); + + div.style.display = 'flex'; + + const format: DisplayFormat = {}; + const context: any = {}; + + pasteDisplayFormatParser(format, div, context, {}); + + expect(format).toEqual({}); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts new file mode 100644 index 00000000000..5caf4aff1c7 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts @@ -0,0 +1,80 @@ +import * as sanitizeElement from '../../lib/utils/sanitizeElement'; +import { ContentModelDocument, DomToModelContext } from 'roosterjs-content-model-types'; +import { createContentModelDocument } from 'roosterjs-content-model-dom'; +import { createPasteEntityProcessor } from '../../lib/override/pasteEntityProcessor'; + +describe('pasteEntityProcessor', () => { + let sanitizeElementSpy: jasmine.Spy; + let entityProcessorSpy: jasmine.Spy; + let context: DomToModelContext; + let sanitizedElement: HTMLElement | undefined; + + beforeEach(() => { + sanitizeElementSpy = spyOn(sanitizeElement, 'sanitizeElement'); + entityProcessorSpy = jasmine + .createSpy('entityProcessor') + .and.callFake((_: ContentModelDocument, element: HTMLElement) => { + sanitizedElement = element; + }); + + context = { + defaultElementProcessors: { + entity: entityProcessorSpy, + }, + } as any; + + sanitizedElement = undefined; + }); + + it('Empty element', () => { + const element = document.createElement('div'); + const group = createContentModelDocument(); + const pasteEntityProcessor = createPasteEntityProcessor({ + additionalAllowedTags: [], + additionalDisallowedTags: [], + } as any); + + sanitizeElementSpy.and.returnValue(element); + + pasteEntityProcessor(group, element, context); + + expect(sanitizedElement?.outerHTML).toEqual('
'); + expect(sanitizeElementSpy).toHaveBeenCalledTimes(1); + expect(sanitizeElementSpy).toHaveBeenCalledWith( + element, + sanitizeElement.AllowedTags, + sanitizeElement.DisallowedTags, + { + position: sanitizeElement.removeStyle, + } + ); + expect(entityProcessorSpy).toHaveBeenCalledTimes(1); + expect(entityProcessorSpy).toHaveBeenCalledWith(group, sanitizedElement, context); + }); + + it('Empty element with allowed and disallowed tags', () => { + const element = document.createElement('div'); + const group = createContentModelDocument(); + const pasteEntityProcessor = createPasteEntityProcessor({ + additionalAllowedTags: ['allowed'], + additionalDisallowedTags: ['disallowed'], + } as any); + + sanitizeElementSpy.and.returnValue(element); + + pasteEntityProcessor(group, element, context); + + expect(sanitizedElement?.outerHTML).toEqual('
'); + expect(sanitizeElementSpy).toHaveBeenCalledTimes(1); + expect(sanitizeElementSpy).toHaveBeenCalledWith( + element, + sanitizeElement.AllowedTags.concat('allowed'), + sanitizeElement.DisallowedTags.concat('disallowed'), + { + position: sanitizeElement.removeStyle, + } + ); + expect(entityProcessorSpy).toHaveBeenCalledTimes(1); + expect(entityProcessorSpy).toHaveBeenCalledWith(group, sanitizedElement, context); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts new file mode 100644 index 00000000000..dc4064577f9 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts @@ -0,0 +1,145 @@ +import * as sanitizeElement from '../../lib/utils/sanitizeElement'; +import { createContentModelDocument } from 'roosterjs-content-model-dom'; +import { createPasteGeneralProcessor } from '../../lib/override/pasteGeneralProcessor'; +import { DomToModelContext } from 'roosterjs-content-model-types'; + +describe('pasteGeneralProcessor', () => { + let createSanitizedElementSpy: jasmine.Spy; + let generalProcessorSpy: jasmine.Spy; + let spanProcessorSpy: jasmine.Spy; + let context: DomToModelContext; + + beforeEach(() => { + createSanitizedElementSpy = spyOn(sanitizeElement, 'createSanitizedElement'); + generalProcessorSpy = jasmine.createSpy('entityProcessor'); + spanProcessorSpy = jasmine.createSpy('spanProcessorSpy'); + + context = { + defaultElementProcessors: { + '*': generalProcessorSpy, + span: spanProcessorSpy, + }, + } as any; + }); + + it('Empty element, DIV', () => { + const element = document.createElement('div'); + const group = createContentModelDocument(); + const pasteGeneralProcessor = createPasteGeneralProcessor({ + additionalAllowedTags: [], + additionalDisallowedTags: [], + } as any); + + createSanitizedElementSpy.and.returnValue(element); + + pasteGeneralProcessor(group, element, context); + + expect(createSanitizedElementSpy).toHaveBeenCalledTimes(1); + expect(createSanitizedElementSpy).toHaveBeenCalledWith( + document, + 'DIV', + element.attributes, + { + position: sanitizeElement.removeStyle, + display: sanitizeElement.removeDisplayFlex, + } + ); + expect(generalProcessorSpy).toHaveBeenCalledTimes(1); + expect(generalProcessorSpy).toHaveBeenCalledWith(group, element, context); + expect(spanProcessorSpy).toHaveBeenCalledTimes(0); + }); + + it('Empty element with unrecognized tag', () => { + const element = document.createElement('test'); + const group = createContentModelDocument(); + const pasteGeneralProcessor = createPasteGeneralProcessor({ + additionalAllowedTags: [], + additionalDisallowedTags: [], + } as any); + + createSanitizedElementSpy.and.returnValue(element); + + pasteGeneralProcessor(group, element, context); + + expect(createSanitizedElementSpy).toHaveBeenCalledTimes(0); + expect(generalProcessorSpy).toHaveBeenCalledTimes(0); + expect(spanProcessorSpy).toHaveBeenCalledTimes(1); + expect(spanProcessorSpy).toHaveBeenCalledWith(group, element, context); + }); + + it('Empty element with unrecognized in allowed list', () => { + const element = document.createElement('test'); + const group = createContentModelDocument(); + const pasteGeneralProcessor = createPasteGeneralProcessor({ + additionalAllowedTags: ['test'], + additionalDisallowedTags: [], + } as any); + + createSanitizedElementSpy.and.returnValue(element); + + pasteGeneralProcessor(group, element, context); + + expect(createSanitizedElementSpy).toHaveBeenCalledTimes(1); + expect(createSanitizedElementSpy).toHaveBeenCalledWith( + document, + 'TEST', + element.attributes, + { + position: sanitizeElement.removeStyle, + display: sanitizeElement.removeDisplayFlex, + } + ); + expect(generalProcessorSpy).toHaveBeenCalledTimes(1); + expect(generalProcessorSpy).toHaveBeenCalledWith(group, element, context); + expect(spanProcessorSpy).toHaveBeenCalledTimes(0); + }); + + it('Empty element with unrecognized in disallowed list', () => { + const element = document.createElement('test'); + const group = createContentModelDocument(); + const pasteGeneralProcessor = createPasteGeneralProcessor({ + additionalAllowedTags: [], + additionalDisallowedTags: ['test'], + } as any); + + createSanitizedElementSpy.and.returnValue(element); + + pasteGeneralProcessor(group, element, context); + + expect(createSanitizedElementSpy).toHaveBeenCalledTimes(0); + expect(generalProcessorSpy).toHaveBeenCalledTimes(0); + expect(spanProcessorSpy).toHaveBeenCalledTimes(0); + }); + + it('Element with display:flex', () => { + const element = document.createElement('div'); + + element.style.display = 'flex'; + + const group = createContentModelDocument(); + const pasteGeneralProcessor = createPasteGeneralProcessor({ + additionalAllowedTags: [], + additionalDisallowedTags: ['test'], + } as any); + + createSanitizedElementSpy.and.callThrough(); + + pasteGeneralProcessor(group, element, context); + + expect(createSanitizedElementSpy).toHaveBeenCalledTimes(1); + expect(createSanitizedElementSpy).toHaveBeenCalledWith( + document, + 'DIV', + element.attributes, + { + position: sanitizeElement.removeStyle, + display: sanitizeElement.removeDisplayFlex, + } + ); + expect(generalProcessorSpy).toHaveBeenCalledTimes(1); + expect((generalProcessorSpy.calls.argsFor(0)[1] as any).outerHTML).toEqual( + '
' + ); + expect(spanProcessorSpy).toHaveBeenCalledTimes(0); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts index 539a8f88a23..071277027e8 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts @@ -81,6 +81,8 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: PasteType.Normal, domToModelOption: { + additionalAllowedTags: [], + additionalDisallowedTags: [], additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, @@ -212,6 +214,8 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: PasteType.MergeFormat, domToModelOption: { + additionalAllowedTags: [], + additionalDisallowedTags: [], additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, @@ -240,6 +244,8 @@ describe('generatePasteOptionFromPlugins', () => { expect(result).toEqual({ fragment: mockedFragment, domToModelOption: { + additionalAllowedTags: [], + additionalDisallowedTags: [], additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts index faeef5847f2..dd2314aa5d7 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts @@ -1,8 +1,11 @@ import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext'; +import * as createPasteEntityProcessor from '../../../lib/override/pasteEntityProcessor'; +import * as createPasteGeneralProcessor from '../../../lib/override/pasteGeneralProcessor'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as mergeModelFile from '../../../lib/publicApi/model/mergeModel'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; import { mergePasteContent } from '../../../lib/utils/paste/mergePasteContent'; +import { pasteDisplayFormatParser } from '../../../lib/override/pasteDisplayFormatParser'; import { PasteType } from 'roosterjs-editor-types'; import { ContentModelDocument, @@ -115,7 +118,7 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: PasteType.Normal, - domToModelOption: {}, + domToModelOption: { additionalAllowedTags: [] }, } as any; const context: FormatWithContentModelContext = { @@ -222,7 +225,7 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: PasteType.Normal, - domToModelOption: {}, + domToModelOption: { additionalAllowedTags: [] }, customizedMerge, } as any; @@ -246,7 +249,7 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: PasteType.MergeFormat, - domToModelOption: {}, + domToModelOption: { additionalAllowedTags: [] }, } as any; mergePasteContent( @@ -308,6 +311,8 @@ describe('mergePasteContent', () => { paragraph: null!, path: [], }; + const mockedPasteGeneralProcessor = 'GENERALPROCESSOR' as any; + const mockedPasteEntityProcessor = 'ENTITYPROCESSOR' as any; const mockedDomToModelContext = { name: 'DOMTOMODELCONTEXT', } as any; @@ -316,6 +321,14 @@ describe('mergePasteContent', () => { pasteModel ); const mergeModelSpy = spyOn(mergeModelFile, 'mergeModel').and.returnValue(insertPoint); + const createPasteGeneralProcessorSpy = spyOn( + createPasteGeneralProcessor, + 'createPasteGeneralProcessor' + ).and.returnValue(mockedPasteGeneralProcessor); + const createPasteEntityProcessorSpy = spyOn( + createPasteEntityProcessor, + 'createPasteEntityProcessor' + ).and.returnValue(mockedPasteEntityProcessor); const createDomToModelContextSpy = spyOn( createDomToModelContext, 'createDomToModelContext' @@ -363,9 +376,20 @@ describe('mergePasteContent', () => { mergeFormat: 'none', mergeTable: false, }); + expect(createPasteGeneralProcessorSpy).toHaveBeenCalledWith(mockedDefaultDomToModelOptions); + expect(createPasteEntityProcessorSpy).toHaveBeenCalledWith(mockedDefaultDomToModelOptions); expect(createDomToModelContextSpy).toHaveBeenCalledWith( undefined, mockedDomToModelOptions, + { + processorOverride: { + entity: mockedPasteEntityProcessor, + '*': mockedPasteGeneralProcessor, + }, + formatParserOverride: { + display: pasteDisplayFormatParser, + }, + }, mockedDefaultDomToModelOptions ); expect(mockedDomToModelContext.segmentFormat).toEqual({ lineHeight: '1pt' }); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts new file mode 100644 index 00000000000..a97945f5ccd --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts @@ -0,0 +1,260 @@ +import { AllowedTags, DisallowedTags, sanitizeElement } from '../../lib/utils/sanitizeElement'; + +describe('sanitizeElement', () => { + it('Allowed element, empty', () => { + const element = document.createElement('div'); + + const result = sanitizeElement(element, AllowedTags, DisallowedTags); + + expect(element.outerHTML).toBe('
'); + expect(result!.outerHTML).toBe('
'); + }); + + it('Allowed element, with child', () => { + const element = document.createElement('div'); + + element.id = 'a'; + element.className = 'b c'; + element.innerHTML = 'test1test2test3'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags); + + expect(element.outerHTML).toBe( + '
test1test2test3
' + ); + expect(result!.outerHTML).toBe('
test1test2test3
'); + }); + + it('Empty element with disallowed tag', () => { + const element = document.createElement('script'); + + const result = sanitizeElement(element, AllowedTags, DisallowedTags); + + expect(element.outerHTML).toBe(''); + expect(result).toBeNull(); + }); + + it('Empty element with additional disallowed tag', () => { + const element = document.createElement('div'); + + const result = sanitizeElement(element, AllowedTags, DisallowedTags.concat(['div'])); + + expect(element.outerHTML).toBe('
'); + expect(result).toBeNull(); + }); + + it('Empty element with additional allowed tag', () => { + const element = document.createElement('test'); + + const result = sanitizeElement(element, AllowedTags.concat('test'), DisallowedTags); + + expect(element.outerHTML).toBe(''); + expect(result!.outerHTML).toBe(''); + }); + + it('Empty element with unrecognized tag', () => { + const element = document.createElement('test'); + + const result = sanitizeElement(element, AllowedTags, DisallowedTags); + + expect(element.outerHTML).toBe(''); + expect(result!.outerHTML).toBe(''); + }); + + it('Empty element with entity element', () => { + const element = document.createElement('div'); + + element.className = '_Entity _EType_A _EId_B _EReadonly_1'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags); + + expect(element.outerHTML).toBe('
'); + expect(result!.outerHTML).toBe('
'); + }); + + it('Empty element with child node', () => { + const element = document.createElement('div'); + + element.id = 'a'; + element.style.color = 'red'; + + element.innerHTML = 'testtest2'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags); + + expect(element.outerHTML).toBe( + '
testtest2
' + ); + expect(result!.outerHTML).toBe( + '
testtest2
' + ); + }); + + it('Empty element with style callback', () => { + const element = document.createElement('div'); + + element.style.color = 'red'; + element.style.position = 'absolute'; + + const positionCallback = jasmine.createSpy('position').and.callFake(() => 'relative'); + + const result = sanitizeElement(element, AllowedTags, DisallowedTags, { + position: positionCallback, + }); + + expect(element.outerHTML).toBe('
'); + expect(result!.outerHTML).toBe('
'); + }); +}); + +describe('sanitizeHtml', () => { + function runTest(source: string, exp: string) { + const doc = new DOMParser().parseFromString(source, 'text/html'); + + const result = sanitizeElement(doc.body, AllowedTags, DisallowedTags); + + expect(result!.innerHTML).toEqual(exp); + } + + it('Valid HTML', () => { + runTest('Test', 'Test'); + runTest( + '
test 1test 2test 3
', + '
test 1test 2test 3
' + ); + }); + + it('Invalid HTML', () => { + runTest('', ''); + runTest(' { + runTest('test', 'test'); + runTest('test1test2', 'test1test2'); + runTest( + 'test3ipt>alert("test")test4', + 'test3ipt>alert("test")test4' + ); + }); + + it('Html contains event handler', () => { + runTest('
bb
aa', '
bb
aa'); + runTest('aaccbb', 'aaccbb'); + runTest('aaccbb', 'aaccbb'); + runTest('aa
cc
bb', 'aaccbb'); + }); + + it('Html contains unnecessary CSS', () => { + runTest( + 'aabbcc', + 'aabbcc' + ); + runTest( + 'aabbcc', + 'aabbcc' + ); + }); + + it('Html contains disallowed CSS', () => { + runTest( + 'aa', + 'aa' + ); + runTest( + 'aa', + 'aa' + ); + }); + + it('Html contains disallowed attributes', () => { + runTest( + 'aa', + 'aa' + ); + }); + + it('Html contains comments', () => { + runTest('
aa
bb
', '
aa
bb
'); + }); + + it('Html contains CSS with escaped quoted values', () => { + let testIn: string = + "aa"; + let testOut: string = + 'aa'; + + runTest(testIn, testOut); + }); + + it('Html contains CSS with double quoted values', () => { + let testIn: string = + "aa"; + let testOut: string = + 'aa'; + + runTest(testIn, testOut); + }); + + it('Html contains CSS with single quoted values', () => { + let testIn: string = + 'aa'; + + runTest(testIn, testIn); + }); + + it('handle normal', () => { + runTest( + '
line \n 1
line \n 2
line \n 3
', + '
line \n 1
line \n 2
line \n 3
' + ); + }); + + it('handle nowrap', () => { + runTest( + '
line \n 1
line \n 2
line \n 3
', + '
line \n 1
line \n 2
line \n 3
' + ); + }); + + it('handle pre', () => { + runTest( + '
line \n 1
line \n 2
line \n 3
', + '
line \n 1
line \n 2
line \n 3
' + ); + }); + + it('handle pre-line', () => { + runTest( + '
line \n 1
line \n 2
line \n 3
', + '
line \n 1
line \n 2
line \n 3
' + ); + }); + + it('handle pre-wrap', () => { + runTest( + '
line \n 1
line \n 2
line \n 3
', + '
line \n 1
line \n 2
line \n 3
' + ); + }); + + it('handle PRE tag', () => { + runTest( + '
line \n 1 ' + '
  line  \n  2  
' + ' line \n 3
', + '
line \n 1
  line  \n  2  
line \n 3
' + ); + }); + + it('handle PRE tag with style', () => { + runTest( + '
line \n 1
  line  \n  2  
line \n 3
', + '
line \n 1
  line  \n  2  
line \n 3
' + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts index 10eb0c81c09..dc0a493603e 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts @@ -116,16 +116,3 @@ function insertDelimiter(doc: Document, element: Element, isAfter: boolean) { return span; } - -/** - * Allowed CSS selector for entity, used by HtmlSanitizer. - * TODO: Revisit paste logic and check if we can remove HtmlSanitizer - */ -export const AllowedEntityClasses: ReadonlyArray = [ - '^' + ENTITY_INFO_NAME + '$', - '^' + ENTITY_ID_PREFIX, - '^' + ENTITY_TYPE_PREFIX, - '^' + ENTITY_READONLY_PREFIX, - '^' + DELIMITER_BEFORE + '$', - '^' + DELIMITER_AFTER + '$', -]; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/index.ts b/packages-content-model/roosterjs-content-model-dom/lib/index.ts index d3927395b34..d0c355cbb9e 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/index.ts @@ -21,7 +21,6 @@ export { default as toArray } from './domUtils/toArray'; export { moveChildNodes, wrapAllChildNodes } from './domUtils/moveChildNodes'; export { wrap } from './domUtils/wrap'; export { - AllowedEntityClasses, isEntityElement, getAllEntityWrappers, parseEntityClassName, diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts index d4637712b46..ed5767a922c 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts @@ -1,6 +1,5 @@ import addParser from './utils/addParser'; import { BorderKeys } from 'roosterjs-content-model-dom'; -import { chainSanitizerCallback } from 'roosterjs-editor-dom'; import { deprecatedBorderColorParser } from './utils/deprecatedColorParser'; import { getPasteSource } from './pasteSourceValidations/getPasteSource'; import { parseLink } from './utils/linkParser'; @@ -19,12 +18,7 @@ import type { FormatParser, PasteType, } from 'roosterjs-content-model-types'; -import type { - EditorPlugin, - HtmlSanitizerOptions, - IEditor, - PluginEvent, -} from 'roosterjs-editor-types'; +import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-editor-types'; // Map old PasteType to new PasteType // TODO: We can remove this once we have standalone editor @@ -51,10 +45,7 @@ export class ContentModelPastePlugin implements EditorPlugin { * @param unknownTagReplacement Replace solution of unknown tags, default behavior is to replace with SPAN * @param allowExcelNoBorderTable Allow table copied from Excel without border */ - constructor( - private unknownTagReplacement: string = 'SPAN', - private allowExcelNoBorderTable?: boolean - ) {} + constructor(private allowExcelNoBorderTable?: boolean) {} /** * Get name of this plugin @@ -122,9 +113,9 @@ export class ContentModelPastePlugin implements EditorPlugin { } break; case 'googleSheets': - ev.sanitizingOption.additionalTagReplacements[ + ev.domToModelOption.additionalAllowedTags.push( PastePropertyNames.GOOGLE_SHEET_NODE_NAME - ] = '*'; + ); break; case 'powerPointDesktop': processPastedContentFromPowerPoint(ev, this.editor.getTrustedHTMLHandler()); @@ -135,14 +126,11 @@ export class ContentModelPastePlugin implements EditorPlugin { addParser(ev.domToModelOption, 'tableCell', deprecatedBorderColorParser); addParser(ev.domToModelOption, 'tableCell', tableBorderParser); addParser(ev.domToModelOption, 'table', deprecatedBorderColorParser); - sanitizeBlockStyles(ev.sanitizingOption); if (pasteType === 'mergeFormat') { addParser(ev.domToModelOption, 'block', blockElementParser); addParser(ev.domToModelOption, 'listLevel', blockElementParser); } - - ev.sanitizingOption.unknownTagReplacement = this.unknownTagReplacement; } } @@ -159,12 +147,6 @@ const blockElementParser: FormatParser = ( } }; -function sanitizeBlockStyles(sanitizingOption: Required) { - chainSanitizerCallback(sanitizingOption.cssStyleCallbacks, 'display', (value: string) => { - return value != 'flex'; // return whether we keep the style - }); -} - const ElementBorderKeys = new Map< keyof BorderFormat, { diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts index 2a3d7d5eb00..b9aa4e89769 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts @@ -62,22 +62,3 @@ export const TEMP_ELEMENTS_CLASSES: string[] = [ export const WAC_IDENTIFY_SELECTOR: string = `ul[class^="${BULLET_LIST_STYLE}"]>.${OUTLINE_ELEMENT},ol[class^="${NUMBER_LIST_STYLE}"]>.${OUTLINE_ELEMENT},span.${IMAGE_CONTAINER},span.${IMAGE_BORDER},.${COMMENT_HIGHLIGHT_CLASS},.${COMMENT_HIGHLIGHT_CLICKED_CLASS},` + WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES.map(c => `table div[class^="${c}"]`).join(','); -/** - * @internal - **/ -export const CLASSES_TO_KEEP: string[] = [ - OUTLINE_ELEMENT, - IMAGE_CONTAINER, - ...TEMP_ELEMENTS_CLASSES, - PARAGRAPH, - IMAGE_BORDER, - TABLE_CONTAINER, - COMMENT_HIGHLIGHT_CLASS, - COMMENT_HIGHLIGHT_CLICKED_CLASS, - 'NumberListStyle', - 'ListContainerWrapper', - 'BulletListStyle', - 'TableCellContent', - 'WACImageContainer', - 'LineBreakBlob', -]; diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts index ab82d145b25..4663d8e7208 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts @@ -1,7 +1,6 @@ import addParser from '../utils/addParser'; import { setProcessor } from '../utils/setProcessor'; import { - CLASSES_TO_KEEP, COMMENT_HIGHLIGHT_CLASS, COMMENT_HIGHLIGHT_CLICKED_CLASS, LIST_CONTAINER_ELEMENT_CLASS_NAME, @@ -202,7 +201,6 @@ export function processPastedContentWacComponents(ev: ContentModelBeforePasteEve setProcessor(ev.domToModelOption, 'li', wacLiElementProcessor); setProcessor(ev.domToModelOption, 'ol', wacListProcessor); setProcessor(ev.domToModelOption, 'ul', wacListProcessor); - ev.sanitizingOption.additionalAllowedCssClasses.push(...CLASSES_TO_KEEP); } /** diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index 3bdae38950f..c3ddaea677f 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -1,8 +1,6 @@ import addParser from '../utils/addParser'; import getStyleMetadata from './getStyleMetadata'; -import { chainSanitizerCallback } from 'roosterjs-editor-dom'; import { getStyles } from '../utils/getStyles'; -import { moveChildNodes } from 'roosterjs-content-model-dom'; import { processWordComments } from './processWordComments'; import { processWordList } from './processWordLists'; import { setProcessor } from '../utils/setProcessor'; @@ -35,22 +33,6 @@ export function processPastedContentFromWordDesktop( addParser(ev.domToModelOption, 'block', removeNonValidLineHeight); addParser(ev.domToModelOption, 'listLevel', listLevelParser); addParser(ev.domToModelOption, 'listItemElement', listItemElementParser); - - // Remove "border:none" for image to fix image resize behavior - // We found a problem that when paste an image with "border:none" then the resize border will be - // displayed incorrectly when resize it. So we need to drop this style - chainSanitizerCallback( - ev.sanitizingOption.cssStyleCallbacks, - 'border', - (value, element) => element.tagName != 'IMG' || value != 'none' - ); - - // Preserve when its innerHTML is " " to avoid dropping an empty line - chainSanitizerCallback(ev.sanitizingOption.elementCallbacks, 'O:P', element => { - moveChildNodes(element); - element.appendChild(element.ownerDocument.createTextNode('\u00A0')); //   - return true; - }); } const wordDesktopElementProcessor = ( diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index 40f727fce4d..94a3daddca1 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -1,5 +1,4 @@ import * as addParser from '../../lib/paste/utils/addParser'; -import * as chainSanitizerCallbackFile from 'roosterjs-editor-dom/lib/htmlSanitizer/chainSanitizerCallback'; import * as ExcelFile from '../../lib/paste/Excel/processPastedContentFromExcel'; import * as getPasteSource from '../../lib/paste/pasteSourceValidations/getPasteSource'; import * as PowerPointFile from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; @@ -22,7 +21,6 @@ describe('Content Model Paste Plugin Test', () => { getTrustedHTMLHandler: () => trustedHTMLHandler, } as any) as IContentModelEditor; spyOn(addParser, 'default').and.callThrough(); - spyOn(chainSanitizerCallbackFile, 'default').and.callThrough(); spyOn(setProcessor, 'setProcessor').and.callThrough(); }); @@ -45,7 +43,7 @@ describe('Content Model Paste Plugin Test', () => { htmlBefore: '', htmlAfter: '', htmlAttributes: {}, - domToModelOption: {}, + domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [] }, }); describe('onPluginEvent', () => { @@ -56,7 +54,7 @@ describe('Content Model Paste Plugin Test', () => { event = ({ eventType: PluginEventType.BeforePaste, - domToModelOption: {}, + domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [] }, sanitizingOption: { elementCallbacks: {}, attributeCallbacks: {}, @@ -85,7 +83,6 @@ describe('Content Model Paste Plugin Test', () => { plugin.onPluginEvent(event); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(3); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); }); @@ -104,7 +101,6 @@ describe('Content Model Paste Plugin Test', () => { ); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); }); it('Excel | image', () => { @@ -121,7 +117,6 @@ describe('Content Model Paste Plugin Test', () => { undefined /*allowExcelNoBorderTable*/ ); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(0); }); @@ -139,7 +134,6 @@ describe('Content Model Paste Plugin Test', () => { ); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); }); it('Excel Online', () => { @@ -156,7 +150,6 @@ describe('Content Model Paste Plugin Test', () => { ); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); }); it('Power Point', () => { @@ -172,7 +165,6 @@ describe('Content Model Paste Plugin Test', () => { ); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(0); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); }); it('Wac', () => { @@ -185,7 +177,6 @@ describe('Content Model Paste Plugin Test', () => { expect(WacFile.processPastedContentWacComponents).toHaveBeenCalledWith(event); expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(4); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); }); it('Default', () => { @@ -196,7 +187,6 @@ describe('Content Model Paste Plugin Test', () => { expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(0); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); }); it('Google Sheets', () => { @@ -207,12 +197,9 @@ describe('Content Model Paste Plugin Test', () => { expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(0); - expect(chainSanitizerCallbackFile.default).toHaveBeenCalledTimes(1); - expect( - event.sanitizingOption.additionalTagReplacements[ - PastePropertyNames.GOOGLE_SHEET_NODE_NAME - ] - ).toEqual('*'); + expect(event.domToModelOption.additionalAllowedTags).toEqual([ + PastePropertyNames.GOOGLE_SHEET_NODE_NAME, + ]); }); }); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index 4d32527f7c7..7fc25f3e16c 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -57,7 +57,7 @@ describe(ID, () => { isImplicit: undefined, segments: [ { - text: 'Test ', + text: 'Test', segmentType: 'Text', isSelected: undefined, format: { fontFamily: 'Calibri, sans-serif', fontSize: '11pt' }, @@ -130,7 +130,7 @@ describe(ID, () => { { segments: [ { - text: 'Asdasdsad ', + text: 'Asdasdsad', segmentType: 'Text', format: {}, }, @@ -170,7 +170,7 @@ describe(ID, () => { { segments: [ { - text: 'asdadasd ', + text: 'asdadasd', segmentType: 'Text', format: {}, }, @@ -232,7 +232,7 @@ describe(ID, () => { { segments: [ { - text: 'asdsadasdasdsadasdsadsad ', + text: 'asdsadasdasdsadasdsadsad', segmentType: 'Text', format: {}, }, diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index c4958d39c11..916cf08a942 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -2,6 +2,7 @@ import { Browser } from 'roosterjs-editor-dom'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createBeforePasteEventMock } from './processPastedContentFromWordDesktopTest'; import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; +import { pasteDisplayFormatParser } from 'roosterjs-content-model-core/lib/override/pasteDisplayFormatParser'; import { processPastedContentWacComponents } from '../../lib/paste/WacComponents/processPastedContentWacComponents'; import { listItemMetadataApplier, @@ -151,7 +152,15 @@ describe('wordOnlineHandler', () => { const model = domToContentModel( fragment, - createDomToModelContext(undefined, event.domToModelOption) + createDomToModelContext( + undefined, + { + formatParserOverride: { + display: pasteDisplayFormatParser, + }, + }, + event.domToModelOption + ) ); if (expectedModel) { expect(model).toEqual(expectedModel); @@ -2688,7 +2697,6 @@ describe('wordOnlineHandler', () => { marginTop: '2px', marginRight: '0px', marginBottom: '2px', - display: 'flex', }, }, ], diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index 408d2e1163c..a39e3ea1720 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -3826,7 +3826,7 @@ export function createBeforePasteEventMock(fragment: DocumentFragment, htmlBefor htmlBefore, htmlAfter: '', htmlAttributes: {}, - domToModelOption: {}, + domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [] }, } as any) as ContentModelBeforePasteEvent; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts index 09b7a83e835..a85198aac95 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts @@ -7,6 +7,32 @@ import type { CompatibleBeforePasteEvent, } from 'roosterjs-editor-types'; +/** + * Options for DOM to Content Model conversion for paste only + */ +export interface DomToModelOptionForPaste extends Required { + /** + * Additional allowed HTML tags in lower case. Element with these tags will be preserved + */ + additionalAllowedTags: Lowercase[]; + + /** + * Additional disallowed HTML tags in lower case. Elements with these tags will be dropped + */ + additionalDisallowedTags: Lowercase[]; +} + +/** + * A function type used by merging pasted content into current Content Model + * @param target Target Content Model to merge into + * @param source Source Content Model to merge from + * @returns Insert point after merge + */ +export type MergePastedContentFunc = ( + target: ContentModelDocument, + source: ContentModelDocument +) => InsertPoint | null; + /** * Data of ContentModelBeforePasteEvent */ @@ -14,15 +40,12 @@ export interface ContentModelBeforePasteEventData extends BeforePasteEventData { /** * domToModel Options to use when creating the content model from the paste fragment */ - domToModelOption: Partial; + domToModelOption: DomToModelOptionForPaste; /** * customizedMerge Customized merge function to use when merging the paste fragment into the editor */ - customizedMerge?: ( - target: ContentModelDocument, - source: ContentModelDocument - ) => InsertPoint | null; + customizedMerge?: MergePastedContentFunc; } /** diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index d6722c1658a..890693dd77e 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -276,6 +276,8 @@ export { EdgeLinkPreview } from './parameter/EdgeLinkPreview'; export { ClipboardData } from './parameter/ClipboardData'; export { + MergePastedContentFunc, + DomToModelOptionForPaste, ContentModelBeforePasteEvent, ContentModelBeforePasteEventData, CompatibleContentModelBeforePasteEvent, From adfa57c3adb16ef172d43c2f92da1b8727f9c7d3 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 21 Dec 2023 15:20:39 -0800 Subject: [PATCH 20/64] Standalone Editor: Decouple core package from roosterjs-editor-dom (#2283) * Standalone Editor: Port paste API step 1 * Standalone Editor: Port paste API step 2 * Standalone Editor: Port paste API step 3 * Standalone Editor: Port paste API step 4 * Standalone Editor: Decouple core package from roosterjs-editor-dom --- .../lib/editor/DarkColorHandlerImpl.ts | 43 +++++++++++++++---- .../roosterjs-content-model-core/package.json | 1 - .../test/coreApi/pasteTest.ts | 5 ++- .../test/editor/DarkColorHandlerImplTest.ts | 12 +++--- .../publicApi/color/transformColorTest.ts | 28 ++++++------ 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts index 0f9687fcc6e..ccfe96901bc 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts @@ -1,4 +1,4 @@ -import { getObjectKeys, parseColor, setColor } from 'roosterjs-editor-dom'; +import { getObjectKeys } from 'roosterjs-content-model-dom'; import type { ColorKeyAndValue, DarkColorHandler, @@ -22,6 +22,10 @@ const ColorAttributeName: { [key in ColorAttributeEnum]: string }[] = [ [ColorAttributeEnum.HtmlColor]: 'bgcolor', }, ]; +const HEX3_REGEX = /^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/; +const HEX6_REGEX = /^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/; +const RGB_REGEX = /^rgb\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)$/; +const RGBA_REGEX = /^rgba\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*\)$/; /** * @internal @@ -126,11 +130,11 @@ export class DarkColorHandlerImpl implements DarkColorHandler { * @param darkColor The existing dark color */ findLightColorFromDarkColor(darkColor: string): string | null { - const rgbSearch = parseColor(darkColor); + const rgbSearch = this.parseColor(darkColor); if (rgbSearch) { const key = getObjectKeys(this.knownColors).find(key => { - const rgbCurrent = parseColor(this.knownColors[key].darkModeColor); + const rgbCurrent = this.parseColor(this.knownColors[key].darkModeColor); return ( rgbCurrent && @@ -161,13 +165,36 @@ export class DarkColorHandlerImpl implements DarkColorHandler { element.getAttribute(names[ColorAttributeEnum.HtmlColor]), !!fromDarkMode ).lightModeColor; + const transformedColor = + color && color != 'inherit' ? this.registerColor(color, !!toDarkMode) : null; - element.style.setProperty(names[ColorAttributeEnum.CssColor], null); + element.style.setProperty(names[ColorAttributeEnum.CssColor], transformedColor); element.removeAttribute(names[ColorAttributeEnum.HtmlColor]); - - if (color && color != 'inherit') { - setColor(element, color, i != 0, toDarkMode, false /*shouldAdaptFontColor*/, this); - } }); } + + /** + * Parse color string to r/g/b value. + * If the given color is not in a recognized format, return null + */ + private parseColor(color: string): [number, number, number] | null { + color = (color || '').trim(); + + let match: RegExpMatchArray | null; + if ((match = color.match(HEX3_REGEX))) { + return [ + parseInt(match[1] + match[1], 16), + parseInt(match[2] + match[2], 16), + parseInt(match[3] + match[3], 16), + ]; + } else if ((match = color.match(HEX6_REGEX))) { + return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)]; + } else if ((match = color.match(RGB_REGEX) || color.match(RGBA_REGEX))) { + return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]; + } else { + // CSS color names such as red, green is not included for now. + // If need, we can add those colors from https://www.w3.org/wiki/CSS/Properties/color/keywords + return null; + } + } } diff --git a/packages-content-model/roosterjs-content-model-core/package.json b/packages-content-model/roosterjs-content-model-core/package.json index c037037dea2..ed1e640d214 100644 --- a/packages-content-model/roosterjs-content-model-core/package.json +++ b/packages-content-model/roosterjs-content-model-core/package.json @@ -4,7 +4,6 @@ "dependencies": { "tslib": "^2.3.1", "roosterjs-editor-types": "", - "roosterjs-editor-dom": "", "roosterjs-content-model-dom": "", "roosterjs-content-model-types": "" }, diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 1a775827442..803c15f02f8 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -1,4 +1,5 @@ import * as addParserF from 'roosterjs-content-model-plugins/lib/paste/utils/addParser'; +import * as cloneModel from '../../lib/publicApi/model/cloneModel'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as ExcelF from 'roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel'; import * as getPasteSourceF from 'roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; @@ -11,6 +12,7 @@ import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/Word import { BeforePasteEvent, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; +import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; import { tableProcessor } from 'roosterjs-content-model-dom'; import { ClipboardData, @@ -21,7 +23,6 @@ import { FormatWithContentModelOptions, IStandaloneEditor, } from 'roosterjs-content-model-types'; -import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; let clipboardData: ClipboardData; @@ -41,11 +42,13 @@ describe('Paste ', () => { let context: FormatWithContentModelContext | undefined; const mockedPos = 'POS' as any; + const mockedCloneModel = 'CloneModel' as any; let div: HTMLDivElement; beforeEach(() => { spyOn(domToContentModel, 'domToContentModel').and.callThrough(); + spyOn(cloneModel, 'cloneModel').and.returnValue(mockedCloneModel); clipboardData = { types: ['image/png', 'text/html'], text: '', diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts index 61ca7c9cc99..e479594d65e 100644 --- a/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/editor/DarkColorHandlerImplTest.ts @@ -418,7 +418,7 @@ describe('DarkColorHandlerImpl.transformElementColor', () => { expect(parseColorSpy).toHaveBeenCalledWith('red', false); expect(parseColorSpy).toHaveBeenCalledWith(null, false); expect(registerColorSpy).toHaveBeenCalledTimes(1); - expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + expect(registerColorSpy).toHaveBeenCalledWith('red', true); }); it('Has simple color in CSS, light to dark', () => { @@ -433,7 +433,7 @@ describe('DarkColorHandlerImpl.transformElementColor', () => { expect(parseColorSpy).toHaveBeenCalledWith('red', false); expect(parseColorSpy).toHaveBeenCalledWith(null, false); expect(registerColorSpy).toHaveBeenCalledTimes(1); - expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + expect(registerColorSpy).toHaveBeenCalledWith('red', true); }); it('Has color in both text and background, light to dark', () => { @@ -453,8 +453,8 @@ describe('DarkColorHandlerImpl.transformElementColor', () => { expect(parseColorSpy).toHaveBeenCalledWith('red'); expect(parseColorSpy).toHaveBeenCalledWith('green'); expect(registerColorSpy).toHaveBeenCalledTimes(2); - expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); - expect(registerColorSpy).toHaveBeenCalledWith('green', true, undefined); + expect(registerColorSpy).toHaveBeenCalledWith('red', true); + expect(registerColorSpy).toHaveBeenCalledWith('green', true); }); it('Has var-based color, light to dark', () => { @@ -470,7 +470,7 @@ describe('DarkColorHandlerImpl.transformElementColor', () => { expect(parseColorSpy).toHaveBeenCalledWith('red'); expect(parseColorSpy).toHaveBeenCalledWith(null, false); expect(registerColorSpy).toHaveBeenCalledTimes(1); - expect(registerColorSpy).toHaveBeenCalledWith('red', true, undefined); + expect(registerColorSpy).toHaveBeenCalledWith('red', true); }); it('No color, dark to light', () => { @@ -539,6 +539,6 @@ describe('DarkColorHandlerImpl.transformElementColor', () => { expect(parseColorSpy).toHaveBeenCalledWith('red'); expect(parseColorSpy).toHaveBeenCalledWith(null, true); expect(registerColorSpy).toHaveBeenCalledTimes(1); - expect(registerColorSpy).toHaveBeenCalledWith('red', false, undefined); + expect(registerColorSpy).toHaveBeenCalledWith('red', false); }); }); diff --git a/packages-content-model/roosterjs-content-model-core/test/publicApi/color/transformColorTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/color/transformColorTest.ts index 58b87118e96..4f3eb8b8d71 100644 --- a/packages-content-model/roosterjs-content-model-core/test/publicApi/color/transformColorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/color/transformColorTest.ts @@ -13,7 +13,7 @@ describe('transform to dark mode', () => { element: HTMLElement, expectedHtml: string, expectedParseValueCalls: string[], - expectedRegisterColorCalls: [string, boolean, string][] + expectedRegisterColorCalls: [string, boolean][] ) { const handler = new DarkColorHandlerImpl(div, getDarkColor); @@ -54,8 +54,8 @@ describe('transform to dark mode', () => { '
', ['red', 'green'], [ - ['blue', true, undefined!], - ['yellow', true, undefined!], + ['blue', true], + ['yellow', true], ] ); }); @@ -70,8 +70,8 @@ describe('transform to dark mode', () => { '
', ['red', 'green'], [ - ['blue', true, undefined!], - ['yellow', true, undefined!], + ['blue', true], + ['yellow', true], ] ); }); @@ -88,8 +88,8 @@ describe('transform to dark mode', () => { '
', ['red', 'green'], [ - ['blue', true, undefined!], - ['yellow', true, undefined!], + ['blue', true], + ['yellow', true], ] ); }); @@ -106,7 +106,7 @@ describe('transform to light mode', () => { element: HTMLElement, expectedHtml: string, expectedParseValueCalls: string[], - expectedRegisterColorCalls: [string, boolean, string][] + expectedRegisterColorCalls: [string, boolean][] ) { const handler = new DarkColorHandlerImpl(div, getDarkColor); const parseColorValue = spyOn(handler, 'parseColorValue').and.callFake((color: string) => ({ @@ -146,8 +146,8 @@ describe('transform to light mode', () => { '
', ['red', 'green'], [ - ['blue', false, undefined!], - ['yellow', false, undefined!], + ['blue', false], + ['yellow', false], ] ); }); @@ -162,8 +162,8 @@ describe('transform to light mode', () => { '
', ['red', 'green'], [ - ['blue', false, undefined!], - ['yellow', false, undefined!], + ['blue', false], + ['yellow', false], ] ); }); @@ -180,8 +180,8 @@ describe('transform to light mode', () => { '
', ['red', 'green'], [ - ['blue', false, undefined!], - ['yellow', false, undefined!], + ['blue', false], + ['yellow', false], ] ); }); From e2ddd5be114b4ba0de3438b38d2f639fd93ca60e Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Fri, 22 Dec 2023 08:35:33 -0600 Subject: [PATCH 21/64] Set Deprecated font color to black instead of undefined (#2290) * init * use jasmine anything --- .../test/coreApi/pasteTest.ts | 10 +- .../lib/formatHandlers/utils/color.ts | 4 +- .../test/paste/e2e/cmPasteFromWacTest.ts | 145 ++++++------ .../test/paste/e2e/cmPasteFromWordTest.ts | 36 +-- .../paste/processPastedContentFromWacTest.ts | 211 +++++++++--------- 5 files changed, 217 insertions(+), 189 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 803c15f02f8..3543e32931f 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -389,7 +389,7 @@ describe('Paste with clipboardData', () => { document.getElementById(ID)?.remove(); }); - it('Remove windowtext from clipboardContent', () => { + it('Replace windowtext with set black font color from clipboardContent', () => { clipboardData.rawHtml = '

Test

'; @@ -410,12 +410,16 @@ describe('Paste with clipboardData', () => { { segmentType: 'Text', text: 'Test', - format: {}, + format: { + textColor: 'rgb(0, 0, 0)', + }, }, { segmentType: 'SelectionMarker', isSelected: true, - format: {}, + format: { + textColor: 'rgb(0, 0, 0)', + }, }, ], format: { diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/color.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/color.ts index 185190e8289..1030d70d4af 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/color.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/color.ts @@ -29,6 +29,8 @@ export const DeprecatedColors: string[] = [ 'window', ]; +const BlackColor = 'rgb(0, 0, 0)'; + /** * Get color from given HTML element * @param element The element to get color from @@ -48,7 +50,7 @@ export function getColor( undefined; if (color && DeprecatedColors.indexOf(color) > -1) { - color = undefined; + color = isBackground ? undefined : BlackColor; } if (darkColorHandler) { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts index 5736f9d17b2..9065ceba71c 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts @@ -66,55 +66,82 @@ describe(ID, () => { blockGroupType: 'Document', blocks: [ { + tagName: 'div', blockType: 'BlockGroup', + format: { + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', + }, blockGroupType: 'FormatContainer', - tagName: 'div', blocks: [ { + tagName: 'div', blockType: 'BlockGroup', + format: { + marginTop: '2px', + marginRight: '0px', + marginBottom: '2px', + }, blockGroupType: 'FormatContainer', - tagName: 'div', blocks: [ { - blockType: 'Table', + widths: jasmine.anything() as any, rows: [ { height: jasmine.anything() as any, - format: {}, cells: [ { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { + tagName: 'div', blockType: 'BlockGroup', + format: { + direction: 'ltr', + textAlign: 'start', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', + paddingRight: '7px', + paddingLeft: '7px', + }, blockGroupType: 'FormatContainer', - tagName: 'div', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: 'Test Table ', + segmentType: 'Text', format: { + textColor: + 'rgb(0, 0, 0)', fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', fontSize: '11pt', - textColor: - 'rgb(0, 0, 0)', lineHeight: '19.7625px', }, }, ], + segmentFormat: { + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', whiteSpace: 'pre-wrap', - marginLeft: '0px', - marginRight: '0px', marginTop: '0px', + marginRight: '0px', marginBottom: '0px', + marginLeft: '0px', }, decorator: { tagName: 'p', @@ -122,16 +149,6 @@ describe(ID, () => { }, }, ], - format: { - direction: 'ltr', - textAlign: 'start', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - paddingRight: '7px', - paddingLeft: '7px', - }, }, ], format: { @@ -144,46 +161,59 @@ describe(ID, () => { verticalAlign: 'top', width: '312px', }, - spanLeft: false, - spanAbove: false, - isHeader: false, dataset: { celllook: '0', }, }, { + spanAbove: false, + spanLeft: false, + isHeader: false, blockGroupType: 'TableCell', blocks: [ { + tagName: 'div', blockType: 'BlockGroup', + format: { + direction: 'ltr', + textAlign: 'start', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', + paddingRight: '7px', + paddingLeft: '7px', + }, blockGroupType: 'FormatContainer', - tagName: 'div', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: 'Test Table ', + segmentType: 'Text', format: { + textColor: + 'rgb(0, 0, 0)', fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', fontSize: '11pt', - textColor: - 'rgb(0, 0, 0)', lineHeight: '19.7625px', }, }, ], + segmentFormat: { + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', whiteSpace: 'pre-wrap', - marginLeft: '0px', - marginRight: '0px', marginTop: '0px', + marginRight: '0px', marginBottom: '0px', + marginLeft: '0px', }, decorator: { tagName: 'p', @@ -191,16 +221,6 @@ describe(ID, () => { }, }, ], - format: { - direction: 'ltr', - textAlign: 'start', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - paddingRight: '7px', - paddingLeft: '7px', - }, }, ], format: { @@ -213,18 +233,16 @@ describe(ID, () => { verticalAlign: 'top', width: '312px', }, - spanLeft: false, - spanAbove: false, - isHeader: false, dataset: { celllook: '0', }, }, ], + format: {}, }, ], - format: { - useBorderBox: true, + blockType: 'Table', + format: { direction: 'ltr', textAlign: 'start', marginTop: '0px', @@ -233,22 +251,21 @@ describe(ID, () => { marginLeft: '0px', width: '0px', tableLayout: 'fixed', + useBorderBox: true, borderCollapse: true, }, - widths: [jasmine.anything() as any, jasmine.anything() as any], dataset: { tablestyle: 'MsoTableGrid', tablelook: '1696', }, }, ], - format: { - marginTop: '2px', - marginRight: '0px', - marginBottom: '2px', - }, }, ], + }, + { + tagName: 'div', + blockType: 'BlockGroup', format: { backgroundColor: 'rgb(255, 255, 255)', marginTop: '0px', @@ -256,27 +273,26 @@ describe(ID, () => { marginBottom: '0px', marginLeft: '0px', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'FormatContainer', - tagName: 'div', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: { + textColor: 'rgb(0, 0, 0)', fontFamily: 'Aptos, Aptos_EmbeddedFont, Aptos_MSFontService, sans-serif', fontSize: '12pt', - textColor: 'rgb(0, 0, 0)', lineHeight: '20.925px', }, }, ], + segmentFormat: { + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -292,20 +308,12 @@ describe(ID, () => { }, }, ], - format: { - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'SelectionMarker', isSelected: true, + segmentType: 'SelectionMarker', format: {}, }, { @@ -313,6 +321,7 @@ describe(ID, () => { format: {}, }, ], + blockType: 'Paragraph', format: {}, }, ], diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index 7fc25f3e16c..b68d8af08ad 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -120,14 +120,13 @@ describe(ID, () => { rows: [ { height: jasmine.anything() as any, + format: {}, cells: [ { - spanAbove: false, - spanLeft: false, - isHeader: false, blockGroupType: 'TableCell', blocks: [ { + blockType: 'Paragraph', segments: [ { text: 'Asdasdsad', @@ -135,7 +134,6 @@ describe(ID, () => { format: {}, }, ], - blockType: 'Paragraph', format: { lineHeight: 'normal', marginTop: '1em', @@ -159,15 +157,16 @@ describe(ID, () => { verticalAlign: 'top', width: '233.75pt', }, + spanLeft: false, + spanAbove: false, + isHeader: false, dataset: {}, }, { - spanAbove: false, - spanLeft: false, - isHeader: false, blockGroupType: 'TableCell', blocks: [ { + blockType: 'Paragraph', segments: [ { text: 'asdadasd', @@ -175,7 +174,6 @@ describe(ID, () => { format: {}, }, ], - blockType: 'Paragraph', format: { lineHeight: 'normal', marginTop: '1em', @@ -198,28 +196,30 @@ describe(ID, () => { verticalAlign: 'top', width: '233.75pt', }, + spanLeft: false, + spanAbove: false, + isHeader: false, dataset: {}, }, ], - format: {}, }, ], blockType: 'Table', format: { - borderCollapse: true, useBorderBox: true, + borderCollapse: true, }, dataset: {}, }, { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: {}, }, ], - blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -230,14 +230,16 @@ describe(ID, () => { }, }, { + blockType: 'Paragraph', segments: [ { text: 'asdsadasdasdsadasdsadsad', segmentType: 'Text', - format: {}, + format: { + textColor: 'rgb(0, 0, 0)', + }, }, ], - blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -248,19 +250,19 @@ describe(ID, () => { }, }, { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: {}, }, { - isSelected: true, segmentType: 'SelectionMarker', + isSelected: true, format: {}, }, ], - blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index 916cf08a942..0540ab4de2b 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -1669,9 +1669,9 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', italic: false, - fontWeight: 'bold', textColor: 'rgb(255, 255, 255)', + fontWeight: 'bold', lineHeight: '41.85px', }, @@ -1684,10 +1684,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(255, 255, 255)', + fontWeight: + 'normal', lineHeight: '41.85px', }, @@ -1699,10 +1699,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(255, 255, 255)', + fontWeight: + 'normal', lineHeight: '41.85px', }, @@ -1715,9 +1715,9 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', italic: false, - fontWeight: 'bold', textColor: 'rgb(255, 255, 255)', + fontWeight: 'bold', lineHeight: '41.85px', }, @@ -1730,10 +1730,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '20pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(255, 255, 255)', + fontWeight: + 'normal', lineHeight: '41.85px', }, @@ -1742,15 +1742,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -1773,12 +1774,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - backgroundColor: 'rgb(21, 96, 130)', - width: '312px', borderTop: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', + backgroundColor: 'rgb(21, 96, 130)', verticalAlign: 'middle', + width: '312px', }, spanLeft: false, spanAbove: false, @@ -1807,9 +1808,9 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '21.5pt', italic: false, - fontWeight: 'bold', textColor: 'rgb(255, 255, 255)', + fontWeight: 'bold', lineHeight: '44.175px', }, @@ -1822,10 +1823,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '21.5pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(255, 255, 255)', + fontWeight: + 'normal', lineHeight: '44.175px', }, @@ -1834,15 +1835,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -1865,12 +1867,12 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - backgroundColor: 'rgb(21, 96, 130)', - width: '312px', borderTop: '1px solid', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', + backgroundColor: 'rgb(21, 96, 130)', verticalAlign: 'middle', + width: '312px', }, spanLeft: false, spanAbove: false, @@ -1904,9 +1906,9 @@ describe('wordOnlineHandler', () => { 'Aptos_MSFontService, Aptos_MSFontService_EmbeddedFont, Aptos_MSFontService_MSFontService, sans-serif', fontSize: '14pt', italic: false, - fontWeight: 'bold', textColor: 'rgb(255, 255, 255)', + fontWeight: 'bold', lineHeight: '24.4125px', }, @@ -1919,10 +1921,10 @@ describe('wordOnlineHandler', () => { 'Aptos_MSFontService, Aptos_MSFontService_EmbeddedFont, Aptos_MSFontService_MSFontService, sans-serif', fontSize: '14pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(255, 255, 255)', + fontWeight: + 'normal', lineHeight: '24.4125px', }, @@ -1931,15 +1933,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -1962,13 +1965,13 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - backgroundColor: 'rgb(0, 0, 0)', - width: '624px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', + backgroundColor: 'rgb(0, 0, 0)', verticalAlign: 'middle', + width: '624px', }, spanLeft: false, spanAbove: false, @@ -1983,13 +1986,13 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - backgroundColor: 'rgb(0, 0, 0)', - width: '624px', borderTop: '1px solid rgb(0, 0, 0)', borderRight: '1px solid', borderBottom: '1px solid rgb(0, 0, 0)', borderLeft: '1px solid', + backgroundColor: 'rgb(0, 0, 0)', verticalAlign: 'middle', + width: '624px', }, spanLeft: true, spanAbove: false, @@ -2023,10 +2026,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2039,10 +2042,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2051,15 +2054,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2077,10 +2081,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2089,15 +2093,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2116,10 +2121,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2132,10 +2137,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2147,10 +2152,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2163,10 +2168,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2175,15 +2180,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2202,15 +2208,14 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, }, - { segmentType: 'Text', text: 'benefits', @@ -2219,10 +2224,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2235,10 +2240,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2251,10 +2256,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2267,10 +2272,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2283,10 +2288,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2295,15 +2300,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2321,10 +2327,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2332,15 +2338,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2359,10 +2366,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2374,10 +2381,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2385,15 +2392,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2411,10 +2419,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2422,15 +2430,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2449,10 +2458,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2464,10 +2473,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2478,10 +2487,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2493,10 +2502,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2508,10 +2517,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '21px', }, }, @@ -2519,15 +2528,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2546,10 +2556,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2562,10 +2572,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2577,10 +2587,10 @@ describe('wordOnlineHandler', () => { 'WordVisiCarriageReturn_MSFontService, "Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2593,10 +2603,10 @@ describe('wordOnlineHandler', () => { '"Segoe UI", "Segoe UI_EmbeddedFont", "Segoe UI_MSFontService", sans-serif', fontSize: '12pt', italic: false, - fontWeight: - 'normal', textColor: 'rgb(0, 0, 0)', + fontWeight: + 'normal', lineHeight: '23.7333px', }, @@ -2605,15 +2615,16 @@ describe('wordOnlineHandler', () => { format: { direction: 'ltr', textAlign: 'start', - whiteSpace: 'pre-wrap', marginLeft: '0px', marginRight: '0px', + whiteSpace: 'pre-wrap', marginTop: '0px', marginBottom: '0px', }, segmentFormat: { - fontWeight: 'normal', italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, decorator: { tagName: 'p', @@ -2683,7 +2694,7 @@ describe('wordOnlineHandler', () => { width: '0px', tableLayout: 'fixed', borderCollapse: true, - } as any, + }, widths: [], dataset: { tablelook: '1696', From bf6767f64db147d5c99016b0b707962551b70722 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Fri, 22 Dec 2023 08:44:23 -0600 Subject: [PATCH 22/64] Remove negative margins from Word (#2277) * Remove negative margins from Word * remove --- .../test/coreApi/pasteTest.ts | 4 +- .../lib/paste/WacComponents/constants.ts | 4 -- .../processPastedContentWacComponents.ts | 8 ++-- .../processPastedContentFromWordDesktop.ts | 9 ++++ .../test/paste/ContentModelPastePluginTest.ts | 4 +- .../paste/processPastedContentFromWacTest.ts | 48 +++++++++++++++++++ ...processPastedContentFromWordDesktopTest.ts | 48 +++++++++++++++++++ 7 files changed, 113 insertions(+), 12 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 3543e32931f..aa8c4fa25af 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -224,7 +224,7 @@ describe('paste with content model & paste plugin', () => { editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); - expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); + expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); }); @@ -235,7 +235,7 @@ describe('paste with content model & paste plugin', () => { editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(4); - expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); + expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts index b9aa4e89769..027909e7e7f 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts @@ -37,10 +37,6 @@ export const PARAGRAPH: string = 'Paragraph'; * @internal **/ export const LIST_CONTAINER_ELEMENT_CLASS_NAME: string = 'ListContainerWrapper'; -/** - * @internal - **/ -export const TABLE_CONTAINER: string = 'TableContainer'; /** * @internal **/ diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts index 4663d8e7208..85665ab5b37 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts @@ -4,7 +4,6 @@ import { COMMENT_HIGHLIGHT_CLASS, COMMENT_HIGHLIGHT_CLICKED_CLASS, LIST_CONTAINER_ELEMENT_CLASS_NAME, - TABLE_CONTAINER, TEMP_ELEMENTS_CLASSES, WAC_IDENTIFY_SELECTOR, } from './constants'; @@ -194,7 +193,8 @@ export function processPastedContentWacComponents(ev: ContentModelBeforePasteEve addParser(ev.domToModelOption, 'segment', wacSubSuperParser); addParser(ev.domToModelOption, 'listItemThread', wacListItemParser); addParser(ev.domToModelOption, 'listLevel', wacListLevelParser); - addParser(ev.domToModelOption, 'container', wacBlockParser); + addParser(ev.domToModelOption, 'container', wacContainerParser); + addParser(ev.domToModelOption, 'table', wacContainerParser); addParser(ev.domToModelOption, 'segment', wacCommentParser); setProcessor(ev.domToModelOption, 'element', wacElementProcessor); @@ -245,11 +245,11 @@ const wacListProcessor: ElementProcessor = } }; -const wacBlockParser: FormatParser = ( +const wacContainerParser: FormatParser = ( format: ContentModelBlockFormat, element: HTMLElement ) => { - if (element.classList.contains(TABLE_CONTAINER) && element.style.marginLeft.startsWith('-')) { + if (element.style.marginLeft.startsWith('-')) { delete format.marginLeft; } }; diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index c3ddaea677f..f0843116691 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -10,6 +10,7 @@ import type { ContentModelBlockFormat, ContentModelListItemFormat, ContentModelListItemLevelFormat, + ContentModelTableFormat, DomToModelContext, ElementProcessor, FormatParser, @@ -33,6 +34,8 @@ export function processPastedContentFromWordDesktop( addParser(ev.domToModelOption, 'block', removeNonValidLineHeight); addParser(ev.domToModelOption, 'listLevel', listLevelParser); addParser(ev.domToModelOption, 'listItemElement', listItemElementParser); + addParser(ev.domToModelOption, 'container', wordTableParser); + addParser(ev.domToModelOption, 'table', wordTableParser); } const wordDesktopElementProcessor = ( @@ -93,3 +96,9 @@ const listItemElementParser: FormatParser = ( format.marginRight = undefined; } }; + +const wordTableParser: FormatParser = (format): void => { + if (format.marginLeft?.startsWith('-')) { + delete format.marginLeft; + } +}; diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index 94a3daddca1..f71f8488526 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -82,7 +82,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); + expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); }); @@ -175,7 +175,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.onPluginEvent(event); expect(WacFile.processPastedContentWacComponents).toHaveBeenCalledWith(event); - expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); + expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(4); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index 0540ab4de2b..19e081eda9a 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -2764,4 +2764,52 @@ describe('wordOnlineHandler', () => { } ); }); + + it('Remove Negative Left margin from table', () => { + runTest( + '
Test
', + '
Test
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + } + ); + }); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index a39e3ea1720..9719b684514 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -244,6 +244,54 @@ describe('processPastedContentFromWordDesktopTest', () => { }); }); + it('Remove Negative Left margin from table', () => { + runTest( + '
Test
', + '
Test
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + } + ); + }); + describe('List Convertion Tests | ', () => { it('List with Headings', () => { const html = From ef5da80b4cc418687c541b145853925e4666c154 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 22 Dec 2023 09:39:58 -0800 Subject: [PATCH 23/64] Standalone Editor step 1 (#2291) --- .../lib/corePlugin/ContentModelCachePlugin.ts | 2 +- .../corePlugin/ContentModelCopyPastePlugin.ts | 16 +-- .../corePlugin/ContentModelFormatPlugin.ts | 2 +- .../lib/corePlugin/DOMEventPlugin.ts | 56 ++------- .../lib/corePlugin/EntityPlugin.ts | 20 ++-- .../lib/corePlugin/LifecyclePlugin.ts | 2 +- .../lib/corePlugin/SelectionPlugin.ts | 4 +- .../lib/corePlugin/UndoPlugin.ts | 2 +- .../corePlugin/utils/applyDefaultFormat.ts | 5 +- .../lib/editor/createStandaloneEditorCore.ts | 3 - .../test/coreApi/pasteTest.ts | 22 ++-- .../ContentModelCopyPastePluginTest.ts | 11 +- .../ContentModelFormatPluginTest.ts | 2 +- .../test/corePlugin/DomEventPluginTest.ts | 73 ++++-------- .../test/corePlugin/EntityPluginTest.ts | 14 +-- .../utils/applyDefaultFormatTest.ts | 5 +- .../utils/applyPendingFormatTest.ts | 11 +- .../lib/corePlugins/ContextMenuPlugin.ts | 110 ++++++++++++++++++ .../lib/corePlugins/createCorePlugins.ts | 14 +-- .../lib/editor/ContentModelEditor.ts | 26 ++++- .../lib/editor/createEditorCore.ts | 9 +- .../lib/index.ts | 1 + .../publicTypes/ContentModelCorePlugins.ts | 6 + .../lib/publicTypes/ContentModelEditorCore.ts | 12 ++ .../lib/publicTypes/ContextMenuPluginState.ts | 11 ++ .../test/corePlugins/ContextMenuPluginTest.ts | 93 +++++++++++++++ .../test/editor/createEditorCoreTest.ts | 12 ++ .../lib/editor/IStandaloneEditor.ts | 65 ++++++++++- .../lib/editor/StandaloneEditorCore.ts | 9 +- .../lib/index.ts | 5 +- .../lib/pluginState/DOMEventPluginState.ts | 7 -- .../StandaloneEditorPluginState.ts | 9 -- 32 files changed, 428 insertions(+), 211 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts create mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContextMenuPluginState.ts create mode 100644 packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts index 72d272f827b..6ccff76c84e 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts @@ -19,7 +19,7 @@ import type { * ContentModel cache plugin manages cached Content Model, and refresh the cache when necessary */ class ContentModelCachePlugin implements PluginWithState { - private editor: (IEditor & IStandaloneEditor) | null = null; + private editor: IStandaloneEditor | null = null; private state: ContentModelCachePluginState; /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts index 180b3f1f15f..7408a547f98 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts @@ -37,7 +37,7 @@ import type { IEditor, PluginWithState } from 'roosterjs-editor-types'; * Copy and paste plugin for handling onCopy and onPaste event */ class ContentModelCopyPastePlugin implements PluginWithState { - private editor: (IStandaloneEditor & IEditor) | null = null; + private editor: IStandaloneEditor | null = null; private disposer: (() => void) | null = null; private state: CopyPastePluginState; @@ -155,15 +155,17 @@ class ContentModelCopyPastePlugin implements PluginWithState { - const editor = e as IStandaloneEditor & IEditor; + doc.defaultView?.requestAnimationFrame(() => { + if (!this.editor) { + return; + } cleanUpAndRestoreSelection(tempDiv); - editor.focus(); - editor.setDOMSelection(selection); + this.editor.focus(); + this.editor.setDOMSelection(selection); if (isCut) { - editor.formatContentModel( + this.editor.formatContentModel( (model, context) => { if ( deleteSelection(model, [deleteEmptyList], context) @@ -200,7 +202,7 @@ class ContentModelCopyPastePlugin implements PluginWithState { if (!editor.isDisposed()) { - editor.paste(clipboardData); + editor.pasteFromClipboard(clipboardData); } }); } diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts index 6a93175b0a0..3cd4ea2862e 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts @@ -19,7 +19,7 @@ const ProcessKey = 'Process'; * 1. Handle pending format changes when selection is collapsed */ class ContentModelFormatPlugin implements PluginWithState { - private editor: (IStandaloneEditor & IEditor) | null = null; + private editor: IStandaloneEditor | null = null; private hasDefaultFormat = false; private state: ContentModelFormatPluginState; diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts index e80f1ac10d9..1ec7ae78567 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts @@ -1,5 +1,6 @@ import { ChangeSource } from '../constants/ChangeSource'; import { isCharacterValue, isCursorMovingKey } from '../publicApi/domUtils/eventUtils'; +import { isNodeOfType } from 'roosterjs-content-model-dom'; import { PluginEventType } from 'roosterjs-editor-types'; import type { DOMEventPluginState, @@ -7,12 +8,7 @@ import type { DOMEventRecord, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; -import type { - ContextMenuProvider, - EditorPlugin, - IEditor, - PluginWithState, -} from 'roosterjs-editor-types'; +import type { IEditor, PluginWithState } from 'roosterjs-editor-types'; /** * DOMEventPlugin handles customized DOM events, including: @@ -26,7 +22,7 @@ import type { * It contains special handling for Safari since Safari cannot get correct selection when onBlur event is triggered in editor. */ class DOMEventPlugin implements PluginWithState { - private editor: (IStandaloneEditor & IEditor) | null = null; + private editor: IStandaloneEditor | null = null; private disposer: (() => void) | null = null; private state: DOMEventPluginState; @@ -39,8 +35,6 @@ class DOMEventPlugin implements PluginWithState { this.state = { isInIME: false, scrollContainer: options.scrollContainer || contentDiv, - contextMenuProviders: - options.plugins?.filter>(isContextMenuProvider) || [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -72,7 +66,6 @@ class DOMEventPlugin implements PluginWithState { // 2. Mouse event mousedown: { beforeDispatch: this.onMouseDown }, - contextmenu: { beforeDispatch: this.onContextMenuEvent }, // 3. IME state management compositionstart: { beforeDispatch: this.onCompositionStart }, @@ -119,17 +112,23 @@ class DOMEventPlugin implements PluginWithState { private onDragStart = (e: Event) => { const dragEvent = e as DragEvent; - const element = this.editor?.getElementAtCursor('*', dragEvent.target as Node); + const node = dragEvent.target as Node; + const element = isNodeOfType(node, 'ELEMENT_NODE') ? node : node.parentElement; if (element && !element.isContentEditable) { dragEvent.preventDefault(); } }; + private onDrop = () => { - this.editor?.runAsync(() => { + const doc = this.editor?.getDocument(); + + doc?.defaultView?.requestAnimationFrame(() => { if (this.editor) { this.editor.takeSnapshot(); - this.editor.triggerContentChangedEvent(ChangeSource.Drop); + this.editor.triggerPluginEvent(PluginEventType.ContentChanged, { + source: ChangeSource.Drop, + }); } }); }; @@ -194,33 +193,6 @@ class DOMEventPlugin implements PluginWithState { } }; - private onContextMenuEvent = (event: MouseEvent) => { - const allItems: any[] = []; - - // TODO: Remove dependency to ContentSearcher - const searcher = this.editor?.getContentSearcherOfCursor(); - const elementBeforeCursor = searcher?.getInlineElementBefore(); - - let eventTargetNode = event.target as Node; - if (event.button != 2 && elementBeforeCursor) { - eventTargetNode = elementBeforeCursor.getContainerNode(); - } - this.state.contextMenuProviders.forEach(provider => { - const items = provider.getContextMenuItems(eventTargetNode) ?? []; - if (items?.length > 0) { - if (allItems.length > 0) { - allItems.push(null); - } - - allItems.push(...items); - } - }); - this.editor?.triggerPluginEvent(PluginEventType.ContextMenu, { - rawEvent: event, - items: allItems, - }); - }; - private onCompositionStart = () => { this.state.isInIME = true; }; @@ -240,10 +212,6 @@ class DOMEventPlugin implements PluginWithState { } } -function isContextMenuProvider(source: EditorPlugin): source is ContextMenuProvider { - return !!(>source)?.getContextMenuItems; -} - /** * @internal * Create a new instance of DOMEventPlugin. diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts index 6d9c2629e03..a5fe63ca299 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts @@ -1,3 +1,4 @@ +import { EntityOperation as LegacyEntityOperation, PluginEventType } from 'roosterjs-editor-types'; import { findAllEntities } from './utils/findAllEntities'; import { transformColor } from '../publicApi/color/transformColor'; import { @@ -8,7 +9,6 @@ import { isEntityElement, parseEntityClassName, } from 'roosterjs-content-model-dom'; -import { EntityOperation as LegacyEntityOperation, PluginEventType } from 'roosterjs-editor-types'; import type { ChangedEntity, ContentModelContentChangedEvent, @@ -43,7 +43,7 @@ const EntityOperationMap: Record = { * Entity Plugin helps handle all operations related to an entity and generate entity specified events */ class EntityPlugin implements PluginWithState { - private editor: (IEditor & IStandaloneEditor) | null = null; + private editor: IStandaloneEditor | null = null; private state: EntityPluginState; /** @@ -109,12 +109,12 @@ class EntityPlugin implements PluginWithState { } } - private handleMouseUpEvent(editor: IEditor & IStandaloneEditor, event: PluginMouseUpEvent) { + private handleMouseUpEvent(editor: IStandaloneEditor, event: PluginMouseUpEvent) { const { rawEvent, isClicking } = event; let node: Node | null = rawEvent.target as Node; if (isClicking && this.editor) { - while (node && this.editor.contains(node)) { + while (node && this.editor.isNodeInEditor(node)) { if (isEntityElement(node)) { this.triggerEvent(editor, node as HTMLElement, 'click', rawEvent); break; @@ -125,10 +125,7 @@ class EntityPlugin implements PluginWithState { } } - private handleContentChangedEvent( - editor: IStandaloneEditor & IEditor, - event?: ContentChangedEvent - ) { + private handleContentChangedEvent(editor: IStandaloneEditor, event?: ContentChangedEvent) { const cmEvent = event as ContentModelContentChangedEvent | undefined; const modifiedEntities: ChangedEntity[] = cmEvent?.changedEntities ?? this.getChangedEntities(editor); @@ -235,10 +232,7 @@ class EntityPlugin implements PluginWithState { return result; } - private handleExtractContentWithDomEvent( - editor: IEditor & IStandaloneEditor, - root: HTMLElement - ) { + private handleExtractContentWithDomEvent(editor: IStandaloneEditor, root: HTMLElement) { getAllEntityWrappers(root).forEach(element => { element.removeAttribute('contentEditable'); @@ -247,7 +241,7 @@ class EntityPlugin implements PluginWithState { } private triggerEvent( - editor: IEditor & IStandaloneEditor, + editor: IStandaloneEditor, wrapper: HTMLElement, operation: EntityOperation, rawEvent?: Event, diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts index 79a05aac573..daac254a6b8 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts @@ -24,7 +24,7 @@ const DefaultBackColor = '#ffffff'; * Lifecycle plugin handles editor initialization and disposing */ class LifecyclePlugin implements PluginWithState { - private editor: (IStandaloneEditor & IEditor) | null = null; + private editor: IStandaloneEditor | null = null; private state: LifecyclePluginState; private initialModel: ContentModelDocument; private initializer: (() => void) | null = null; diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index e3792491c42..a6752370240 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -13,7 +13,7 @@ const MouseMiddleButton = 1; const MouseRightButton = 2; class SelectionPlugin implements PluginWithState { - private editor: (IStandaloneEditor & IEditor) | null = null; + private editor: IStandaloneEditor | null = null; private state: SelectionPluginState; private disposer: (() => void) | null = null; @@ -198,7 +198,7 @@ class SelectionPlugin implements PluginWithState { }; private onMouseDownDocument = (event: MouseEvent) => { - if (this.editor && !this.editor.contains(event.target as Node)) { + if (this.editor && !this.editor.isNodeInEditor(event.target as Node)) { this.onBlur(); } }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts index f34d2cec6de..8d4dc7668c9 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts @@ -23,7 +23,7 @@ const Enter = 'Enter'; * Provides snapshot based undo service for Editor */ class UndoPlugin implements PluginWithState { - private editor: (IStandaloneEditor & IEditor) | null = null; + private editor: IStandaloneEditor | null = null; private state: UndoPluginState; /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts index c7cfe090961..56defc297df 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts @@ -1,6 +1,5 @@ import { deleteSelection } from '../../publicApi/selection/deleteSelection'; import { isBlockElement, isNodeOfType, normalizeContentModel } from 'roosterjs-content-model-dom'; -import type { IEditor } from 'roosterjs-editor-types'; import type { ContentModelSegmentFormat, IStandaloneEditor } from 'roosterjs-content-model-types'; /** @@ -10,7 +9,7 @@ import type { ContentModelSegmentFormat, IStandaloneEditor } from 'roosterjs-con * @param defaultFormat The default segment format to apply */ export function applyDefaultFormat( - editor: IStandaloneEditor & IEditor, + editor: IStandaloneEditor, defaultFormat: ContentModelSegmentFormat ) { const selection = editor.getDOMSelection(); @@ -21,7 +20,7 @@ export function applyDefaultFormat( if (posContainer) { let node: Node | null = posContainer; - while (node && editor.contains(node)) { + while (node && editor.isNodeInEditor(node)) { if (isNodeOfType(node, 'ELEMENT_NODE')) { if (node.getAttribute?.('style')) { return; diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts index 5ff1f87ec93..a2704c6bb79 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts @@ -13,7 +13,6 @@ import type { StandaloneEditorCorePlugins, StandaloneEditorOptions, UnportedCoreApiMap, - UnportedCorePluginState, } from 'roosterjs-content-model-types'; /** @@ -25,7 +24,6 @@ export function createStandaloneEditorCore( contentDiv: HTMLDivElement, options: StandaloneEditorOptions, unportedCoreApiMap: UnportedCoreApiMap, - unportedCorePluginState: UnportedCorePluginState, tempPlugins: EditorPlugin[] ): StandaloneEditorCore { const corePlugins = createStandaloneEditorCorePlugins(options, contentDiv); @@ -54,7 +52,6 @@ export function createStandaloneEditorCore( domToModelSettings: createDomToModelSettings(options), modelToDomSettings: createModelToDomSettings(options), ...getPluginState(corePlugins), - ...unportedCorePluginState, }; } diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index aa8c4fa25af..44c2faa8a8a 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -9,7 +9,7 @@ import * as PPT from 'roosterjs-content-model-plugins/lib/paste/PowerPoint/proce import * as setProcessorF from 'roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; import * as WacComponents from 'roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; -import { BeforePasteEvent, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { BeforePasteEvent, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; @@ -29,7 +29,7 @@ let clipboardData: ClipboardData; const DEFAULT_TIMES_ADD_PARSER_CALLED = 4; describe('Paste ', () => { - let editor: IStandaloneEditor & IEditor; + let editor: IStandaloneEditor; let createContentModel: jasmine.Spy; let focus: jasmine.Spy; let mockedModel: ContentModelDocument; @@ -124,18 +124,14 @@ describe('Paste ', () => { }); it('Execute', () => { - try { - editor.paste(clipboardData); - } catch (e) { - console.log(e); - } + editor.pasteFromClipboard(clipboardData); expect(formatResult).toBeTrue(); expect(mockedModel).toEqual(mockedMergeModel); }); it('Execute | As plain text', () => { - editor.paste(clipboardData, true /* asText */); + editor.pasteFromClipboard(clipboardData, 'asPlainText'); expect(formatResult).toBeTrue(); expect(mockedModel).toEqual(mockedMergeModel); @@ -159,7 +155,7 @@ describe('Paste ', () => { }, }); - editor.paste(clipboardData); + editor.pasteFromClipboard(clipboardData); editor.createContentModel({ processorOverride: { @@ -367,7 +363,7 @@ describe('paste with content model & paste plugin', () => { }); describe('Paste with clipboardData', () => { - let editor: IEditor & IStandaloneEditor = undefined!; + let editor: IStandaloneEditor = undefined!; const ID = 'EDITOR_ID'; beforeEach(() => { @@ -393,7 +389,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = '

Test

'; - editor.paste(clipboardData); + editor.pasteFromClipboard(clipboardData); const model = editor.createContentModel({ processorOverride: { @@ -440,7 +436,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = 'Link'; - editor.paste(clipboardData); + editor.pasteFromClipboard(clipboardData); const model = editor.createContentModel({ processorOverride: { @@ -472,7 +468,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = 'Link'; - editor.paste(clipboardData); + editor.pasteFromClipboard(clipboardData); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index ad027127cde..21ed4713585 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -141,12 +141,19 @@ describe('ContentModelCopyPastePlugin |', () => { getDOMSelection: getDOMSelectionSpy, setDOMSelection: setDOMSelectionSpy, getDocument() { - return document; + return { + createRange: () => document.createRange(), + defaultView: { + requestAnimationFrame: (func: Function) => { + func(); + }, + }, + }; }, isDarkMode: () => { return false; }, - paste: (ar1: any) => { + pasteFromClipboard: (ar1: any) => { pasteSpy(ar1); }, getDarkColorHandler: () => mockedDarkColorHandler, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts index 6d4b534e3f6..a451fe197fe 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts @@ -278,7 +278,7 @@ describe('ContentModelFormatPlugin for default format', () => { contentDiv = document.createElement('div'); editor = ({ - contains: (e: Node) => contentDiv != e && contentDiv.contains(e), + isNodeInEditor: (e: Node) => contentDiv != e && contentDiv.contains(e), getDOMSelection, getPendingFormat: getPendingFormatSpy, cacheContentModel: cacheContentModelSpy, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts index 6a5491f8406..844bcd0ad46 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts @@ -31,7 +31,6 @@ describe('DOMEventPlugin', () => { expect(state).toEqual({ isInIME: false, scrollContainer: div, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -80,7 +79,6 @@ describe('DOMEventPlugin', () => { expect(state).toEqual({ isInIME: false, scrollContainer: divScrollContainer, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -129,7 +127,6 @@ describe('DOMEventPlugin verify event handlers while disallow keyboard event pro expect(eventMap.keyup.beforeDispatch).toBeDefined(); expect(eventMap.input.beforeDispatch).toBeDefined(); expect(eventMap.mousedown).toBeDefined(); - expect(eventMap.contextmenu).toBeDefined(); expect(eventMap.compositionstart).toBeDefined(); expect(eventMap.compositionend).toBeDefined(); expect(eventMap.dragstart).toBeDefined(); @@ -234,7 +231,6 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: 100, mouseDownY: 200, mouseUpEventListerAdded: true, @@ -253,7 +249,6 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: 100, mouseDownY: 200, mouseUpEventListerAdded: true, @@ -270,7 +265,6 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: 100, mouseDownY: 200, mouseUpEventListerAdded: false, @@ -293,7 +287,6 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: 100, mouseDownY: 200, mouseUpEventListerAdded: true, @@ -310,7 +303,6 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: 100, mouseDownY: 200, mouseUpEventListerAdded: false, @@ -325,16 +317,14 @@ describe('DOMEventPlugin handle other event', () => { let triggerPluginEvent: jasmine.Spy; let eventMap: Record; let scrollContainer: HTMLElement; - let getElementAtCursorSpy: jasmine.Spy; - let triggerContentChangedEventSpy: jasmine.Spy; + let addEventListenerSpy: jasmine.Spy; let editor: IEditor & IStandaloneEditor; beforeEach(() => { addEventListener = jasmine.createSpy('addEventListener'); removeEventListener = jasmine.createSpy('.removeEventListener'); triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getElementAtCursorSpy = jasmine.createSpy('getElementAtCursor'); - triggerContentChangedEventSpy = jasmine.createSpy('triggerContentChangedEvent'); + addEventListenerSpy = jasmine.createSpy('addEventListener'); scrollContainer = { addEventListener: () => {}, @@ -351,6 +341,13 @@ describe('DOMEventPlugin handle other event', () => { getDocument: () => ({ addEventListener, removeEventListener, + defaultView: { + requestAnimationFrame: (callback: Function) => { + callback(); + }, + addEventListener: addEventListenerSpy, + removeEventListener: () => {}, + }, }), triggerPluginEvent, getEnvironment: () => ({}), @@ -358,8 +355,6 @@ describe('DOMEventPlugin handle other event', () => { eventMap = map; return jasmine.createSpy('disposer'); }, - getElementAtCursor: getElementAtCursorSpy, - triggerContentChangedEvent: triggerContentChangedEventSpy, }); plugin.initialize(editor); }); @@ -373,7 +368,6 @@ describe('DOMEventPlugin handle other event', () => { expect(plugin.getState()).toEqual({ isInIME: true, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -386,7 +380,6 @@ describe('DOMEventPlugin handle other event', () => { expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -394,22 +387,23 @@ describe('DOMEventPlugin handle other event', () => { expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.CompositionEnd, { rawEvent: mockedEvent, }); + expect(addEventListenerSpy).toHaveBeenCalledTimes(2); }); it('Trigger onDragStart event', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedEvent = { preventDefault: preventDefaultSpy, + target: { + nodeType: Node.ELEMENT_NODE, + isContentEditable: true, + }, } as any; - getElementAtCursorSpy.and.returnValue({ - isContentEditable: true, - }); eventMap.dragstart.beforeDispatch(mockedEvent); expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -423,16 +417,16 @@ describe('DOMEventPlugin handle other event', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedEvent = { preventDefault: preventDefaultSpy, + target: { + nodeType: Node.ELEMENT_NODE, + isContentEditable: false, + }, } as any; - getElementAtCursorSpy.and.returnValue({ - isContentEditable: false, - }); eventMap.dragstart.beforeDispatch(mockedEvent); expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, @@ -444,46 +438,19 @@ describe('DOMEventPlugin handle other event', () => { it('Trigger onDrop event', () => { const takeSnapshotSpy = jasmine.createSpy('takeSnapshot'); - editor.runAsync = (callback: Function) => callback(editor); editor.takeSnapshot = takeSnapshotSpy; eventMap.drop.beforeDispatch(); expect(plugin.getState()).toEqual({ isInIME: false, scrollContainer: scrollContainer, - contextMenuProviders: [], mouseDownX: null, mouseDownY: null, mouseUpEventListerAdded: false, }); expect(takeSnapshotSpy).toHaveBeenCalledWith(); - expect(triggerContentChangedEventSpy).toHaveBeenCalledWith(ChangeSource.Drop); - }); - - it('Trigger contextmenu event, skip reselect', () => { - editor.getContentSearcherOfCursor = () => null!; - const state = plugin.getState(); - const mockedItems1 = ['Item1', 'Item2']; - const mockedItems2 = ['Item3', 'Item4']; - - state.contextMenuProviders = [ - { - getContextMenuItems: () => mockedItems1, - } as any, - { - getContextMenuItems: () => mockedItems2, - } as any, - ]; - - const mockedEvent = { - target: {}, - }; - - eventMap.contextmenu.beforeDispatch(mockedEvent); - - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContextMenu, { - rawEvent: mockedEvent, - items: ['Item1', 'Item2', null, 'Item3', 'Item4'], + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { + source: ChangeSource.Drop, }); }); }); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts index 1cb5ce1dab0..0d10f8d8388 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts @@ -17,7 +17,7 @@ describe('EntityPlugin', () => { let createContentModelSpy: jasmine.Spy; let triggerPluginEventSpy: jasmine.Spy; let isDarkModeSpy: jasmine.Spy; - let containsSpy: jasmine.Spy; + let isNodeInEditorSpy: jasmine.Spy; let transformColorSpy: jasmine.Spy; let mockedDarkColorHandler: DarkColorHandler; @@ -25,7 +25,7 @@ describe('EntityPlugin', () => { createContentModelSpy = jasmine.createSpy('createContentModel'); triggerPluginEventSpy = jasmine.createSpy('triggerPluginEvent'); isDarkModeSpy = jasmine.createSpy('isDarkMode'); - containsSpy = jasmine.createSpy('contains'); + isNodeInEditorSpy = jasmine.createSpy('isNodeInEditor'); transformColorSpy = spyOn(transformColor, 'transformColor'); mockedDarkColorHandler = 'DARKCOLORHANDLER' as any; @@ -33,7 +33,7 @@ describe('EntityPlugin', () => { createContentModel: createContentModelSpy, triggerPluginEvent: triggerPluginEventSpy, isDarkMode: isDarkModeSpy, - contains: containsSpy, + isNodeInEditor: isNodeInEditorSpy, getDarkColorHandler: () => mockedDarkColorHandler, } as any; plugin = createEntityPlugin(); @@ -561,7 +561,7 @@ describe('EntityPlugin', () => { target: mockedNode, } as any; - containsSpy.and.returnValue(true); + isNodeInEditorSpy.and.returnValue(true); plugin.onPluginEvent({ eventType: PluginEventType.MouseUp, @@ -581,7 +581,7 @@ describe('EntityPlugin', () => { target: mockedNode, } as any; - containsSpy.and.returnValue(true); + isNodeInEditorSpy.and.returnValue(true); spyOn(entityUtils, 'isEntityElement').and.returnValue(true); plugin.onPluginEvent({ @@ -617,7 +617,7 @@ describe('EntityPlugin', () => { target: mockedNode2, } as any; - containsSpy.and.returnValue(true); + isNodeInEditorSpy.and.returnValue(true); spyOn(entityUtils, 'isEntityElement').and.callFake(node => node == mockedNode1); plugin.onPluginEvent({ @@ -649,7 +649,7 @@ describe('EntityPlugin', () => { target: mockedNode, } as any; - containsSpy.and.returnValue(true); + isNodeInEditorSpy.and.returnValue(true); spyOn(entityUtils, 'isEntityElement').and.returnValue(true); plugin.onPluginEvent({ diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyDefaultFormatTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyDefaultFormatTest.ts index c02b40d93b4..3c0b2050ddd 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyDefaultFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyDefaultFormatTest.ts @@ -1,7 +1,6 @@ import * as deleteSelection from '../../../lib/publicApi/selection/deleteSelection'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import { applyDefaultFormat } from '../../../lib/corePlugin/utils/applyDefaultFormat'; -import { IEditor } from 'roosterjs-editor-types'; import { ContentModelDocument, ContentModelFormatter, @@ -21,7 +20,7 @@ import { } from 'roosterjs-content-model-dom'; describe('applyDefaultFormat', () => { - let editor: IStandaloneEditor & IEditor; + let editor: IStandaloneEditor; let getDOMSelectionSpy: jasmine.Spy; let formatContentModelSpy: jasmine.Spy; let deleteSelectionSpy: jasmine.Spy; @@ -65,7 +64,7 @@ describe('applyDefaultFormat', () => { ); editor = { - contains: () => true, + isNodeInEditor: () => true, getDOMSelection: getDOMSelectionSpy, formatContentModel: formatContentModelSpy, takeSnapshot: takeSnapshotSpy, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts index d05da7ffa3e..01b26ea8e36 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts @@ -1,7 +1,6 @@ import * as iterateSelections from '../../../lib/publicApi/selection/iterateSelections'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import { applyPendingFormat } from '../../../lib/corePlugin/utils/applyPendingFormat'; -import { IEditor } from 'roosterjs-editor-types'; import { ContentModelDocument, ContentModelParagraph, @@ -55,7 +54,7 @@ describe('applyPendingFormat', () => { const editor = ({ formatContentModel: formatContentModelSpy, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); @@ -129,7 +128,7 @@ describe('applyPendingFormat', () => { const editor = ({ formatContentModel: formatContentModelSpy, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); @@ -188,7 +187,7 @@ describe('applyPendingFormat', () => { const formatContentModelSpy = jasmine.createSpy('formatContentModel'); const editor = ({ formatContentModel: formatContentModelSpy, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); @@ -248,7 +247,7 @@ describe('applyPendingFormat', () => { const editor = ({ formatContentModel: formatContentModelSpy, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [text]); @@ -299,7 +298,7 @@ describe('applyPendingFormat', () => { const editor = ({ formatContentModel: formatContentModelSpy, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts new file mode 100644 index 00000000000..570d9be53c1 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts @@ -0,0 +1,110 @@ +import { PluginEventType } from 'roosterjs-editor-types'; +import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; +import type { ContextMenuPluginState } from '../publicTypes/ContextMenuPluginState'; +import type { + ContextMenuProvider, + EditorPlugin, + IEditor, + PluginEvent, + PluginWithState, +} from 'roosterjs-editor-types'; + +/** + * Edit Component helps handle Content edit features + */ +class ContextMenuPlugin implements PluginWithState { + private editor: IEditor | null = null; + private state: ContextMenuPluginState; + private disposer: (() => void) | null = null; + + /** + * Construct a new instance of EditPlugin + * @param options The editor options + */ + constructor(options: ContentModelEditorOptions) { + this.state = { + contextMenuProviders: + options.plugins?.filter>(isContextMenuProvider) || [], + }; + } + + /** + * Get a friendly name of this plugin + */ + getName() { + return 'Edit'; + } + + /** + * Initialize this plugin. This should only be called from Editor + * @param editor Editor instance + */ + initialize(editor: IEditor) { + this.editor = editor; + this.disposer = this.editor.addDomEventHandler('contextmenu', this.onContextMenuEvent); + } + + /** + * Dispose this plugin + */ + dispose() { + this.disposer?.(); + this.disposer = null; + this.editor = null; + } + + /** + * Get plugin state object + */ + getState() { + return this.state; + } + + /** + * Handle events triggered from editor + * @param event PluginEvent object + */ + onPluginEvent(event: PluginEvent) {} + + private onContextMenuEvent = (e: Event) => { + const event = e as MouseEvent; + const allItems: any[] = []; + + // TODO: Remove dependency to ContentSearcher + const searcher = this.editor?.getContentSearcherOfCursor(); + const elementBeforeCursor = searcher?.getInlineElementBefore(); + + let eventTargetNode = event.target as Node; + if (event.button != 2 && elementBeforeCursor) { + eventTargetNode = elementBeforeCursor.getContainerNode(); + } + this.state.contextMenuProviders.forEach(provider => { + const items = provider.getContextMenuItems(eventTargetNode) ?? []; + if (items?.length > 0) { + if (allItems.length > 0) { + allItems.push(null); + } + + allItems.push(...items); + } + }); + this.editor?.triggerPluginEvent(PluginEventType.ContextMenu, { + rawEvent: event, + items: allItems, + }); + }; +} + +function isContextMenuProvider(source: EditorPlugin): source is ContextMenuProvider { + return !!(>source)?.getContextMenuItems; +} + +/** + * @internal + * Create a new instance of EditPlugin. + */ +export function createContextMenuPlugin( + options: ContentModelEditorOptions +): PluginWithState { + return new ContextMenuPlugin(options); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts index 5b7706a9834..77699423ee2 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts @@ -1,8 +1,8 @@ +import { createContextMenuPlugin } from './ContextMenuPlugin'; import { createEditPlugin } from './EditPlugin'; import { createEventTypeTranslatePlugin } from './EventTypeTranslatePlugin'; import { createNormalizeTablePlugin } from './NormalizeTablePlugin'; import type { UnportedCorePlugins } from '../publicTypes/ContentModelCorePlugins'; -import type { UnportedCorePluginState } from 'roosterjs-content-model-types'; import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; /** @@ -19,16 +19,6 @@ export function createCorePlugins(options: ContentModelEditorOptions): UnportedC eventTranslate: map.eventTranslate || createEventTypeTranslatePlugin(), edit: map.edit || createEditPlugin(), normalizeTable: map.normalizeTable || createNormalizeTablePlugin(), - }; -} - -/** - * @internal - * Get plugin state of core plugins - * @param corePlugins ContentModelCorePlugins object - */ -export function getPluginState(corePlugins: UnportedCorePlugins): UnportedCorePluginState { - return { - edit: corePlugins.edit.getState(), + contextMenu: map.contextMenu || createContextMenuPlugin(options), }; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index eb630d5f06f..997cb777f2a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -91,6 +91,7 @@ import type { Snapshot, SnapshotsManager, DOMEventRecord, + PasteType, } from 'roosterjs-content-model-types'; /** @@ -448,9 +449,7 @@ export class ContentModelEditor implements IContentModelEditor { applyCurrentFormat: boolean = false, pasteAsImage: boolean = false ) { - const core = this.getCore(); - core.api.paste( - core, + this.pasteFromClipboard( clipboardData, pasteAsText ? 'asPlainText' @@ -1212,6 +1211,27 @@ export class ContentModelEditor implements IContentModelEditor { return core.api.getVisibleViewport(core); } + /** + * Check if the given DOM node is in editor + * @param node The node to check + */ + isNodeInEditor(node: Node): boolean { + const core = this.getCore(); + + return core.contentDiv.contains(node); + } + + /** + * Paste into editor using a clipboardData object + * @param clipboardData Clipboard data retrieved from clipboard + * @param pasteType Type of paste + */ + pasteFromClipboard(clipboardData: ClipboardData, pasteType: PasteType = 'normal') { + const core = this.getCore(); + + core.api.paste(core, clipboardData, pasteType); + } + /** * @returns the current ContentModelEditorCore object * @throws a standard Error if there's no core object diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts index 6174bdc7958..dae16cdd6dd 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts @@ -1,5 +1,6 @@ import { coreApiMap } from '../coreApi/coreApiMap'; -import { createCorePlugins, getPluginState } from '../corePlugins/createCorePlugins'; +import { createContextMenuPlugin } from '../corePlugins/ContextMenuPlugin'; +import { createCorePlugins } from '../corePlugins/createCorePlugins'; import { createModelFromHtml, createStandaloneEditorCore } from 'roosterjs-content-model-core'; import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore'; import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; @@ -16,11 +17,11 @@ export function createEditorCore( options: ContentModelEditorOptions ): ContentModelEditorCore { const corePlugins = createCorePlugins(options); - const pluginState = getPluginState(corePlugins); const additionalPlugins: EditorPlugin[] = [ corePlugins.eventTranslate, corePlugins.edit, ...(options.plugins ?? []), + createContextMenuPlugin(options), corePlugins.normalizeTable, ].filter(x => !!x); @@ -40,13 +41,13 @@ export function createEditorCore( contentDiv, options, coreApiMap, - pluginState, additionalPlugins ); const core: ContentModelEditorCore = { ...standaloneEditorCore, - ...pluginState, + edit: corePlugins.edit.getState(), + contextMenu: corePlugins.contextMenu.getState(), zoomScale: zoomScale, sizeTransformer: (size: number) => size / zoomScale, disposeErrorHandler: options.disposeErrorHandler, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/index.ts b/packages-content-model/roosterjs-content-model-editor/lib/index.ts index ac1905956cb..5396d32f028 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/index.ts @@ -4,6 +4,7 @@ export { ContentModelCorePlugins, UnportedCorePlugins, } from './publicTypes/ContentModelCorePlugins'; +export { ContextMenuPluginState } from './publicTypes/ContextMenuPluginState'; export { ContentModelEditor } from './editor/ContentModelEditor'; export { isContentModelEditor } from './editor/isContentModelEditor'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts index 73332c80dfc..2a9f0a9101a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts @@ -1,3 +1,4 @@ +import type { ContextMenuPluginState } from './ContextMenuPluginState'; import type { StandaloneEditorCorePlugins } from 'roosterjs-content-model-types'; import type { EditPluginState, EditorPlugin, PluginWithState } from 'roosterjs-editor-types'; @@ -20,6 +21,11 @@ export interface UnportedCorePlugins { * NormalizeTable plugin makes sure each table in editor has TBODY/THEAD/TFOOT tag around TR tags */ readonly normalizeTable: EditorPlugin; + + /** + * ContextMenu plugin handles Context Menu + */ + readonly contextMenu: PluginWithState; } /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts index 11f1707122f..edd87f2c783 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts @@ -1,6 +1,8 @@ +import type { ContextMenuPluginState } from './ContextMenuPluginState'; import type { CompatibleExperimentalFeatures } from 'roosterjs-editor-types/lib/compatibleTypes'; import type { CustomData, + EditPluginState, EditorPlugin, ExperimentalFeatures, SizeTransformer, @@ -49,4 +51,14 @@ export interface ContentModelEditorCore extends StandaloneEditorCore { * Enabled experimental features */ experimentalFeatures: (ExperimentalFeatures | CompatibleExperimentalFeatures)[]; + + /** + * Unported core plugin state: EditPlugin + */ + edit: EditPluginState; + + /** + * Unported core plugin state: ContextMenuPlugin + */ + contextMenu: ContextMenuPluginState; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContextMenuPluginState.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContextMenuPluginState.ts new file mode 100644 index 00000000000..7b2f58a8d66 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContextMenuPluginState.ts @@ -0,0 +1,11 @@ +import type { ContextMenuProvider } from 'roosterjs-editor-types'; + +/** + * The state object for DOMEventPlugin + */ +export interface ContextMenuPluginState { + /** + * Context menu providers, that can provide context menu items + */ + contextMenuProviders: ContextMenuProvider[]; +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts new file mode 100644 index 00000000000..9df59088b80 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts @@ -0,0 +1,93 @@ +import { ContextMenuPluginState } from '../../lib/publicTypes/ContextMenuPluginState'; +import { createContextMenuPlugin } from '../../lib/corePlugins/ContextMenuPlugin'; +import { IEditor, PluginEventType, PluginWithState } from 'roosterjs-editor-types'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; + +describe('ContextMenu handle other event', () => { + let plugin: PluginWithState; + let addEventListener: jasmine.Spy; + let removeEventListener: jasmine.Spy; + let triggerPluginEvent: jasmine.Spy; + let eventMap: Record; + let getElementAtCursorSpy: jasmine.Spy; + let triggerContentChangedEventSpy: jasmine.Spy; + let editor: IEditor & IStandaloneEditor; + + beforeEach(() => { + addEventListener = jasmine.createSpy('addEventListener'); + removeEventListener = jasmine.createSpy('.removeEventListener'); + triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + getElementAtCursorSpy = jasmine.createSpy('getElementAtCursor'); + triggerContentChangedEventSpy = jasmine.createSpy('triggerContentChangedEvent'); + + editor = ({ + getDocument: () => ({ + addEventListener, + removeEventListener, + }), + triggerPluginEvent, + getEnvironment: () => ({}), + addDomEventHandler: (name: string, handler: Function) => { + eventMap = { + [name]: { + beforeDispatch: handler, + }, + }; + }, + getElementAtCursor: getElementAtCursorSpy, + triggerContentChangedEvent: triggerContentChangedEventSpy, + }); + }); + + afterEach(() => { + plugin.dispose(); + }); + + it('Ctor with parameter', () => { + const mockedPlugin1 = {} as any; + const mockedPlugin2 = { + getContextMenuItems: () => {}, + } as any; + + plugin = createContextMenuPlugin({ + plugins: [mockedPlugin1, mockedPlugin2], + }); + plugin.initialize(editor); + + const state = plugin.getState(); + + expect(state).toEqual({ + contextMenuProviders: [mockedPlugin2], + }); + }); + + it('Trigger contextmenu event, skip reselect', () => { + plugin = createContextMenuPlugin({}); + plugin.initialize(editor); + + editor.getContentSearcherOfCursor = () => null!; + const state = plugin.getState(); + const mockedItems1 = ['Item1', 'Item2']; + const mockedItems2 = ['Item3', 'Item4']; + + state.contextMenuProviders = [ + { + getContextMenuItems: () => mockedItems1, + } as any, + { + getContextMenuItems: () => mockedItems2, + } as any, + ]; + + const mockedEvent = { + target: {}, + }; + + eventMap.contextmenu.beforeDispatch(mockedEvent); + + expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContextMenu, { + rawEvent: mockedEvent, + items: ['Item1', 'Item2', null, 'Item3', 'Item4'], + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts index 5d1496aff1e..f7b2c36f075 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts @@ -1,6 +1,7 @@ import * as ContentModelCachePlugin from 'roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin'; import * as ContentModelCopyPastePlugin from 'roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin'; import * as ContentModelFormatPlugin from 'roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin'; +import * as ContextMenuPlugin from '../../lib/corePlugins/ContextMenuPlugin'; import * as createStandaloneEditorDefaultSettings from 'roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings'; import * as DOMEventPlugin from 'roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin'; import * as EditPlugin from '../../lib/corePlugins/EditPlugin'; @@ -21,6 +22,7 @@ const mockedLifecycleState = 'LIFECYCLESTATE' as any; const mockedUndoState = 'UNDOSTATE' as any; const mockedEntityState = 'ENTITYSTATE' as any; const mockedCopyPasteState = 'COPYPASTESTATE' as any; +const mockedContextMenuState = 'CONTEXTMENU' as any; const mockedCacheState = 'CACHESTATE' as any; const mockedFormatState = 'FORMATSTATE' as any; const mockedSelectionState = 'SELECTION' as any; @@ -34,6 +36,9 @@ const mockedCachePlugin = { const mockedCopyPastePlugin = { getState: () => mockedCopyPasteState, } as any; +const mockedContextMenuPlugin = { + getState: () => mockedContextMenuState, +} as any; const mockedEditPlugin = { getState: () => mockedEditState, } as any; @@ -79,6 +84,9 @@ describe('createEditorCore', () => { mockedCopyPastePlugin ); spyOn(EditPlugin, 'createEditPlugin').and.returnValue(mockedEditPlugin); + spyOn(ContextMenuPlugin, 'createContextMenuPlugin').and.returnValue( + mockedContextMenuPlugin + ); spyOn(UndoPlugin, 'createUndoPlugin').and.returnValue(mockedUndoPlugin); spyOn(DOMEventPlugin, 'createDOMEventPlugin').and.returnValue(mockedDOMEventPlugin); spyOn(SelectionPlugin, 'createSelectionPlugin').and.returnValue(mockedSelectionPlugin); @@ -113,6 +121,7 @@ describe('createEditorCore', () => { mockedEntityPlugin, mockedEventTranslatePlugin, mockedEditPlugin, + mockedContextMenuPlugin, mockedNormalizeTablePlugin, mockedUndoPlugin, mockedLifecyclePlugin, @@ -126,6 +135,7 @@ describe('createEditorCore', () => { cache: mockedCacheState, format: mockedFormatState, selection: mockedSelectionState, + contextMenu: mockedContextMenuState, trustedHTMLHandler: defaultTrustHtmlHandler, zoomScale: 1, sizeTransformer: jasmine.anything(), @@ -173,6 +183,7 @@ describe('createEditorCore', () => { mockedEntityPlugin, mockedEventTranslatePlugin, mockedEditPlugin, + mockedContextMenuPlugin, mockedNormalizeTablePlugin, mockedUndoPlugin, mockedLifecyclePlugin, @@ -186,6 +197,7 @@ describe('createEditorCore', () => { cache: mockedCacheState, format: mockedFormatState, selection: mockedSelectionState, + contextMenu: mockedContextMenuState, trustedHTMLHandler: defaultTrustHtmlHandler, zoomScale: 1, sizeTransformer: jasmine.anything(), diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts index f744b8c4339..eef3ea1d137 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -1,3 +1,5 @@ +import type { PasteType } from '../enum/PasteType'; +import type { ClipboardData } from '../parameter/ClipboardData'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { SnapshotsManager } from '../parameter/SnapshotsManager'; import type { Snapshot } from '../parameter/Snapshot'; @@ -13,7 +15,12 @@ import type { ContentModelFormatter, FormatWithContentModelOptions, } from '../parameter/FormatWithContentModelOptions'; -import type { PluginEventData, PluginEventFromType, PluginEventType } from 'roosterjs-editor-types'; +import type { + DarkColorHandler, + PluginEventData, + PluginEventFromType, + PluginEventType, +} from 'roosterjs-editor-types'; /** * An interface of standalone Content Model editor. @@ -80,8 +87,6 @@ export interface IStandaloneEditor { */ getPendingFormat(): ContentModelSegmentFormat | null; - //#region Editor API copied from legacy editor, will be ported to use Content Model instead - /** * Get whether this editor is disposed * @returns True if editor is disposed, otherwise false @@ -125,6 +130,12 @@ export interface IStandaloneEditor { */ isDarkMode(): boolean; + /** + * Set the dark mode state and transforms the content to match the new state. + * @param isDarkMode The next status of dark mode. True if the editor should be in dark mode, false if not. + */ + setDarkModeState(isDarkMode?: boolean): void; + /** * Get current zoom scale, default value is 1 * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale @@ -156,5 +167,51 @@ export interface IStandaloneEditor { */ attachDomEvent(eventMap: Record): () => void; - //#endregion + /** + * Check if editor is in Shadow Edit mode + */ + isInShadowEdit(): boolean; + + /** + * Make the editor in "Shadow Edit" mode. + * In Shadow Edit mode, all format change will finally be ignored. + * This can be used for building a live preview feature for format button, to allow user + * see format result without really apply it. + * This function can be called repeated. If editor is already in shadow edit mode, we can still + * use this function to do more shadow edit operation. + */ + startShadowEdit(): void; + + /** + * Leave "Shadow Edit" mode, all changes made during shadow edit will be discarded + */ + stopShadowEdit(): void; + + /** + * Check if the given DOM node is in editor + * @param node The node to check + */ + isNodeInEditor(node: Node): boolean; + + /** + * Paste into editor using a clipboardData object + * @param clipboardData Clipboard data retrieved from clipboard + * @param pasteType Type of paste + */ + pasteFromClipboard(clipboardData: ClipboardData, pasteType?: PasteType): void; + + /** + * Get a darkColorHandler object for this editor. + */ + getDarkColorHandler(): DarkColorHandler; + + /** + * Dispose this editor, dispose all plugins and custom data + */ + dispose(): void; + /** + * Check if focus is in editor now + * @returns true if focus is in editor, otherwise false + */ + hasFocus(): boolean; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index 84a635b241a..0aa4dae314e 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -17,10 +17,7 @@ import type { TrustedHTMLHandler, } from 'roosterjs-editor-types'; import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { - StandaloneEditorCorePluginState, - UnportedCorePluginState, -} from '../pluginState/StandaloneEditorPluginState'; +import type { StandaloneEditorCorePluginState } from '../pluginState/StandaloneEditorPluginState'; import type { DOMSelection } from '../selection/DOMSelection'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { DomToModelSettings } from '../context/DomToModelSettings'; @@ -412,9 +409,7 @@ export interface StandaloneCoreApiMap extends PortedCoreApiMap, UnportedCoreApiM /** * Represents the core data structure of a Content Model editor */ -export interface StandaloneEditorCore - extends StandaloneEditorCorePluginState, - UnportedCorePluginState { +export interface StandaloneEditorCore extends StandaloneEditorCorePluginState { /** * The content DIV element of this editor */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 890693dd77e..84d86ed0291 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -226,10 +226,7 @@ export { export { StandaloneEditorCorePlugins } from './editor/StandaloneEditorCorePlugins'; export { ContentModelCachePluginState } from './pluginState/ContentModelCachePluginState'; -export { - StandaloneEditorCorePluginState, - UnportedCorePluginState, -} from './pluginState/StandaloneEditorPluginState'; +export { StandaloneEditorCorePluginState } from './pluginState/StandaloneEditorPluginState'; export { ContentModelFormatPluginState, PendingFormat, diff --git a/packages-content-model/roosterjs-content-model-types/lib/pluginState/DOMEventPluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/DOMEventPluginState.ts index 1c578e0edc5..895fc2da3a1 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/pluginState/DOMEventPluginState.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/pluginState/DOMEventPluginState.ts @@ -1,5 +1,3 @@ -import type { ContextMenuProvider } from 'roosterjs-editor-types'; - /** * The state object for DOMEventPlugin */ @@ -14,11 +12,6 @@ export interface DOMEventPluginState { */ scrollContainer: HTMLElement; - /** - * Context menu providers, that can provide context menu items - */ - contextMenuProviders: ContextMenuProvider[]; - /** * Whether mouse up event handler is added */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts index 81b4865899a..fed7e413701 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts @@ -1,7 +1,6 @@ import type { CopyPastePluginState } from './CopyPastePluginState'; import type { UndoPluginState } from './UndoPluginState'; import type { SelectionPluginState } from './SelectionPluginState'; -import type { EditPluginState } from 'roosterjs-editor-types'; import type { ContentModelCachePluginState } from './ContentModelCachePluginState'; import type { ContentModelFormatPluginState } from './ContentModelFormatPluginState'; import type { DOMEventPluginState } from './DOMEventPluginState'; @@ -53,11 +52,3 @@ export interface StandaloneEditorCorePluginState { */ undo: UndoPluginState; } - -/** - * Temporary core plugin state for Content Model editor (unported part) - * TODO: Port these plugins - */ -export interface UnportedCorePluginState { - edit: EditPluginState; -} From b77293bec652d083e0c6191d18b34f8b2886399c Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Tue, 26 Dec 2023 17:31:30 -0600 Subject: [PATCH 24/64] Fix GetFormatState not returning Font size after paste (#2299) * init * fix build --- .../common/retrieveModelFormatState.ts | 14 +++++++-- .../common/retrieveModelFormatStateTest.ts | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts index 76bf4ab95f7..55e499ce428 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts @@ -1,3 +1,4 @@ +import { parseValueWithUnit } from 'roosterjs-content-model-dom'; import { extractBorderValues, getClosestAncestorBlockGroupIndex, @@ -150,7 +151,13 @@ function retrieveSegmentFormat( mergeValue(result, 'letterSpacing', mergedFormat.letterSpacing, isFirst); mergeValue(result, 'fontName', mergedFormat.fontFamily, isFirst); - mergeValue(result, 'fontSize', mergedFormat.fontSize, isFirst); + mergeValue( + result, + 'fontSize', + mergedFormat.fontSize, + isFirst, + val => parseValueWithUnit(val, undefined, 'pt') + 'pt' + ); mergeValue(result, 'backgroundColor', mergedFormat.backgroundColor, isFirst); mergeValue(result, 'textColor', mergedFormat.textColor, isFirst); mergeValue(result, 'fontWeight', mergedFormat.fontWeight, isFirst); @@ -232,13 +239,14 @@ function mergeValue( format: ContentModelFormatState, key: K, newValue: ContentModelFormatState[K] | undefined, - isFirst: boolean + isFirst: boolean, + parseFn: (val: ContentModelFormatState[K]) => ContentModelFormatState[K] = val => val ) { if (isFirst) { if (newValue !== undefined) { format[key] = newValue; } - } else if (newValue !== format[key]) { + } else if (parseFn(newValue) !== parseFn(format[key])) { delete format[key]; } } diff --git a/packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts index 45d9439fe76..21dc7d91fd5 100644 --- a/packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts @@ -781,4 +781,34 @@ describe('retrieveModelFormatState', () => { canAddImageAltText: false, }); }); + + it('With same format but using px and pt', () => { + const model = createContentModelDocument({}); + const result: ContentModelFormatState = {}; + const para = createParagraph(); + const text1 = createText('test1', { fontSize: '16pt' }); + const text2 = createText('test2', { fontSize: '21.3333px' }); + para.segments.push(text1, text2); + + text1.isSelected = true; + text2.isSelected = true; + + spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + callback([path], undefined, para, [text1, text2]); + return false; + }); + + retrieveModelFormatState(model, null, result); + + expect(result).toEqual({ + isBlockQuote: false, + isBold: false, + isSuperscript: false, + isSubscript: false, + fontSize: '16pt', + isCodeInline: false, + canUnlink: false, + canAddImageAltText: false, + }); + }); }); From 236d0afdfb5f94f80151abaa02040a82d49197de Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 27 Dec 2023 16:11:53 -0800 Subject: [PATCH 25/64] Standalone Editor step 2 (#2292) --- .../lib/editor/DarkColorHandlerImpl.ts | 10 + .../lib/editor/StandaloneEditor.ts | 373 +++++++++ .../lib/editor/createStandaloneEditorCore.ts | 37 +- .../lib/editor/standaloneCoreApiMap.ts | 4 +- .../roosterjs-content-model-core/lib/index.ts | 2 +- .../test/coreApi/pasteTest.ts | 4 +- .../test/editor/StandaloneEditorTest.ts | 732 ++++++++++++++++++ .../editor/createStandaloneEditorCoreTest.ts | 351 +++++++++ ...eateStandaloneEditorDefaultSettingsTest.ts | 129 +++ .../lib/coreApi/coreApiMap.ts | 4 +- .../lib/coreApi/ensureTypeInContainer.ts | 20 +- .../lib/coreApi/getContent.ts | 21 +- .../lib/coreApi/getStyleBasedFormatState.ts | 8 +- .../lib/coreApi/insertNode.ts | 28 +- .../lib/coreApi/setContent.ts | 42 +- .../lib/corePlugins/createCorePlugins.ts | 4 +- .../lib/editor/ContentModelEditor.ts | 385 ++------- .../lib/editor/createEditorCore.ts | 52 +- .../lib/editor/utils/buildRangeEx.ts | 4 +- .../lib/index.ts | 14 +- .../publicTypes/ContentModelCorePlugins.ts | 22 +- .../lib/publicTypes/ContentModelEditorCore.ts | 162 +++- .../lib/publicTypes/IContentModelEditor.ts | 18 +- .../test/editor/ContentModelEditorTest.ts | 9 +- .../test/editor/createEditorCoreTest.ts | 236 +----- .../lib/editor/StandaloneEditorCore.ts | 139 +--- .../lib/editor/StandaloneEditorOptions.ts | 15 + .../lib/index.ts | 7 - 28 files changed, 2028 insertions(+), 804 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorDefaultSettingsTest.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts index ccfe96901bc..253d4a35af2 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/DarkColorHandlerImpl.ts @@ -198,3 +198,13 @@ export class DarkColorHandlerImpl implements DarkColorHandler { } } } + +/** + * @internal + */ +export function createDarkColorHandler( + contentDiv: HTMLElement, + getDarkColor: (color: string) => string +): DarkColorHandler { + return new DarkColorHandlerImpl(contentDiv, getDarkColor); +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts new file mode 100644 index 00000000000..dc1a90734aa --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -0,0 +1,373 @@ +import { ChangeSource } from '../constants/ChangeSource'; +import { createStandaloneEditorCore } from './createStandaloneEditorCore'; +import { PluginEventType } from 'roosterjs-editor-types'; +import { transformColor } from '../publicApi/color/transformColor'; +import type { + DarkColorHandler, + IEditor, + PluginEventData, + PluginEventFromType, +} from 'roosterjs-editor-types'; +import type { CompatiblePluginEventType } from 'roosterjs-editor-types/lib/compatibleTypes'; +import type { + ClipboardData, + ContentModelDocument, + ContentModelFormatter, + ContentModelSegmentFormat, + DOMEventRecord, + DOMSelection, + DomToModelOption, + EditorEnvironment, + FormatWithContentModelOptions, + IStandaloneEditor, + ModelToDomOption, + OnNodeCreated, + PasteType, + Snapshot, + SnapshotsManager, + StandaloneEditorCore, + StandaloneEditorOptions, +} from 'roosterjs-content-model-types'; + +/** + * The standalone editor class based on Content Model + */ +export class StandaloneEditor implements IStandaloneEditor { + private core: StandaloneEditorCore | null = null; + + /** + * Creates an instance of Editor + * @param contentDiv The DIV HTML element which will be the container element of editor + * @param options An optional options object to customize the editor + */ + constructor( + contentDiv: HTMLDivElement, + options: StandaloneEditorOptions = {}, + onBeforeInitializePlugins?: () => void + ) { + this.core = createStandaloneEditorCore(contentDiv, options); + + onBeforeInitializePlugins?.(); + + // TODO: Remove this type cast + const editor: IStandaloneEditor = this; + this.getCore().plugins.forEach(plugin => + plugin.initialize(editor as IStandaloneEditor & IEditor) + ); + } + + /** + * Dispose this editor, dispose all plugins and custom data + */ + dispose() { + const core = this.getCore(); + + for (let i = core.plugins.length - 1; i >= 0; i--) { + const plugin = core.plugins[i]; + + try { + plugin.dispose(); + } catch (e) { + // Cache the error and pass it out, then keep going since dispose should always succeed + core.disposeErrorHandler?.(plugin, e as Error); + } + } + + core.darkColorHandler.reset(); + this.core = null; + } + + /** + * Get whether this editor is disposed + * @returns True if editor is disposed, otherwise false + */ + isDisposed(): boolean { + return !this.core; + } + + /** + * Create Content Model from DOM tree in this editor + * @param option The option to customize the behavior of DOM to Content Model conversion + */ + createContentModel( + option?: DomToModelOption, + selectionOverride?: DOMSelection + ): ContentModelDocument { + const core = this.getCore(); + + return core.api.createContentModel(core, option, selectionOverride); + } + + /** + * Set content with content model + * @param model The content model to set + * @param option Additional options to customize the behavior of Content Model to DOM conversion + * @param onNodeCreated An optional callback that will be called when a DOM node is created + */ + setContentModel( + model: ContentModelDocument, + option?: ModelToDomOption, + onNodeCreated?: OnNodeCreated + ): DOMSelection | null { + const core = this.getCore(); + + return core.api.setContentModel(core, model, option, onNodeCreated); + } + + /** + * Get current running environment, such as if editor is running on Mac + */ + getEnvironment(): EditorEnvironment { + return this.getCore().environment; + } + + /** + * Get current DOM selection + */ + getDOMSelection(): DOMSelection | null { + const core = this.getCore(); + + return core.api.getDOMSelection(core); + } + + /** + * Set DOMSelection into editor content. + * @param selection The selection to set + */ + setDOMSelection(selection: DOMSelection | null) { + const core = this.getCore(); + + core.api.setDOMSelection(core, selection); + } + + /** + * The general API to do format change with Content Model + * It will grab a Content Model for current editor content, and invoke a callback function + * to do format change. Then according to the return value, write back the modified content model into editor. + * If there is cached model, it will be used and updated. + * @param formatter Formatter function, see ContentModelFormatter + * @param options More options, see FormatWithContentModelOptions + */ + formatContentModel( + formatter: ContentModelFormatter, + options?: FormatWithContentModelOptions + ): void { + const core = this.getCore(); + + core.api.formatContentModel(core, formatter, options); + } + + /** + * Get pending format of editor if any, or return null + */ + getPendingFormat(): ContentModelSegmentFormat | null { + return this.getCore().format.pendingFormat?.format ?? null; + } + + /** + * Add a single undo snapshot to undo stack + */ + takeSnapshot(): void { + const core = this.getCore(); + + core.api.addUndoSnapshot(core, false /*canUndoByBackspace*/); + } + + /** + * Restore an undo snapshot into editor + * @param snapshot The snapshot to restore + */ + restoreSnapshot(snapshot: Snapshot): void { + const core = this.getCore(); + + core.api.restoreUndoSnapshot(core, snapshot); + } + + /** + * Get document which contains this editor + * @returns The HTML document which contains this editor + */ + getDocument(): Document { + return this.getCore().contentDiv.ownerDocument; + } + + /** + * Focus to this editor, the selection was restored to where it was before, no unexpected scroll. + */ + focus() { + const core = this.getCore(); + core.api.focus(core); + } + + /** + * Check if focus is in editor now + * @returns true if focus is in editor, otherwise false + */ + hasFocus(): boolean { + const core = this.getCore(); + return core.api.hasFocus(core); + } + + /** + * Trigger an event to be dispatched to all plugins + * @param eventType Type of the event + * @param data data of the event with given type, this is the rest part of PluginEvent with the given type + * @param broadcast indicates if the event needs to be dispatched to all plugins + * True means to all, false means to allow exclusive handling from one plugin unless no one wants that + * @returns the event object which is really passed into plugins. Some plugin may modify the event object so + * the result of this function provides a chance to read the modified result + */ + triggerPluginEvent( + eventType: T, + data: PluginEventData, + broadcast: boolean = false + ): PluginEventFromType { + const core = this.getCore(); + const event = ({ + eventType, + ...data, + } as any) as PluginEventFromType; + core.api.triggerEvent(core, event, broadcast); + + return event; + } + + /** + * Attach a DOM event to the editor content DIV + * @param eventMap A map from event name to its handler + */ + attachDomEvent(eventMap: Record): () => void { + const core = this.getCore(); + return core.api.attachDomEvent(core, eventMap); + } + + /** + * Get undo snapshots manager + */ + getSnapshotsManager(): SnapshotsManager { + const core = this.getCore(); + + return core.undo.snapshotsManager; + } + + /** + * Check if the editor is in dark mode + * @returns True if the editor is in dark mode, otherwise false + */ + isDarkMode(): boolean { + return this.getCore().lifecycle.isDarkMode; + } + + /** + * Set the dark mode state and transforms the content to match the new state. + * @param isDarkMode The next status of dark mode. True if the editor should be in dark mode, false if not. + */ + setDarkModeState(isDarkMode?: boolean) { + const core = this.getCore(); + + if (!!isDarkMode != core.lifecycle.isDarkMode) { + transformColor( + core.contentDiv, + true /*includeSelf*/, + isDarkMode ? 'lightToDark' : 'darkToLight', + core.darkColorHandler + ); + + core.api.triggerEvent( + core, + { + eventType: PluginEventType.ContentChanged, + source: isDarkMode + ? ChangeSource.SwitchToDarkMode + : ChangeSource.SwitchToLightMode, + }, + true + ); + } + } + + /** + * Check if editor is in IME input sequence + * @returns True if editor is in IME input sequence, otherwise false + */ + isInIME(): boolean { + return this.getCore().domEvent.isInIME; + } + + /** + * Check if editor is in Shadow Edit mode + */ + isInShadowEdit() { + return !!this.getCore().lifecycle.shadowEditFragment; + } + + /** + * Make the editor in "Shadow Edit" mode. + * In Shadow Edit mode, all format change will finally be ignored. + * This can be used for building a live preview feature for format button, to allow user + * see format result without really apply it. + * This function can be called repeated. If editor is already in shadow edit mode, we can still + * use this function to do more shadow edit operation. + */ + startShadowEdit() { + const core = this.getCore(); + core.api.switchShadowEdit(core, true /*isOn*/); + } + + /** + * Leave "Shadow Edit" mode, all changes made during shadow edit will be discarded + */ + stopShadowEdit() { + const core = this.getCore(); + core.api.switchShadowEdit(core, false /*isOn*/); + } + + /** + * Paste into editor using a clipboardData object + * @param clipboardData Clipboard data retrieved from clipboard + * @param pasteType Type of paste + */ + pasteFromClipboard(clipboardData: ClipboardData, pasteType: PasteType = 'normal') { + const core = this.getCore(); + + core.api.paste(core, clipboardData, pasteType); + } + + /** + * Get a darkColorHandler object for this editor. + */ + getDarkColorHandler(): DarkColorHandler { + return this.getCore().darkColorHandler; + } + + /** + * Check if the given DOM node is in editor + * @param node The node to check + */ + isNodeInEditor(node: Node): boolean { + const core = this.getCore(); + + return core.contentDiv.contains(node); + } + + /** + * Get current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale + * to let editor behave correctly especially for those mouse drag/drop behaviors + * @returns current zoom scale number + */ + getZoomScale(): number { + return this.getCore().zoomScale; + } + + /** + * @returns the current StandaloneEditorCore object + * @throws a standard Error if there's no core object + */ + protected getCore(): StandaloneEditorCore { + if (!this.core) { + throw new Error('Editor is already disposed'); + } + return this.core; + } +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts index a2704c6bb79..5161773dca2 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts @@ -1,37 +1,34 @@ +import { createDarkColorHandler } from './DarkColorHandlerImpl'; import { createStandaloneEditorCorePlugins } from '../corePlugin/createStandaloneEditorCorePlugins'; -import { DarkColorHandlerImpl } from './DarkColorHandlerImpl'; import { standaloneCoreApiMap } from './standaloneCoreApiMap'; import { createDomToModelSettings, createModelToDomSettings, } from './createStandaloneEditorDefaultSettings'; -import type { EditorPlugin } from 'roosterjs-editor-types'; import type { EditorEnvironment, StandaloneEditorCore, StandaloneEditorCorePluginState, StandaloneEditorCorePlugins, StandaloneEditorOptions, - UnportedCoreApiMap, } from 'roosterjs-content-model-types'; /** + * @internal * A temporary function to create Standalone Editor core * @param contentDiv Editor content DIV * @param options Editor options */ export function createStandaloneEditorCore( contentDiv: HTMLDivElement, - options: StandaloneEditorOptions, - unportedCoreApiMap: UnportedCoreApiMap, - tempPlugins: EditorPlugin[] + options: StandaloneEditorOptions ): StandaloneEditorCore { const corePlugins = createStandaloneEditorCorePlugins(options, contentDiv); return { contentDiv, - api: { ...standaloneCoreApiMap, ...unportedCoreApiMap, ...options.coreApiOverride }, - originalApi: { ...standaloneCoreApiMap, ...unportedCoreApiMap }, + api: { ...standaloneCoreApiMap, ...options.coreApiOverride }, + originalApi: { ...standaloneCoreApiMap }, plugins: [ corePlugins.cache, corePlugins.format, @@ -39,12 +36,12 @@ export function createStandaloneEditorCore( corePlugins.domEvent, corePlugins.selection, corePlugins.entity, - ...tempPlugins, + ...(options.plugins ?? []).filter(x => !!x), corePlugins.undo, corePlugins.lifecycle, ], - environment: createEditorEnvironment(), - darkColorHandler: new DarkColorHandlerImpl( + environment: createEditorEnvironment(contentDiv), + darkColorHandler: createDarkColorHandler( contentDiv, options.getDarkColor ?? getDarkColorFallback ), @@ -52,15 +49,18 @@ export function createStandaloneEditorCore( domToModelSettings: createDomToModelSettings(options), modelToDomSettings: createModelToDomSettings(options), ...getPluginState(corePlugins), + disposeErrorHandler: options.disposeErrorHandler, + zoomScale: (options.zoomScale ?? -1) > 0 ? options.zoomScale! : 1, }; } -function createEditorEnvironment(): EditorEnvironment { - // It is ok to use global window here since the environment should always be the same for all windows in one session - const userAgent = window.navigator.userAgent; +function createEditorEnvironment(contentDiv: HTMLElement): EditorEnvironment { + const navigator = contentDiv.ownerDocument.defaultView?.navigator; + const userAgent = navigator?.userAgent ?? ''; + const appVersion = navigator?.appVersion ?? ''; return { - isMac: window.navigator.appVersion.indexOf('Mac') != -1, + isMac: appVersion.indexOf('Mac') != -1, isAndroid: /android/i.test(userAgent), isSafari: userAgent.indexOf('Safari') >= 0 && @@ -89,7 +89,10 @@ function getPluginState(corePlugins: StandaloneEditorCorePlugins): StandaloneEdi }; } -// A fallback function, always return original color -function getDarkColorFallback(color: string) { +/** + * @internal Export for test only + * A fallback function, always return original color + */ +export function getDarkColorFallback(color: string) { return color; } diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts index e5bd01e83d3..fadf02808ff 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/standaloneCoreApiMap.ts @@ -13,13 +13,13 @@ import { setContentModel } from '../coreApi/setContentModel'; import { setDOMSelection } from '../coreApi/setDOMSelection'; import { switchShadowEdit } from '../coreApi/switchShadowEdit'; import { triggerEvent } from '../coreApi/triggerEvent'; -import type { PortedCoreApiMap } from 'roosterjs-content-model-types'; +import type { StandaloneCoreApiMap } from 'roosterjs-content-model-types'; /** * @internal * Core API map for Standalone Content Model Editor */ -export const standaloneCoreApiMap: PortedCoreApiMap = { +export const standaloneCoreApiMap: StandaloneCoreApiMap = { createContentModel: createContentModel, createEditorContext: createEditorContext, formatContentModel: formatContentModel, diff --git a/packages-content-model/roosterjs-content-model-core/lib/index.ts b/packages-content-model/roosterjs-content-model-core/lib/index.ts index 54569e3f00d..feafe9f9dff 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/index.ts @@ -57,5 +57,5 @@ export { BulletListType } from './constants/BulletListType'; export { NumberingListType } from './constants/NumberingListType'; export { TableBorderFormat } from './constants/TableBorderFormat'; -export { createStandaloneEditorCore } from './editor/createStandaloneEditorCore'; +export { StandaloneEditor } from './editor/StandaloneEditor'; export { createSnapshotsManager } from './editor/SnapshotsManagerImpl'; diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 44c2faa8a8a..6dfddfdfcd7 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -108,10 +108,12 @@ describe('Paste ', () => { coreApiOverride: { focus, createContentModel, - getContent, getVisibleViewport, formatContentModel, }, + legacyCoreApiOverride: { + getContent, + }, }); spyOn(editor, 'getDocument').and.callThrough(); diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts new file mode 100644 index 00000000000..9c7fd7a93e9 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts @@ -0,0 +1,732 @@ +import * as createStandaloneEditorCore from '../../lib/editor/createStandaloneEditorCore'; +import * as transformColor from '../../lib/publicApi/color/transformColor'; +import { ChangeSource } from '../../lib/constants/ChangeSource'; +import { PluginEventType } from 'roosterjs-editor-types'; +import { StandaloneEditor } from '../../lib/editor/StandaloneEditor'; + +describe('StandaloneEditor', () => { + let createEditorCoreSpy: jasmine.Spy; + + beforeEach(() => { + createEditorCoreSpy = spyOn( + createStandaloneEditorCore, + 'createStandaloneEditorCore' + ).and.callThrough(); + }); + + it('ctor and dispose, no options', () => { + const div = document.createElement('div'); + const editor = new StandaloneEditor(div); + + expect(createEditorCoreSpy).toHaveBeenCalledWith(div, {}); + expect(editor.isDisposed()).toBeFalse(); + expect(editor.getDocument()).toBe(document); + expect(editor.isDarkMode()).toBeFalse(); + expect(editor.isInIME()).toBeFalse(); + expect(editor.isInShadowEdit()).toBeFalse(); + + editor.dispose(); + + expect(editor.isDisposed()).toBeTrue(); + expect(() => { + editor.focus(); + }).toThrow(); + }); + + it('ctor and dispose, with options', () => { + const div = document.createElement('div'); + const initSpy1 = jasmine.createSpy('init1'); + const initSpy2 = jasmine.createSpy('init2'); + const disposeSpy1 = jasmine.createSpy('dispose1'); + const disposeSpy2 = jasmine.createSpy('dispose2').and.throwError('test'); + const mockedPlugin1 = { + initialize: initSpy1, + dispose: disposeSpy1, + } as any; + const mockedPlugin2 = { + initialize: initSpy2, + dispose: disposeSpy2, + } as any; + + const disposeErrorHandlerSpy = jasmine.createSpy('disposeErrorHandler'); + const options = { + plugins: [mockedPlugin1, mockedPlugin2], + disposeErrorHandler: disposeErrorHandlerSpy, + inDarkMode: true, + }; + + const editor = new StandaloneEditor(div, options); + + expect(createEditorCoreSpy).toHaveBeenCalledWith(div, options); + expect(editor.isDisposed()).toBeFalse(); + expect(editor.getDocument()).toBe(document); + expect(editor.isDarkMode()).toBeTrue(); + expect(editor.isInIME()).toBeFalse(); + expect(editor.isInShadowEdit()).toBeFalse(); + + expect(initSpy1).toHaveBeenCalledWith(editor); + expect(initSpy2).toHaveBeenCalledWith(editor); + expect(initSpy1).toHaveBeenCalledBefore(initSpy2); + expect(disposeSpy1).not.toHaveBeenCalled(); + expect(disposeSpy2).not.toHaveBeenCalled(); + + editor.dispose(); + + expect(editor.isDisposed()).toBeTrue(); + expect(() => { + editor.focus(); + }).toThrow(); + + expect(disposeSpy1).toHaveBeenCalled(); + expect(disposeSpy2).toHaveBeenCalled(); + expect(disposeSpy2).toHaveBeenCalledBefore(disposeSpy1); + expect(disposeErrorHandlerSpy).toHaveBeenCalledWith(mockedPlugin2, new Error('test')); + }); + + it('createContentModel', () => { + const div = document.createElement('div'); + const mockedModel = 'MODEL' as any; + const createContentModelSpy = jasmine + .createSpy('createContentModel') + .and.returnValue(mockedModel); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + createContentModel: createContentModelSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const model1 = editor.createContentModel(); + + expect(model1).toBe(mockedModel); + expect(createContentModelSpy).toHaveBeenCalledWith(mockedCore, undefined, undefined); + + const mockedOptions = 'OPTIONS' as any; + const selectionOverride = 'SELECTION' as any; + + const model2 = editor.createContentModel(mockedOptions, selectionOverride); + + expect(model2).toBe(mockedModel); + expect(createContentModelSpy).toHaveBeenCalledWith( + mockedCore, + mockedOptions, + selectionOverride + ); + + editor.dispose(); + expect(() => editor.createContentModel()).toThrow(); + }); + + it('setContentModel', () => { + const div = document.createElement('div'); + const setContentModelSpy = jasmine.createSpy('setContentModel'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + setContentModel: setContentModelSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const mockedModel = 'MODEL' as any; + const editor = new StandaloneEditor(div); + + editor.setContentModel(mockedModel); + + expect(setContentModelSpy).toHaveBeenCalledWith( + mockedCore, + mockedModel, + undefined, + undefined + ); + + const mockedOptions = 'OPTIONS' as any; + const mockedOnNodeCreated = 'ONNODECREATED' as any; + + editor.setContentModel(mockedModel, mockedOptions, mockedOnNodeCreated); + + expect(setContentModelSpy).toHaveBeenCalledWith( + mockedCore, + mockedModel, + mockedOptions, + mockedOnNodeCreated + ); + + editor.dispose(); + expect(() => editor.setContentModel(mockedModel)).toThrow(); + }); + + it('getEnvironment', () => { + const div = document.createElement('div'); + const mockedEnvironment = 'ENVIRONMENT' as any; + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + environment: mockedEnvironment, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const result = editor.getEnvironment(); + + expect(result).toBe(mockedEnvironment); + + editor.dispose(); + expect(() => editor.getEnvironment()).toThrow(); + }); + + it('getDOMSelection', () => { + const div = document.createElement('div'); + const mockedSelection = 'SELECTION' as any; + const getDOMSelectionSpy = jasmine + .createSpy('getDOMSelection') + .and.returnValue(mockedSelection); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + getDOMSelection: getDOMSelectionSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const result = editor.getDOMSelection(); + + expect(result).toBe(mockedSelection); + expect(getDOMSelectionSpy).toHaveBeenCalledWith(mockedCore); + + editor.dispose(); + expect(() => editor.getDOMSelection()).toThrow(); + }); + + it('setDOMSelection', () => { + const div = document.createElement('div'); + const mockedSelection = 'SELECTION' as any; + const setDOMSelectionSpy = jasmine.createSpy('setDOMSelection'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + setDOMSelection: setDOMSelectionSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + editor.setDOMSelection(null); + + expect(setDOMSelectionSpy).toHaveBeenCalledWith(mockedCore, null); + + editor.setDOMSelection(mockedSelection); + + expect(setDOMSelectionSpy).toHaveBeenCalledWith(mockedCore, mockedSelection); + + editor.dispose(); + expect(() => editor.setDOMSelection(null)).toThrow(); + }); + + it('formatContentModel', () => { + const div = document.createElement('div'); + const mockedFormatter = 'FORMATTER' as any; + const mockedOptions = 'OPTIONS' as any; + const formatContentModelSpy = jasmine.createSpy('formatContentModel'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + formatContentModel: formatContentModelSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + editor.formatContentModel(mockedFormatter); + + expect(formatContentModelSpy).toHaveBeenCalledWith(mockedCore, mockedFormatter, undefined); + + editor.formatContentModel(mockedFormatter, mockedOptions); + + expect(formatContentModelSpy).toHaveBeenCalledWith( + mockedCore, + mockedFormatter, + mockedOptions + ); + + editor.dispose(); + expect(() => editor.formatContentModel(mockedFormatter)).toThrow(); + }); + + it('getPendingFormat', () => { + const div = document.createElement('div'); + const mockedFormat = 'FORMAT' as any; + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + format: {}, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const result1 = editor.getPendingFormat(); + + expect(result1).toBeNull(); + + mockedCore.format.pendingFormat = { + format: mockedFormat, + }; + const result2 = editor.getPendingFormat(); + + expect(result2).toBe(mockedFormat); + + editor.dispose(); + expect(() => editor.getPendingFormat()).toThrow(); + }); + + it('formatContentModel', () => { + const div = document.createElement('div'); + const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + addUndoSnapshot: addUndoSnapshotSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + editor.takeSnapshot(); + + expect(addUndoSnapshotSpy).toHaveBeenCalledWith(mockedCore, false); + + editor.dispose(); + expect(() => editor.takeSnapshot()).toThrow(); + }); + + it('restoreSnapshot', () => { + const div = document.createElement('div'); + const mockedSnapshot = 'SNAPSHOT' as any; + const restoreUndoSnapshotSpy = jasmine.createSpy('restoreUndoSnapshot'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + restoreUndoSnapshot: restoreUndoSnapshotSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + editor.restoreSnapshot(mockedSnapshot); + + expect(restoreUndoSnapshotSpy).toHaveBeenCalledWith(mockedCore, mockedSnapshot); + + editor.dispose(); + expect(() => editor.restoreSnapshot(mockedSnapshot)).toThrow(); + }); + + it('focus', () => { + const div = document.createElement('div'); + const focusSpy = jasmine.createSpy('focus'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + focus: focusSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + editor.focus(); + expect(focusSpy).toHaveBeenCalledWith(mockedCore); + + editor.dispose(); + + expect(() => editor.focus()).toThrow(); + }); + + it('hasFocus', () => { + const div = document.createElement('div'); + const mockedResult = 'RESULT' as any; + const hasFocusSpy = jasmine.createSpy('hasFocus').and.returnValue(mockedResult); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + hasFocus: hasFocusSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const result = editor.hasFocus(); + + expect(result).toBe(mockedResult); + expect(hasFocusSpy).toHaveBeenCalledWith(mockedCore); + + editor.dispose(); + + expect(() => editor.hasFocus()).toThrow(); + }); + + it('triggerPluginEvent', () => { + const div = document.createElement('div'); + const mockedEventData = { + event: 'Mocked', + } as any; + const triggerEventSpy = jasmine.createSpy('triggerEvent').and.callFake((core, data) => { + data.a = 'b'; + }); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + triggerEvent: triggerEventSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + const mockedEventType = 'EVENTTYPE' as any; + + const result = editor.triggerPluginEvent(mockedEventType, mockedEventData, true); + + expect(result).toEqual({ + eventType: mockedEventType, + event: 'Mocked', + a: 'b', + } as any); + expect(triggerEventSpy).toHaveBeenCalledWith( + mockedCore, + { + eventType: mockedEventType, + event: 'Mocked', + a: 'b', + } as any, + true + ); + + editor.dispose(); + + expect(() => editor.triggerPluginEvent(mockedEventType, mockedEventData, true)).toThrow(); + }); + + it('attachDomEvent', () => { + const div = document.createElement('div'); + const mockedDisposer = 'DISPOSER' as any; + const attachDomEventSpy = jasmine + .createSpy('attachDomEvent') + .and.returnValue(mockedDisposer); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + attachDomEvent: attachDomEventSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const mockedEventMap = 'EVENTMAP' as any; + const editor = new StandaloneEditor(div); + + const result = editor.attachDomEvent(mockedEventMap); + + expect(result).toBe(mockedDisposer); + expect(attachDomEventSpy).toHaveBeenCalledWith(mockedCore, mockedEventMap); + + editor.dispose(); + + expect(() => editor.attachDomEvent(mockedEventMap)).toThrow(); + }); + + it('getSnapshotsManager', () => { + const div = document.createElement('div'); + const mockedSnapshotManager = 'MANAGER' as any; + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + undo: { + snapshotsManager: mockedSnapshotManager, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const result = editor.getSnapshotsManager(); + + expect(result).toBe(mockedSnapshotManager); + + editor.dispose(); + + expect(() => editor.getSnapshotsManager()).toThrow(); + }); + + it('shadow edit', () => { + const div = document.createElement('div'); + const switchShadowEditSpy = jasmine + .createSpy('switchShadowEdit') + .and.callFake((core, isOn) => { + mockedCore.lifecycle.shadowEditFragment = isOn; + }); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + lifecycle: {}, + api: { + switchShadowEdit: switchShadowEditSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + expect(editor.isInShadowEdit()).toBeFalse(); + + editor.startShadowEdit(); + + expect(editor.isInShadowEdit()).toBeTrue(); + expect(switchShadowEditSpy).toHaveBeenCalledTimes(1); + expect(switchShadowEditSpy).toHaveBeenCalledWith(mockedCore, true); + + editor.stopShadowEdit(); + + expect(editor.isInShadowEdit()).toBeFalse(); + expect(switchShadowEditSpy).toHaveBeenCalledTimes(2); + expect(switchShadowEditSpy).toHaveBeenCalledWith(mockedCore, false); + + editor.dispose(); + + expect(() => editor.isInShadowEdit()).toThrow(); + expect(() => editor.startShadowEdit()).toThrow(); + expect(() => editor.stopShadowEdit()).toThrow(); + }); + + it('pasteFromClipboard', () => { + const div = document.createElement('div'); + const pasteSpy = jasmine.createSpy('paste'); + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + api: { + paste: pasteSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const mockedClipboardData = 'ClipboardData' as any; + const mockedPasteType = 'PASTETYPE' as any; + + const editor = new StandaloneEditor(div); + + editor.pasteFromClipboard(mockedClipboardData); + + expect(pasteSpy).toHaveBeenCalledWith(mockedCore, mockedClipboardData, 'normal'); + + editor.pasteFromClipboard(mockedClipboardData, mockedPasteType); + + expect(pasteSpy).toHaveBeenCalledWith(mockedCore, mockedClipboardData, mockedPasteType); + + editor.dispose(); + + expect(() => editor.pasteFromClipboard(mockedClipboardData)).toThrow(); + }); + + it('getDarkColorHandler', () => { + const div = document.createElement('div'); + const resetSpy = jasmine.createSpy('reset'); + const mockedColorHandler = { + reset: resetSpy, + } as any; + const mockedCore = { + plugins: [], + darkColorHandler: mockedColorHandler, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + const result = editor.getDarkColorHandler(); + + expect(resetSpy).not.toHaveBeenCalled(); + expect(result).toBe(mockedColorHandler); + + editor.dispose(); + + expect(resetSpy).toHaveBeenCalledWith(); + expect(() => editor.getDarkColorHandler()).toThrow(); + }); + + it('isNodeInEditor', () => { + const mockedResult = 'RESULT' as any; + const containsSpy = jasmine.createSpy('contains').and.returnValue(mockedResult); + const div = { + contains: containsSpy, + } as any; + const mockedCore = { + plugins: [], + darkColorHandler: { + reset: () => {}, + }, + contentDiv: div, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + const mockedNode = 'NODE' as any; + + const result = editor.isNodeInEditor(mockedNode); + + expect(result).toBe(mockedResult); + expect(containsSpy).toHaveBeenCalledWith(mockedNode); + + editor.dispose(); + + expect(() => editor.isNodeInEditor(mockedNode)).toThrow(); + }); + + it('dark mode', () => { + const transformColorSpy = spyOn(transformColor, 'transformColor'); + const triggerEventSpy = jasmine.createSpy('triggerEvent').and.callFake((core, event) => { + mockedCore.lifecycle.isDarkMode = event.source == ChangeSource.SwitchToDarkMode; + }); + const div = document.createElement('div'); + const mockedColorHandler = { + reset: () => {}, + } as any; + const mockedCore = { + plugins: [], + darkColorHandler: mockedColorHandler, + contentDiv: div, + lifecycle: { + isDarkMode: false, + }, + api: { + triggerEvent: triggerEventSpy, + }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new StandaloneEditor(div); + + expect(editor.isDarkMode()).toBeFalse(); + + editor.setDarkModeState(false); + + expect(editor.isDarkMode()).toBeFalse(); + expect(transformColorSpy).not.toHaveBeenCalled(); + expect(triggerEventSpy).not.toHaveBeenCalled(); + + editor.setDarkModeState(true); + + expect(editor.isDarkMode()).toBeTrue(); + expect(transformColorSpy).toHaveBeenCalledTimes(1); + expect(transformColorSpy).toHaveBeenCalledWith( + div, + true, + 'lightToDark', + mockedColorHandler + ); + expect(triggerEventSpy).toHaveBeenCalledTimes(1); + expect(triggerEventSpy).toHaveBeenCalledWith( + mockedCore, + { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SwitchToDarkMode, + }, + true + ); + + editor.setDarkModeState(false); + + expect(editor.isDarkMode()).toBeFalse(); + expect(transformColorSpy).toHaveBeenCalledTimes(2); + expect(transformColorSpy).toHaveBeenCalledWith( + div, + true, + 'darkToLight', + mockedColorHandler + ); + expect(triggerEventSpy).toHaveBeenCalledTimes(2); + expect(triggerEventSpy).toHaveBeenCalledWith( + mockedCore, + { + eventType: PluginEventType.ContentChanged, + source: ChangeSource.SwitchToLightMode, + }, + true + ); + + editor.dispose(); + + expect(() => editor.isDarkMode()).toThrow(); + expect(() => editor.setDarkModeState()).toThrow(); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts new file mode 100644 index 00000000000..fcdb91dc7e6 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorCoreTest.ts @@ -0,0 +1,351 @@ +import * as createDefaultSettings from '../../lib/editor/createStandaloneEditorDefaultSettings'; +import * as createStandaloneEditorCorePlugins from '../../lib/corePlugin/createStandaloneEditorCorePlugins'; +import * as DarkColorHandlerImpl from '../../lib/editor/DarkColorHandlerImpl'; +import { + createStandaloneEditorCore, + defaultTrustHtmlHandler, + getDarkColorFallback, +} from '../../lib/editor/createStandaloneEditorCore'; +import { standaloneCoreApiMap } from '../../lib/editor/standaloneCoreApiMap'; +import { StandaloneEditorCore, StandaloneEditorOptions } from 'roosterjs-content-model-types'; + +describe('createEditorCore', () => { + function createMockedPlugin(stateName: string): any { + return { + getState: () => stateName, + }; + } + + const mockedCachePlugin = createMockedPlugin('cache'); + const mockedFormatPlugin = createMockedPlugin('format'); + const mockedCopyPastePlugin = createMockedPlugin('copyPaste'); + const mockedDomEventPlugin = createMockedPlugin('domEvent'); + const mockedLifeCyclePlugin = createMockedPlugin('lifecycle'); + const mockedEntityPlugin = createMockedPlugin('entity'); + const mockedSelectionPlugin = createMockedPlugin('selection'); + const mockedUndoPlugin = createMockedPlugin('undo'); + const mockedPlugins = { + cache: mockedCachePlugin, + format: mockedFormatPlugin, + copyPaste: mockedCopyPastePlugin, + domEvent: mockedDomEventPlugin, + lifecycle: mockedLifeCyclePlugin, + entity: mockedEntityPlugin, + selection: mockedSelectionPlugin, + undo: mockedUndoPlugin, + }; + const mockedDarkColorHandler = 'DARKCOLOR' as any; + const mockedDomToModelSettings = 'DOMTOMODEL' as any; + const mockedModelToDomSettings = 'MODELTODOM' as any; + + beforeEach(() => { + spyOn( + createStandaloneEditorCorePlugins, + 'createStandaloneEditorCorePlugins' + ).and.returnValue(mockedPlugins); + spyOn(DarkColorHandlerImpl, 'createDarkColorHandler').and.returnValue( + mockedDarkColorHandler + ); + spyOn(createDefaultSettings, 'createDomToModelSettings').and.returnValue( + mockedDomToModelSettings + ); + spyOn(createDefaultSettings, 'createModelToDomSettings').and.returnValue( + mockedModelToDomSettings + ); + }); + + function runTest( + contentDiv: HTMLDivElement, + options: StandaloneEditorOptions, + additionalResult: Partial + ) { + const core = createStandaloneEditorCore(contentDiv, options); + + expect(core).toEqual({ + contentDiv: contentDiv, + api: standaloneCoreApiMap, + originalApi: standaloneCoreApiMap, + plugins: [ + mockedCachePlugin, + mockedFormatPlugin, + mockedCopyPastePlugin, + mockedDomEventPlugin, + mockedSelectionPlugin, + mockedEntityPlugin, + mockedUndoPlugin, + mockedLifeCyclePlugin, + ], + environment: { + isMac: false, + isAndroid: false, + isSafari: false, + }, + darkColorHandler: mockedDarkColorHandler, + trustedHTMLHandler: defaultTrustHtmlHandler, + domToModelSettings: mockedDomToModelSettings, + modelToDomSettings: mockedModelToDomSettings, + cache: 'cache' as any, + format: 'format' as any, + copyPaste: 'copyPaste' as any, + domEvent: 'domEvent' as any, + lifecycle: 'lifecycle' as any, + entity: 'entity' as any, + selection: 'selection' as any, + undo: 'undo' as any, + disposeErrorHandler: undefined, + zoomScale: 1, + ...additionalResult, + }); + + expect( + createStandaloneEditorCorePlugins.createStandaloneEditorCorePlugins + ).toHaveBeenCalledWith(options, contentDiv); + expect(createDefaultSettings.createDomToModelSettings).toHaveBeenCalledWith(options); + expect(createDefaultSettings.createModelToDomSettings).toHaveBeenCalledWith(options); + } + + it('No options', () => { + const mockedDiv = { + ownerDocument: {}, + attributes: { + a: 'b', + }, + } as any; + runTest( + mockedDiv, + { + name: 'Options', + } as any, + {} + ); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); + + it('With options', () => { + const mockedDiv = { + ownerDocument: {}, + attributes: { + a: 'b', + }, + } as any; + const mockedPlugin1 = 'P1' as any; + const mockedPlugin2 = 'P2' as any; + const mockedGetDarkColor = 'DARK' as any; + const mockedTrustHtmlHandler = 'TRUST' as any; + const mockedDisposeErrorHandler = 'DISPOSE' as any; + const mockedOptions = { + coreApiOverride: { + a: 'b', + }, + plugins: [mockedPlugin1, null, mockedPlugin2], + getDarkColor: mockedGetDarkColor, + trustedHTMLHandler: mockedTrustHtmlHandler, + disposeErrorHandler: mockedDisposeErrorHandler, + zoomScale: 2, + } as any; + + runTest(mockedDiv, mockedOptions, { + contentDiv: mockedDiv, + api: { ...standaloneCoreApiMap, a: 'b' } as any, + plugins: [ + mockedCachePlugin, + mockedFormatPlugin, + mockedCopyPastePlugin, + mockedDomEventPlugin, + mockedSelectionPlugin, + mockedEntityPlugin, + mockedPlugin1, + mockedPlugin2, + mockedUndoPlugin, + mockedLifeCyclePlugin, + ], + darkColorHandler: mockedDarkColorHandler, + trustedHTMLHandler: mockedTrustHtmlHandler, + disposeErrorHandler: mockedDisposeErrorHandler, + zoomScale: 2, + }); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + mockedGetDarkColor + ); + }); + + it('Invalid zoom scale', () => { + const mockedDiv = { + ownerDocument: {}, + attributes: { + a: 'b', + }, + } as any; + const mockedOptions = { + zoomScale: -1, + } as any; + + runTest(mockedDiv, mockedOptions, {}); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); + + it('Android', () => { + const mockedDiv = { + ownerDocument: { + defaultView: { + navigator: { + userAgent: 'Android', + }, + }, + }, + attributes: { + a: 'b', + }, + } as any; + const mockedOptions = { + zoomScale: -1, + } as any; + + runTest(mockedDiv, mockedOptions, { + environment: { + isMac: false, + isAndroid: true, + isSafari: false, + }, + }); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); + + it('Android+Safari', () => { + const mockedDiv = { + ownerDocument: { + defaultView: { + navigator: { + userAgent: 'Android Safari', + }, + }, + }, + attributes: { + a: 'b', + }, + } as any; + const mockedOptions = { + zoomScale: -1, + } as any; + + runTest(mockedDiv, mockedOptions, { + environment: { + isMac: false, + isAndroid: true, + isSafari: false, + }, + }); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); + + it('Mac', () => { + const mockedDiv = { + ownerDocument: { + defaultView: { + navigator: { + appVersion: 'Mac', + }, + }, + }, + attributes: { + a: 'b', + }, + } as any; + const mockedOptions = { + zoomScale: -1, + } as any; + + runTest(mockedDiv, mockedOptions, { + environment: { + isMac: true, + isAndroid: false, + isSafari: false, + }, + }); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); + + it('Safari', () => { + const mockedDiv = { + ownerDocument: { + defaultView: { + navigator: { + userAgent: 'Safari', + }, + }, + }, + attributes: { + a: 'b', + }, + } as any; + const mockedOptions = { + zoomScale: -1, + } as any; + + runTest(mockedDiv, mockedOptions, { + environment: { + isMac: false, + isAndroid: false, + isSafari: true, + }, + }); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); + + it('Chrome', () => { + const mockedDiv = { + ownerDocument: { + defaultView: { + navigator: { + userAgent: 'Safari Chrome', + }, + }, + }, + attributes: { + a: 'b', + }, + } as any; + const mockedOptions = { + zoomScale: -1, + } as any; + + runTest(mockedDiv, mockedOptions, { + environment: { + isMac: false, + isAndroid: false, + isSafari: false, + }, + }); + + expect(DarkColorHandlerImpl.createDarkColorHandler).toHaveBeenCalledWith( + mockedDiv, + getDarkColorFallback + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorDefaultSettingsTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorDefaultSettingsTest.ts new file mode 100644 index 00000000000..fad2b2eb099 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/createStandaloneEditorDefaultSettingsTest.ts @@ -0,0 +1,129 @@ +import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext'; +import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext'; +import { tablePreProcessor } from '../../lib/override/tablePreProcessor'; +import { + listItemMetadataApplier, + listLevelMetadataApplier, +} from '../../lib/metadata/updateListMetadata'; +import { + createDomToModelSettings, + createModelToDomSettings, +} from '../../lib/editor/createStandaloneEditorDefaultSettings'; + +describe('createDomToModelSettings', () => { + const mockedCalculatedConfig = 'CONFIG' as any; + + beforeEach(() => { + spyOn(createDomToModelContext, 'createDomToModelConfig').and.returnValue( + mockedCalculatedConfig + ); + }); + + it('No options', () => { + const settings = createDomToModelSettings({}); + + expect(settings).toEqual({ + builtIn: { + processorOverride: { + table: tablePreProcessor, + }, + }, + customized: {}, + calculated: mockedCalculatedConfig, + }); + expect(createDomToModelContext.createDomToModelConfig).toHaveBeenCalledWith([ + { + processorOverride: { + table: tablePreProcessor, + }, + }, + {}, + ]); + }); + + it('Has options', () => { + const defaultDomToModelOptions = 'MockedOptions' as any; + const settings = createDomToModelSettings({ + defaultDomToModelOptions: defaultDomToModelOptions, + }); + + expect(settings).toEqual({ + builtIn: { + processorOverride: { + table: tablePreProcessor, + }, + }, + customized: defaultDomToModelOptions, + calculated: mockedCalculatedConfig, + }); + expect(createDomToModelContext.createDomToModelConfig).toHaveBeenCalledWith([ + { + processorOverride: { + table: tablePreProcessor, + }, + }, + defaultDomToModelOptions, + ]); + }); +}); + +describe('createModelToDomSettings', () => { + const mockedCalculatedConfig = 'CONFIG' as any; + + beforeEach(() => { + spyOn(createModelToDomContext, 'createModelToDomConfig').and.returnValue( + mockedCalculatedConfig + ); + }); + + it('No options', () => { + const settings = createModelToDomSettings({}); + + expect(settings).toEqual({ + builtIn: { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + customized: {}, + calculated: mockedCalculatedConfig, + }); + expect(createModelToDomContext.createModelToDomConfig).toHaveBeenCalledWith([ + { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + {}, + ]); + }); + + it('Has options', () => { + const defaultModelToDomOptions = 'MockedOptions' as any; + const settings = createModelToDomSettings({ + defaultModelToDomOptions: defaultModelToDomOptions, + }); + + expect(settings).toEqual({ + builtIn: { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + customized: defaultModelToDomOptions, + calculated: mockedCalculatedConfig, + }); + expect(createModelToDomContext.createModelToDomConfig).toHaveBeenCalledWith([ + { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + defaultModelToDomOptions, + ]); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts index e61538c2781..6d8ebc84677 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/coreApiMap.ts @@ -3,12 +3,12 @@ import { getContent } from './getContent'; import { getStyleBasedFormatState } from './getStyleBasedFormatState'; import { insertNode } from './insertNode'; import { setContent } from './setContent'; -import type { UnportedCoreApiMap } from 'roosterjs-content-model-types'; +import type { ContentModelCoreApiMap } from '../publicTypes/ContentModelEditorCore'; /** * @internal */ -export const coreApiMap: UnportedCoreApiMap = { +export const coreApiMap: ContentModelCoreApiMap = { ensureTypeInContainer, getContent, getStyleBasedFormatState, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/ensureTypeInContainer.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/ensureTypeInContainer.ts index 4f845aa26ea..0a92376edc1 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/ensureTypeInContainer.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/ensureTypeInContainer.ts @@ -8,15 +8,21 @@ import { Position, safeInstanceOf, } from 'roosterjs-editor-dom'; -import type { EnsureTypeInContainer } from 'roosterjs-content-model-types'; +import type { EnsureTypeInContainer } from '../publicTypes/ContentModelEditorCore'; /** * @internal * When typing goes directly under content div, many things can go wrong * We fix it by wrapping it with a div and reposition cursor within the div */ -export const ensureTypeInContainer: EnsureTypeInContainer = (core, position, keyboardEvent) => { - const table = findClosestElementAncestor(position.node, core.contentDiv, 'table'); +export const ensureTypeInContainer: EnsureTypeInContainer = ( + core, + innerCore, + position, + keyboardEvent +) => { + const { contentDiv, api } = innerCore; + const table = findClosestElementAncestor(position.node, contentDiv, 'table'); let td: HTMLElement | null; if (table && (td = table.querySelector('td,th'))) { @@ -24,7 +30,7 @@ export const ensureTypeInContainer: EnsureTypeInContainer = (core, position, key } position = position.normalize(); - const block = getBlockElementAtNode(core.contentDiv, position.node); + const block = getBlockElementAtNode(contentDiv, position.node); let formatNode: HTMLElement | null; if (block) { @@ -46,9 +52,9 @@ export const ensureTypeInContainer: EnsureTypeInContainer = (core, position, key // The fix is to add a DIV wrapping, apply default format and move cursor over formatNode = createElement( KnownCreateElementDataIndex.EmptyLine, - core.contentDiv.ownerDocument + contentDiv.ownerDocument ) as HTMLElement; - core.api.insertNode(core, formatNode, { + core.api.insertNode(core, innerCore, formatNode, { position: ContentPosition.End, updateCursor: false, replaceSelection: false, @@ -61,7 +67,7 @@ export const ensureTypeInContainer: EnsureTypeInContainer = (core, position, key // If this is triggered by a keyboard event, let's select the new position if (keyboardEvent) { - core.api.setDOMSelection(core, { + api.setDOMSelection(innerCore, { type: 'range', range: createRange(new Position(position)), }); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts index bac5e626323..e1994e354cd 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts @@ -7,7 +7,7 @@ import { getTextContent, safeInstanceOf, } from 'roosterjs-editor-dom'; -import type { GetContent } from 'roosterjs-content-model-types'; +import type { GetContent } from '../publicTypes/ContentModelEditorCore'; /** * @internal @@ -16,14 +16,15 @@ import type { GetContent } from 'roosterjs-content-model-types'; * @param mode specify what kind of HTML content to retrieve * @returns HTML string representing current editor content */ -export const getContent: GetContent = (core, mode): string => { +export const getContent: GetContent = (core, innerCore, mode): string => { let content: string | null = ''; const triggerExtractContentEvent = mode == GetContentMode.CleanHTML; const includeSelectionMarker = mode == GetContentMode.RawHTMLWithSelection; + const { lifecycle, contentDiv, api, darkColorHandler } = innerCore; // When there is fragment for shadow edit, always use the cached fragment as document since HTML node in editor // has been changed by uncommitted shadow edit which should be ignored. - const root = core.lifecycle.shadowEditFragment || core.contentDiv; + const root = lifecycle.shadowEditFragment || contentDiv; if (mode == GetContentMode.PlainTextFast) { content = root.textContent; @@ -33,22 +34,22 @@ export const getContent: GetContent = (core, mode): string => { const clonedRoot = cloneNode(root); clonedRoot.normalize(); - const originalRange = core.api.getDOMSelection(core); + const originalRange = api.getDOMSelection(innerCore); const path = - !includeSelectionMarker || core.lifecycle.shadowEditFragment + !includeSelectionMarker || lifecycle.shadowEditFragment ? null : originalRange?.type == 'range' - ? getSelectionPath(core.contentDiv, originalRange.range) + ? getSelectionPath(contentDiv, originalRange.range) : null; const range = path && createRange(clonedRoot, path.start, path.end); - if (core.lifecycle.isDarkMode) { - transformColor(clonedRoot, false /*includeSelf*/, 'darkToLight', core.darkColorHandler); + if (lifecycle.isDarkMode) { + transformColor(clonedRoot, false /*includeSelf*/, 'darkToLight', darkColorHandler); } if (triggerExtractContentEvent) { - core.api.triggerEvent( - core, + api.triggerEvent( + innerCore, { eventType: PluginEventType.ExtractContentWithDom, clonedRoot, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getStyleBasedFormatState.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getStyleBasedFormatState.ts index 230236ca8e0..aa1d8881a58 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getStyleBasedFormatState.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getStyleBasedFormatState.ts @@ -1,6 +1,6 @@ import { contains, getComputedStyles } from 'roosterjs-editor-dom'; import { NodeType } from 'roosterjs-editor-types'; -import type { GetStyleBasedFormatState } from 'roosterjs-content-model-types'; +import type { GetStyleBasedFormatState } from '../publicTypes/ContentModelEditorCore'; /** * @internal @@ -8,7 +8,7 @@ import type { GetStyleBasedFormatState } from 'roosterjs-content-model-types'; * @param core The StandaloneEditorCore objects * @param node The node to get style from */ -export const getStyleBasedFormatState: GetStyleBasedFormatState = (core, node) => { +export const getStyleBasedFormatState: GetStyleBasedFormatState = (core, innerCore, node) => { if (!node) { return {}; } @@ -27,7 +27,7 @@ export const getStyleBasedFormatState: GetStyleBasedFormatState = (core, node) = 'font-weight', ]) : []; - const { contentDiv, darkColorHandler } = core; + const { contentDiv, darkColorHandler, lifecycle } = innerCore; let styleTextColor: string | undefined; let styleBackColor: string | undefined; @@ -46,7 +46,7 @@ export const getStyleBasedFormatState: GetStyleBasedFormatState = (core, node) = node = node.parentNode; } - if (!core.lifecycle.isDarkMode && node == core.contentDiv) { + if (!lifecycle.isDarkMode && node == contentDiv) { styleTextColor = styleTextColor || styles[2]; styleBackColor = styleBackColor || styles[3]; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/insertNode.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/insertNode.ts index 0639aa27799..937eda1bcd0 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/insertNode.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/insertNode.ts @@ -16,7 +16,8 @@ import { splitTextNode, splitParentNode, } from 'roosterjs-editor-dom'; -import type { InsertNode, StandaloneEditorCore } from 'roosterjs-content-model-types'; +import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; +import type { InsertNode } from '../publicTypes/ContentModelEditorCore'; function getInitialRange( core: StandaloneEditorCore, @@ -29,6 +30,7 @@ function getInitialRange( const selection = core.api.getDOMSelection(core); let range = selection?.type == 'range' ? selection.range : null; let rangeToRestore = null; + if (option.position == ContentPosition.Range) { rangeToRestore = range; range = option.range; @@ -42,14 +44,10 @@ function getInitialRange( /** * @internal * Insert a DOM node into editor content - * @param core The StandaloneEditorCore object. No op if null. + * @param core The ContentModelEditorCore object. No op if null. * @param option An insert option object to specify how to insert the node */ -export const insertNode: InsertNode = ( - core: StandaloneEditorCore, - node: Node, - option: InsertOption | null -) => { +export const insertNode: InsertNode = (core, innerCore, node, option) => { option = option || { position: ContentPosition.SelectionStart, insertOnNewLine: false, @@ -57,10 +55,10 @@ export const insertNode: InsertNode = ( replaceSelection: true, insertToRegionRoot: false, }; - const contentDiv = core.contentDiv; + const { contentDiv, api, lifecycle, darkColorHandler } = innerCore; if (option.updateCursor) { - core.api.focus(core); + api.focus(innerCore); } if (option.position == ContentPosition.Outside) { @@ -68,8 +66,8 @@ export const insertNode: InsertNode = ( return true; } - if (core.lifecycle.isDarkMode) { - transformColor(node, true /*includeSelf*/, 'lightToDark', core.darkColorHandler); + if (lifecycle.isDarkMode) { + transformColor(node, true /*includeSelf*/, 'lightToDark', darkColorHandler); } switch (option.position) { @@ -134,7 +132,7 @@ export const insertNode: InsertNode = ( break; case ContentPosition.Range: case ContentPosition.SelectionStart: - let { range, rangeToRestore } = getInitialRange(core, option); + let { range, rangeToRestore } = getInitialRange(innerCore, option); if (!range) { break; } @@ -148,12 +146,12 @@ export const insertNode: InsertNode = ( let blockElement: BlockElement | null; if (option.insertOnNewLine && option.insertToRegionRoot) { - pos = adjustInsertPositionRegionRoot(core, range, pos); + pos = adjustInsertPositionRegionRoot(innerCore, range, pos); } else if ( option.insertOnNewLine && (blockElement = getBlockElementAtNode(contentDiv, pos.normalize().node)) ) { - pos = adjustInsertPositionNewLine(blockElement, core, pos); + pos = adjustInsertPositionNewLine(blockElement, innerCore, pos); } else { pos = adjustInsertPosition(contentDiv, node, pos, range); } @@ -171,7 +169,7 @@ export const insertNode: InsertNode = ( } if (rangeToRestore) { - core.api.setDOMSelection(core, { + api.setDOMSelection(innerCore, { type: 'range', range: rangeToRestore, }); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts index 00eb9d59e60..525b5b0c9bf 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts @@ -3,23 +3,33 @@ import { convertMetadataToDOMSelection } from '../editor/utils/selectionConverte import { extractContentMetadata, restoreContentWithEntityPlaceholder } from 'roosterjs-editor-dom'; import { PluginEventType } from 'roosterjs-editor-types'; import type { ContentMetadata } from 'roosterjs-editor-types'; -import type { SetContent, StandaloneEditorCore } from 'roosterjs-content-model-types'; +import type { SetContent } from '../publicTypes/ContentModelEditorCore'; +import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; /** * @internal * Set HTML content to this editor. All existing content will be replaced. A ContentChanged event will be triggered * if triggerContentChangedEvent is set to true - * @param core The StandaloneEditorCore object + * @param core The ContentModelEditorCore object * @param content HTML content to set in * @param triggerContentChangedEvent True to trigger a ContentChanged event. Default value is true * @param metadata @optional Metadata of the content that helps editor know the selection and color mode. * If not passed, we will treat content as in light mode without selection */ -export const setContent: SetContent = (core, content, triggerContentChangedEvent, metadata) => { +export const setContent: SetContent = ( + core, + innerCore, + content, + triggerContentChangedEvent, + metadata +) => { + const { contentDiv, api, entity, trustedHTMLHandler, lifecycle, darkColorHandler } = innerCore; + let contentChanged = false; - if (core.contentDiv.innerHTML != content) { - core.api.triggerEvent( - core, + + if (innerCore.contentDiv.innerHTML != content) { + api.triggerEvent( + innerCore, { eventType: PluginEventType.BeforeSetContent, newContent: content, @@ -27,36 +37,36 @@ export const setContent: SetContent = (core, content, triggerContentChangedEvent true /*broadcast*/ ); - const entities = core.entity.entityMap; + const entities = entity.entityMap; const html = content || ''; const body = new DOMParser().parseFromString( - core.trustedHTMLHandler?.(html) ?? html, + trustedHTMLHandler?.(html) ?? html, 'text/html' ).body; - restoreContentWithEntityPlaceholder(body, core.contentDiv, entities); + restoreContentWithEntityPlaceholder(body, contentDiv, entities); - const metadataFromContent = extractContentMetadata(core.contentDiv); + const metadataFromContent = extractContentMetadata(contentDiv); metadata = metadata || metadataFromContent; - selectContentMetadata(core, metadata); + selectContentMetadata(innerCore, metadata); contentChanged = true; } - const isDarkMode = core.lifecycle.isDarkMode; + const isDarkMode = lifecycle.isDarkMode; if ((!metadata && isDarkMode) || (metadata && !!metadata.isDarkMode != !!isDarkMode)) { transformColor( - core.contentDiv, + contentDiv, false /*includeSelf*/, isDarkMode ? 'lightToDark' : 'darkToLight', - core.darkColorHandler + darkColorHandler ); contentChanged = true; } if (triggerContentChangedEvent && contentChanged) { - core.api.triggerEvent( - core, + api.triggerEvent( + innerCore, { eventType: PluginEventType.ContentChanged, source: ChangeSource.SetContent, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts index 77699423ee2..13cc54a3078 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts @@ -2,7 +2,7 @@ import { createContextMenuPlugin } from './ContextMenuPlugin'; import { createEditPlugin } from './EditPlugin'; import { createEventTypeTranslatePlugin } from './EventTypeTranslatePlugin'; import { createNormalizeTablePlugin } from './NormalizeTablePlugin'; -import type { UnportedCorePlugins } from '../publicTypes/ContentModelCorePlugins'; +import type { ContentModelCorePlugins } from '../publicTypes/ContentModelCorePlugins'; import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; /** @@ -10,7 +10,7 @@ import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEdit * Create Core Plugins * @param options Editor options */ -export function createCorePlugins(options: ContentModelEditorOptions): UnportedCorePlugins { +export function createCorePlugins(options: ContentModelEditorOptions): ContentModelCorePlugins { const map = options.corePluginOverride || {}; // The order matters, some plugin needs to be put before/after others to make sure event diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index 997cb777f2a..e22e8b84d4f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -1,8 +1,17 @@ import { buildRangeEx } from './utils/buildRangeEx'; +import { createCorePlugins } from '../corePlugins/createCorePlugins'; import { createEditorCore } from './createEditorCore'; import { getObjectKeys } from 'roosterjs-content-model-dom'; import { getPendableFormatState } from './utils/getPendableFormatState'; -import { isBold, redo, transformColor, undo } from 'roosterjs-content-model-core'; +import type { ContentModelCorePluginState } from '../publicTypes/ContentModelCorePlugins'; +import { + createModelFromHtml, + isBold, + redo, + StandaloneEditor, + transformColor, + undo, +} from 'roosterjs-content-model-core'; import { ChangeSource, ColorTransformDirection, @@ -18,7 +27,6 @@ import type { ContentChangedData, ContentChangedEvent, DOMEventHandler, - DarkColorHandler, DefaultFormat, EditorUndoState, ExperimentalFeatures, @@ -29,8 +37,6 @@ import type { NodePosition, PendableFormatState, PluginEvent, - PluginEventData, - PluginEventFromType, PositionType, Rect, Region, @@ -51,7 +57,6 @@ import type { CompatibleContentPosition, CompatibleExperimentalFeatures, CompatibleGetContentMode, - CompatiblePluginEventType, CompatibleQueryScope, CompatibleRegionType, } from 'roosterjs-editor-types/lib/compatibleTypes'; @@ -78,28 +83,14 @@ import type { ContentModelEditorOptions, IContentModelEditor, } from '../publicTypes/IContentModelEditor'; -import type { - ContentModelDocument, - ContentModelSegmentFormat, - DOMSelection, - DomToModelOption, - ModelToDomOption, - OnNodeCreated, - ContentModelFormatter, - FormatWithContentModelOptions, - EditorEnvironment, - Snapshot, - SnapshotsManager, - DOMEventRecord, - PasteType, -} from 'roosterjs-content-model-types'; +import type { DOMEventRecord } from 'roosterjs-content-model-types'; /** * Editor for Content Model. * (This class is still under development, and may still be changed in the future with some breaking changes) */ -export class ContentModelEditor implements IContentModelEditor { - private core: ContentModelEditorCore | null = null; +export class ContentModelEditor extends StandaloneEditor implements IContentModelEditor { + private contentModelEditorCore: ContentModelEditorCore | undefined; /** * Creates an instance of Editor @@ -107,125 +98,51 @@ export class ContentModelEditor implements IContentModelEditor { * @param options An optional options object to customize the editor */ constructor(contentDiv: HTMLDivElement, options: ContentModelEditorOptions = {}) { - this.core = createEditorCore(contentDiv, options); - this.core.plugins.forEach(plugin => plugin.initialize(this)); - } - - /** - * Create Content Model from DOM tree in this editor - * @param option The option to customize the behavior of DOM to Content Model conversion - */ - createContentModel( - option?: DomToModelOption, - selectionOverride?: DOMSelection - ): ContentModelDocument { - const core = this.getCore(); - - return core.api.createContentModel(core, option, selectionOverride); - } - - /** - * Set content with content model - * @param model The content model to set - * @param option Additional options to customize the behavior of Content Model to DOM conversion - * @param onNodeCreated An optional callback that will be called when a DOM node is created - */ - setContentModel( - model: ContentModelDocument, - option?: ModelToDomOption, - onNodeCreated?: OnNodeCreated - ): DOMSelection | null { - const core = this.getCore(); - - return core.api.setContentModel(core, model, option, onNodeCreated); - } - - /** - * Get current running environment, such as if editor is running on Mac - */ - getEnvironment(): EditorEnvironment { - return this.getCore().environment; - } - - /** - * Get current DOM selection - */ - getDOMSelection(): DOMSelection | null { - const core = this.getCore(); - - return core.api.getDOMSelection(core); - } - - /** - * Set DOMSelection into editor content. - * This is the replacement of IEditor.select. - * @param selection The selection to set - */ - setDOMSelection(selection: DOMSelection | null) { - const core = this.getCore(); - - core.api.setDOMSelection(core, selection); - } - - /** - * The general API to do format change with Content Model - * It will grab a Content Model for current editor content, and invoke a callback function - * to do format change. Then according to the return value, write back the modified content model into editor. - * If there is cached model, it will be used and updated. - * @param formatter Formatter function, see ContentModelFormatter - * @param options More options, see FormatWithContentModelOptions - */ - formatContentModel( - formatter: ContentModelFormatter, - options?: FormatWithContentModelOptions - ): void { - const core = this.getCore(); - - core.api.formatContentModel(core, formatter, options); - } - - /** - * Get pending format of editor if any, or return null - */ - getPendingFormat(): ContentModelSegmentFormat | null { - return this.getCore().format.pendingFormat?.format ?? null; - } - - /** - * Add a single undo snapshot to undo stack - */ - takeSnapshot(): void { - const core = this.getCore(); - - core.api.addUndoSnapshot(core, false /*canUndoByBackspace*/); - } - - /** - * Restore an undo snapshot into editor - * @param snapshot The snapshot to restore - */ - restoreSnapshot(snapshot: Snapshot): void { - const core = this.getCore(); + const corePlugins = createCorePlugins(options); + const plugins = [ + corePlugins.eventTranslate, + corePlugins.edit, + ...(options.plugins ?? []), + corePlugins.contextMenu, + corePlugins.normalizeTable, + ]; + const initContent = options.initialContent ?? contentDiv.innerHTML; + const initialModel = + initContent && !options.initialModel + ? createModelFromHtml( + initContent, + options.defaultDomToModelOptions, + options.trustedHTMLHandler, + options.defaultSegmentFormat + ) + : options.initialModel; + const standaloneEditorOptions: ContentModelEditorOptions = { + ...options, + plugins: plugins, + initialModel, + }; + const corePluginState: ContentModelCorePluginState = { + edit: corePlugins.edit.getState(), + contextMenu: corePlugins.contextMenu.getState(), + }; - core.api.restoreUndoSnapshot(core, snapshot); + super(contentDiv, standaloneEditorOptions, () => { + // Need to create Content Model Editor Core before initialize plugins since some plugins need this object + this.contentModelEditorCore = createEditorCore( + options, + corePluginState, + size => size / this.getCore().zoomScale + ); + }); } /** * Dispose this editor, dispose all plugins and custom data */ dispose(): void { - const core = this.getCore(); - - for (let i = core.plugins.length - 1; i >= 0; i--) { - const plugin = core.plugins[i]; + super.dispose(); - try { - plugin.dispose(); - } catch (e) { - // Cache the error and pass it out, then keep going since dispose should always succeed - core.disposeErrorHandler?.(plugin, e as Error); - } - } + const core = this.getContentModelEditorCore(); getObjectKeys(core.customData).forEach(key => { const data = core.customData[key]; @@ -237,9 +154,7 @@ export class ContentModelEditor implements IContentModelEditor { delete core.customData[key]; }); - core.darkColorHandler.reset(); - - this.core = null; + this.contentModelEditorCore = undefined; } /** @@ -247,7 +162,7 @@ export class ContentModelEditor implements IContentModelEditor { * @returns True if editor is disposed, otherwise false */ isDisposed(): boolean { - return !this.core; + return super.isDisposed() || !this.contentModelEditorCore; } /** @@ -261,8 +176,10 @@ export class ContentModelEditor implements IContentModelEditor { * @returns true if node is inserted. Otherwise false */ insertNode(node: Node, option?: InsertOption): boolean { - const core = this.getCore(); - return node ? core.api.insertNode(core, node, option ?? null) : false; + const core = this.getContentModelEditorCore(); + const innerCore = this.getCore(); + + return node ? core.api.insertNode(core, innerCore, node, option ?? null) : false; } /** @@ -378,8 +295,10 @@ export class ContentModelEditor implements IContentModelEditor { * @returns HTML string representing current editor content */ getContent(mode: GetContentMode | CompatibleGetContentMode = GetContentMode.CleanHTML): string { - const core = this.getCore(); - return core.api.getContent(core, mode); + const core = this.getContentModelEditorCore(); + const innerCore = this.getCore(); + + return core.api.getContent(core, innerCore, mode); } /** @@ -388,8 +307,10 @@ export class ContentModelEditor implements IContentModelEditor { * @param triggerContentChangedEvent True to trigger a ContentChanged event. Default value is true */ setContent(content: string, triggerContentChangedEvent: boolean = true) { - const core = this.getCore(); - core.api.setContent(core, content, triggerContentChangedEvent); + const core = this.getContentModelEditorCore(); + const innerCore = this.getCore(); + + core.api.setContent(core, innerCore, content, triggerContentChangedEvent); } /** @@ -501,23 +422,6 @@ export class ContentModelEditor implements IContentModelEditor { return range && getSelectionPath(this.getCore().contentDiv, range); } - /** - * Check if focus is in editor now - * @returns true if focus is in editor, otherwise false - */ - hasFocus(): boolean { - const core = this.getCore(); - return core.api.hasFocus(core); - } - - /** - * Focus to this editor, the selection was restored to where it was before, no unexpected scroll. - */ - focus() { - const core = this.getCore(); - core.api.focus(core); - } - select( arg1: Range | SelectionRangeEx | NodePosition | Node | SelectionPath | null, arg2?: NodePosition | number | PositionType | TableSelection | null, @@ -611,15 +515,6 @@ export class ContentModelEditor implements IContentModelEditor { //#region EVENT API - /** - * Attach a DOM event to the editor content DIV - * @param eventMap A map from event name to its handler - */ - attachDomEvent(eventMap: Record): () => void { - const core = this.getCore(); - return core.api.attachDomEvent(core, eventMap); - } - addDomEventHandler( nameOrMap: string | Record, handler?: DOMEventHandler @@ -648,30 +543,6 @@ export class ContentModelEditor implements IContentModelEditor { return this.attachDomEvent(eventsMapResult); } - /** - * Trigger an event to be dispatched to all plugins - * @param eventType Type of the event - * @param data data of the event with given type, this is the rest part of PluginEvent with the given type - * @param broadcast indicates if the event needs to be dispatched to all plugins - * True means to all, false means to allow exclusive handling from one plugin unless no one wants that - * @returns the event object which is really passed into plugins. Some plugin may modify the event object so - * the result of this function provides a chance to read the modified result - */ - triggerPluginEvent( - eventType: T, - data: PluginEventData, - broadcast: boolean = false - ): PluginEventFromType { - const core = this.getCore(); - const event = ({ - eventType, - ...data, - } as any) as PluginEventFromType; - core.api.triggerEvent(core, event, broadcast); - - return event; - } - /** * Trigger a ContentChangedEvent * @param source Source of this event, by default is 'SetContent' @@ -691,15 +562,6 @@ export class ContentModelEditor implements IContentModelEditor { //#region Undo API - /** - * Get undo snapshots manager - */ - getSnapshotsManager(): SnapshotsManager { - const core = this.getCore(); - - return core.undo.snapshotsManager; - } - /** * Undo last edit operation */ @@ -811,14 +673,6 @@ export class ContentModelEditor implements IContentModelEditor { //#region Misc - /** - * Get document which contains this editor - * @returns The HTML document which contains this editor - */ - getDocument(): Document { - return this.getCore().contentDiv.ownerDocument; - } - /** * Get the scroll container of the editor */ @@ -835,21 +689,13 @@ export class ContentModelEditor implements IContentModelEditor { * dispose editor. */ getCustomData(key: string, getter?: () => T, disposer?: (value: T) => void): T { - const core = this.getCore(); + const core = this.getContentModelEditorCore(); return (core.customData[key] = core.customData[key] || { value: getter ? getter() : undefined, disposer, }).value as T; } - /** - * Check if editor is in IME input sequence - * @returns True if editor is in IME input sequence, otherwise false - */ - isInIME(): boolean { - return this.getCore().domEvent.isInIME; - } - /** * Get default format of this editor * @returns Default format object of this editor @@ -992,7 +838,7 @@ export class ContentModelEditor implements IContentModelEditor { * @param feature The feature to add */ addContentEditFeature(feature: GenericContentEditFeature) { - const core = this.getCore(); + const core = this.getContentModelEditorCore(); feature?.keys.forEach(key => { const array = core.edit.features[key] || []; array.push(feature); @@ -1005,7 +851,7 @@ export class ContentModelEditor implements IContentModelEditor { * @param feature The feature to remove */ removeContentEditFeature(feature: GenericContentEditFeature) { - const core = this.getCore(); + const core = this.getContentModelEditorCore(); feature?.keys.forEach(key => { const featureSet = core.edit.features[key]; const index = featureSet?.indexOf(feature) ?? -1; @@ -1026,8 +872,10 @@ export class ContentModelEditor implements IContentModelEditor { const range = this.getSelectionRange(); node = (range && Position.getStart(range).normalize().node) ?? undefined; } - const core = this.getCore(); - return core.api.getStyleBasedFormatState(core, node ?? null); + const core = this.getContentModelEditorCore(); + const innerCore = this.getCore(); + + return core.api.getStyleBasedFormatState(core, innerCore, node ?? null); } /** @@ -1046,8 +894,9 @@ export class ContentModelEditor implements IContentModelEditor { * @param keyboardEvent Optional keyboard event object */ ensureTypeInContainer(position: NodePosition, keyboardEvent?: KeyboardEvent) { - const core = this.getCore(); - core.api.ensureTypeInContainer(core, position, keyboardEvent); + const core = this.getContentModelEditorCore(); + const innerCore = this.getCore(); + core.api.ensureTypeInContainer(core, innerCore, position, keyboardEvent); } //#endregion @@ -1078,14 +927,6 @@ export class ContentModelEditor implements IContentModelEditor { ); } - /** - * Check if the editor is in dark mode - * @returns True if the editor is in dark mode, otherwise false - */ - isDarkMode(): boolean { - return this.getCore().lifecycle.isDarkMode; - } - /** * Transform the given node and all its child nodes to dark mode color if editor is in dark mode * @param node The node to transform @@ -1107,47 +948,12 @@ export class ContentModelEditor implements IContentModelEditor { ); } - /** - * Get a darkColorHandler object for this editor. - */ - getDarkColorHandler(): DarkColorHandler { - return this.getCore().darkColorHandler; - } - - /** - * Make the editor in "Shadow Edit" mode. - * In Shadow Edit mode, all format change will finally be ignored. - * This can be used for building a live preview feature for format button, to allow user - * see format result without really apply it. - * This function can be called repeated. If editor is already in shadow edit mode, we can still - * use this function to do more shadow edit operation. - */ - startShadowEdit() { - const core = this.getCore(); - core.api.switchShadowEdit(core, true /*isOn*/); - } - - /** - * Leave "Shadow Edit" mode, all changes made during shadow edit will be discarded - */ - stopShadowEdit() { - const core = this.getCore(); - core.api.switchShadowEdit(core, false /*isOn*/); - } - - /** - * Check if editor is in Shadow Edit mode - */ - isInShadowEdit() { - return !!this.getCore().lifecycle.shadowEditFragment; - } - /** * Check if the given experimental feature is enabled * @param feature The feature to check */ isFeatureEnabled(feature: ExperimentalFeatures | CompatibleExperimentalFeatures): boolean { - return this.getCore().experimentalFeatures.indexOf(feature) >= 0; + return this.getContentModelEditorCore().experimentalFeatures.indexOf(feature) >= 0; } /** @@ -1164,17 +970,7 @@ export class ContentModelEditor implements IContentModelEditor { * @deprecated Use getZoomScale() instead */ getSizeTransformer(): SizeTransformer { - return this.getCore().sizeTransformer; - } - - /** - * Get current zoom scale, default value is 1 - * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale - * to let editor behave correctly especially for those mouse drag/drop behaviors - * @returns current zoom scale number - */ - getZoomScale(): number { - return this.getCore().zoomScale; + return this.getContentModelEditorCore().sizeTransformer; } /** @@ -1185,6 +981,7 @@ export class ContentModelEditor implements IContentModelEditor { */ setZoomScale(scale: number): void { const core = this.getCore(); + if (scale > 0 && scale <= 10) { const oldValue = core.zoomScale; core.zoomScale = scale; @@ -1211,35 +1008,15 @@ export class ContentModelEditor implements IContentModelEditor { return core.api.getVisibleViewport(core); } - /** - * Check if the given DOM node is in editor - * @param node The node to check - */ - isNodeInEditor(node: Node): boolean { - const core = this.getCore(); - - return core.contentDiv.contains(node); - } - - /** - * Paste into editor using a clipboardData object - * @param clipboardData Clipboard data retrieved from clipboard - * @param pasteType Type of paste - */ - pasteFromClipboard(clipboardData: ClipboardData, pasteType: PasteType = 'normal') { - const core = this.getCore(); - - core.api.paste(core, clipboardData, pasteType); - } - /** * @returns the current ContentModelEditorCore object * @throws a standard Error if there's no core object */ - private getCore(): ContentModelEditorCore { - if (!this.core) { + private getContentModelEditorCore(): ContentModelEditorCore { + if (!this.contentModelEditorCore) { throw new Error('Editor is already disposed'); } - return this.core; + + return this.contentModelEditorCore; } } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts index dae16cdd6dd..099e2068305 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/createEditorCore.ts @@ -1,58 +1,28 @@ import { coreApiMap } from '../coreApi/coreApiMap'; -import { createContextMenuPlugin } from '../corePlugins/ContextMenuPlugin'; -import { createCorePlugins } from '../corePlugins/createCorePlugins'; -import { createModelFromHtml, createStandaloneEditorCore } from 'roosterjs-content-model-core'; +import type { ContentModelCorePluginState } from '../publicTypes/ContentModelCorePlugins'; import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore'; import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; -import type { EditorPlugin } from 'roosterjs-editor-types'; +import type { SizeTransformer } from 'roosterjs-editor-types'; /** * @internal * Create a new instance of Content Model Editor Core * @param contentDiv The DIV HTML element which will be the container element of editor - * @param options An optional options object to customize the editor + * @param corePluginState Core plugin state for Content Model editor + * @param sizeTransformer @deprecated A size transformer function to calculate size when editor is zoomed */ export function createEditorCore( - contentDiv: HTMLDivElement, - options: ContentModelEditorOptions + options: ContentModelEditorOptions, + corePluginState: ContentModelCorePluginState, + sizeTransformer: SizeTransformer ): ContentModelEditorCore { - const corePlugins = createCorePlugins(options); - const additionalPlugins: EditorPlugin[] = [ - corePlugins.eventTranslate, - corePlugins.edit, - ...(options.plugins ?? []), - createContextMenuPlugin(options), - corePlugins.normalizeTable, - ].filter(x => !!x); - - const zoomScale: number = (options.zoomScale ?? -1) > 0 ? options.zoomScale! : 1; - const initContent = options.initialContent ?? contentDiv.innerHTML; - - if (initContent && !options.initialModel) { - options.initialModel = createModelFromHtml( - initContent, - options.defaultDomToModelOptions, - options.trustedHTMLHandler, - options.defaultSegmentFormat - ); - } - - const standaloneEditorCore = createStandaloneEditorCore( - contentDiv, - options, - coreApiMap, - additionalPlugins - ); - const core: ContentModelEditorCore = { - ...standaloneEditorCore, - edit: corePlugins.edit.getState(), - contextMenu: corePlugins.contextMenu.getState(), - zoomScale: zoomScale, - sizeTransformer: (size: number) => size / zoomScale, - disposeErrorHandler: options.disposeErrorHandler, + api: { ...coreApiMap, ...options.legacyCoreApiOverride }, + originalApi: coreApiMap, customData: {}, experimentalFeatures: options.experimentalFeatures ?? [], + sizeTransformer, + ...corePluginState, }; return core; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/buildRangeEx.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/buildRangeEx.ts index 9acba99f71a..72f6affc879 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/buildRangeEx.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/buildRangeEx.ts @@ -1,6 +1,6 @@ import { createRange, safeInstanceOf } from 'roosterjs-editor-dom'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; -import type { ContentModelEditorCore } from '../../publicTypes/ContentModelEditorCore'; +import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; import type { NodePosition, PositionType, @@ -13,7 +13,7 @@ import type { * @internal */ export function buildRangeEx( - core: ContentModelEditorCore, + core: StandaloneEditorCore, arg1: Range | SelectionRangeEx | NodePosition | Node | SelectionPath | null, arg2?: NodePosition | number | PositionType | TableSelection | null, arg3?: Node, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/index.ts b/packages-content-model/roosterjs-content-model-editor/lib/index.ts index 5396d32f028..8d68b1d4181 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/index.ts @@ -1,10 +1,18 @@ -export { ContentModelEditorCore } from './publicTypes/ContentModelEditorCore'; +export { + ContentModelEditorCore, + ContentModelCoreApiMap, + SetContent, + InsertNode, + GetContent, + GetStyleBasedFormatState, + EnsureTypeInContainer, +} from './publicTypes/ContentModelEditorCore'; export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor'; +export { ContextMenuPluginState } from './publicTypes/ContextMenuPluginState'; export { ContentModelCorePlugins, - UnportedCorePlugins, + ContentModelCorePluginState, } from './publicTypes/ContentModelCorePlugins'; -export { ContextMenuPluginState } from './publicTypes/ContextMenuPluginState'; export { ContentModelEditor } from './editor/ContentModelEditor'; export { isContentModelEditor } from './editor/isContentModelEditor'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts index 2a9f0a9101a..b3b29f49ad9 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts @@ -1,12 +1,10 @@ import type { ContextMenuPluginState } from './ContextMenuPluginState'; -import type { StandaloneEditorCorePlugins } from 'roosterjs-content-model-types'; -import type { EditPluginState, EditorPlugin, PluginWithState } from 'roosterjs-editor-types'; +import type { EditorPlugin, EditPluginState, PluginWithState } from 'roosterjs-editor-types'; /** - * An interface for unported core plugins - * TODO: Port these plugins + * An interface for Content Model editor core plugins */ -export interface UnportedCorePlugins { +export interface ContentModelCorePlugins { /** * Translate Standalone editor event type to legacy event type */ @@ -29,6 +27,16 @@ export interface UnportedCorePlugins { } /** - * An interface for Content Model editor core plugins. + * Core plugin state for Content Model Editor */ -export interface ContentModelCorePlugins extends StandaloneEditorCorePlugins, UnportedCorePlugins {} +export interface ContentModelCorePluginState { + /** + * Plugin state of EditPlugin + */ + readonly edit: EditPluginState; + + /** + * Plugin state of ContextMenuPlugin + */ + readonly contextMenu: ContextMenuPluginState; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts index edd87f2c783..8c88702cd75 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts @@ -1,64 +1,166 @@ -import type { ContextMenuPluginState } from './ContextMenuPluginState'; -import type { CompatibleExperimentalFeatures } from 'roosterjs-editor-types/lib/compatibleTypes'; +import type { ContentModelCorePluginState } from './ContentModelCorePlugins'; +import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; +import type { + CompatibleGetContentMode, + CompatibleExperimentalFeatures, +} from 'roosterjs-editor-types/lib/compatibleTypes'; import type { CustomData, - EditPluginState, - EditorPlugin, ExperimentalFeatures, + ContentMetadata, + GetContentMode, + InsertOption, + NodePosition, + StyleBasedFormatState, SizeTransformer, } from 'roosterjs-editor-types'; -import type { StandaloneCoreApiMap, StandaloneEditorCore } from 'roosterjs-content-model-types'; /** - * Represents the core data structure of a Content Model editor + * Set HTML content to this editor. All existing content will be replaced. A ContentChanged event will be triggered + * if triggerContentChangedEvent is set to true + * @param core The ContentModelEditorCore object + * @param innerCore The StandaloneEditorCore object + * @param content HTML content to set in + * @param triggerContentChangedEvent True to trigger a ContentChanged event. Default value is true + */ +export type SetContent = ( + core: ContentModelEditorCore, + innerCore: StandaloneEditorCore, + content: string, + triggerContentChangedEvent: boolean, + metadata?: ContentMetadata +) => void; + +/** + * Get current editor content as HTML string + * @param core The ContentModelEditorCore object + * @param innerCore The StandaloneEditorCore object + * @param mode specify what kind of HTML content to retrieve + * @returns HTML string representing current editor content + */ +export type GetContent = ( + core: ContentModelEditorCore, + innerCore: StandaloneEditorCore, + mode: GetContentMode | CompatibleGetContentMode +) => string; + +/** + * Insert a DOM node into editor content + * @param core The ContentModelEditorCore object. No op if null. + * @param innerCore The StandaloneEditorCore object + * @param option An insert option object to specify how to insert the node + */ +export type InsertNode = ( + core: ContentModelEditorCore, + innerCore: StandaloneEditorCore, + node: Node, + option: InsertOption | null +) => boolean; + +/** + * Get style based format state from current selection, including font name/size and colors + * @param core The ContentModelEditorCore objects + * @param innerCore The StandaloneEditorCore object + * @param node The node to get style from + */ +export type GetStyleBasedFormatState = ( + core: ContentModelEditorCore, + innerCore: StandaloneEditorCore, + node: Node | null +) => StyleBasedFormatState; + +/** + * Ensure user will type into a container element rather than into the editor content DIV directly + * @param core The ContentModelEditorCore object. + * @param innerCore The StandaloneEditorCore object + * @param position The position that user is about to type to + * @param keyboardEvent Optional keyboard event object + * @param deprecated Deprecated parameter, not used */ -export interface ContentModelEditorCore extends StandaloneEditorCore { +export type EnsureTypeInContainer = ( + core: ContentModelEditorCore, + innerCore: StandaloneEditorCore, + position: NodePosition, + keyboardEvent?: KeyboardEvent, + deprecated?: boolean +) => void; + +/** + * Core API map for Content Model editor + */ +export interface ContentModelCoreApiMap { /** - * Core API map of this editor + * Set HTML content to this editor. All existing content will be replaced. A ContentChanged event will be triggered + * if triggerContentChangedEvent is set to true + * @param core The ContentModelEditorCore object + * @param innerCore The StandaloneEditorCore object + * @param content HTML content to set in + * @param triggerContentChangedEvent True to trigger a ContentChanged event. Default value is true */ - readonly api: StandaloneCoreApiMap; + setContent: SetContent; /** - * Original API map of this editor. Overridden core API can use API from this map to call the original version of core API. + * Insert a DOM node into editor content + * @param core The ContentModelEditorCore object. No op if null. + * @param innerCore The StandaloneEditorCore object + * @param option An insert option object to specify how to insert the node */ - readonly originalApi: StandaloneCoreApiMap; + insertNode: InsertNode; - /* - * Current zoom scale, default value is 1 - * When editor is put under a zoomed container, need to pass the zoom scale number using this property - * to let editor behave correctly especially for those mouse drag/drop behaviors + /** + * Get current editor content as HTML string + * @param core The ContentModelEditorCore object + * @param innerCore The StandaloneEditorCore object + * @param mode specify what kind of HTML content to retrieve + * @returns HTML string representing current editor content */ - zoomScale: number; + getContent: GetContent; /** - * @deprecated Use zoomScale instead + * Get style based format state from current selection, including font name/size and colors + * @param core The ContentModelEditorCore objects + * @param innerCore The StandaloneEditorCore object + * @param node The node to get style from */ - sizeTransformer: SizeTransformer; + getStyleBasedFormatState: GetStyleBasedFormatState; /** - * A callback to be invoked when any exception is thrown during disposing editor - * @param plugin The plugin that causes exception - * @param error The error object we got + * Ensure user will type into a container element rather than into the editor content DIV directly + * @param core The EditorCore object. + * @param innerCore The StandaloneEditorCore object + * @param position The position that user is about to type to + * @param keyboardEvent Optional keyboard event object + * @param deprecated Deprecated parameter, not used */ - disposeErrorHandler?: (plugin: EditorPlugin, error: Error) => void; + ensureTypeInContainer: EnsureTypeInContainer; +} +/** + * Represents the core data structure of a Content Model editor + */ +export interface ContentModelEditorCore extends ContentModelCorePluginState { /** - * Custom data of this editor + * Core API map of this editor */ - customData: Record; + readonly api: ContentModelCoreApiMap; /** - * Enabled experimental features + * Original API map of this editor. Overridden core API can use API from this map to call the original version of core API. + */ + readonly originalApi: ContentModelCoreApiMap; + + /** + * Custom data of this editor */ - experimentalFeatures: (ExperimentalFeatures | CompatibleExperimentalFeatures)[]; + readonly customData: Record; /** - * Unported core plugin state: EditPlugin + * Enabled experimental features */ - edit: EditPluginState; + readonly experimentalFeatures: (ExperimentalFeatures | CompatibleExperimentalFeatures)[]; /** - * Unported core plugin state: ContextMenuPlugin + * @deprecated Use zoomScale instead */ - contextMenu: ContextMenuPluginState; + readonly sizeTransformer: SizeTransformer; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts index e25aa2bf649..d1aa0ef21fc 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts @@ -1,5 +1,6 @@ import type { ContentModelCorePlugins } from './ContentModelCorePlugins'; -import type { EditorPlugin, ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; +import type { ContentModelCoreApiMap } from './ContentModelEditorCore'; +import type { ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; import type { StandaloneEditorOptions, IStandaloneEditor } from 'roosterjs-content-model-types'; /** @@ -28,18 +29,9 @@ export interface ContentModelEditorOptions extends StandaloneEditorOptions { * Specify the enabled experimental features */ experimentalFeatures?: ExperimentalFeatures[]; - /** - * Current zoom scale, @default value is 1 - * When editor is put under a zoomed container, need to pass the zoom scale number using this property - * to let editor behave correctly especially for those mouse drag/drop behaviors - */ - zoomScale?: number; - - /** - * A callback to be invoked when any exception is thrown during disposing editor - * @param plugin The plugin that causes exception - * @param error The error object we got + * A function map to override default core API implementation + * Default value is null */ - disposeErrorHandler?: (plugin: EditorPlugin, error: Error) => void; + legacyCoreApiOverride?: Partial; } diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts index f9f2d178473..3bd949a0d57 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts @@ -3,10 +3,13 @@ import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToM import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as findAllEntities from 'roosterjs-content-model-core/lib/corePlugin/utils/findAllEntities'; -import { ContentModelDocument, EditorContext } from 'roosterjs-content-model-types'; import { ContentModelEditor } from '../../lib/editor/ContentModelEditor'; -import { ContentModelEditorCore } from '../../lib/publicTypes/ContentModelEditorCore'; import { EditorPlugin, PluginEventType } from 'roosterjs-editor-types'; +import { + ContentModelDocument, + EditorContext, + StandaloneEditorCore, +} from 'roosterjs-content-model-types'; const editorContext: EditorContext = { isDarkMode: false, @@ -267,7 +270,7 @@ describe('ContentModelEditor', () => { it('getPendingFormat', () => { const div = document.createElement('div'); const editor = new ContentModelEditor(div); - const core: ContentModelEditorCore = (editor as any).core; + const core: StandaloneEditorCore = (editor as any).core; const mockedFormat = 'FORMAT' as any; expect(editor.getPendingFormat()).toBeNull(); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts index f7b2c36f075..63498927e71 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/createEditorCoreTest.ts @@ -1,217 +1,61 @@ -import * as ContentModelCachePlugin from 'roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin'; -import * as ContentModelCopyPastePlugin from 'roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin'; -import * as ContentModelFormatPlugin from 'roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin'; -import * as ContextMenuPlugin from '../../lib/corePlugins/ContextMenuPlugin'; -import * as createStandaloneEditorDefaultSettings from 'roosterjs-content-model-core/lib/editor/createStandaloneEditorDefaultSettings'; -import * as DOMEventPlugin from 'roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin'; -import * as EditPlugin from '../../lib/corePlugins/EditPlugin'; -import * as EntityPlugin from 'roosterjs-content-model-core/lib/corePlugin/EntityPlugin'; -import * as EventTranslate from '../../lib/corePlugins/EventTypeTranslatePlugin'; -import * as LifecyclePlugin from 'roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin'; -import * as NormalizeTablePlugin from '../../lib/corePlugins/NormalizeTablePlugin'; -import * as SelectionPlugin from 'roosterjs-content-model-core/lib/corePlugin/SelectionPlugin'; -import * as UndoPlugin from 'roosterjs-content-model-core/lib/corePlugin/UndoPlugin'; import { coreApiMap } from '../../lib/coreApi/coreApiMap'; import { createEditorCore } from '../../lib/editor/createEditorCore'; -import { defaultTrustHtmlHandler } from 'roosterjs-content-model-core/lib/editor/createStandaloneEditorCore'; -import { standaloneCoreApiMap } from 'roosterjs-content-model-core/lib/editor/standaloneCoreApiMap'; - -const mockedDomEventState = 'DOMEVENTSTATE' as any; -const mockedEditState = 'EDITSTATE' as any; -const mockedLifecycleState = 'LIFECYCLESTATE' as any; -const mockedUndoState = 'UNDOSTATE' as any; -const mockedEntityState = 'ENTITYSTATE' as any; -const mockedCopyPasteState = 'COPYPASTESTATE' as any; -const mockedContextMenuState = 'CONTEXTMENU' as any; -const mockedCacheState = 'CACHESTATE' as any; -const mockedFormatState = 'FORMATSTATE' as any; -const mockedSelectionState = 'SELECTION' as any; - -const mockedFormatPlugin = { - getState: () => mockedFormatState, -} as any; -const mockedCachePlugin = { - getState: () => mockedCacheState, -} as any; -const mockedCopyPastePlugin = { - getState: () => mockedCopyPasteState, -} as any; -const mockedContextMenuPlugin = { - getState: () => mockedContextMenuState, -} as any; -const mockedEditPlugin = { - getState: () => mockedEditState, -} as any; -const mockedUndoPlugin = { - getState: () => mockedUndoState, -} as any; -const mockedDOMEventPlugin = { - getState: () => mockedDomEventState, -} as any; -const mockedEntityPlugin = { - getState: () => mockedEntityState, -} as any; -const mockedSelectionPlugin = { - getState: () => mockedSelectionState, -} as any; -const mockedNormalizeTablePlugin = 'NormalizeTablePlugin' as any; -const mockedLifecyclePlugin = { - getState: () => mockedLifecycleState, -} as any; -const mockedEventTranslatePlugin = 'EventTranslate' as any; -const mockedDefaultDomToModelSettings = { - settings: 'DOMTOMODELSETTINGS', -} as any; -const mockedDefaultModelToDomSettings = { - settings: 'MODELTODOMSETTINGS', -} as any; describe('createEditorCore', () => { - let contentDiv: any; - - beforeEach(() => { - contentDiv = { - style: {}, - } as any; + const mockedSizeTransformer = 'TRANSFORMER' as any; + const mockedEditPluginState = 'EDITSTATE' as any; + const mockedContextMenuPluginState = 'CONTEXTMENUSTATE' as any; - spyOn(ContentModelFormatPlugin, 'createContentModelFormatPlugin').and.returnValue( - mockedFormatPlugin - ); - spyOn(ContentModelCachePlugin, 'createContentModelCachePlugin').and.returnValue( - mockedCachePlugin - ); - spyOn(ContentModelCopyPastePlugin, 'createContentModelCopyPastePlugin').and.returnValue( - mockedCopyPastePlugin - ); - spyOn(EditPlugin, 'createEditPlugin').and.returnValue(mockedEditPlugin); - spyOn(ContextMenuPlugin, 'createContextMenuPlugin').and.returnValue( - mockedContextMenuPlugin - ); - spyOn(UndoPlugin, 'createUndoPlugin').and.returnValue(mockedUndoPlugin); - spyOn(DOMEventPlugin, 'createDOMEventPlugin').and.returnValue(mockedDOMEventPlugin); - spyOn(SelectionPlugin, 'createSelectionPlugin').and.returnValue(mockedSelectionPlugin); - spyOn(EntityPlugin, 'createEntityPlugin').and.returnValue(mockedEntityPlugin); - spyOn(NormalizeTablePlugin, 'createNormalizeTablePlugin').and.returnValue( - mockedNormalizeTablePlugin - ); - spyOn(LifecyclePlugin, 'createLifecyclePlugin').and.returnValue(mockedLifecyclePlugin); - spyOn(EventTranslate, 'createEventTypeTranslatePlugin').and.returnValue( - mockedEventTranslatePlugin - ); - spyOn(createStandaloneEditorDefaultSettings, 'createDomToModelSettings').and.returnValue( - mockedDefaultDomToModelSettings - ); - spyOn(createStandaloneEditorDefaultSettings, 'createModelToDomSettings').and.returnValue( - mockedDefaultModelToDomSettings + it('No additional option', () => { + const core = createEditorCore( + {}, + { + edit: mockedEditPluginState, + contextMenu: mockedContextMenuPluginState, + }, + mockedSizeTransformer ); - }); - it('No additional option', () => { - const core = createEditorCore(contentDiv, {}); expect(core).toEqual({ - contentDiv, - api: { ...coreApiMap, ...standaloneCoreApiMap }, - originalApi: { ...coreApiMap, ...standaloneCoreApiMap }, - plugins: [ - mockedCachePlugin, - mockedFormatPlugin, - mockedCopyPastePlugin, - mockedDOMEventPlugin, - mockedSelectionPlugin, - mockedEntityPlugin, - mockedEventTranslatePlugin, - mockedEditPlugin, - mockedContextMenuPlugin, - mockedNormalizeTablePlugin, - mockedUndoPlugin, - mockedLifecyclePlugin, - ], - domEvent: mockedDomEventState, - edit: mockedEditState, - lifecycle: mockedLifecycleState, - undo: mockedUndoState, - entity: mockedEntityState, - copyPaste: mockedCopyPasteState, - cache: mockedCacheState, - format: mockedFormatState, - selection: mockedSelectionState, - contextMenu: mockedContextMenuState, - trustedHTMLHandler: defaultTrustHtmlHandler, - zoomScale: 1, - sizeTransformer: jasmine.anything(), - darkColorHandler: jasmine.anything(), - disposeErrorHandler: undefined, - domToModelSettings: mockedDefaultDomToModelSettings, - modelToDomSettings: mockedDefaultModelToDomSettings, - environment: { - isMac: false, - isAndroid: false, - isSafari: false, - }, + api: { ...coreApiMap }, + originalApi: { ...coreApiMap }, customData: {}, experimentalFeatures: [], + edit: mockedEditPluginState, + contextMenu: mockedContextMenuPluginState, + sizeTransformer: mockedSizeTransformer, }); }); - it('With additional option', () => { - const defaultDomToModelOptions = { a: '1' } as any; - const defaultModelToDomOptions = { b: '2' } as any; - - const options = { - defaultDomToModelOptions, - defaultModelToDomOptions, - }; - const core = createEditorCore(contentDiv, options); + it('With additional plugins', () => { + const mockedPlugin1 = 'P1' as any; + const mockedPlugin2 = 'P2' as any; + const mockedFeatures = 'FEATURES' as any; + const mockedCoreApi = { + a: 'b', + } as any; - expect(createStandaloneEditorDefaultSettings.createDomToModelSettings).toHaveBeenCalledWith( - options - ); - expect(createStandaloneEditorDefaultSettings.createModelToDomSettings).toHaveBeenCalledWith( - options + const core = createEditorCore( + { + plugins: [mockedPlugin1, mockedPlugin2], + experimentalFeatures: mockedFeatures, + legacyCoreApiOverride: mockedCoreApi, + }, + { + edit: mockedEditPluginState, + contextMenu: mockedContextMenuPluginState, + }, + mockedSizeTransformer ); expect(core).toEqual({ - contentDiv, - api: { ...coreApiMap, ...standaloneCoreApiMap }, - originalApi: { ...coreApiMap, ...standaloneCoreApiMap }, - plugins: [ - mockedCachePlugin, - mockedFormatPlugin, - mockedCopyPastePlugin, - mockedDOMEventPlugin, - mockedSelectionPlugin, - mockedEntityPlugin, - mockedEventTranslatePlugin, - mockedEditPlugin, - mockedContextMenuPlugin, - mockedNormalizeTablePlugin, - mockedUndoPlugin, - mockedLifecyclePlugin, - ], - domEvent: mockedDomEventState, - edit: mockedEditState, - lifecycle: mockedLifecycleState, - undo: mockedUndoState, - entity: mockedEntityState, - copyPaste: mockedCopyPasteState, - cache: mockedCacheState, - format: mockedFormatState, - selection: mockedSelectionState, - contextMenu: mockedContextMenuState, - trustedHTMLHandler: defaultTrustHtmlHandler, - zoomScale: 1, - sizeTransformer: jasmine.anything(), - darkColorHandler: jasmine.anything(), - disposeErrorHandler: undefined, - domToModelSettings: mockedDefaultDomToModelSettings, - modelToDomSettings: mockedDefaultModelToDomSettings, - environment: { - isMac: false, - isAndroid: false, - isSafari: false, - }, + api: { ...coreApiMap, a: 'b' } as any, + originalApi: { ...coreApiMap }, customData: {}, - experimentalFeatures: [], + experimentalFeatures: mockedFeatures, + edit: mockedEditPluginState, + contextMenu: mockedContextMenuPluginState, + sizeTransformer: mockedSizeTransformer, }); }); }); diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index 0aa4dae314e..f53963adad3 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -3,17 +3,11 @@ import type { PasteType } from '../enum/PasteType'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { Snapshot } from '../parameter/Snapshot'; import type { EntityState } from '../parameter/FormatWithContentModelContext'; -import type { CompatibleGetContentMode } from 'roosterjs-editor-types/lib/compatibleTypes'; import type { - ContentMetadata, DarkColorHandler, EditorPlugin, - GetContentMode, - InsertOption, - NodePosition, PluginEvent, Rect, - StyleBasedFormatState, TrustedHTMLHandler, } from 'roosterjs-editor-types'; import type { ContentModelDocument } from '../group/ContentModelDocument'; @@ -134,20 +128,6 @@ export type AddUndoSnapshot = ( */ export type GetVisibleViewport = (core: StandaloneEditorCore) => Rect | null; -/** - * Set HTML content to this editor. All existing content will be replaced. A ContentChanged event will be triggered - * if triggerContentChangedEvent is set to true - * @param core The StandaloneEditorCore object - * @param content HTML content to set in - * @param triggerContentChangedEvent True to trigger a ContentChanged event. Default value is true - */ -export type SetContent = ( - core: StandaloneEditorCore, - content: string, - triggerContentChangedEvent: boolean, - metadata?: ContentMetadata -) => void; - /** * Check if the editor has focus now * @param core The StandaloneEditorCore object @@ -161,17 +141,6 @@ export type HasFocus = (core: StandaloneEditorCore) => boolean; */ export type Focus = (core: StandaloneEditorCore) => void; -/** - * Insert a DOM node into editor content - * @param core The StandaloneEditorCore object. No op if null. - * @param option An insert option object to specify how to insert the node - */ -export type InsertNode = ( - core: StandaloneEditorCore, - node: Node, - option: InsertOption | null -) => boolean; - /** * Attach a DOM event to the editor content DIV * @param core The StandaloneEditorCore object @@ -182,27 +151,6 @@ export type AttachDomEvent = ( eventMap: Record ) => () => void; -/** - * Get current editor content as HTML string - * @param core The StandaloneEditorCore object - * @param mode specify what kind of HTML content to retrieve - * @returns HTML string representing current editor content - */ -export type GetContent = ( - core: StandaloneEditorCore, - mode: GetContentMode | CompatibleGetContentMode -) => string; - -/** - * Get style based format state from current selection, including font name/size and colors - * @param core The StandaloneEditorCore objects - * @param node The node to get style from - */ -export type GetStyleBasedFormatState = ( - core: StandaloneEditorCore, - node: Node | null -) => StyleBasedFormatState; - /** * Restore an undo snapshot into editor * @param core The StandaloneEditorCore object @@ -210,20 +158,6 @@ export type GetStyleBasedFormatState = ( */ export type RestoreUndoSnapshot = (core: StandaloneEditorCore, snapshot: Snapshot) => void; -/** - * Ensure user will type into a container element rather than into the editor content DIV directly - * @param core The StandaloneEditorCore object. - * @param position The position that user is about to type to - * @param keyboardEvent Optional keyboard event object - * @param deprecated Deprecated parameter, not used - */ -export type EnsureTypeInContainer = ( - core: StandaloneEditorCore, - position: NodePosition, - keyboardEvent?: KeyboardEvent, - deprecated?: boolean -) => void; - /** * Paste into editor using a clipboardData object * @param core The StandaloneEditorCore object. @@ -237,10 +171,10 @@ export type Paste = ( ) => void; /** - * Temp interface - * TODO: Port other core API + * The interface for the map of core API for Content Model editor. + * Editor can call call API from this map under StandaloneEditorCore object */ -export interface PortedCoreApiMap { +export interface StandaloneCoreApiMap { /** * Create a EditorContext object used by ContentModel API * @param core The StandaloneEditorCore object @@ -354,58 +288,6 @@ export interface PortedCoreApiMap { paste: Paste; } -/** - * Temp interface - * TODO: Port these core API - */ -export interface UnportedCoreApiMap { - /** - * Set HTML content to this editor. All existing content will be replaced. A ContentChanged event will be triggered - * if triggerContentChangedEvent is set to true - * @param core The StandaloneEditorCore object - * @param content HTML content to set in - * @param triggerContentChangedEvent True to trigger a ContentChanged event. Default value is true - */ - setContent: SetContent; - - /** - * Insert a DOM node into editor content - * @param core The StandaloneEditorCore object. No op if null. - * @param option An insert option object to specify how to insert the node - */ - insertNode: InsertNode; - - /** - * Get current editor content as HTML string - * @param core The StandaloneEditorCore object - * @param mode specify what kind of HTML content to retrieve - * @returns HTML string representing current editor content - */ - getContent: GetContent; - - /** - * Get style based format state from current selection, including font name/size and colors - * @param core The StandaloneEditorCore objects - * @param node The node to get style from - */ - getStyleBasedFormatState: GetStyleBasedFormatState; - - /** - * Ensure user will type into a container element rather than into the editor content DIV directly - * @param core The EditorCore object. - * @param position The position that user is about to type to - * @param keyboardEvent Optional keyboard event object - * @param deprecated Deprecated parameter, not used - */ - ensureTypeInContainer: EnsureTypeInContainer; -} - -/** - * The interface for the map of core API for Content Model editor. - * Editor can call call API from this map under StandaloneEditorCore object - */ -export interface StandaloneCoreApiMap extends PortedCoreApiMap, UnportedCoreApiMap {} - /** * Represents the core data structure of a Content Model editor */ @@ -457,6 +339,21 @@ export interface StandaloneEditorCore extends StandaloneEditorCorePluginState { * To override, pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler */ readonly trustedHTMLHandler: TrustedHTMLHandler; + + /** + * A callback to be invoked when any exception is thrown during disposing editor + * @param plugin The plugin that causes exception + * @param error The error object we got + */ + readonly disposeErrorHandler?: (plugin: EditorPlugin, error: Error) => void; + + /** + * @deprecated Will be removed soon. + * Current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using this property + * to let editor behave correctly especially for those mouse drag/drop behaviors + */ + zoomScale: number; } /** diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts index 0168cc7477f..5150595b221 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts @@ -96,4 +96,19 @@ export interface StandaloneEditorOptions { * When this property is set, value of undoSnapshotService will be ignored. */ snapshotsManager?: SnapshotsManager; + + /** + * A callback to be invoked when any exception is thrown during disposing editor + * @param plugin The plugin that causes exception + * @param error The error object we got + */ + disposeErrorHandler?: (plugin: EditorPlugin, error: Error) => void; + + /** + * @deprecated + * Current zoom scale, @default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using this property + * to let editor behave correctly especially for those mouse drag/drop behaviors + */ + zoomScale?: number; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 84d86ed0291..4a6ee232753 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -209,17 +209,10 @@ export { SwitchShadowEdit, TriggerEvent, AddUndoSnapshot, - PortedCoreApiMap, - UnportedCoreApiMap, - SetContent, HasFocus, Focus, - InsertNode, AttachDomEvent, - GetContent, - GetStyleBasedFormatState, RestoreUndoSnapshot, - EnsureTypeInContainer, GetVisibleViewport, Paste, } from './editor/StandaloneEditorCore'; From 1254d7d7efc9a2af1921ff0648260cfeb4c6928f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 28 Dec 2023 14:34:52 -0300 Subject: [PATCH 26/64] keyboard input on mac --- .../lib/edit/keyboardInput.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index 45ecfcc30ad..0adf1fa26b2 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -8,8 +8,9 @@ import type { DOMSelection } from 'roosterjs-content-model-types'; */ export function keyboardInput(editor: IContentModelEditor, rawEvent: KeyboardEvent) { const selection = editor.getDOMSelection(); + const isMac = editor.getEnvironment().isMac; - if (shouldInputWithContentModel(selection, rawEvent)) { + if (shouldInputWithContentModel(selection, rawEvent, isMac)) { editor.takeSnapshot(); editor.formatContentModel( @@ -44,14 +45,18 @@ export function keyboardInput(editor: IContentModelEditor, rawEvent: KeyboardEve } } -function shouldInputWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) { +function shouldInputWithContentModel( + selection: DOMSelection | null, + rawEvent: KeyboardEvent, + isMac?: boolean +) { 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; // TODO: Also handle Enter key even selection is collapsed + return selection.type != 'range' || (!selection.range.collapsed && !isMac); // TODO: Also handle Enter key even selection is collapsed } else { return false; } From d3f3c20238bd7adc0a27ead2f8ef3c0a4683338e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 28 Dec 2023 14:56:54 -0300 Subject: [PATCH 27/64] fix test --- .../test/edit/keyboardInputTest.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts index 331584f45d4..0da2f964183 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts @@ -14,6 +14,7 @@ describe('keyboardInput', () => { let formatContentModelSpy: jasmine.Spy; let getDOMSelectionSpy: jasmine.Spy; let deleteSelectionSpy: jasmine.Spy; + let getEnvironmentSpy: jasmine.Spy; let mockedModel: ContentModelDocument; let normalizeContentModelSpy: jasmine.Spy; let mockedContext: FormatWithContentModelContext; @@ -37,11 +38,15 @@ describe('keyboardInput', () => { getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); deleteSelectionSpy = spyOn(deleteSelection, 'deleteSelection'); normalizeContentModelSpy = spyOn(normalizeContentModel, 'normalizeContentModel'); + getEnvironmentSpy = jasmine.createSpy('getEnvironment').and.returnValue({ + isMac: false, + }); editor = { getDOMSelection: getDOMSelectionSpy, takeSnapshot: takeSnapshotSpy, formatContentModel: formatContentModelSpy, + getEnvironment: getEnvironmentSpy, } as any; }); From 9aad217774b32cb3f9f75bd4309d869e3a1022dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 28 Dec 2023 16:36:08 -0300 Subject: [PATCH 28/64] change isMac for isIME --- .../lib/edit/keyboardInput.ts | 7 +++---- .../test/edit/keyboardInputTest.ts | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index 0adf1fa26b2..762538fab01 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -8,9 +8,8 @@ import type { DOMSelection } from 'roosterjs-content-model-types'; */ export function keyboardInput(editor: IContentModelEditor, rawEvent: KeyboardEvent) { const selection = editor.getDOMSelection(); - const isMac = editor.getEnvironment().isMac; - if (shouldInputWithContentModel(selection, rawEvent, isMac)) { + if (shouldInputWithContentModel(selection, rawEvent, editor.isInIME())) { editor.takeSnapshot(); editor.formatContentModel( @@ -48,7 +47,7 @@ export function keyboardInput(editor: IContentModelEditor, rawEvent: KeyboardEve function shouldInputWithContentModel( selection: DOMSelection | null, rawEvent: KeyboardEvent, - isMac?: boolean + isInIME: boolean ) { if (!selection) { return false; // Nothing to delete @@ -56,7 +55,7 @@ function shouldInputWithContentModel( !isModifierKey(rawEvent) && (rawEvent.key == 'Enter' || rawEvent.key == 'Space' || rawEvent.key.length == 1) ) { - return selection.type != 'range' || (!selection.range.collapsed && !isMac); // TODO: Also handle Enter key even selection is collapsed + return selection.type != 'range' || (!selection.range.collapsed && !isInIME); // TODO: Also handle Enter key even selection is collapsed } else { return false; } diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts index 0da2f964183..e59f6476b6d 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts @@ -14,7 +14,7 @@ describe('keyboardInput', () => { let formatContentModelSpy: jasmine.Spy; let getDOMSelectionSpy: jasmine.Spy; let deleteSelectionSpy: jasmine.Spy; - let getEnvironmentSpy: jasmine.Spy; + let isInIMESpy: jasmine.Spy; let mockedModel: ContentModelDocument; let normalizeContentModelSpy: jasmine.Spy; let mockedContext: FormatWithContentModelContext; @@ -38,15 +38,13 @@ describe('keyboardInput', () => { getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); deleteSelectionSpy = spyOn(deleteSelection, 'deleteSelection'); normalizeContentModelSpy = spyOn(normalizeContentModel, 'normalizeContentModel'); - getEnvironmentSpy = jasmine.createSpy('getEnvironment').and.returnValue({ - isMac: false, - }); + isInIMESpy = jasmine.createSpy('isInIME').and.returnValue(false); editor = { getDOMSelection: getDOMSelectionSpy, takeSnapshot: takeSnapshotSpy, formatContentModel: formatContentModelSpy, - getEnvironment: getEnvironmentSpy, + isInIME: isInIMESpy, } as any; }); From 4e769b5f9925c8f0ccf519fc74297049aba65050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 28 Dec 2023 16:46:24 -0300 Subject: [PATCH 29/64] iscomposing --- .../lib/edit/keyboardInput.ts | 5 ++++- .../test/edit/keyboardInputTest.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index 762538fab01..363b54093f5 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -55,7 +55,10 @@ function shouldInputWithContentModel( !isModifierKey(rawEvent) && (rawEvent.key == 'Enter' || rawEvent.key == 'Space' || rawEvent.key.length == 1) ) { - return selection.type != 'range' || (!selection.range.collapsed && !isInIME); // TODO: Also handle Enter key even selection is collapsed + return ( + selection.type != 'range' || + (!selection.range.collapsed && !rawEvent.isComposing && !isInIME) + ); // TODO: Also handle Enter key even selection is collapsed } else { return false; } diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts index e59f6476b6d..373c289a8cf 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardInputTest.ts @@ -91,6 +91,7 @@ describe('keyboardInput', () => { const rawEvent = { key: 'A', + isComposing: false, } as any; keyboardInput(editor, rawEvent); From 5cd567943da25ab78621efd66d07c8670404d682 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 3 Jan 2024 08:44:59 -0600 Subject: [PATCH 30/64] Support rem unit (#2300) * Support rem unit * Use already existing case --- .../lib/formatHandlers/utils/parseValueWithUnit.ts | 1 + .../test/formatHandlers/utils/parseValueWithUnitTest.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/parseValueWithUnit.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/parseValueWithUnit.ts index 81a4f4d9091..f5a5a7adea4 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/parseValueWithUnit.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/utils/parseValueWithUnit.ts @@ -26,6 +26,7 @@ export function parseValueWithUnit( result = ptToPx(num); break; case 'em': + case 'rem': result = getFontSize(currentSizePxOrElement) * num; break; case 'ex': diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/parseValueWithUnitTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/parseValueWithUnitTest.ts index 29e3a4d05e3..989766c9f4c 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/parseValueWithUnitTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/utils/parseValueWithUnitTest.ts @@ -43,6 +43,10 @@ describe('parseValueWithUnit with element', () => { runTest('ex', [0, 10, 11, -11]); }); + it('rem', () => { + runTest('rem', [0, 20, 22, -22]); + }); + it('no unit', () => { runTest('', [0, 0, 0, 0]); }); From 3465a3115403422c5bfade44e6e3389b3a949f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Wed, 3 Jan 2024 16:25:56 -0300 Subject: [PATCH 31/64] justify-content-api --- .../contentModel/ContentModelRibbon.tsx | 2 + .../contentModel/alignJustifyButton.ts | 19 +++++ .../lib/modelApi/block/setModelAlignment.ts | 9 ++- .../lib/publicApi/block/setAlignment.ts | 2 +- .../modelApi/block/setModelAlignmentTest.ts | 69 +++++++++++++++++++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx index 97ece1f2b28..fdd99c1f47c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { alignCenterButton } from './alignCenterButton'; +import { alignJustifyButton } from './alignJustifyButton'; import { alignLeftButton } from './alignLeftButton'; import { alignRightButton } from './alignRightButton'; import { backgroundColorButton } from './backgroundColorButton'; @@ -83,6 +84,7 @@ const buttons = [ alignLeftButton, alignCenterButton, alignRightButton, + alignJustifyButton, insertLinkButton, removeLinkButton, insertTableButton, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts new file mode 100644 index 00000000000..9b7803f26ff --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts @@ -0,0 +1,19 @@ +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { RibbonButton } from 'roosterjs-react'; +import { setAlignment } from 'roosterjs-content-model-api'; + +/** + * @internal + * "Align justify" button on the format ribbon + */ +export const alignJustifyButton: RibbonButton<'buttonNameAlignJustify'> = { + key: 'buttonNameAlignJustify', + unlocalizedText: 'Align justify', + iconName: 'AlignJustify', + onClick: editor => { + if (isContentModelEditor(editor)) { + setAlignment(editor, 'justify'); + } + return true; + }, +}; diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts index 7195148b4f1..9fc0ea21c2a 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts @@ -47,7 +47,7 @@ const TableAlignMap: Record< */ export function setModelAlignment( model: ContentModelDocument, - alignment: 'left' | 'center' | 'right' + alignment: 'left' | 'center' | 'right' | 'justify' ) { const paragraphOrListItemOrTable = getOperationalBlocks( model, @@ -56,8 +56,11 @@ export function setModelAlignment( ); paragraphOrListItemOrTable.forEach(({ block }) => { - const newAligment = ResultMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr']; - if (block.blockType === 'Table') { + const newAligment = + alignment === 'justify' + ? 'justify' + : ResultMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr']; + if (block.blockType === 'Table' && alignment !== 'justify') { alignTable( block, TableAlignMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr'] diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setAlignment.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setAlignment.ts index 64e975c9493..9a9392477ed 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setAlignment.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setAlignment.ts @@ -8,7 +8,7 @@ import type { IStandaloneEditor } from 'roosterjs-content-model-types'; */ export default function setAlignment( editor: IStandaloneEditor, - alignment: 'left' | 'center' | 'right' + alignment: 'left' | 'center' | 'right' | 'justify' ) { editor.focus(); diff --git a/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts index d7f14a394d0..e9f695a57fc 100644 --- a/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts @@ -904,4 +904,73 @@ describe('align left', () => { }); expect(result).toBeTrue(); }); + + it('align justify paragraph', () => { + const group = createContentModelDocument(); + const para = createParagraph(); + const text = createText('test'); + text.isSelected = true; + para.segments.push(text); + + group.blocks.push(para); + + const result = setModelAlignment(group, 'justify'); + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + textAlign: 'justify', + }, + segments: [text], + }, + ], + }); + expect(result).toBeTruthy(); + }); + + it('align justify list item', () => { + const group = createContentModelDocument(); + const listLevel = createListLevel('OL'); + const listItem = createListItem([listLevel]); + const para = createParagraph(); + const para2 = createParagraph(); + const text = createText('test'); + const text2 = createText('test2'); + text.isSelected = true; + text2.isSelected = true; + para.segments.push(text); + para2.segments.push(text2); + + listItem.blocks.push(para, para2); + + group.blocks.push(listItem); + + const result = setModelAlignment(group, 'justify'); + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockGroupType: 'ListItem', + blockType: 'BlockGroup', + levels: [ + { + listType: 'OL', + dataset: {}, + format: {}, + }, + ], + blocks: [para, para2], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + format: { textAlign: 'justify' }, + }, + ], + }); + expect(result).toBeTruthy(); + }); }); From b9cd29b1d8a15d5ed64b07ad73ce8d731d662d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 4 Jan 2024 13:56:18 -0300 Subject: [PATCH 32/64] aligment list item --- .../lib/modelApi/block/setModelAlignment.ts | 10 +- .../test/publicApi/block/setAlignmentTest.ts | 98 ++++++++++++++++++- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts index 9fc0ea21c2a..5806dc00f8a 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts @@ -56,7 +56,7 @@ export function setModelAlignment( ); paragraphOrListItemOrTable.forEach(({ block }) => { - const newAligment = + const newAlignment = alignment === 'justify' ? 'justify' : ResultMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr']; @@ -66,8 +66,14 @@ export function setModelAlignment( TableAlignMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr'] ); } else if (block) { + if (block.blockType === 'BlockGroup' && block.blockGroupType === 'ListItem') { + block.blocks.forEach(b => { + const { format } = b; + format.textAlign = newAlignment; + }); + } const { format } = block; - format.textAlign = newAligment; + format.textAlign = newAlignment; } }); diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts index c6fc09cedb2..ea10eb6806f 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts @@ -845,7 +845,7 @@ describe('setAlignment in list', () => { function runTest( list: ContentModelListItem, - alignment: 'left' | 'right' | 'center', + alignment: 'left' | 'right' | 'center' | 'justify', expectedList: ContentModelListItem | null ) { const model = createContentModelDocument(); @@ -916,7 +916,9 @@ describe('setAlignment in list', () => { blocks: [ { blockType: 'Paragraph', - format: {}, + format: { + textAlign: 'start', + }, segments: [ { segmentType: 'Text', @@ -948,7 +950,9 @@ describe('setAlignment in list', () => { blocks: [ { blockType: 'Paragraph', - format: {}, + format: { + textAlign: 'start', + }, segments: [ { segmentType: 'Text', @@ -1022,7 +1026,9 @@ describe('setAlignment in list', () => { blocks: [ { blockType: 'Paragraph', - format: {}, + format: { + textAlign: 'center', + }, segments: [ { segmentType: 'Text', @@ -1098,7 +1104,9 @@ describe('setAlignment in list', () => { blocks: [ { blockType: 'Paragraph', - format: {}, + format: { + textAlign: 'end', + }, segments: [ { segmentType: 'Text', @@ -1124,4 +1132,84 @@ describe('setAlignment in list', () => { } ); }); + + it('List - apply justify', () => { + runTest( + { + blockGroupType: 'ListItem', + blockType: 'BlockGroup', + levels: [ + { + listType: 'OL', + dataset: {}, + format: {}, + }, + ], + blocks: [ + { + blockType: 'Paragraph', + format: { + textAlign: 'end', + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + formatHolder: { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + format: { + textAlign: 'end', + }, + }, + 'justify', + { + blockGroupType: 'ListItem', + blockType: 'BlockGroup', + levels: [ + { + listType: 'OL', + dataset: {}, + format: {}, + }, + ], + blocks: [ + { + blockType: 'Paragraph', + format: { + textAlign: 'justify', + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + format: { + textAlign: 'justify', + }, + } + ); + }); }); From 6283ab3a25162ae954a24747d81b7c8b1f4709d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 4 Jan 2024 15:44:50 -0300 Subject: [PATCH 33/64] add justify to map --- .../lib/modelApi/block/setModelAlignment.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts index 5806dc00f8a..5a46366499b 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts @@ -7,8 +7,8 @@ import type { } from 'roosterjs-content-model-types'; const ResultMap: Record< - 'left' | 'center' | 'right', - Record<'ltr' | 'rtl', 'start' | 'center' | 'end'> + 'left' | 'center' | 'right' | 'justify', + Record<'ltr' | 'rtl', 'start' | 'center' | 'end' | 'justify'> > = { left: { ltr: 'start', @@ -22,6 +22,10 @@ const ResultMap: Record< ltr: 'end', rtl: 'start', }, + justify: { + ltr: 'justify', + rtl: 'justify', + }, }; const TableAlignMap: Record< @@ -56,10 +60,7 @@ export function setModelAlignment( ); paragraphOrListItemOrTable.forEach(({ block }) => { - const newAlignment = - alignment === 'justify' - ? 'justify' - : ResultMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr']; + const newAlignment = ResultMap[alignment][block.format.direction == 'rtl' ? 'rtl' : 'ltr']; if (block.blockType === 'Table' && alignment !== 'justify') { alignTable( block, From ccdb5e6b118c1b262194a865fc6fb556179bcb95 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 4 Jan 2024 13:54:23 -0800 Subject: [PATCH 34/64] Content Model: Improve paste and sanitization behavior (#2304) --- .../override/containerWidthFormatParser.ts | 11 +++ .../lib/override/pasteEntityProcessor.ts | 29 +++++--- .../lib/override/pasteGeneralProcessor.ts | 58 ++++++++------- .../lib/override/pasteTextProcessor.ts | 15 ++++ .../lib/publicApi/selection/deleteSegment.ts | 2 +- .../paste/generatePasteOptionFromPlugins.ts | 2 + .../lib/utils/paste/mergePasteContent.ts | 6 ++ .../lib/utils/sanitizeElement.ts | 63 ++++++++-------- .../containerWidthFormatParserTest.ts | 37 ++++++++++ .../overrides/pasteEntityProcessorTest.ts | 19 +++-- .../overrides/pasteGeneralProcessorTest.ts | 67 ++++++++++++++--- .../test/overrides/pasteTextProcessorTest.ts | 72 +++++++++++++++++++ .../generatePasteOptionFromPluginsTest.ts | 6 ++ .../test/utils/paste/mergePasteContentTest.ts | 6 ++ .../test/utils/sanitizeElementTest.ts | 52 ++++++++++++++ .../domToModel/processors/textProcessor.ts | 6 +- .../lib/domUtils/isWhiteSpacePreserved.ts | 10 +++ .../roosterjs-content-model-dom/lib/index.ts | 2 +- .../modelApi/common/isWhiteSpacePreserved.ts | 16 ----- .../lib/modelApi/common/normalizeParagraph.ts | 4 +- .../isWhiteSpacePreservedTest.ts | 15 +--- .../edit/deleteSteps/deleteWordSelection.ts | 2 +- .../lib/event/ContentModelBeforePasteEvent.ts | 11 +++ .../lib/index.ts | 1 + .../lib/parameter/ValueSanitizer.ts | 10 +++ 25 files changed, 402 insertions(+), 120 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/override/pasteTextProcessor.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/pasteTextProcessorTest.ts create mode 100644 packages-content-model/roosterjs-content-model-dom/lib/domUtils/isWhiteSpacePreserved.ts delete mode 100644 packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isWhiteSpacePreserved.ts rename packages-content-model/roosterjs-content-model-dom/test/{modelApi/common => domUtils}/isWhiteSpacePreservedTest.ts (50%) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/ValueSanitizer.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts b/packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts new file mode 100644 index 00000000000..6ea362ffbc1 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts @@ -0,0 +1,11 @@ +import type { FormatParser, SizeFormat } from 'roosterjs-content-model-types'; + +/** + * @internal Do not paste width for Format Containers since it may be generated by browser according to temp div width + */ +export const containerWidthFormatParser: FormatParser = (format, element) => { + // For pasted content, there may be existing width generated by browser from the temp DIV. So we need to remove it. + if (element.tagName == 'DIV') { + delete format.width; + } +}; diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts b/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts index 67df4b70e71..8424d684dec 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/override/pasteEntityProcessor.ts @@ -1,10 +1,13 @@ -import { - AllowedTags, - DisallowedTags, - removeStyle, - sanitizeElement, -} from '../utils/sanitizeElement'; -import type { DomToModelOptionForPaste, ElementProcessor } from 'roosterjs-content-model-types'; +import { AllowedTags, DisallowedTags, sanitizeElement } from '../utils/sanitizeElement'; +import type { + DomToModelOptionForPaste, + ElementProcessor, + ValueSanitizer, +} from 'roosterjs-content-model-types'; + +const DefaultStyleSanitizers: Readonly> = { + position: false, +}; /** * @internal @@ -14,11 +17,17 @@ export function createPasteEntityProcessor( ): ElementProcessor { const allowedTags = AllowedTags.concat(options.additionalAllowedTags); const disallowedTags = DisallowedTags.concat(options.additionalDisallowedTags); + const styleSanitizers = Object.assign({}, DefaultStyleSanitizers, options.styleSanitizers); + const attrSanitizers = options.attributeSanitizers; return (group, element, context) => { - const sanitizedElement = sanitizeElement(element, allowedTags, disallowedTags, { - position: removeStyle, - }); + const sanitizedElement = sanitizeElement( + element, + allowedTags, + disallowedTags, + styleSanitizers, + attrSanitizers + ); if (sanitizedElement) { context.defaultElementProcessors.entity(group, sanitizedElement, context); diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts b/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts index 071726bd940..025601b625a 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/override/pasteGeneralProcessor.ts @@ -1,12 +1,22 @@ +import { AllowedTags, createSanitizedElement, DisallowedTags } from '../utils/sanitizeElement'; import { moveChildNodes } from 'roosterjs-content-model-dom'; -import { - AllowedTags, - createSanitizedElement, - DisallowedTags, - removeDisplayFlex, - removeStyle, -} from '../utils/sanitizeElement'; -import type { DomToModelOptionForPaste, ElementProcessor } from 'roosterjs-content-model-types'; +import type { + DomToModelOptionForPaste, + ElementProcessor, + ValueSanitizer, +} from 'roosterjs-content-model-types'; + +/** + * @internal Export for test only + */ +export const removeDisplayFlex: ValueSanitizer = value => { + return value == 'flex' ? null : value; +}; + +const DefaultStyleSanitizers: Readonly> = { + position: false, + display: removeDisplayFlex, +}; /** * @internal @@ -16,12 +26,25 @@ export function createPasteGeneralProcessor( ): ElementProcessor { const allowedTags = AllowedTags.concat(options.additionalAllowedTags); const disallowedTags = DisallowedTags.concat(options.additionalDisallowedTags); + const styleSanitizers = Object.assign({}, DefaultStyleSanitizers, options.styleSanitizers); + const attrSanitizers = options.attributeSanitizers; return (group, element, context) => { const tag = element.tagName.toLowerCase(); - const processor = + const processor: ElementProcessor | undefined = allowedTags.indexOf(tag) >= 0 - ? internalGeneralProcessor + ? (group, element, context) => { + const sanitizedElement = createSanitizedElement( + element.ownerDocument, + element.tagName, + element.attributes, + styleSanitizers, + attrSanitizers + ); + + moveChildNodes(sanitizedElement, element); + context.defaultElementProcessors['*']?.(group, sanitizedElement, context); + } : disallowedTags.indexOf(tag) >= 0 ? undefined // Ignore those disallowed tags : context.defaultElementProcessors.span; // For other unknown tags, treat them as SPAN @@ -29,18 +52,3 @@ export function createPasteGeneralProcessor( processor?.(group, element, context); }; } - -const internalGeneralProcessor: ElementProcessor = (group, element, context) => { - const sanitizedElement = createSanitizedElement( - element.ownerDocument, - element.tagName, - element.attributes, - { - position: removeStyle, - display: removeDisplayFlex, - } - ); - - moveChildNodes(sanitizedElement, element); - context.defaultElementProcessors['*']?.(group, sanitizedElement, context); -}; diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/pasteTextProcessor.ts b/packages-content-model/roosterjs-content-model-core/lib/override/pasteTextProcessor.ts new file mode 100644 index 00000000000..512a7f6b37b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/override/pasteTextProcessor.ts @@ -0,0 +1,15 @@ +import { isWhiteSpacePreserved } from 'roosterjs-content-model-dom'; +import type { ElementProcessor } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export const pasteTextProcessor: ElementProcessor = (group, text, context) => { + const whiteSpace = context.blockFormat.whiteSpace; + + if (isWhiteSpacePreserved(whiteSpace)) { + text.nodeValue = text.nodeValue?.replace(/\u0020\u0020/g, '\u0020\u00A0') ?? ''; + } + + context.defaultElementProcessors['#text'](group, text, context); +}; diff --git a/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts index 95ce0c57bba..87ffca8a863 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts @@ -24,7 +24,7 @@ export function deleteSegment( ): boolean { const segments = paragraph.segments; const index = segments.indexOf(segmentToDelete); - const preserveWhiteSpace = isWhiteSpacePreserved(paragraph); + const preserveWhiteSpace = isWhiteSpacePreserved(paragraph.format.whiteSpace); const isForward = direction == 'forward'; const isBackward = direction == 'backward'; diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts index ec156e42079..a0f1a0c346b 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts @@ -33,6 +33,8 @@ export function generatePasteOptionFromPlugins( additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, }; const event: ContentModelBeforePasteEvent = { diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts index ba1b6c5cde1..f63843cbbe6 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts @@ -1,3 +1,4 @@ +import { containerWidthFormatParser } from '../../override/containerWidthFormatParser'; import { createDomToModelContext, domToContentModel } from 'roosterjs-content-model-dom'; import { createPasteEntityProcessor } from '../../override/pasteEntityProcessor'; import { createPasteGeneralProcessor } from '../../override/pasteGeneralProcessor'; @@ -5,6 +6,7 @@ import { getSegmentTextFormat } from '../../publicApi/domUtils/getSegmentTextFor import { getSelectedSegments } from '../../publicApi/selection/collectSelections'; import { mergeModel } from '../../publicApi/model/mergeModel'; import { pasteDisplayFormatParser } from '../../override/pasteDisplayFormatParser'; +import { pasteTextProcessor } from '../../override/pasteTextProcessor'; import { PasteType } from 'roosterjs-editor-types'; import type { MergeModelOption } from '../../publicApi/model/mergeModel'; import type { @@ -45,12 +47,16 @@ export function mergePasteContent( defaultDomToModelOptions, { processorOverride: { + '#text': pasteTextProcessor, entity: createPasteEntityProcessor(domToModelOption), '*': createPasteGeneralProcessor(domToModelOption), }, formatParserOverride: { display: pasteDisplayFormatParser, }, + additionalFormatParsers: { + container: [containerWidthFormatParser], + }, }, domToModelOption ); diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts index a99dc177b53..20f75114fa3 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/sanitizeElement.ts @@ -1,4 +1,5 @@ import { isNodeOfType } from 'roosterjs-content-model-dom'; +import type { ValueSanitizer } from 'roosterjs-content-model-types'; /** * @internal @@ -269,7 +270,8 @@ export function sanitizeElement( element: HTMLElement, allowedTags: ReadonlyArray, disallowedTags: ReadonlyArray, - styleCallbacks?: Record string | null> + styleSanitizers?: Readonly>, + attributeSanitizers?: Readonly> ): HTMLElement | null { const tag = element.tagName.toLowerCase(); const sanitizedElement = @@ -279,13 +281,14 @@ export function sanitizeElement( element.ownerDocument, allowedTags.indexOf(tag) >= 0 ? tag : 'span', element.attributes, - styleCallbacks + styleSanitizers, + attributeSanitizers ); if (sanitizedElement) { for (let child = element.firstChild; child; child = child.nextSibling) { const newChild = isNodeOfType(child, 'ELEMENT_NODE') - ? sanitizeElement(child, allowedTags, disallowedTags, styleCallbacks) + ? sanitizeElement(child, allowedTags, disallowedTags, styleSanitizers) : isNodeOfType(child, 'TEXT_NODE') ? child.cloneNode() : null; @@ -306,7 +309,8 @@ export function createSanitizedElement( doc: Document, tag: string, attributes: NamedNodeMap, - styleCallbacks?: Record string | null> + styleSanitizers?: Readonly>, + attributeSanitizers?: Readonly> ): HTMLElement { const element = doc.createElement(tag); @@ -315,9 +319,16 @@ export function createSanitizedElement( const name = attribute.name.toLowerCase().trim(); const value = attribute.value; + const sanitizer = attributeSanitizers?.[name]; const newValue = name == 'style' - ? processStyles(tag, value, styleCallbacks) + ? processStyles(tag, value, styleSanitizers) + : typeof sanitizer == 'function' + ? sanitizer(value, tag) + : typeof sanitizer === 'boolean' + ? sanitizer + ? value + : null : AllowedAttributes.indexOf(name) >= 0 || name.indexOf('data-') == 0 ? value : null; @@ -334,24 +345,10 @@ export function createSanitizedElement( return element; } -/** - * @internal - */ -export function removeStyle(): string | null { - return null; -} - -/** - * @internal - */ -export function removeDisplayFlex(value: string) { - return value == 'flex' ? null : value; -} - function processStyles( tagName: string, value: string, - styleCallbacks?: Record string | null> + styleSanitizers?: Readonly> ) { const pairs = value.split(';'); const result: string[] = []; @@ -359,28 +356,30 @@ function processStyles( pairs.forEach(pair => { const valueIndex = pair.indexOf(':'); const name = pair.slice(0, valueIndex).trim(); - let value: string | null = pair.slice(valueIndex + 1).trim(); + let value: string = pair.slice(valueIndex + 1).trim(); if (name && value) { if (isCssVariable(value)) { value = processCssVariable(value); } - const callback = styleCallbacks?.[name]; - - if (callback) { - value = callback(value, tagName); - } + const sanitizer = styleSanitizers?.[name]; + const sanitizedValue = + typeof sanitizer == 'function' + ? sanitizer(value, tagName) + : sanitizer === false + ? null + : value; if ( - !!value && - value != 'inherit' && - value != 'initial' && - value.indexOf('expression') < 0 && + !!sanitizedValue && + sanitizedValue != 'inherit' && + sanitizedValue != 'initial' && + sanitizedValue.indexOf('expression') < 0 && !name.startsWith('-') && - DefaultStyleValue[name] != value + DefaultStyleValue[name] != sanitizedValue ) { - result.push(`${name}:${value}`); + result.push(`${name}:${sanitizedValue}`); } } }); diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts new file mode 100644 index 00000000000..14e5e65c9d4 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts @@ -0,0 +1,37 @@ +import { containerWidthFormatParser } from '../../lib/override/containerWidthFormatParser'; +import { SizeFormat } from 'roosterjs-content-model-types'; + +describe('containerWidthFormatParser', () => { + it('DIV without width', () => { + const div = document.createElement('div'); + const format: SizeFormat = {}; + + containerWidthFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('DIV with width', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + width: '10px', + }; + + containerWidthFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('SPAN with width', () => { + const div = document.createElement('span'); + const format: SizeFormat = { + width: '10px', + }; + + containerWidthFormatParser(format, div, null!, {}); + + expect(format).toEqual({ + width: '10px', + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts index 5caf4aff1c7..e79751910f3 100644 --- a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteEntityProcessorTest.ts @@ -32,6 +32,8 @@ describe('pasteEntityProcessor', () => { const pasteEntityProcessor = createPasteEntityProcessor({ additionalAllowedTags: [], additionalDisallowedTags: [], + styleSanitizers: {}, + attributeSanitizers: {}, } as any); sanitizeElementSpy.and.returnValue(element); @@ -45,8 +47,9 @@ describe('pasteEntityProcessor', () => { sanitizeElement.AllowedTags, sanitizeElement.DisallowedTags, { - position: sanitizeElement.removeStyle, - } + position: false, + }, + {} ); expect(entityProcessorSpy).toHaveBeenCalledTimes(1); expect(entityProcessorSpy).toHaveBeenCalledWith(group, sanitizedElement, context); @@ -58,6 +61,12 @@ describe('pasteEntityProcessor', () => { const pasteEntityProcessor = createPasteEntityProcessor({ additionalAllowedTags: ['allowed'], additionalDisallowedTags: ['disallowed'], + styleSanitizers: { + color: true, + }, + attributeSanitizers: { + id: true, + }, } as any); sanitizeElementSpy.and.returnValue(element); @@ -71,8 +80,10 @@ describe('pasteEntityProcessor', () => { sanitizeElement.AllowedTags.concat('allowed'), sanitizeElement.DisallowedTags.concat('disallowed'), { - position: sanitizeElement.removeStyle, - } + position: false, + color: true, + }, + { id: true } ); expect(entityProcessorSpy).toHaveBeenCalledTimes(1); expect(entityProcessorSpy).toHaveBeenCalledWith(group, sanitizedElement, context); diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts index dc4064577f9..7053cf21809 100644 --- a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteGeneralProcessorTest.ts @@ -1,7 +1,10 @@ import * as sanitizeElement from '../../lib/utils/sanitizeElement'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; -import { createPasteGeneralProcessor } from '../../lib/override/pasteGeneralProcessor'; import { DomToModelContext } from 'roosterjs-content-model-types'; +import { + createPasteGeneralProcessor, + removeDisplayFlex, +} from '../../lib/override/pasteGeneralProcessor'; describe('pasteGeneralProcessor', () => { let createSanitizedElementSpy: jasmine.Spy; @@ -11,7 +14,7 @@ describe('pasteGeneralProcessor', () => { beforeEach(() => { createSanitizedElementSpy = spyOn(sanitizeElement, 'createSanitizedElement'); - generalProcessorSpy = jasmine.createSpy('entityProcessor'); + generalProcessorSpy = jasmine.createSpy('generalProcessor'); spanProcessorSpy = jasmine.createSpy('spanProcessorSpy'); context = { @@ -28,6 +31,8 @@ describe('pasteGeneralProcessor', () => { const pasteGeneralProcessor = createPasteGeneralProcessor({ additionalAllowedTags: [], additionalDisallowedTags: [], + styleSanitizers: {}, + attributeSanitizers: {}, } as any); createSanitizedElementSpy.and.returnValue(element); @@ -40,9 +45,10 @@ describe('pasteGeneralProcessor', () => { 'DIV', element.attributes, { - position: sanitizeElement.removeStyle, - display: sanitizeElement.removeDisplayFlex, - } + position: false, + display: removeDisplayFlex, + }, + {} ); expect(generalProcessorSpy).toHaveBeenCalledTimes(1); expect(generalProcessorSpy).toHaveBeenCalledWith(group, element, context); @@ -73,6 +79,8 @@ describe('pasteGeneralProcessor', () => { const pasteGeneralProcessor = createPasteGeneralProcessor({ additionalAllowedTags: ['test'], additionalDisallowedTags: [], + styleSanitizers: {}, + attributeSanitizers: {}, } as any); createSanitizedElementSpy.and.returnValue(element); @@ -85,9 +93,10 @@ describe('pasteGeneralProcessor', () => { 'TEST', element.attributes, { - position: sanitizeElement.removeStyle, - display: sanitizeElement.removeDisplayFlex, - } + position: false, + display: removeDisplayFlex, + }, + {} ); expect(generalProcessorSpy).toHaveBeenCalledTimes(1); expect(generalProcessorSpy).toHaveBeenCalledWith(group, element, context); @@ -111,6 +120,39 @@ describe('pasteGeneralProcessor', () => { expect(spanProcessorSpy).toHaveBeenCalledTimes(0); }); + it('Empty element with sanitizers', () => { + const element = document.createElement('div'); + const group = createContentModelDocument(); + const pasteGeneralProcessor = createPasteGeneralProcessor({ + additionalAllowedTags: [], + styleSanitizers: { + color: true, + }, + attributeSanitizers: { + id: true, + }, + } as any); + + createSanitizedElementSpy.and.returnValue(element); + + pasteGeneralProcessor(group, element, context); + + expect(createSanitizedElementSpy).toHaveBeenCalledTimes(1); + expect(createSanitizedElementSpy).toHaveBeenCalledWith( + document, + 'DIV', + element.attributes, + { + position: false, + display: removeDisplayFlex, + color: true, + }, + { id: true } + ); + expect(generalProcessorSpy).toHaveBeenCalledTimes(1); + expect(spanProcessorSpy).toHaveBeenCalledTimes(0); + }); + it('Element with display:flex', () => { const element = document.createElement('div'); @@ -120,6 +162,8 @@ describe('pasteGeneralProcessor', () => { const pasteGeneralProcessor = createPasteGeneralProcessor({ additionalAllowedTags: [], additionalDisallowedTags: ['test'], + styleSanitizers: {}, + attributeSanitizers: {}, } as any); createSanitizedElementSpy.and.callThrough(); @@ -132,9 +176,10 @@ describe('pasteGeneralProcessor', () => { 'DIV', element.attributes, { - position: sanitizeElement.removeStyle, - display: sanitizeElement.removeDisplayFlex, - } + position: false, + display: removeDisplayFlex, + }, + {} ); expect(generalProcessorSpy).toHaveBeenCalledTimes(1); expect((generalProcessorSpy.calls.argsFor(0)[1] as any).outerHTML).toEqual( diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/pasteTextProcessorTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteTextProcessorTest.ts new file mode 100644 index 00000000000..4ded1653556 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/pasteTextProcessorTest.ts @@ -0,0 +1,72 @@ +import * as isWhiteSpacePreserved from 'roosterjs-content-model-dom/lib/domUtils/isWhiteSpacePreserved'; +import { DomToModelContext } from 'roosterjs-content-model-types'; +import { pasteTextProcessor } from '../../lib/override/pasteTextProcessor'; + +describe('pasteTextProcessor', () => { + let isWhiteSpacePreservedSpy: jasmine.Spy; + let defaultProcessorSpy: jasmine.Spy; + let mockedContext: DomToModelContext; + const mockedGroup = 'GROUP' as any; + const mockedWhiteSpace = 'WHITESPACE' as any; + + beforeEach(() => { + isWhiteSpacePreservedSpy = spyOn(isWhiteSpacePreserved, 'isWhiteSpacePreserved'); + defaultProcessorSpy = jasmine.createSpy('#text'); + mockedContext = { + blockFormat: { + whiteSpace: mockedWhiteSpace, + }, + defaultElementProcessors: { + '#text': defaultProcessorSpy, + }, + } as any; + }); + + it('empty text node, isWhiteSpacePreserved=false', () => { + const text = document.createTextNode(''); + + isWhiteSpacePreservedSpy.and.returnValue(false); + + pasteTextProcessor(mockedGroup, text, mockedContext); + + expect(isWhiteSpacePreservedSpy).toHaveBeenCalledWith(mockedWhiteSpace); + expect(defaultProcessorSpy).toHaveBeenCalledWith(mockedGroup, text, mockedContext); + expect(text.nodeValue).toBe(''); + }); + + it('empty text node, isWhiteSpacePreserved=true', () => { + const text = document.createTextNode(''); + + isWhiteSpacePreservedSpy.and.returnValue(true); + + pasteTextProcessor(mockedGroup, text, mockedContext); + + expect(isWhiteSpacePreservedSpy).toHaveBeenCalledWith(mockedWhiteSpace); + expect(defaultProcessorSpy).toHaveBeenCalledWith(mockedGroup, text, mockedContext); + expect(text.nodeValue).toBe(''); + }); + + it('text node with space, isWhiteSpacePreserved=false', () => { + const text = document.createTextNode(' '); + + isWhiteSpacePreservedSpy.and.returnValue(false); + + pasteTextProcessor(mockedGroup, text, mockedContext); + + expect(isWhiteSpacePreservedSpy).toHaveBeenCalledWith(mockedWhiteSpace); + expect(defaultProcessorSpy).toHaveBeenCalledWith(mockedGroup, text, mockedContext); + expect(text.nodeValue).toBe(' '); + }); + + it('text node with space, isWhiteSpacePreserved=true', () => { + const text = document.createTextNode(' '); + + isWhiteSpacePreservedSpy.and.returnValue(true); + + pasteTextProcessor(mockedGroup, text, mockedContext); + + expect(isWhiteSpacePreservedSpy).toHaveBeenCalledWith(mockedWhiteSpace); + expect(defaultProcessorSpy).toHaveBeenCalledWith(mockedGroup, text, mockedContext); + expect(text.nodeValue).toBe(' \u00A0 \u00A0'); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts index 071277027e8..1b0c8eb33ff 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts @@ -86,6 +86,8 @@ describe('generatePasteOptionFromPlugins', () => { additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, }, sanitizingOption, }); @@ -219,6 +221,8 @@ describe('generatePasteOptionFromPlugins', () => { additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, }, sanitizingOption, }); @@ -249,6 +253,8 @@ describe('generatePasteOptionFromPlugins', () => { additionalFormatParsers: {}, formatParserOverride: {}, processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, }, pasteType: PasteType.AsPlainText, eventType: PluginEventType.BeforePaste, diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts index dd2314aa5d7..bb4e68be828 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts @@ -3,9 +3,11 @@ import * as createPasteEntityProcessor from '../../../lib/override/pasteEntityPr import * as createPasteGeneralProcessor from '../../../lib/override/pasteGeneralProcessor'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as mergeModelFile from '../../../lib/publicApi/model/mergeModel'; +import { containerWidthFormatParser } from '../../../lib/override/containerWidthFormatParser'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; import { mergePasteContent } from '../../../lib/utils/paste/mergePasteContent'; import { pasteDisplayFormatParser } from '../../../lib/override/pasteDisplayFormatParser'; +import { pasteTextProcessor } from '../../../lib/override/pasteTextProcessor'; import { PasteType } from 'roosterjs-editor-types'; import { ContentModelDocument, @@ -383,12 +385,16 @@ describe('mergePasteContent', () => { mockedDomToModelOptions, { processorOverride: { + '#text': pasteTextProcessor, entity: mockedPasteEntityProcessor, '*': mockedPasteGeneralProcessor, }, formatParserOverride: { display: pasteDisplayFormatParser, }, + additionalFormatParsers: { + container: [containerWidthFormatParser], + }, }, mockedDefaultDomToModelOptions ); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts index a97945f5ccd..c24c35e6749 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/sanitizeElementTest.ts @@ -105,6 +105,58 @@ describe('sanitizeElement', () => { expect(element.outerHTML).toBe('
'); expect(result!.outerHTML).toBe('
'); }); + + it('styleCallbacks', () => { + const element = document.createElement('div'); + const sanitizerSpy = jasmine.createSpy('sanitizer').and.returnValue('green'); + + element.style.color = 'red'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags, { + color: sanitizerSpy, + }); + + expect(result!.outerHTML).toBe('
'); + expect(sanitizerSpy).toHaveBeenCalledWith('red', 'div'); + }); + + it('styleCallbacks with boolean', () => { + const element = document.createElement('div'); + + element.style.color = 'red'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags, { + color: false, + }); + + expect(result!.outerHTML).toBe('
'); + }); + + it('attributeCallbacks', () => { + const element = document.createElement('div'); + const sanitizerSpy = jasmine.createSpy('sanitizer').and.returnValue('b'); + + element.id = 'a'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags, undefined, { + id: sanitizerSpy, + }); + + expect(result!.outerHTML).toBe('
'); + expect(sanitizerSpy).toHaveBeenCalledWith('a', 'div'); + }); + + it('attributeCallbacks with boolean', () => { + const element = document.createElement('div'); + + element.id = 'a'; + + const result = sanitizeElement(element, AllowedTags, DisallowedTags, undefined, { + id: false, + }); + + expect(result!.outerHTML).toBe('
'); + }); }); describe('sanitizeHtml', () => { diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts index e2de0dd87e9..29b78ef4637 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts @@ -5,6 +5,7 @@ import { createText } from '../../modelApi/creators/createText'; import { ensureParagraph } from '../../modelApi/common/ensureParagraph'; import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets'; import { hasSpacesOnly } from '../../modelApi/common/hasSpacesOnly'; +import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved'; import type { ContentModelBlockGroup, ContentModelParagraph, @@ -62,9 +63,6 @@ export const textProcessor: ElementProcessor = ( ); }; -// When we see these values of white-space style, need to preserve spaces and line-breaks and let browser handle it for us. -const WhiteSpaceValuesNeedToHandle = ['pre', 'pre-wrap', 'pre-line', 'break-spaces']; - function addTextSegment( group: ContentModelBlockGroup, text: string, @@ -77,7 +75,7 @@ function addTextSegment( if ( !hasSpacesOnly(text) || (paragraph?.segments.length ?? 0) > 0 || - WhiteSpaceValuesNeedToHandle.indexOf(paragraph?.format.whiteSpace || '') >= 0 + isWhiteSpacePreserved(paragraph?.format.whiteSpace) ) { textModel = createText(text, context.segmentFormat); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domUtils/isWhiteSpacePreserved.ts b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/isWhiteSpacePreserved.ts new file mode 100644 index 00000000000..21f30644ea0 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/isWhiteSpacePreserved.ts @@ -0,0 +1,10 @@ +// According to https://developer.mozilla.org/en-US/docs/Web/CSS/white-space, these style values will need to preserve white spaces +const WHITESPACE_PRE_VALUES = ['pre', 'pre-wrap', 'break-spaces']; + +/** + * Check if the given white-space style value will cause preserving white space + * @param whiteSpace The white-space style value to check + */ +export function isWhiteSpacePreserved(whiteSpace: string | undefined): boolean { + return !!whiteSpace && WHITESPACE_PRE_VALUES.indexOf(whiteSpace) >= 0; +} diff --git a/packages-content-model/roosterjs-content-model-dom/lib/index.ts b/packages-content-model/roosterjs-content-model-dom/lib/index.ts index d0c355cbb9e..e8d31ce0df8 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/index.ts @@ -28,6 +28,7 @@ export { addDelimiters, } from './domUtils/entityUtils'; export { reuseCachedElement } from './domUtils/reuseCachedElement'; +export { isWhiteSpacePreserved } from './domUtils/isWhiteSpacePreserved'; export { createBr } from './modelApi/creators/createBr'; export { createListItem } from './modelApi/creators/createListItem'; @@ -54,7 +55,6 @@ export { normalizeContentModel } from './modelApi/common/normalizeContentModel'; export { isGeneralSegment } from './modelApi/common/isGeneralSegment'; export { unwrapBlock } from './modelApi/common/unwrapBlock'; export { addSegment } from './modelApi/common/addSegment'; -export { isWhiteSpacePreserved } from './modelApi/common/isWhiteSpacePreserved'; export { normalizeSingleSegment } from './modelApi/common/normalizeSegment'; export { setParagraphNotImplicit } from './modelApi/block/setParagraphNotImplicit'; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isWhiteSpacePreserved.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isWhiteSpacePreserved.ts deleted file mode 100644 index b3122e11d17..00000000000 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isWhiteSpacePreserved.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ContentModelParagraph } from 'roosterjs-content-model-types'; - -// According to https://developer.mozilla.org/en-US/docs/Web/CSS/white-space, these style values will need to preserve white spaces -const WHITESPACE_PRE_VALUES = ['pre', 'pre-wrap', 'break-spaces']; - -/** - * Check if we have white-space to be preserved for a given paragraph - * @param paragraph The paragraph to check - */ -export function isWhiteSpacePreserved(paragraph: ContentModelParagraph): boolean { - return ( - (paragraph.format.whiteSpace && - WHITESPACE_PRE_VALUES.indexOf(paragraph.format.whiteSpace) >= 0) || - false - ); -} diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts index 7853f801ce0..1edd90acb8b 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/normalizeParagraph.ts @@ -1,7 +1,7 @@ import { areSameFormats } from '../../domToModel/utils/areSameFormats'; import { createBr } from '../creators/createBr'; import { isSegmentEmpty } from './isEmpty'; -import { isWhiteSpacePreserved } from './isWhiteSpacePreserved'; +import { isWhiteSpacePreserved } from '../../domUtils/isWhiteSpacePreserved'; import { normalizeAllSegments } from './normalizeSegment'; import type { ContentModelParagraph } from 'roosterjs-content-model-types'; /** @@ -33,7 +33,7 @@ export function normalizeParagraph(paragraph: ContentModelParagraph) { } } - if (!isWhiteSpacePreserved(paragraph)) { + if (!isWhiteSpacePreserved(paragraph.format.whiteSpace)) { normalizeAllSegments(paragraph); } diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isWhiteSpacePreservedTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domUtils/isWhiteSpacePreservedTest.ts similarity index 50% rename from packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isWhiteSpacePreservedTest.ts rename to packages-content-model/roosterjs-content-model-dom/test/domUtils/isWhiteSpacePreservedTest.ts index 06fd459b37b..fc798b2d612 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isWhiteSpacePreservedTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domUtils/isWhiteSpacePreservedTest.ts @@ -1,19 +1,8 @@ -import { ContentModelParagraph } from 'roosterjs-content-model-types'; -import { isWhiteSpacePreserved } from '../../../lib/modelApi/common/isWhiteSpacePreserved'; +import { isWhiteSpacePreserved } from '../../lib/domUtils/isWhiteSpacePreserved'; describe('isWhiteSpacePreserved', () => { function runTest(style: string | undefined, expected: boolean) { - const paragraph: ContentModelParagraph = { - blockType: 'Paragraph', - format: {}, - segments: [], - }; - - if (style) { - paragraph.format.whiteSpace = style; - } - - const result = isWhiteSpacePreserved(paragraph); + const result = isWhiteSpacePreserved(style); expect(result).toBe(expected); } diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts index 261f93036d8..1217a6648d1 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts @@ -104,7 +104,7 @@ function* iterateSegments( ): Generator { const step = forward ? 1 : -1; const segments = paragraph.segments; - const preserveWhiteSpace = isWhiteSpacePreserved(paragraph); + const preserveWhiteSpace = isWhiteSpacePreserved(paragraph.format.whiteSpace); for (let i = markerIndex + step; i >= 0 && i < segments.length; i += step) { const segment = segments[i]; diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts index a85198aac95..3a4f8d90711 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts @@ -1,3 +1,4 @@ +import type { ValueSanitizer } from '../parameter/ValueSanitizer'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { InsertPoint } from '../selection/InsertPoint'; @@ -20,6 +21,16 @@ export interface DomToModelOptionForPaste extends Required { * Additional disallowed HTML tags in lower case. Elements with these tags will be dropped */ additionalDisallowedTags: Lowercase[]; + + /** + * Additional sanitizers for CSS styles + */ + styleSanitizers: Record; + + /** + * Additional sanitizers for CSS styles + */ + attributeSanitizers: Record; } /** diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 4a6ee232753..01953755fbb 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -264,6 +264,7 @@ export { SnapshotsManager } from './parameter/SnapshotsManager'; export { DOMEventHandlerFunction, DOMEventRecord } from './parameter/DOMEventRecord'; export { EdgeLinkPreview } from './parameter/EdgeLinkPreview'; export { ClipboardData } from './parameter/ClipboardData'; +export { ValueSanitizer } from './parameter/ValueSanitizer'; export { MergePastedContentFunc, diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/ValueSanitizer.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/ValueSanitizer.ts new file mode 100644 index 00000000000..5a4a6099f44 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/ValueSanitizer.ts @@ -0,0 +1,10 @@ +/** + * Specify how to sanitize a value, can be a callback function or a boolean value. + * True: Keep this value + * False: Remove this value + * A callback: Let the callback function to decide how to deal this value. + * @param value The original value + * @param tagName Tag name of the element of this value + * @returns Return a non-empty string means use this value to replace the original value. Otherwise remove this value + */ +export type ValueSanitizer = ((value: string, tagName: string) => string | null) | boolean; From 24e0667e13c9f560d4b3ae02cda116010d21bd8d Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Fri, 5 Jan 2024 11:57:24 -0600 Subject: [PATCH 35/64] Fix couple of issues in Word Desktop Paste (#2311) * init * fix build * try fix build * fix again * Add a test * Re-activate tested without model check --- .../test/coreApi/pasteTest.ts | 2 +- .../roosterjs-content-model-dom/lib/index.ts | 1 + .../lib/modelApi/common/isEmpty.ts | 3 +- .../lib/paste/WordDesktop/getStyleMetadata.ts | 2 +- .../processPastedContentFromWordDesktop.ts | 14 - .../lib/paste/WordDesktop/processWordLists.ts | 93 +- .../test/paste/ContentModelPastePluginTest.ts | 2 +- .../test/paste/e2e/testUtils.ts | 1 + .../test/paste/getStyleMetadataTest.ts | 26 +- ...processPastedContentFromWordDesktopTest.ts | 2716 ++++++++++++----- 10 files changed, 2086 insertions(+), 774 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 6dfddfdfcd7..233bb3de808 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -222,7 +222,7 @@ describe('paste with content model & paste plugin', () => { editor?.paste(clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); - expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); + expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 4); expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); }); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/index.ts b/packages-content-model/roosterjs-content-model-dom/lib/index.ts index e8d31ce0df8..c8efa5291b9 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/index.ts @@ -55,6 +55,7 @@ export { normalizeContentModel } from './modelApi/common/normalizeContentModel'; export { isGeneralSegment } from './modelApi/common/isGeneralSegment'; export { unwrapBlock } from './modelApi/common/unwrapBlock'; export { addSegment } from './modelApi/common/addSegment'; +export { isEmpty } from './modelApi/common/isEmpty'; export { normalizeSingleSegment } from './modelApi/common/normalizeSegment'; export { setParagraphNotImplicit } from './modelApi/block/setParagraphNotImplicit'; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts index 37efdb34e39..64a1944437b 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/common/isEmpty.ts @@ -65,7 +65,8 @@ export function isSegmentEmpty(segment: ContentModelSegment): boolean { } /** - * @internal + * Get whether the model is empty. + * @returns true if the model is empty. */ export function isEmpty( model: ContentModelBlock | ContentModelBlockGroup | ContentModelSegment diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts index 1c596d23d4b..8d25601a7e7 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts @@ -72,7 +72,7 @@ export default function getStyleMetadata( const data: WordMetadata = { 'mso-level-number-format': record['mso-level-number-format'], - 'mso-level-start-at': record['mso-level-start-at'], + 'mso-level-start-at': record['mso-level-start-at'] || '1', 'mso-level-text': record['mso-level-text'], }; if (getObjectKeys(data).some(key => !!data[key])) { diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index f0843116691..63e66f2009c 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -8,7 +8,6 @@ import type { WordMetadata } from './WordMetadata'; import type { ContentModelBeforePasteEvent, ContentModelBlockFormat, - ContentModelListItemFormat, ContentModelListItemLevelFormat, ContentModelTableFormat, DomToModelContext, @@ -33,7 +32,6 @@ export function processPastedContentFromWordDesktop( setProcessor(ev.domToModelOption, 'element', wordDesktopElementProcessor(metadataMap)); addParser(ev.domToModelOption, 'block', removeNonValidLineHeight); addParser(ev.domToModelOption, 'listLevel', listLevelParser); - addParser(ev.domToModelOption, 'listItemElement', listItemElementParser); addParser(ev.domToModelOption, 'container', wordTableParser); addParser(ev.domToModelOption, 'table', wordTableParser); } @@ -85,18 +83,6 @@ function listLevelParser( format.marginBottom = undefined; } -const listItemElementParser: FormatParser = ( - format: ContentModelListItemFormat, - element: HTMLElement -): void => { - if (element.style.marginLeft) { - format.marginLeft = undefined; - } - if (element.style.marginRight) { - format.marginRight = undefined; - } -}; - const wordTableParser: FormatParser = (format): void => { if (format.marginLeft?.startsWith('-')) { delete format.marginLeft; diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts index 880bc1f39bb..2099511fc90 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts @@ -4,22 +4,23 @@ import { addBlock, createListItem, createListLevel, + isEmpty, parseFormat, } from 'roosterjs-content-model-dom'; import type { ContentModelBlockGroup, + ContentModelListItem, + ContentModelListItemFormat, ContentModelListItemLevelFormat, ContentModelListLevel, DomToModelContext, DomToModelListFormat, - FormatParser, } from 'roosterjs-content-model-types'; /** Word list metadata style name */ const MSO_LIST = 'mso-list'; const MSO_LIST_IGNORE = 'ignore'; const WORD_FIRST_LIST = 'l0'; - const TEMPLATE_VALUE_REGEX = /%[0-9a-zA-Z]+/g; interface WordDesktopListFormat extends DomToModelListFormat { @@ -28,6 +29,10 @@ interface WordDesktopListFormat extends DomToModelListFormat { wordKnownLevels?: Map; } +interface WordListFormat extends ContentModelListItemFormat { + wordList?: string; +} + const BULLET_METADATA = 'bullet'; /** * @internal @@ -78,7 +83,12 @@ export function processWordList( // Create the new level of the list item and parse the format const newLevel: ContentModelListLevel = createListLevel(listType); - parseFormat(element, context.formatParsers.listLevel, newLevel.format, context); + parseFormat( + element, + [...context.formatParsers.listLevel, wordListPaddingParser], + newLevel.format, + context + ); // If the list format is in a different level, update the array so we get the new item // To be in the same level as the provided level metadata. @@ -90,6 +100,8 @@ export function processWordList( listFormat.levels.splice(wordLevel, listFormat.levels.length - 1); listFormat.levels[wordLevel - 1] = newLevel; } + (listFormat.levels[listFormat.levels.length - 1] + .format as WordListFormat).wordList = wordList; listFormat.listParent = group; @@ -132,12 +144,7 @@ function processAsListItem( parseFormat(element, context.formatParsers.listItemElement, listItem.format, context); if (listType == 'OL') { - parseFormat( - element, - [startNumberOverrideParser(listMetadata)], - listItem.levels[listItem.levels.length - 1].format, - context - ); + setStartNumber(listItem, context, listMetadata); } context.elementProcessors.child(listItem, element, context); @@ -193,23 +200,57 @@ function getBulletFromMetadata(listMetadata: WordMetadata | undefined, listType: return getListStyleTypeFromString(listType, templateFinal); } -function startNumberOverrideParser( +function wordListPaddingParser( + format: ContentModelListItemLevelFormat, + element: HTMLElement +): void { + if (element.style.marginLeft && element.style.marginLeft != '0in') { + format.paddingLeft = '0px'; + } + if (element.style.marginRight && element.style.marginRight != '0in') { + format.paddingRight = '0px'; + } +} + +function setStartNumber( + listItem: ContentModelListItem, + context: DomToModelContext, listMetadata: WordMetadata | undefined -): FormatParser | null { - return (format, _, context) => { - const { - wordKnownLevels, - wordLevel, - wordList, - levels, - } = context.listFormat as WordDesktopListFormat; - if (typeof wordLevel === 'number' && wordList) { - const start = parseInt(listMetadata?.['mso-level-start-at'] || '1'); - const knownLevel = wordKnownLevels?.get(wordList) || []; - - if (start != undefined && !isNaN(start) && knownLevel.length != levels.length) { - format.startNumberOverride = start; - } +) { + const { + listParent, + wordList, + wordKnownLevels, + wordLevel, + levels, + } = context.listFormat as WordDesktopListFormat; + + const block = getLastNotEmptyBlock(listParent); + if ( + (block?.blockType != 'BlockGroup' || + block.blockGroupType != 'ListItem' || + (wordLevel && + (block.levels[wordLevel]?.format as WordListFormat)?.wordList != wordList)) && + wordList + ) { + const start = listMetadata?.['mso-level-start-at'] + ? parseInt(listMetadata['mso-level-start-at']) + : NaN; + const knownLevel = wordKnownLevels?.get(wordList) || []; + + if (start != undefined && !isNaN(start) && knownLevel.length != levels.length) { + listItem.levels[listItem.levels.length - 1].format.startNumberOverride = start; } - }; + } +} + +function getLastNotEmptyBlock(listParent: ContentModelBlockGroup | undefined) { + for (let index = (listParent?.blocks.length || 0) - 1; index > 0; index--) { + const result = listParent?.blocks[index]; + if (result && !isEmpty(result)) { + return result; + } + } + + return undefined; } diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index f71f8488526..957efc9e6d0 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -82,7 +82,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 5); + expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 4); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(1); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts index 69579d81fb6..3d03d83a631 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts @@ -40,5 +40,6 @@ export function expectEqual(model1: ContentModelDocument, model2: ContentModelDo }) ) ); + expect(newModel).toEqual(model2); } diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts index 68a66e80f30..c62b569af11 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts @@ -11,39 +11,47 @@ describe('getStyleMetadata', () => { expect(result.get('l0:level1')).toEqual({ 'mso-level-number-format': 'roman-upper', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', 'mso-level-text': '%1)', }); expect(result.get('l0:level2')).toEqual({ 'mso-level-number-format': 'alpha-lower', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', 'mso-level-text': undefined, }); expect(result.get('l0:level3')).toEqual({ 'mso-level-number-format': 'roman-lower', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', + 'mso-level-text': undefined, + }); + expect(result.get('l0:level4')).toEqual({ + 'mso-level-number-format': undefined, + 'mso-level-start-at': '1', 'mso-level-text': undefined, }); - expect(result.get('l0:level4')).toEqual(undefined); expect(result.get('l0:level5')).toEqual({ 'mso-level-number-format': 'alpha-lower', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', 'mso-level-text': undefined, }); expect(result.get('l0:level6')).toEqual({ 'mso-level-number-format': 'roman-lower', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', + 'mso-level-text': undefined, + }); + expect(result.get('l0:level7')).toEqual({ + 'mso-level-number-format': undefined, + 'mso-level-start-at': '1', 'mso-level-text': undefined, }); - expect(result.get('l0:level7')).toEqual(undefined); expect(result.get('l0:level8')).toEqual({ 'mso-level-number-format': 'alpha-lower', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', 'mso-level-text': undefined, }); expect(result.get('l0:level9')).toEqual({ 'mso-level-number-format': 'roman-lower', - 'mso-level-start-at': undefined, + 'mso-level-start-at': '1', 'mso-level-text': undefined, }); expect(result.get('l0:level10')).toEqual(undefined); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index 9719b684514..b0b8a367f1c 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -1,22 +1,11 @@ import * as getStyleMetadata from '../../lib/paste/WordDesktop/getStyleMetadata'; +import { ClipboardData, ContentModelBeforePasteEvent } from 'roosterjs-content-model-types'; import { expectEqual } from './e2e/testUtils'; -import { expectHtml } from 'roosterjs-editor-api/test/TestHelper'; import { PluginEventType } from 'roosterjs-editor-types'; import { processPastedContentFromWordDesktop } from '../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { WordMetadata } from '../../lib/paste/WordDesktop/WordMetadata'; import { - ClipboardData, - ContentModelBeforePasteEvent, - ContentModelDocument, -} from 'roosterjs-content-model-types'; -import { - listItemMetadataApplier, - listLevelMetadataApplier, -} from 'roosterjs-content-model-core/lib/metadata/updateListMetadata'; -import { - contentModelToDom, createDomToModelContext, - createModelToDomContext, domToContentModel, moveChildNodes, } from 'roosterjs-content-model-dom'; @@ -24,13 +13,12 @@ import { describe('processPastedContentFromWordDesktopTest', () => { let div: HTMLElement; let fragment: DocumentFragment; - let htmlBefore: string = ''; function runTest( source?: string, - expected?: string | string[], - expectedModel?: ContentModelDocument, - removeUndefinedValues?: boolean + expectedModel?: any, + removeUndefinedValues?: boolean, + htmlBefore?: string ) { //Act if (source) { @@ -53,36 +41,12 @@ describe('processPastedContentFromWordDesktopTest', () => { expect(model).toEqual(expectedModel); } } - - contentModelToDom( - document, - div, - model, - createModelToDomContext( - { - isDarkMode: false, - }, - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - } - ) - ); - - document.body.appendChild(div); - if (expected) { - expectHtml(div.innerHTML, expected); - } - div.parentElement?.removeChild(div); - htmlBefore = ''; } it('Remove Comment | mso-element:comment-list', () => { let source = '
Test
'; - runTest(source, '', { + runTest(source, { blockGroupType: 'Document', blocks: [], }); @@ -91,7 +55,7 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Remove Comment | #_msocom_', () => { let source = '

[BV11]

'; - runTest(source, '', { + runTest(source, { blockGroupType: 'Document', blocks: [], }); @@ -101,7 +65,7 @@ describe('processPastedContentFromWordDesktopTest', () => { let source = '

TestTest

'; - runTest(source, '

TestTest

', { + runTest(source, { blockGroupType: 'Document', blocks: [ { @@ -133,7 +97,7 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Remove Comment | mso-comment-continuation, remove style 1', () => { let source = 'TestTest'; - runTest(source, 'TestTest', { + runTest(source, { blockGroupType: 'Document', blocks: [ { @@ -159,22 +123,50 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Remove Comment | mso-comment-done, remove style', () => { let source = 'Test'; - runTest(source, 'Test'); + runTest( + source, + { + blockGroupType: 'Document', + blocks: [ + { + isImplicit: true, + segments: [{ text: 'Test', segmentType: 'Text', format: {} }], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + true + ); }); it('Remove Comment | mso-special-character:comment', () => { let source = 'Test'; - runTest(source, ''); + runTest(source, { blockGroupType: 'Document', blocks: [] }, true); }); it('Remove Line height less than default', () => { let source = '

Test

'; - runTest(source, '

Test

'); + runTest( + source, + { + blockGroupType: 'Document', + blocks: [ + { + segments: [{ text: 'Test', segmentType: 'Text', format: {} }], + blockType: 'Paragraph', + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true + ); }); it(' Line height, not percentage do not remove', () => { let source = '

Test

'; - runTest(source, undefined /* expected html */, { + runTest(source, { blockGroupType: 'Document', blocks: [ { @@ -198,7 +190,7 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Remove Line height, not percentage 2', () => { let source = '

Test

'; - runTest(source, undefined, { + runTest(source, { blockGroupType: 'Document', blocks: [ { @@ -222,7 +214,7 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Remove Line height, percentage greater than default', () => { let source = '

Test

'; - runTest(source, undefined, { + runTest(source, { blockGroupType: 'Document', blocks: [ { @@ -247,7 +239,6 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Remove Negative Left margin from table', () => { runTest( '
Test
', - '
Test
', { blockGroupType: 'Document', blocks: [ @@ -304,7 +295,7 @@ describe('processPastedContentFromWordDesktopTest', () => { new Map().set('l0:level1', dta) ); - runTest(html, undefined /* expected html */, { + runTest(html, { blockGroupType: 'Document', blocks: [ { @@ -331,6 +322,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -367,6 +359,7 @@ describe('processPastedContentFromWordDesktopTest', () => { dataset: {}, format: { marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -393,7 +386,7 @@ describe('processPastedContentFromWordDesktopTest', () => { new Map().set('l0:level1', dta).set('l0:level2', dta) ); - runTest(html, undefined, { + runTest(html, { blockGroupType: 'Document', blocks: [ { @@ -420,6 +413,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -457,6 +451,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, { @@ -464,6 +459,7 @@ describe('processPastedContentFromWordDesktopTest', () => { dataset: {}, format: { marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -490,7 +486,7 @@ describe('processPastedContentFromWordDesktopTest', () => { new Map().set('l0:level1', dta).set('l0:level3', dta) ); - runTest(html, undefined, { + runTest(html, { blockGroupType: 'Document', blocks: [ { @@ -517,6 +513,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -554,6 +551,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, { @@ -561,6 +559,7 @@ describe('processPastedContentFromWordDesktopTest', () => { dataset: {}, format: { marginBottom: undefined, + wordList: 'l0', }, }, { @@ -568,6 +567,7 @@ describe('processPastedContentFromWordDesktopTest', () => { dataset: {}, format: { marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -593,7 +593,7 @@ describe('processPastedContentFromWordDesktopTest', () => { spyOn(getStyleMetadata, 'default').and.returnValue( new Map().set('l0:level1', dta).set('l1:level3', dta) ); - runTest(html, undefined, { + runTest(html, { blockGroupType: 'Document', blocks: [ { @@ -620,6 +620,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -657,6 +658,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, { @@ -664,6 +666,7 @@ describe('processPastedContentFromWordDesktopTest', () => { dataset: {}, format: { marginBottom: undefined, + wordList: 'l1', }, }, { @@ -671,6 +674,7 @@ describe('processPastedContentFromWordDesktopTest', () => { dataset: {}, format: { marginBottom: undefined, + wordList: 'l1', }, }, ], @@ -684,6 +688,7 @@ describe('processPastedContentFromWordDesktopTest', () => { ], }); }); + it('Complex list inside a Table cell', () => { const html = '
' + @@ -707,7 +712,7 @@ describe('processPastedContentFromWordDesktopTest', () => { fragment = document.createDocumentFragment(); div.innerHTML = html; moveChildNodes(fragment, div); - runTest(undefined, undefined, { + runTest(undefined, { blockGroupType: 'Document', blocks: [ { @@ -744,6 +749,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -752,6 +758,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -760,6 +767,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -768,6 +776,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, ], @@ -802,6 +811,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -810,6 +820,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -818,6 +829,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, ], @@ -852,6 +864,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -860,6 +873,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, ], @@ -894,6 +908,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -902,6 +917,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -910,6 +926,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -918,6 +935,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, { @@ -926,6 +944,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l1', }, }, ], @@ -987,66 +1006,58 @@ describe('processPastedContentFromWordDesktopTest', () => { '' + '' + '', - undefined, { blockGroupType: 'Document', blocks: [ { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123123', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { - startNumberOverride: 1, + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - format: {}, - }, - { blockType: 'BlockGroup', + format: { + marginLeft: '0in', + }, blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123123', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', - format: {}, + format: { + wordList: 'l0', + }, dataset: { editingInfo: '{"orderedStyleType":1}', }, @@ -1054,48 +1065,54 @@ describe('processPastedContentFromWordDesktopTest', () => { { listType: 'OL', format: { - startNumberOverride: 1, + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - format: {}, - }, - { blockType: 'BlockGroup', + format: { + marginLeft: '0in', + }, blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123123', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', - format: {}, + format: { + wordList: 'l0', + }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, { listType: 'OL', - format: {}, + format: { + wordList: 'l0', + }, dataset: { editingInfo: '{"orderedStyleType":1}', }, @@ -1104,50 +1121,57 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', + marginLeft: '1.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123123', segmentType: 'Text', - text: '123123123', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', - format: {}, + format: { + wordList: 'l0', + }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, { listType: 'OL', - format: {}, + format: { + wordList: 'l0', + }, dataset: { editingInfo: '{"orderedStyleType":1}', }, @@ -1156,6 +1180,8 @@ describe('processPastedContentFromWordDesktopTest', () => { listType: 'OL', format: { marginTop: '1em', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -1163,18 +1189,33 @@ describe('processPastedContentFromWordDesktopTest', () => { }, { listType: 'OL', - format: {}, + format: { + wordList: 'l0', + }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, + blockType: 'BlockGroup', + format: { + marginLeft: '0in', }, - format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: '123123123', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, ], }, @@ -1189,10 +1230,11 @@ describe('processPastedContentFromWordDesktopTest', () => { spyOn(getStyleMetadata, 'default').and.returnValue( new Map().set('l0:level1', { 'mso-level-number-format': 'bullet', + 'mso-level-start-at': '1', }) ); - runTest(source, undefined, { + runTest(source, { blockGroupType: 'Document', blocks: [ { @@ -1271,6 +1313,8 @@ describe('processPastedContentFromWordDesktopTest', () => { marginRight: '0in', marginBottom: undefined, marginLeft: undefined, + paddingLeft: '0px', + wordList: 'l0', }, dataset: {}, }, @@ -1282,9 +1326,9 @@ describe('processPastedContentFromWordDesktopTest', () => { }, format: { marginTop: '0in', - marginRight: undefined, + marginRight: '0in', marginBottom: '0in', - marginLeft: undefined, + marginLeft: '0.5in', }, }, ], @@ -1299,7 +1343,6 @@ describe('processPastedContentFromWordDesktopTest', () => { it('Word doc created online but edited and copied from Desktop', () => { runTest( '

it went:

1.     Test

2.     Test2

', - undefined, { blockGroupType: 'Document', blocks: [ @@ -1339,7 +1382,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, - startNumberOverride: 1, + wordList: 'l0', }, }, ], @@ -1374,6 +1417,7 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '1em', marginBottom: undefined, + wordList: 'l0', }, }, ], @@ -1422,254 +1466,258 @@ describe('processPastedContentFromWordDesktopTest', () => { * B. */ it('multiple OL lists with different bullet types', () => { - htmlBefore = + const htmlBefore = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'; runTest( '\n\n

\x3C!--[if !supportLists]-->       \nI)           \n\x3C!--[endif]-->123123

\n\n

\x3C!--[if !supportLists]-->     \nII)           \n\x3C!--[endif]-->123123

\n\n

\x3C!--[if !supportLists]-->   \nIII)           \n\x3C!--[endif]-->123123

\n\n

123123123

\n\n

\x3C!--[if !supportLists]-->zz)  \n\x3C!--[endif]-->123123

\n\n

\x3C!--[if !supportLists]-->aaa)                    \n\x3C!--[endif]-->12123

\n\n\n\n

 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->a)     \n\x3C!--[endif]-->123

\n\n

\x3C!--[if !supportLists]-->b)     \n\x3C!--[endif]--> 

\n\n

\x3C!--[if !supportLists]-->LXV)           \n\x3C!--[endif]-->123

\n\n

\x3C!--[if !supportLists]-->LXVI)           \n\x3C!--[endif]--> 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->15)  \x3C!--[endif]-->123

\n\n

\x3C!--[if !supportLists]-->16)  \x3C!--[endif]--> 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->       \nI.           \n\x3C!--[endif]-->123

\n\n

\x3C!--[if !supportLists]-->     \nII.           \n\x3C!--[endif]--> 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->a.     \n\x3C!--[endif]-->123

\n\n

\x3C!--[if !supportLists]-->b.     \n\x3C!--[endif]--> 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->1.     \n\x3C!--[endif]-->123

\n\n

\x3C!--[if !supportLists]-->2.     \n\x3C!--[endif]--> 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->       \ni.           \n\x3C!--[endif]-->Asd

\n\n

\x3C!--[if !supportLists]-->     \nii.           \n\x3C!--[endif]--> 

\n\n

 

\n\n

\x3C!--[if !supportLists]-->A.    \n\x3C!--[endif]-->Asd

\n\n', - undefined, { blockGroupType: 'Document', blocks: [ { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l5', + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":18}', + }, + }, + ], blockType: 'BlockGroup', + format: { + marginTop: '1em', + marginBottom: '0in', + }, blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123123', + segmentType: 'Text', format: { underline: true, textColor: 'rgb(70, 120, 134)', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', - startNumberOverride: 1, + wordList: 'l5', }, dataset: { editingInfo: '{"orderedStyleType":18}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '0in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123123', + segmentType: 'Text', format: { underline: true, textColor: 'rgb(70, 120, 134)', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l5', }, dataset: { editingInfo: '{"orderedStyleType":18}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '0in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123123', + segmentType: 'Text', format: { underline: true, textColor: 'rgb(70, 120, 134)', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], - levels: [ + }, + { + segments: [ { - listType: 'OL', + text: '123123123', + segmentType: 'Text', format: { - marginTop: '1em', - }, - dataset: { - editingInfo: '{"orderedStyleType":18}', + underline: true, + textColor: 'rgb(70, 120, 134)', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '0in', }, + decorator: { + tagName: 'p', + format: {}, + }, }, { - blockType: 'Paragraph', - segments: [ + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ { - segmentType: 'Text', - text: '123123123', + listType: 'OL', format: { - underline: true, - textColor: 'rgb(70, 120, 134)', + marginTop: '1em', + wordList: 'l1', + startNumberOverride: 52, + }, + dataset: { + editingInfo: '{"orderedStyleType":6}', }, }, ], + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '0in', }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123123', + segmentType: 'Text', format: { underline: true, textColor: 'rgb(70, 120, 134)', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', - startNumberOverride: 52, + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":6}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '0in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '12123', + segmentType: 'Text', format: { underline: true, textColor: 'rgb(70, 120, 134)', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, - }, - ], - levels: [ - { - listType: 'OL', - format: { - marginTop: '1em', - }, - dataset: { - editingInfo: '{"orderedStyleType":6}', - }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - format: { - marginTop: '1em', - marginBottom: '0in', - }, }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: { fontSize: '11pt', lineHeight: '116%', }, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '0in', @@ -1680,14 +1728,14 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -1698,27 +1746,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l8', startNumberOverride: 1, }, dataset: { @@ -1726,76 +1764,78 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l8', }, dataset: { editingInfo: '{"orderedStyleType":6}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: ' ', segmentType: 'Text', - text: '123', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l6', startNumberOverride: 65, }, dataset: { @@ -1803,63 +1843,75 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l6', }, dataset: { editingInfo: '{"orderedStyleType":18}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -1870,27 +1922,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l4', startNumberOverride: 15, }, dataset: { @@ -1898,63 +1940,75 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l4', }, dataset: { editingInfo: '{"orderedStyleType":3}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'Paragraph', - segments: [ + blockGroupType: 'ListItem', + blocks: [ { - segmentType: 'Text', - text: ' ', + isImplicit: true, + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -1965,27 +2019,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l0', startNumberOverride: 1, }, dataset: { @@ -1993,63 +2037,75 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":17}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -2060,27 +2116,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l7', startNumberOverride: 1, }, dataset: { @@ -2088,63 +2134,75 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l7', }, dataset: { editingInfo: '{"orderedStyleType":5}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -2155,27 +2213,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l9', startNumberOverride: 1, }, dataset: { @@ -2183,63 +2231,75 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l9', }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -2250,27 +2310,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'Asd', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l3', startNumberOverride: 1, }, dataset: { @@ -2278,63 +2328,75 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: 'Asd', segmentType: 'Text', - text: ' ', format: {}, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, { - blockType: 'Paragraph', segments: [ { - segmentType: 'Text', text: ' ', + segmentType: 'Text', format: {}, }, ], + blockType: 'Paragraph', format: { marginTop: '1em', marginBottom: '1em', @@ -2345,27 +2407,17 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'Asd', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '1em', + wordList: 'l2', startNumberOverride: 1, }, dataset: { @@ -2373,19 +2425,31 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { marginTop: '1em', marginBottom: '1em', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'Asd', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, ], }, - true + true, + htmlBefore ); }); @@ -2401,40 +2465,27 @@ describe('processPastedContentFromWordDesktopTest', () => { * vi. */ it('9 Depth list', () => { - htmlBefore = + const htmlBefore = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\x3C!--[if gte mso 9]>\r\n \r\n \r\n \r\n\r\n\r\n\r\n\x3C!--[if gte mso 9]>\r\n \r\n Normal\r\n 0\r\n \r\n \r\n \r\n \r\n false\r\n false\r\n false\r\n \r\n EN-US\r\n JA\r\n AR-SA\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\x3C!--[if gte mso 9]>\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\x3C!--[if gte mso 10]>\r\n\r\n\r\n\r\n\r\n\r\n'; runTest( '

100.                    \n123

a.     \n123

                                                             \ni.     \n123

1.     \n123

1)     \n213

                                                                                                                                     \ni.     \n123

                                                                                                                                                       \ni.           \n123

ffffffffffffffffffff.         \n213

                                                                                                                                                                                                          \nvi.     \n213

gggggggggggggggggggg.     123

                                                                                                                                                     \nii.           \n123

                                                                                                                                   \nii.     \n123

2)     \n123

2.     \n123

                                                           \nii.     \n123

b.     \n123

502.                    \n123

', - undefined, { blockGroupType: 'Document', blocks: [ { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: '123', - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, - ], - format: {}, - isImplicit: true, - }, - ], + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', startNumberOverride: 100, }, dataset: { @@ -2442,43 +2493,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '0.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2489,6 +2547,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', startNumberOverride: 1, }, dataset: { @@ -2496,46 +2556,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '1in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2546,6 +2610,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -2556,6 +2622,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', startNumberOverride: 1, }, dataset: { @@ -2563,46 +2631,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '1.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2613,6 +2685,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -2623,6 +2697,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -2633,6 +2709,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', startNumberOverride: 1, }, dataset: { @@ -2640,46 +2718,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '2in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: '213', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2690,6 +2772,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -2700,6 +2784,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -2710,6 +2796,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2720,6 +2808,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', startNumberOverride: 1, }, dataset: { @@ -2727,46 +2817,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '2.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '213', segmentType: 'Text', - text: '123', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2777,6 +2871,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -2787,6 +2883,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -2797,6 +2895,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2807,6 +2907,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -2817,6 +2919,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', startNumberOverride: 1, }, dataset: { @@ -2824,46 +2928,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '3in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2874,6 +2982,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -2884,6 +2994,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -2894,6 +3006,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2904,6 +3018,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -2914,6 +3030,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -2924,6 +3042,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', startNumberOverride: 1, }, dataset: { @@ -2931,46 +3051,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '3.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '123', segmentType: 'Text', - text: '213', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -2981,6 +3105,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -2991,6 +3117,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3001,6 +3129,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3011,6 +3141,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -3021,6 +3153,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3031,6 +3165,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3041,6 +3177,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', startNumberOverride: 500, }, dataset: { @@ -3048,46 +3186,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '4in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '213', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3098,6 +3240,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3108,6 +3252,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3118,6 +3264,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3128,6 +3276,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -3138,6 +3288,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3148,6 +3300,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3158,6 +3312,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3168,6 +3324,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', startNumberOverride: 6, }, dataset: { @@ -3175,46 +3333,50 @@ describe('processPastedContentFromWordDesktopTest', () => { }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '4.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { + text: '213', segmentType: 'Text', - text: '123', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3225,6 +3387,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3235,6 +3399,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3245,6 +3411,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3255,6 +3423,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -3265,6 +3435,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3275,6 +3447,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3285,53 +3459,58 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 500, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":5}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '4in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3342,6 +3521,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3352,6 +3533,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3362,6 +3545,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3372,6 +3557,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -3382,6 +3569,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3392,53 +3581,58 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 1, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '3.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3449,6 +3643,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3459,6 +3655,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3469,6 +3667,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3479,6 +3679,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', @@ -3489,53 +3691,58 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 1, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '3in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3546,6 +3753,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3556,6 +3765,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3566,6 +3777,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3576,53 +3789,58 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 1, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":3}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '2.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3633,6 +3851,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3643,6 +3863,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', }, dataset: { editingInfo: '{"orderedStyleType":13}', @@ -3653,53 +3875,58 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 1, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '2in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3710,6 +3937,8 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', }, dataset: { editingInfo: '{"orderedStyleType":5}', @@ -3720,53 +3949,58 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 1, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":13}', }, }, ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', - }, - }, + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', + marginRight: '0in', marginBottom: '0in', + marginLeft: '1.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', format: {}, - isImplicit: true, }, ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, levels: [ { listType: 'OL', format: { marginTop: '0in', marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', }, dataset: { editingInfo: '{"orderedStyleType":1}', @@ -3777,77 +4011,1117 @@ describe('processPastedContentFromWordDesktopTest', () => { format: { marginTop: '0in', marginRight: '0in', - startNumberOverride: 1, + paddingLeft: '0px', + wordList: 'l3', }, dataset: { editingInfo: '{"orderedStyleType":5}', }, }, ], + blockType: 'BlockGroup', + format: { + lineHeight: '116%', + marginTop: '0in', + marginRight: '0in', + marginBottom: '0in', + marginLeft: '1in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: '123', + segmentType: 'Text', + format: { + fontFamily: 'Aptos, sans-serif', + fontSize: '12pt', + }, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { formatHolder: { - segmentType: 'SelectionMarker', isSelected: true, + segmentType: 'SelectionMarker', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l3', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', format: { lineHeight: '116%', marginTop: '0in', - marginBottom: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '0.5in', }, - }, - { - blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { - blockType: 'Paragraph', + isImplicit: true, segments: [ { - segmentType: 'Text', text: '123', + segmentType: 'Text', format: { fontFamily: 'Aptos, sans-serif', fontSize: '12pt', }, }, ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ], + }, + true, + htmlBefore + ); + }); + + /** + * mso-list: 10 + * 1. text + * * text + * .... + * + * Text + * + * mso-list: l0 (Should reset back to marker 1) + * 1. text + */ + it('Multiple lists with the same mso-list', () => { + const htmlBefore = + '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\x3C!--[if gte mso 9]>\r\n \r\n \r\n \r\n\r\n\r\n\r\n\x3C!--[if gte mso 9]>\r\n \r\n Normal\r\n 0\r\n \r\n \r\n \r\n \r\n false\r\n false\r\n false\r\n \r\n EN-US\r\n JA\r\n X-NONE\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\x3C!--[if gte mso 9]>\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\x3C!--[if gte mso 10]>\r\n\r\n\r\n\r\n\r\n\r\n'; + + runTest( + '\r\n\r\n

Text

\r\n\r\n

1.      text.

\r\n\r\n

2.     \r\ntext

\r\n\r\n

o  \r\ntext

\r\n\r\n

3.     \r\ntext

\r\n\r\n

o  \r\ntext

\r\n\r\n

o  \r\ntext

\r\n\r\n

4.     \r\ntext

\r\n\r\n

o  \r\ntext

\r\n\r\n

Text

\r\n\r\n

text

\r\n\r\n

text

\r\n\r\n

1.      \r\ntext

\r\n\r\n

o  \r\ntext

\r\n\r\n

2.      \r\ntext

\r\n\r\n

o  \r\ntext 

\r\n\r\n', + { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + text: 'Text', + segmentType: 'Text', format: {}, - isImplicit: true, }, ], + blockType: 'Paragraph', + format: {}, + decorator: { + tagName: 'h2', + format: { + fontSize: '1.5em', + fontWeight: 'bold', + }, + }, + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, levels: [ { listType: 'OL', format: { - marginTop: '0in', - marginRight: '0in', - startNumberOverride: 501, + marginTop: '1em', + wordList: 'l2', + startNumberOverride: 1, }, dataset: { editingInfo: '{"orderedStyleType":1}', }, }, ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '1em', + marginBottom: '8pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + { + text: '.', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { formatHolder: { - segmentType: 'SelectionMarker', isSelected: true, - format: { - fontFamily: 'Aptos, sans-serif', - fontSize: '12pt', + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '1em', + marginBottom: '8pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', format: { - lineHeight: '116%', + lineHeight: '107%', marginTop: '0in', + marginRight: '0in', marginBottom: '8pt', + marginLeft: '1in', }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], }, - ], - }, - true + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '1em', + marginBottom: '8pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '1in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', + }, + dataset: {}, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l1', + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '117pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '1em', + marginBottom: '8pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l2', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l2', + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '1in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: 'Text', + segmentType: 'Text', + format: { + fontSize: '16pt', + }, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '107%', + marginTop: '1em', + marginBottom: '8pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '105%', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '1in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '105%', + marginTop: '1em', + marginBottom: '8pt', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'UL', + format: { + marginTop: '0in', + marginRight: '0in', + paddingLeft: '0px', + wordList: 'l0', + }, + dataset: {}, + }, + ], + blockType: 'BlockGroup', + format: { + lineHeight: '105%', + marginTop: '0in', + marginRight: '0in', + marginBottom: '8pt', + marginLeft: '1in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'text', + segmentType: 'Text', + format: {}, + }, + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ], + }, + true, + htmlBefore + ); + }); + + /** + * 1. Text + * Dummy List Item + * 2. List + * Dummy List Item + */ + it('List with dummy list from Word Desktop', () => { + const htmlBefore = + '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\x3C!--[if gte mso 9]>\r\n \r\n \r\n \r\n\r\n\r\n\r\n\x3C!--[if gte mso 9]>\r\n \r\n Normal\r\n 0\r\n \r\n \r\n \r\n \r\n false\r\n false\r\n false\r\n \r\n EN-US\r\n JA\r\n X-NONE\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\x3C!--[if gte mso 9]>\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\x3C!--[if gte mso 10]>\r\n\r\n\r\n\r\n\r\n\r\n'; + + runTest( + '\r\n\r\n

1.     \r\nList 1

\r\n\r\n

2.     \r\nList 2

\r\n\r\n

List without bullet

\r\n\r\n

 

\r\n\r\n

3.     \r\nList

\r\n\r\n

Text

\r\n\r\n

a.     \r\nList

\r\n\r\n

Text

\r\n\r\n

                                                              \r\ni.     \r\nList

\r\n\r\n

Text

\r\n\r\n', + { + blockGroupType: 'Document', + blocks: [ + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'List 1', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'List 2', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: 'List without bullet', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + segments: [ + { + text: ' ', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'List', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: 'Text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'OL', + format: { + marginTop: '1em', + paddingLeft: '0px', + wordList: 'l0', + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":5}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + marginTop: '1em', + marginBottom: '1em', + marginLeft: '1in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'List', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: 'Text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + marginLeft: '1in', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + formatHolder: { + isSelected: true, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [ + { + listType: 'OL', + format: { + marginTop: '1em', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":1}', + }, + }, + { + listType: 'OL', + format: { + marginTop: '1em', + paddingLeft: '0px', + wordList: 'l0', + }, + dataset: { + editingInfo: '{"orderedStyleType":5}', + }, + }, + { + listType: 'OL', + format: { + marginTop: '1em', + paddingLeft: '0px', + wordList: 'l0', + startNumberOverride: 1, + }, + dataset: { + editingInfo: '{"orderedStyleType":13}', + }, + }, + ], + blockType: 'BlockGroup', + format: { + marginTop: '1em', + marginBottom: '1em', + marginLeft: '1.5in', + }, + blockGroupType: 'ListItem', + blocks: [ + { + isImplicit: true, + segments: [ + { + text: 'List', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + { + segments: [ + { + text: 'Text', + segmentType: 'Text', + format: {}, + }, + ], + blockType: 'Paragraph', + format: { + marginTop: '1em', + marginBottom: '1em', + marginLeft: '1.5in', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + ], + }, + true, + htmlBefore ); }); }); From 212c7b1eb508d0f7a75d2af878a346bf396977f7 Mon Sep 17 00:00:00 2001 From: Andres-CT98 <107568016+Andres-CT98@users.noreply.github.com> Date: Fri, 5 Jan 2024 12:25:49 -0600 Subject: [PATCH 36/64] Change borderLeft/Right to borderInlineStart/End (#2286) * fix RTL border application * fix table direction change * add tests --- .../lib/modelApi/block/setModelDirection.ts | 42 ++- .../publicApi/table/applyTableBorderFormat.ts | 54 +++- .../modelApi/block/setModelDirectionTest.ts | 226 +++++++++++++- .../table/applyTableBorderFormatTest.ts | 291 +++++++++++++++++- 4 files changed, 589 insertions(+), 24 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts index 3a984275972..c2170883717 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts @@ -1,6 +1,13 @@ import { findListItemsInSameThread } from '../list/findListItemsInSameThread'; -import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-core'; +import { + applyTableFormat, + getOperationalBlocks, + isBlockGroupOfType, + updateTableCellMetadata, +} from 'roosterjs-content-model-core'; import type { + BorderFormat, + ContentModelBlock, ContentModelBlockFormat, ContentModelDocument, ContentModelListItem, @@ -30,14 +37,18 @@ export function setModelDirection(model: ContentModelDocument, direction: 'ltr' item.blocks.forEach(block => internalSetDirection(block.format, direction)); }); } else if (block) { - internalSetDirection(block.format, direction); + internalSetDirection(block.format, direction, block); } }); return paragraphOrListItemOrTable.length > 0; } -function internalSetDirection(format: ContentModelBlockFormat, direction: 'ltr' | 'rtl') { +function internalSetDirection( + format: ContentModelBlockFormat, + direction: 'ltr' | 'rtl', + block?: ContentModelBlock +) { const wasRtl = format.direction == 'rtl'; const isRtl = direction == 'rtl'; @@ -46,7 +57,6 @@ function internalSetDirection(format: ContentModelBlockFormat, direction: 'ltr' // Adjust margin when change direction // TODO: make margin and padding direction-aware, like what we did for textAlign. So no need to adjust them here - // TODO: Do we also need to handle border here? const marginLeft = format.marginLeft; const paddingLeft = format.paddingLeft; @@ -54,12 +64,32 @@ function internalSetDirection(format: ContentModelBlockFormat, direction: 'ltr' setProperty(format, 'marginRight', marginLeft); setProperty(format, 'paddingLeft', format.paddingRight); setProperty(format, 'paddingRight', paddingLeft); + + // If whole Table direction changed, flip cell side borders + if (block && block.blockType == 'Table') { + block.rows.forEach(row => { + row.cells.forEach(cell => { + // Optimise by skipping cells with unchanged borders + updateTableCellMetadata(cell, metadata => { + if (metadata?.borderOverride) { + const storeBorderLeft = cell.format.borderLeft; + setProperty(cell.format, 'borderLeft', cell.format.borderRight); + setProperty(cell.format, 'borderRight', storeBorderLeft); + } + return metadata; + }); + }); + }); + + // Apply changed borders + applyTableFormat(block, undefined /* newFormat */, true /* keepCellShade*/); + } } } function setProperty( - format: MarginFormat & PaddingFormat, - key: keyof (MarginFormat & PaddingFormat), + format: MarginFormat & PaddingFormat & BorderFormat, + key: keyof (MarginFormat & PaddingFormat & BorderFormat), value: string | undefined ) { if (value) { diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts index 938820bb2db..f7c92cc7069 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts @@ -87,6 +87,9 @@ export default function applyTableBorderFormat( borderFormat = `${borderFormat} ${borderColor}`; } + // undefined is treated as Left to Right + const isRtl = tableModel.format.direction == 'rtl'; + if (sel) { const operations: BorderOperations[] = [operation]; while (operations.length) { @@ -132,13 +135,16 @@ export default function applyTableBorderFormat( rowIndex <= sel.lastRow; rowIndex++ ) { - const cell = tableModel.rows[rowIndex].cells[sel.firstColumn]; + const cell = + tableModel.rows[rowIndex].cells[ + isRtl ? sel.lastColumn : sel.firstColumn + ]; // Format cells - Left border applyBorderFormat(cell, borderFormat, leftBorder); } // Format perimeter - perimeter.Left = true; + isRtl ? (perimeter.Right = true) : (perimeter.Left = true); break; case 'rightBorders': const rightBorder: BorderPositions[] = ['borderRight']; @@ -147,13 +153,16 @@ export default function applyTableBorderFormat( rowIndex <= sel.lastRow; rowIndex++ ) { - const cell = tableModel.rows[rowIndex].cells[sel.lastColumn]; + const cell = + tableModel.rows[rowIndex].cells[ + isRtl ? sel.firstColumn : sel.lastColumn + ]; // Format cells - Right border applyBorderFormat(cell, borderFormat, rightBorder); } // Format perimeter - perimeter.Right = true; + isRtl ? (perimeter.Left = true) : (perimeter.Right = true); break; case 'topBorders': const topBorder: BorderPositions[] = ['borderTop']; @@ -222,7 +231,9 @@ export default function applyTableBorderFormat( // Single row selection if (singleRow) { applyBorderFormat( - tableModel.rows[sel.firstRow].cells[sel.firstColumn], + tableModel.rows[sel.firstRow].cells[ + isRtl ? sel.lastColumn : sel.firstColumn + ], borderFormat, ['borderRight'] ); @@ -238,7 +249,9 @@ export default function applyTableBorderFormat( ]); } applyBorderFormat( - tableModel.rows[sel.firstRow].cells[sel.lastColumn], + tableModel.rows[sel.firstRow].cells[ + isRtl ? sel.firstColumn : sel.lastColumn + ], borderFormat, ['borderLeft'] ); @@ -248,25 +261,33 @@ export default function applyTableBorderFormat( // For multiple rows and columns selections // Top left cell applyBorderFormat( - tableModel.rows[sel.firstRow].cells[sel.firstColumn], + tableModel.rows[sel.firstRow].cells[ + isRtl ? sel.lastColumn : sel.firstColumn + ], borderFormat, ['borderBottom', 'borderRight'] ); // Top right cell applyBorderFormat( - tableModel.rows[sel.firstRow].cells[sel.lastColumn], + tableModel.rows[sel.firstRow].cells[ + isRtl ? sel.firstColumn : sel.lastColumn + ], borderFormat, ['borderBottom', 'borderLeft'] ); // Bottom left cell applyBorderFormat( - tableModel.rows[sel.lastRow].cells[sel.firstColumn], + tableModel.rows[sel.lastRow].cells[ + isRtl ? sel.lastColumn : sel.firstColumn + ], borderFormat, ['borderTop', 'borderRight'] ); // Bottom right cell applyBorderFormat( - tableModel.rows[sel.lastRow].cells[sel.lastColumn], + tableModel.rows[sel.lastRow].cells[ + isRtl ? sel.firstColumn : sel.lastColumn + ], borderFormat, ['borderTop', 'borderLeft'] ); @@ -306,7 +327,7 @@ export default function applyTableBorderFormat( applyBorderFormat(cell, borderFormat, [ 'borderTop', 'borderBottom', - 'borderRight', + isRtl ? 'borderLeft' : 'borderRight', ]); } // Last column @@ -319,7 +340,7 @@ export default function applyTableBorderFormat( applyBorderFormat(cell, borderFormat, [ 'borderTop', 'borderBottom', - 'borderLeft', + isRtl ? 'borderRight' : 'borderLeft', ]); } // Inner cells @@ -342,7 +363,7 @@ export default function applyTableBorderFormat( } //Format perimeter if necessary or possible - modifyPerimeter(tableModel, sel, borderFormat, perimeter); + modifyPerimeter(tableModel, sel, borderFormat, perimeter, isRtl); } return true; @@ -395,7 +416,8 @@ function modifyPerimeter( tableModel: ContentModelTable, sel: TableSelectionCoordinates, borderFormat: string, - perimeter: Perimeter + perimeter: Perimeter, + isRtl: boolean ) { // Top of selection if (perimeter.Top && sel.firstRow - 1 >= 0) { @@ -415,14 +437,14 @@ function modifyPerimeter( if (perimeter.Left && sel.firstColumn - 1 >= 0) { for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { const cell = tableModel.rows[rowIndex].cells[sel.firstColumn - 1]; - applyBorderFormat(cell, borderFormat, ['borderRight']); + applyBorderFormat(cell, borderFormat, [isRtl ? 'borderLeft' : 'borderRight']); } } // Right of selection if (perimeter.Right && sel.lastColumn + 1 < tableModel.rows[0].cells.length) { for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { const cell = tableModel.rows[rowIndex].cells[sel.lastColumn + 1]; - applyBorderFormat(cell, borderFormat, ['borderLeft']); + applyBorderFormat(cell, borderFormat, [isRtl ? 'borderRight' : 'borderLeft']); } } } diff --git a/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts index e2098d3ab20..0b986bbbbbc 100644 --- a/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts @@ -2,15 +2,25 @@ import { ContentModelDocument } from 'roosterjs-content-model-types'; import { setModelDirection } from '../../../lib/modelApi/block/setModelDirection'; describe('setModelDirection', () => { + const width = '3px'; + const style = 'double'; + const color = '#AABBCC'; + const testBorderString = `${width} ${style} ${color}`; + function runTest( model: ContentModelDocument, direction: 'ltr' | 'rtl', expectedModel: ContentModelDocument, - expectedResult: boolean + expectedResult: boolean, + tableTest?: boolean ) { const result = setModelDirection(model, direction); expect(result).toBe(expectedResult); + + if (tableTest && model.blocks[0].blockType == 'Table') { + model.blocks[0].dataset = {}; + } expect(model).toEqual(expectedModel); } @@ -229,4 +239,218 @@ describe('setModelDirection', () => { true ); }); + + it('flip direction for table - LTR -> RTL', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderRight: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderLeft: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + format: {}, + }, + 'rtl', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderLeft: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderRight: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + ], + }, + ], + format: { + direction: 'rtl', + }, + widths: [], + dataset: {}, + }, + ], + format: {}, + }, + true, + true + ); + }); + + it('flip direction for table - RTL -> LTR', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderRight: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderLeft: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + ], + }, + ], + format: { + direction: 'rtl', + }, + widths: [], + dataset: {}, + }, + ], + format: {}, + }, + 'ltr', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderLeft: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + borderRight: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + ], + }, + ], + format: { + direction: 'ltr', + }, + widths: [], + dataset: {}, + }, + ], + format: {}, + }, + true, + true + ); + }); }); diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts index 5644056ff92..6d65809ef1b 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts @@ -48,7 +48,7 @@ describe('applyTableBorderFormat', () => { function runTest( table: ContentModelTable, - expectedTable: ContentModelTable | null, + expectedTable: ContentModelTable, border: Border, operation: BorderOperations ) { @@ -1561,4 +1561,293 @@ describe('applyTableBorderFormat', () => { 'insideBorders' ); }); + + it('RTL - Right Borders', () => { + const testTable = createTestTable(3, 3, { direction: 'rtl' }); + testTable.format.direction = 'rtl'; + runTest( + testTable, + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + borderLeft: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + + borderRight: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + direction: 'rtl', + }, + widths: [], + dataset: {}, + }, + testBorder, + 'rightBorders' + ); + }); + it('RTL - Left Borders', () => { + const testTable = createTestTable(3, 3, { direction: 'rtl' }); + testTable.format.direction = 'rtl'; + runTest( + testTable, + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + + borderLeft: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + isSelected: true, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + borderRight: testBorderString, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: { + editingInfo: '{"borderOverride":true}', + }, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + direction: 'rtl', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + direction: 'rtl', + }, + widths: [], + dataset: {}, + }, + testBorder, + 'leftBorders' + ); + }); }); From b7f2e5644e663017b44b59c43430c19434ccc8ca Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 5 Jan 2024 16:02:40 -0800 Subject: [PATCH 37/64] Standalone Editor step 3: Create EditorPlugin type for Standalone Editor (#2293) * Standalone Editor step 2 * Standalone Editor step 3 * improve --------- Co-authored-by: Bryan Valverde U --- .../controls/ContentModelEditorMainPane.tsx | 2 +- .../editor/ContentModelRooster.tsx | 31 ++-- .../test/publicApi/link/insertLinkTest.ts | 2 +- .../lib/coreApi/triggerEvent.ts | 4 +- .../lib/corePlugin/ContentModelCachePlugin.ts | 12 +- .../corePlugin/ContentModelCopyPastePlugin.ts | 6 +- .../corePlugin/ContentModelFormatPlugin.ts | 7 +- .../lib/corePlugin/DOMEventPlugin.ts | 6 +- .../lib/corePlugin/EntityPlugin.ts | 13 +- .../lib/corePlugin/LifecyclePlugin.ts | 7 +- .../lib/corePlugin/SelectionPlugin.ts | 7 +- .../lib/corePlugin/UndoPlugin.ts | 12 +- .../lib/editor/StandaloneEditor.ts | 7 +- .../test/coreApi/pasteTest.ts | 6 +- .../test/coreApi/triggerEventTest.ts | 4 +- .../corePlugin/ContentModelCachePluginTest.ts | 7 +- .../ContentModelCopyPastePluginTest.ts | 7 +- .../ContentModelFormatPluginTest.ts | 22 +-- .../test/corePlugin/DomEventPluginTest.ts | 20 ++- .../test/corePlugin/EntityPluginTest.ts | 5 +- .../test/corePlugin/LifecyclePluginTest.ts | 11 +- .../test/corePlugin/SelectionPluginTest.ts | 19 ++- .../test/corePlugin/UndoPluginTest.ts | 5 +- .../lib/corePlugins/BridgePlugin.ts | 122 ++++++++++++++ .../lib/corePlugins/ContextMenuPlugin.ts | 3 +- .../lib/editor/ContentModelEditor.ts | 22 +-- .../lib/publicTypes/IContentModelEditor.ts | 13 +- .../test/corePlugins/BridgePluginTest.ts | 152 ++++++++++++++++++ .../test/corePlugins/ContextMenuPluginTest.ts | 2 +- .../test/editor/ContentModelEditorTest.ts | 2 +- .../test/paste/e2e/testUtils.ts | 2 +- .../lib/editor/EditorPlugin.ts | 45 ++++++ .../lib/editor/PluginWithState.ts | 12 ++ .../lib/editor/StandaloneEditorCore.ts | 2 +- .../lib/editor/StandaloneEditorCorePlugins.ts | 2 +- .../lib/editor/StandaloneEditorOptions.ts | 3 +- .../lib/index.ts | 2 + .../lib/createContentModelEditor.ts | 2 +- 38 files changed, 474 insertions(+), 134 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts create mode 100644 packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/editor/PluginWithState.ts diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index b6e9a6e5720..2643eabb305 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -244,7 +244,7 @@ class ContentModelEditorMainPane extends MainPaneBase {this.state.editorCreator && ( (null); const theme = useTheme(); - const { focusOnInit, editorCreator, zoomScale, inDarkMode, plugins } = props; + const { focusOnInit, editorCreator, zoomScale, inDarkMode, plugins, legacyPlugins } = props; React.useEffect(() => { - if (plugins && editorDiv.current) { + if (editorDiv.current) { const uiUtilities = createUIUtilities(editorDiv.current, theme); - plugins.forEach(plugin => { - if (isReactEditorPlugin(plugin)) { - plugin.setUIUtilities(uiUtilities); - } - }); + setUIUtilities(uiUtilities, plugins); + setUIUtilities(uiUtilities, legacyPlugins); } }, [theme, editorCreator]); @@ -86,10 +84,23 @@ export default function ContentModelRooster(props: ContentModelRoosterProps) { return
; } +function setUIUtilities( + uiUtilities: UIUtilities, + plugins: (LegacyEditorPlugin | EditorPlugin)[] | undefined +) { + plugins?.forEach(plugin => { + if (isReactEditorPlugin(plugin)) { + plugin.setUIUtilities(uiUtilities); + } + }); +} + function defaultEditorCreator(div: HTMLDivElement, options: ContentModelEditorOptions) { return new ContentModelEditor(div, options); } -function isReactEditorPlugin(plugin: EditorPlugin): plugin is ReactEditorPlugin { +function isReactEditorPlugin( + plugin: LegacyEditorPlugin | EditorPlugin +): plugin is ReactEditorPlugin { return !!(plugin as ReactEditorPlugin)?.setUIUtilities; } diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts index dc478caac08..27224bb8001 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts @@ -331,7 +331,7 @@ describe('insertLink', () => { onPluginEvent: onPluginEvent, }; const editor = new ContentModelEditor(div, { - plugins: [mockedPlugin], + legacyPlugins: [mockedPlugin], }); editor.focus(); diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts index 23a68882ac1..45172495ce9 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts @@ -1,6 +1,6 @@ import { PluginEventType } from 'roosterjs-editor-types'; -import type { TriggerEvent } from 'roosterjs-content-model-types'; -import type { EditorPlugin, PluginEvent } from 'roosterjs-editor-types'; +import type { EditorPlugin, TriggerEvent } from 'roosterjs-content-model-types'; +import type { PluginEvent } from 'roosterjs-editor-types'; const allowedEventsInShadowEdit: PluginEventType[] = [ PluginEventType.EditorReady, diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts index 6ccff76c84e..082de09701a 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts @@ -6,14 +6,10 @@ import type { ContentModelCachePluginState, ContentModelContentChangedEvent, IStandaloneEditor, + PluginWithState, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; -import type { - IEditor, - PluginEvent, - PluginKeyDownEvent, - PluginWithState, -} from 'roosterjs-editor-types'; +import type { PluginEvent, PluginKeyDownEvent } from 'roosterjs-editor-types'; /** * ContentModel cache plugin manages cached Content Model, and refresh the cache when necessary @@ -45,9 +41,9 @@ class ContentModelCachePlugin implements PluginWithState this.onPaste(e), diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts index 3cd4ea2862e..5825582cc57 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts @@ -3,10 +3,11 @@ import { applyPendingFormat } from './utils/applyPendingFormat'; import { getObjectKeys } from 'roosterjs-content-model-dom'; import { isCharacterValue, isCursorMovingKey } from '../publicApi/domUtils/eventUtils'; import { PluginEventType } from 'roosterjs-editor-types'; -import type { IEditor, PluginEvent, PluginWithState } from 'roosterjs-editor-types'; +import type { PluginEvent } from 'roosterjs-editor-types'; import type { ContentModelFormatPluginState, IStandaloneEditor, + PluginWithState, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; @@ -47,9 +48,9 @@ class ContentModelFormatPlugin implements PluginWithState typeof this.state.defaultFormat[x] !== 'undefined' diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts index 1ec7ae78567..df45d1fe804 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/DOMEventPlugin.ts @@ -7,8 +7,8 @@ import type { IStandaloneEditor, DOMEventRecord, StandaloneEditorOptions, + PluginWithState, } from 'roosterjs-content-model-types'; -import type { IEditor, PluginWithState } from 'roosterjs-editor-types'; /** * DOMEventPlugin handles customized DOM events, including: @@ -52,8 +52,8 @@ class DOMEventPlugin implements PluginWithState { * Initialize this plugin. This should only be called from Editor * @param editor Editor instance */ - initialize(editor: IEditor) { - this.editor = editor as IStandaloneEditor & IEditor; + initialize(editor: IStandaloneEditor) { + this.editor = editor; const document = this.editor.getDocument(); const eventHandlers: Partial< diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts index a5fe63ca299..0759a9cb23e 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts @@ -16,14 +16,9 @@ import type { EntityOperation, EntityPluginState, IStandaloneEditor, -} from 'roosterjs-content-model-types'; -import type { - ContentChangedEvent, - IEditor, - PluginEvent, - PluginMouseUpEvent, PluginWithState, -} from 'roosterjs-editor-types'; +} from 'roosterjs-content-model-types'; +import type { ContentChangedEvent, PluginEvent, PluginMouseUpEvent } from 'roosterjs-editor-types'; const ENTITY_ID_REGEX = /_(\d{1,8})$/; @@ -66,8 +61,8 @@ class EntityPlugin implements PluginWithState { * Initialize this plugin. This should only be called from Editor * @param editor Editor instance */ - initialize(editor: IEditor) { - this.editor = editor as IStandaloneEditor & IEditor; + initialize(editor: IStandaloneEditor) { + this.editor = editor; } /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts index daac254a6b8..7e1cb2174e9 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts @@ -12,9 +12,10 @@ import type { ContentModelSegmentFormat, IStandaloneEditor, LifecyclePluginState, + PluginWithState, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; -import type { IEditor, PluginWithState, PluginEvent } from 'roosterjs-editor-types'; +import type { PluginEvent } from 'roosterjs-editor-types'; const ContentEditableAttributeName = 'contenteditable'; const DefaultTextColor = '#000000'; @@ -75,8 +76,8 @@ class LifecyclePlugin implements PluginWithState { * Initialize this plugin. This should only be called from Editor * @param editor Editor instance */ - initialize(editor: IEditor) { - this.editor = editor as IEditor & IStandaloneEditor; + initialize(editor: IStandaloneEditor) { + this.editor = editor; this.editor.setContentModel(this.initialModel, { ignoreSelection: true }); diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index a6752370240..28a9957ab04 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -1,10 +1,11 @@ import { isElementOfType, isNodeOfType, toArray } from 'roosterjs-content-model-dom'; import { isModifierKey } from '../publicApi/domUtils/eventUtils'; import { PluginEventType } from 'roosterjs-editor-types'; -import type { IEditor, PluginEvent, PluginWithState } from 'roosterjs-editor-types'; +import type { PluginEvent } from 'roosterjs-editor-types'; import type { DOMSelection, IStandaloneEditor, + PluginWithState, SelectionPluginState, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; @@ -29,8 +30,8 @@ class SelectionPlugin implements PluginWithState { return 'Selection'; } - initialize(editor: IEditor) { - this.editor = editor as IEditor & IStandaloneEditor; + initialize(editor: IStandaloneEditor) { + this.editor = editor; const doc = this.editor.getDocument(); const styleNode = doc.createElement('style'); diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts index 8d4dc7668c9..9fef80e748b 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts @@ -5,15 +5,11 @@ import { PluginEventType } from 'roosterjs-editor-types'; import { undo } from '../publicApi/undo/undo'; import type { IStandaloneEditor, + PluginWithState, StandaloneEditorOptions, UndoPluginState, } from 'roosterjs-content-model-types'; -import type { - ContentChangedEvent, - IEditor, - PluginEvent, - PluginWithState, -} from 'roosterjs-editor-types'; +import type { ContentChangedEvent, PluginEvent } from 'roosterjs-editor-types'; const Backspace = 'Backspace'; const Delete = 'Delete'; @@ -52,8 +48,8 @@ class UndoPlugin implements PluginWithState { * Initialize this plugin. This should only be called from Editor * @param editor Editor instance */ - initialize(editor: IEditor): void { - this.editor = editor as IEditor & IStandaloneEditor; + initialize(editor: IStandaloneEditor): void { + this.editor = editor; } /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts index dc1a90734aa..12bd22bed16 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -4,7 +4,6 @@ import { PluginEventType } from 'roosterjs-editor-types'; import { transformColor } from '../publicApi/color/transformColor'; import type { DarkColorHandler, - IEditor, PluginEventData, PluginEventFromType, } from 'roosterjs-editor-types'; @@ -49,11 +48,7 @@ export class StandaloneEditor implements IStandaloneEditor { onBeforeInitializePlugins?.(); - // TODO: Remove this type cast - const editor: IStandaloneEditor = this; - this.getCore().plugins.forEach(plugin => - plugin.initialize(editor as IStandaloneEditor & IEditor) - ); + this.getCore().plugins.forEach(plugin => plugin.initialize(this)); } /** diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 233bb3de808..f9285159a85 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -104,7 +104,7 @@ describe('Paste ', () => { context = undefined; editor = new ContentModelEditor(div, { - plugins: [new ContentModelPastePlugin()], + legacyPlugins: [new ContentModelPastePlugin()], coreApiOverride: { focus, createContentModel, @@ -194,7 +194,7 @@ describe('paste with content model & paste plugin', () => { div = document.createElement('div'); document.body.appendChild(div); editor = new ContentModelEditor(div, { - plugins: [new ContentModelPastePlugin()], + legacyPlugins: [new ContentModelPastePlugin()], }); spyOn(addParserF, 'default').and.callThrough(); spyOn(setProcessorF, 'setProcessor').and.callThrough(); @@ -341,7 +341,7 @@ describe('paste with content model & paste plugin', () => { let eventChecker: BeforePasteEvent = {}; editor = new ContentModelEditor(div!, { - plugins: [ + legacyPlugins: [ { initialize: () => {}, dispose: () => {}, diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts index cb4cc9710b0..4952b9810b1 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts @@ -1,5 +1,5 @@ -import { EditorPlugin, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; -import { StandaloneEditorCore } from 'roosterjs-content-model-types'; +import { EditorPlugin, StandaloneEditorCore } from 'roosterjs-content-model-types'; +import { PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { triggerEvent } from '../../lib/coreApi/triggerEvent'; describe('triggerEvent', () => { diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts index 801e7066e3c..07528b7c91c 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts @@ -1,14 +1,15 @@ import { createContentModelCachePlugin } from '../../lib/corePlugin/ContentModelCachePlugin'; -import { IEditor, PluginEventType, PluginWithState } from 'roosterjs-editor-types'; +import { PluginEventType } from 'roosterjs-editor-types'; import { ContentModelCachePluginState, ContentModelDomIndexer, IStandaloneEditor, + PluginWithState, } from 'roosterjs-content-model-types'; describe('ContentModelCachePlugin', () => { let plugin: PluginWithState; - let editor: IStandaloneEditor & IEditor; + let editor: IStandaloneEditor; let addEventListenerSpy: jasmine.Spy; let removeEventListenerSpy: jasmine.Spy; @@ -37,7 +38,7 @@ describe('ContentModelCachePlugin', () => { removeEventListener: removeEventListenerSpy, }; }, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; plugin = createContentModelCachePlugin({}); plugin.initialize(editor); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index 21ed4713585..f3d1083b600 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -8,7 +8,7 @@ import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi import * as transformColor from '../../lib/publicApi/color/transformColor'; import { createModelToDomContext, createTable, createTableCell } from 'roosterjs-content-model-dom'; import { createRange } from 'roosterjs-editor-dom'; -import { DarkColorHandler, IEditor, PluginWithState } from 'roosterjs-editor-types'; +import { DarkColorHandler } from 'roosterjs-editor-types'; import { setEntityElementClasses } from 'roosterjs-content-model-dom/test/domUtils/entityUtilTest'; import { ContentModelDocument, @@ -19,6 +19,7 @@ import { DOMEventRecord, ClipboardData, CopyPastePluginState, + PluginWithState, } from 'roosterjs-content-model-types'; import { adjustSelectionForCopyCut, @@ -59,7 +60,7 @@ describe('ContentModelCopyPastePlugin.Ctor', () => { }); describe('ContentModelCopyPastePlugin |', () => { - let editor: IEditor & IStandaloneEditor = null!; + let editor: IStandaloneEditor = null!; let plugin: PluginWithState; let domEvents: Record = {}; let div: HTMLDivElement; @@ -123,7 +124,7 @@ describe('ContentModelCopyPastePlugin |', () => { allowedCustomPasteType, }); plugin.getState().tempDiv = div; - editor = ({ + editor = ({ attachDomEvent: (eventMap: Record) => { domEvents = eventMap; }, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts index a451fe197fe..530209ce7d2 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts @@ -1,7 +1,7 @@ import * as applyPendingFormat from '../../lib/corePlugin/utils/applyPendingFormat'; import { createContentModelFormatPlugin } from '../../lib/corePlugin/ContentModelFormatPlugin'; -import { IEditor, PluginEventType } from 'roosterjs-editor-types'; import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { PluginEventType } from 'roosterjs-editor-types'; import { addSegment, createContentModelDocument, @@ -21,7 +21,7 @@ describe('ContentModelFormatPlugin', () => { const editor = ({ cacheContentModel: () => {}, isDarkMode: () => false, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); plugin.initialize(editor); @@ -43,7 +43,7 @@ describe('ContentModelFormatPlugin', () => { isInIME: () => false, cacheContentModel: () => {}, getEnvironment: () => ({}), - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); const model = createContentModelDocument(); @@ -80,7 +80,7 @@ describe('ContentModelFormatPlugin', () => { createContentModel: () => model, cacheContentModel: () => {}, getEnvironment: () => ({}), - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); plugin.initialize(editor); @@ -114,7 +114,7 @@ describe('ContentModelFormatPlugin', () => { isDarkMode: () => false, triggerPluginEvent, getVisibleViewport, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); const state = plugin.getState(); @@ -143,7 +143,7 @@ describe('ContentModelFormatPlugin', () => { const editor = ({ createContentModel: () => model, cacheContentModel: () => {}, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); plugin.initialize(editor); @@ -175,7 +175,7 @@ describe('ContentModelFormatPlugin', () => { callback(); }, cacheContentModel: () => {}, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); const state = plugin.getState(); @@ -204,7 +204,7 @@ describe('ContentModelFormatPlugin', () => { const editor = ({ createContentModel: () => model, cacheContentModel: () => {}, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); const state = plugin.getState(); @@ -234,7 +234,7 @@ describe('ContentModelFormatPlugin', () => { createContentModel: () => model, cacheContentModel: () => {}, getEnvironment: () => ({}), - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); const state = plugin.getState(); @@ -260,7 +260,7 @@ describe('ContentModelFormatPlugin', () => { }); describe('ContentModelFormatPlugin for default format', () => { - let editor: IStandaloneEditor & IEditor; + let editor: IStandaloneEditor; let contentDiv: HTMLDivElement; let getDOMSelection: jasmine.Spy; let getPendingFormatSpy: jasmine.Spy; @@ -284,7 +284,7 @@ describe('ContentModelFormatPlugin for default format', () => { cacheContentModel: cacheContentModelSpy, takeSnapshot: takeSnapshotSpy, formatContentModel: formatContentModelSpy, - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; }); it('Collapsed range, text input, under editor directly', () => { diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts index 844bcd0ad46..673f9684a07 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts @@ -1,7 +1,11 @@ import * as eventUtils from '../../lib/publicApi/domUtils/eventUtils'; -import { ChangeSource, IEditor, PluginEventType, PluginWithState } from 'roosterjs-editor-types'; +import { ChangeSource, PluginEventType } from 'roosterjs-editor-types'; import { createDOMEventPlugin } from '../../lib/corePlugin/DOMEventPlugin'; -import { DOMEventPluginState, IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + DOMEventPluginState, + IStandaloneEditor, + PluginWithState, +} from 'roosterjs-content-model-types'; const getDocument = () => document; @@ -21,7 +25,7 @@ describe('DOMEventPlugin', () => { getDocument, attachDomEvent, getEnvironment: () => ({}), - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; plugin.initialize(editor); @@ -66,7 +70,7 @@ describe('DOMEventPlugin', () => { const attachDomEvent = jasmine .createSpy('attachDomEvent') .and.returnValue(jasmine.createSpy('disposer')); - plugin.initialize(({ + plugin.initialize(({ getDocument, attachDomEvent, getEnvironment: () => ({}), @@ -101,7 +105,7 @@ describe('DOMEventPlugin verify event handlers while disallow keyboard event pro }; plugin = createDOMEventPlugin({}, div); - plugin.initialize(({ + plugin.initialize(({ getDocument, attachDomEvent: (map: Record) => { eventMap = map; @@ -201,7 +205,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { }, null! ); - plugin.initialize(({ + plugin.initialize(({ getDocument: () => ({ addEventListener, removeEventListener, @@ -318,7 +322,7 @@ describe('DOMEventPlugin handle other event', () => { let eventMap: Record; let scrollContainer: HTMLElement; let addEventListenerSpy: jasmine.Spy; - let editor: IEditor & IStandaloneEditor; + let editor: IStandaloneEditor; beforeEach(() => { addEventListener = jasmine.createSpy('addEventListener'); @@ -337,7 +341,7 @@ describe('DOMEventPlugin handle other event', () => { null! ); - editor = ({ + editor = ({ getDocument: () => ({ addEventListener, removeEventListener, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts index 0d10f8d8388..85b44e03d18 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts @@ -2,17 +2,16 @@ import * as entityUtils from 'roosterjs-content-model-dom/lib/domUtils/entityUti import * as transformColor from '../../lib/publicApi/color/transformColor'; import { createContentModelDocument, createEntity } from '../../../roosterjs-content-model-dom/lib'; import { createEntityPlugin } from '../../lib/corePlugin/EntityPlugin'; +import { IStandaloneEditor, PluginWithState } from 'roosterjs-content-model-types'; import { DarkColorHandler, EntityOperation, EntityPluginState, - IEditor, PluginEventType, - PluginWithState, } from 'roosterjs-editor-types'; describe('EntityPlugin', () => { - let editor: IEditor; + let editor: IStandaloneEditor; let plugin: PluginWithState; let createContentModelSpy: jasmine.Spy; let triggerPluginEventSpy: jasmine.Spy; diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts index eb7a0fdca53..c3afc6b6d80 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts @@ -1,5 +1,6 @@ import { createLifecyclePlugin } from '../../lib/corePlugin/LifecyclePlugin'; -import { DarkColorHandler, IEditor, PluginEventType } from 'roosterjs-editor-types'; +import { DarkColorHandler, PluginEventType } from 'roosterjs-editor-types'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; describe('LifecyclePlugin', () => { it('init', () => { @@ -9,7 +10,7 @@ describe('LifecyclePlugin', () => { const state = plugin.getState(); const setContentModelSpy = jasmine.createSpy('setContentModel'); - plugin.initialize(({ + plugin.initialize(({ triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, @@ -67,7 +68,7 @@ describe('LifecyclePlugin', () => { const state = plugin.getState(); const setContentModelSpy = jasmine.createSpy('setContentModel'); - plugin.initialize(({ + plugin.initialize(({ triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, @@ -101,7 +102,7 @@ describe('LifecyclePlugin', () => { const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); const setContentModelSpy = jasmine.createSpy('setContentModel'); - plugin.initialize(({ + plugin.initialize(({ triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, @@ -144,7 +145,7 @@ describe('LifecyclePlugin', () => { const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); const setContentModelSpy = jasmine.createSpy('setContentModel'); - plugin.initialize(({ + plugin.initialize(({ triggerPluginEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts index 44347183609..8c184a3e202 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts @@ -1,6 +1,11 @@ import { createSelectionPlugin } from '../../lib/corePlugin/SelectionPlugin'; -import { EditorPlugin, IEditor, PluginEventType, PluginWithState } from 'roosterjs-editor-types'; -import { IStandaloneEditor, SelectionPluginState } from 'roosterjs-content-model-types'; +import { PluginEventType } from 'roosterjs-editor-types'; +import { + EditorPlugin, + IStandaloneEditor, + PluginWithState, + SelectionPluginState, +} from 'roosterjs-content-model-types'; const MockedStyleNode = 'STYLENODE' as any; @@ -26,7 +31,7 @@ describe('SelectionPlugin', () => { getDocument: getDocumentSpy, attachDomEvent, getEnvironment: () => ({}), - } as any) as IStandaloneEditor & IEditor; + } as any) as IStandaloneEditor; plugin.initialize(editor); @@ -67,7 +72,7 @@ describe('SelectionPlugin', () => { removeEventListener: removeEventListenerSpy, }); - plugin.initialize(({ + plugin.initialize(({ getDocument: getDocumentSpy, attachDomEvent, getEnvironment: () => ({}), @@ -96,7 +101,7 @@ describe('SelectionPlugin handle onFocus and onBlur event', () => { let setDOMSelectionSpy: jasmine.Spy; let removeEventListenerSpy: jasmine.Spy; - let editor: IEditor; + let editor: IStandaloneEditor; beforeEach(() => { triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); @@ -115,7 +120,7 @@ describe('SelectionPlugin handle onFocus and onBlur event', () => { plugin = createSelectionPlugin({}); - editor = ({ + editor = ({ getDocument: getDocumentSpy, triggerPluginEvent, getEnvironment: () => ({}), @@ -168,7 +173,7 @@ describe('SelectionPlugin handle onFocus and onBlur event', () => { describe('SelectionPlugin handle image selection', () => { let plugin: EditorPlugin; - let editor: IEditor; + let editor: IStandaloneEditor; let getDOMSelectionSpy: jasmine.Spy; let setDOMSelectionSpy: jasmine.Spy; let getDocumentSpy: jasmine.Spy; diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts index d7294223716..c2a94dcc613 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts @@ -2,15 +2,16 @@ import * as SnapshotsManagerImpl from '../../lib/editor/SnapshotsManagerImpl'; import * as undo from '../../lib/publicApi/undo/undo'; import { ChangeSource } from '../../lib/constants/ChangeSource'; import { createUndoPlugin } from '../../lib/corePlugin/UndoPlugin'; -import { IEditor, PluginEventType, PluginWithState } from 'roosterjs-editor-types'; +import { PluginEventType } from 'roosterjs-editor-types'; import { IStandaloneEditor, + PluginWithState, SnapshotsManager, UndoPluginState, } from 'roosterjs-content-model-types'; describe('UndoPlugin', () => { - let editor: IEditor & IStandaloneEditor; + let editor: IStandaloneEditor; let createSnapshotsManagerSpy: jasmine.Spy; let getDOMSelectionSpy: jasmine.Spy; let canUndoAutoCompleteSpy: jasmine.Spy; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts new file mode 100644 index 00000000000..43759089f85 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts @@ -0,0 +1,122 @@ +import { createContextMenuPlugin } from './ContextMenuPlugin'; +import { createEditPlugin } from './EditPlugin'; +import { createEventTypeTranslatePlugin } from './EventTypeTranslatePlugin'; +import { createNormalizeTablePlugin } from './NormalizeTablePlugin'; +import { PluginEventType } from 'roosterjs-editor-types'; +import type { ContentModelCorePluginState } from '../publicTypes/ContentModelCorePlugins'; +import type { + ContentModelEditorOptions, + IContentModelEditor, +} from '../publicTypes/IContentModelEditor'; +import type { EditorPlugin as LegacyEditorPlugin, PluginEvent } from 'roosterjs-editor-types'; +import type { EditorPlugin } from 'roosterjs-content-model-types'; + +const ExclusivelyHandleEventPluginKey = '__ExclusivelyHandleEventPlugin'; + +/** + * @internal + * Act as a bridge between Standalone editor and Content Model editor, translate Standalone editor event type to legacy event type + */ +export class BridgePlugin implements EditorPlugin { + private legacyPlugins: LegacyEditorPlugin[]; + private corePluginState: ContentModelCorePluginState; + private outerEditor: IContentModelEditor | null = null; + + constructor(options: ContentModelEditorOptions) { + const translatePlugin = createEventTypeTranslatePlugin(); + const editPlugin = createEditPlugin(); + const contextMenuPlugin = createContextMenuPlugin(options); + const normalizeTablePlugin = createNormalizeTablePlugin(); + + this.legacyPlugins = [ + translatePlugin, + editPlugin, + ...(options.legacyPlugins ?? []).filter(x => !!x), + contextMenuPlugin, + normalizeTablePlugin, + ]; + this.corePluginState = { + edit: editPlugin.getState(), + contextMenu: contextMenuPlugin.getState(), + }; + } + + /** + * Get a friendly name of this plugin + */ + getName() { + return 'Bridge'; + } + + /** + * Get core plugin state + */ + getCorePluginState(): ContentModelCorePluginState { + return this.corePluginState; + } + + /** + * Initialize this plugin. This should only be called from Editor + * @param editor Editor instance + */ + initialize() { + if (this.outerEditor) { + const editor = this.outerEditor; + + this.legacyPlugins.forEach(plugin => plugin.initialize(editor)); + + this.legacyPlugins.forEach(plugin => + plugin.onPluginEvent?.({ + eventType: PluginEventType.EditorReady, + }) + ); + } + } + + /** + * Initialize all inner plugins with Content Model Editor + */ + setOuterEditor(editor: IContentModelEditor) { + this.outerEditor = editor; + } + + /** + * Dispose this plugin + */ + dispose() { + for (let i = this.legacyPlugins.length - 1; i >= 0; i--) { + const plugin = this.legacyPlugins[i]; + + plugin.dispose(); + } + } + + willHandleEventExclusively(event: PluginEvent) { + for (let i = 0; i < this.legacyPlugins.length; i++) { + const plugin = this.legacyPlugins[i]; + + if (plugin.willHandleEventExclusively?.(event)) { + if (!event.eventDataCache) { + event.eventDataCache = {}; + } + + event.eventDataCache[ExclusivelyHandleEventPluginKey] = plugin; + return true; + } + } + + return false; + } + + onPluginEvent(event: PluginEvent) { + const exclusivelyHandleEventPlugin = event.eventDataCache?.[ + ExclusivelyHandleEventPluginKey + ] as EditorPlugin | undefined; + + if (exclusivelyHandleEventPlugin) { + exclusivelyHandleEventPlugin.onPluginEvent?.(event); + } else { + this.legacyPlugins.forEach(plugin => plugin.onPluginEvent?.(event)); + } + } +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts index 570d9be53c1..850101d5447 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/ContextMenuPlugin.ts @@ -24,7 +24,8 @@ class ContextMenuPlugin implements PluginWithState { constructor(options: ContentModelEditorOptions) { this.state = { contextMenuProviders: - options.plugins?.filter>(isContextMenuProvider) || [], + options.legacyPlugins?.filter>(isContextMenuProvider) || + [], }; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index e22e8b84d4f..fb9427bbb69 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -1,9 +1,8 @@ +import { BridgePlugin } from '../corePlugins/BridgePlugin'; import { buildRangeEx } from './utils/buildRangeEx'; -import { createCorePlugins } from '../corePlugins/createCorePlugins'; import { createEditorCore } from './createEditorCore'; import { getObjectKeys } from 'roosterjs-content-model-dom'; import { getPendableFormatState } from './utils/getPendableFormatState'; -import type { ContentModelCorePluginState } from '../publicTypes/ContentModelCorePlugins'; import { createModelFromHtml, isBold, @@ -98,14 +97,8 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode * @param options An optional options object to customize the editor */ constructor(contentDiv: HTMLDivElement, options: ContentModelEditorOptions = {}) { - const corePlugins = createCorePlugins(options); - const plugins = [ - corePlugins.eventTranslate, - corePlugins.edit, - ...(options.plugins ?? []), - corePlugins.contextMenu, - corePlugins.normalizeTable, - ]; + const bridgePlugin = new BridgePlugin(options); + const plugins = [bridgePlugin, ...(options.plugins ?? [])]; const initContent = options.initialContent ?? contentDiv.innerHTML; const initialModel = initContent && !options.initialModel @@ -118,13 +111,10 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode : options.initialModel; const standaloneEditorOptions: ContentModelEditorOptions = { ...options, - plugins: plugins, + plugins, initialModel, }; - const corePluginState: ContentModelCorePluginState = { - edit: corePlugins.edit.getState(), - contextMenu: corePlugins.contextMenu.getState(), - }; + const corePluginState = bridgePlugin.getCorePluginState(); super(contentDiv, standaloneEditorOptions, () => { // Need to create Content Model Editor Core before initialize plugins since some plugins need this object @@ -133,6 +123,8 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode corePluginState, size => size / this.getCore().zoomScale ); + + bridgePlugin.setOuterEditor(this); }); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts index d1aa0ef21fc..7263ce3959e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts @@ -1,6 +1,6 @@ import type { ContentModelCorePlugins } from './ContentModelCorePlugins'; import type { ContentModelCoreApiMap } from './ContentModelEditorCore'; -import type { ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; +import type { EditorPlugin, ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; import type { StandaloneEditorOptions, IStandaloneEditor } from 'roosterjs-content-model-types'; /** @@ -25,13 +25,18 @@ export interface ContentModelEditorOptions extends StandaloneEditorOptions { */ corePluginOverride?: Partial; + /** + * A function map to override default core API implementation + * Default value is null + */ + legacyCoreApiOverride?: Partial; + /** * Specify the enabled experimental features */ experimentalFeatures?: ExperimentalFeatures[]; /** - * A function map to override default core API implementation - * Default value is null + * Legacy plugins using IEditor interface */ - legacyCoreApiOverride?: Partial; + legacyPlugins?: EditorPlugin[]; } diff --git a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts new file mode 100644 index 00000000000..1ba9a7c5f4d --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts @@ -0,0 +1,152 @@ +import * as ContextMenuPlugin from '../../lib/corePlugins/ContextMenuPlugin'; +import * as EditPlugin from '../../lib/corePlugins/EditPlugin'; +import * as EventTypeTranslatePlugin from '../../lib/corePlugins/EventTypeTranslatePlugin'; +import * as NormalizeTablePlugin from '../../lib/corePlugins/NormalizeTablePlugin'; +import { BridgePlugin } from '../../lib/corePlugins/BridgePlugin'; +import { PluginEventType } from 'roosterjs-editor-types'; + +describe('BridgePlugin', () => { + function createMockedPlugin(name: string) { + return { + initialize: () => {}, + dispose: () => {}, + getState: () => name, + } as any; + } + beforeEach(() => { + spyOn(ContextMenuPlugin, 'createContextMenuPlugin').and.returnValue( + createMockedPlugin('contextMenu') + ); + spyOn(EditPlugin, 'createEditPlugin').and.returnValue(createMockedPlugin('edit')); + spyOn(EventTypeTranslatePlugin, 'createEventTypeTranslatePlugin').and.returnValue( + createMockedPlugin('eventTypeTranslate') + ); + spyOn(NormalizeTablePlugin, 'createNormalizeTablePlugin').and.returnValue( + createMockedPlugin('normalizeTable') + ); + }); + + it('Ctor and init', () => { + const initializeSpy = jasmine.createSpy('initialize'); + const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1'); + const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2'); + const disposeSpy = jasmine.createSpy('dispose'); + + const mockedPlugin1 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy1, + dispose: disposeSpy, + } as any; + const mockedPlugin2 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy2, + dispose: disposeSpy, + } as any; + const mockedEditor = 'EDITOR' as any; + + const plugin = new BridgePlugin({ + legacyPlugins: [mockedPlugin1, mockedPlugin2], + }); + expect(initializeSpy).not.toHaveBeenCalled(); + expect(onPluginEventSpy1).not.toHaveBeenCalled(); + expect(onPluginEventSpy2).not.toHaveBeenCalled(); + expect(disposeSpy).not.toHaveBeenCalled(); + + expect(plugin.getCorePluginState()).toEqual({ + edit: 'edit', + contextMenu: 'contextMenu', + } as any); + + plugin.setOuterEditor(mockedEditor); + + expect(initializeSpy).toHaveBeenCalledTimes(0); + expect(onPluginEventSpy1).toHaveBeenCalledTimes(0); + expect(onPluginEventSpy2).toHaveBeenCalledTimes(0); + expect(disposeSpy).not.toHaveBeenCalled(); + + plugin.initialize(); + + expect(initializeSpy).toHaveBeenCalledTimes(2); + expect(onPluginEventSpy1).toHaveBeenCalledTimes(1); + expect(onPluginEventSpy2).toHaveBeenCalledTimes(1); + expect(disposeSpy).not.toHaveBeenCalled(); + + expect(initializeSpy).toHaveBeenCalledWith(mockedEditor); + expect(onPluginEventSpy1).toHaveBeenCalledWith({ + eventType: PluginEventType.EditorReady, + }); + expect(onPluginEventSpy2).toHaveBeenCalledWith({ + eventType: PluginEventType.EditorReady, + }); + + plugin.dispose(); + + expect(disposeSpy).toHaveBeenCalledTimes(2); + }); + + it('willHandleEventExclusively', () => { + const initializeSpy = jasmine.createSpy('initialize'); + const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1'); + const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2'); + const disposeSpy = jasmine.createSpy('dispose'); + const willHandleEventExclusivelySpy = jasmine + .createSpy('willHandleEventExclusively') + .and.returnValue(true); + + const mockedPlugin1 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy1, + dispose: disposeSpy, + } as any; + const mockedPlugin2 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy2, + willHandleEventExclusively: willHandleEventExclusivelySpy, + dispose: disposeSpy, + } as any; + const mockedEditor = 'EDITOR' as any; + const plugin = new BridgePlugin({ + legacyPlugins: [mockedPlugin1, mockedPlugin2], + }); + + plugin.setOuterEditor(mockedEditor); + + const mockedEvent = {} as any; + const result = plugin.willHandleEventExclusively(mockedEvent); + + expect(result).toBeTrue(); + expect(mockedEvent).toEqual({ + eventDataCache: { + __ExclusivelyHandleEventPlugin: mockedPlugin2, + }, + }); + + plugin.initialize(); + plugin.onPluginEvent(mockedEvent); + + expect(onPluginEventSpy1).toHaveBeenCalledTimes(1); + expect(onPluginEventSpy2).toHaveBeenCalledTimes(2); + + expect(onPluginEventSpy1).toHaveBeenCalledWith({ + eventType: PluginEventType.EditorReady, + }); + expect(onPluginEventSpy2).toHaveBeenCalledWith({ + eventType: PluginEventType.EditorReady, + }); + expect(onPluginEventSpy2).toHaveBeenCalledWith(mockedEvent); + + const mockedEvent2 = { + eventType: 'MockedEvent2', + } as any; + + plugin.onPluginEvent(mockedEvent2); + + expect(onPluginEventSpy1).toHaveBeenCalledWith(mockedEvent2); + expect(onPluginEventSpy2).toHaveBeenCalledWith(mockedEvent2); + expect(mockedEvent2).toEqual({ + eventType: 'MockedEvent2', + }); + + plugin.dispose(); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts index 9df59088b80..763cda3b4c0 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/ContextMenuPluginTest.ts @@ -50,7 +50,7 @@ describe('ContextMenu handle other event', () => { } as any; plugin = createContextMenuPlugin({ - plugins: [mockedPlugin1, mockedPlugin2], + legacyPlugins: [mockedPlugin1, mockedPlugin2], }); plugin.initialize(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts index 3bd949a0d57..d1fc150a7ea 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts @@ -185,7 +185,7 @@ describe('ContentModelEditor', () => { }, }; const editor = new ContentModelEditor(div, { - plugins: [plugin], + legacyPlugins: [plugin], }); editor.dispose(); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts index 3d03d83a631..1c0bb630ea2 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts @@ -13,7 +13,7 @@ export function initEditor(id: string): IContentModelEditor { document.body.insertBefore(node, document.body.childNodes[0]); let options: ContentModelEditorOptions = { - plugins: [new ContentModelPastePlugin()], + legacyPlugins: [new ContentModelPastePlugin()], coreApiOverride: { getVisibleViewport: () => { return { diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts new file mode 100644 index 00000000000..063811e4c1d --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts @@ -0,0 +1,45 @@ +import type { IStandaloneEditor } from './IStandaloneEditor'; +import type { PluginEvent } from 'roosterjs-editor-types'; + +/** + * Interface of an editor plugin + */ +export interface EditorPlugin { + /** + * Get a friendly name of this plugin + */ + getName: () => string; + + /** + * The first method that editor will call to a plugin when editor is initializing. + * It will pass in the editor instance, plugin should take this chance to save the + * editor reference so that it can call to any editor method or format API later. + * @param editor The editor object + */ + initialize: (editor: IStandaloneEditor) => void; + + /** + * The last method that editor will call to a plugin before it is disposed. + * Plugin can take this chance to clear the reference to editor. After this method is + * called, plugin should not call to any editor method since it will result in error. + */ + dispose: () => void; + + /** + * Check if the plugin should handle the given event exclusively. + * Handle an event exclusively means other plugin will not receive this event in + * onPluginEvent method. + * If two plugins will return true in willHandleEventExclusively() for the same event, + * the final result depends on the order of the plugins are added into editor + * @param event The event to check: + */ + willHandleEventExclusively?: (event: PluginEvent) => boolean; + + /** + * Core method for a plugin. Once an event happens in editor, editor will call this + * method of each plugin to handle the event as long as the event is not handled + * exclusively by another plugin. + * @param event The event to handle: + */ + onPluginEvent?: (event: PluginEvent) => void; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/PluginWithState.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/PluginWithState.ts new file mode 100644 index 00000000000..0c38b4ab217 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/PluginWithState.ts @@ -0,0 +1,12 @@ +import type { EditorPlugin } from './EditorPlugin'; + +/** + * An editor plugin which have a state object stored on editor core + * so that editor and core api can access it + */ +export interface PluginWithState extends EditorPlugin { + /** + * Get plugin state object + */ + getState(): T; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index f53963adad3..e6e3df47df3 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -1,3 +1,4 @@ +import type { EditorPlugin } from './EditorPlugin'; import type { ClipboardData } from '../parameter/ClipboardData'; import type { PasteType } from '../enum/PasteType'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; @@ -5,7 +6,6 @@ import type { Snapshot } from '../parameter/Snapshot'; import type { EntityState } from '../parameter/FormatWithContentModelContext'; import type { DarkColorHandler, - EditorPlugin, PluginEvent, Rect, TrustedHTMLHandler, diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCorePlugins.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCorePlugins.ts index 2482b487061..007a3cb6da7 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCorePlugins.ts @@ -1,3 +1,4 @@ +import type { PluginWithState } from './PluginWithState'; import type { CopyPastePluginState } from '../pluginState/CopyPastePluginState'; import type { UndoPluginState } from '../pluginState/UndoPluginState'; import type { SelectionPluginState } from '../pluginState/SelectionPluginState'; @@ -6,7 +7,6 @@ import type { LifecyclePluginState } from '../pluginState/LifecyclePluginState'; import type { DOMEventPluginState } from '../pluginState/DOMEventPluginState'; import type { ContentModelCachePluginState } from '../pluginState/ContentModelCachePluginState'; import type { ContentModelFormatPluginState } from '../pluginState/ContentModelFormatPluginState'; -import type { PluginWithState } from 'roosterjs-editor-types'; /** * Core plugins for standalone editor diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts index 5150595b221..8e2432b4cef 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts @@ -1,6 +1,7 @@ +import type { EditorPlugin } from './EditorPlugin'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { StandaloneCoreApiMap } from './StandaloneEditorCore'; -import type { EditorPlugin, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { ModelToDomOption } from '../context/ModelToDomOption'; import type { ContentModelDocument } from '../group/ContentModelDocument'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 01953755fbb..0f894baed90 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -217,6 +217,8 @@ export { Paste, } from './editor/StandaloneEditorCore'; export { StandaloneEditorCorePlugins } from './editor/StandaloneEditorCorePlugins'; +export { EditorPlugin } from './editor/EditorPlugin'; +export { PluginWithState } from './editor/PluginWithState'; export { ContentModelCachePluginState } from './pluginState/ContentModelCachePluginState'; export { StandaloneEditorCorePluginState } from './pluginState/StandaloneEditorPluginState'; diff --git a/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts b/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts index 676a8b26397..4c8242a6639 100644 --- a/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts @@ -31,7 +31,7 @@ export function createContentModelEditor( ); const options: ContentModelEditorOptions = { - plugins: plugins, + legacyPlugins: plugins, initialContent: initialContent, defaultSegmentFormat: { fontFamily: 'Calibri,Arial,Helvetica,sans-serif', From c8698ebbe3d7fd5a09381963609d742fe5934014 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 8 Jan 2024 09:21:58 -0800 Subject: [PATCH 38/64] Standalone Editor step 4: Port demo site ribbon buttons (#2294) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * fix demo * Fix buttons * fix build --- .../controls/ContentModelEditorMainPane.tsx | 7 +- demo/scripts/controls/MainPaneBase.tsx | 5 +- .../editor/ContentModelRooster.tsx | 17 +- .../ContentModelFormatPainterPlugin.ts | 88 ++++---- .../contentModel/ContentModelRibbon.tsx | 190 +++++++++++++++++- .../contentModel/ContentModelRibbonButton.ts | 74 +++++++ .../contentModel/ContentModelRibbonPlugin.ts | 21 +- .../contentModel/RibbonPlugin.ts | 49 +++++ .../contentModel/alignCenterButton.ts | 10 +- .../contentModel/alignJustifyButton.ts | 9 +- .../contentModel/alignLeftButton.ts | 10 +- .../contentModel/alignRightButton.ts | 10 +- .../contentModel/backgroundColorButton.ts | 6 +- .../contentModel/blockQuoteButton.ts | 10 +- .../ribbonButtons/contentModel/boldButton.ts | 10 +- .../contentModel/bulletedListButton.ts | 10 +- .../contentModel/changeImageButton.ts | 33 ++- .../contentModel/clearFormatButton.ts | 10 +- .../ribbonButtons/contentModel/codeButton.ts | 10 +- .../ribbonButtons/contentModel/darkMode.ts | 25 +++ .../contentModel/decreaseFontSizeButton.ts | 10 +- .../contentModel/decreaseIndentButton.ts | 10 +- .../ribbonButtons/contentModel/export.ts | 74 +++++++ .../ribbonButtons/contentModel/fontButton.ts | 10 +- .../contentModel/fontSizeButton.ts | 10 +- .../contentModel/formatPainterButton.ts | 9 +- .../contentModel/formatTableButton.ts | 7 +- .../contentModel/imageBorderColorButton.ts | 9 +- .../contentModel/imageBorderRemoveButton.ts | 9 +- .../contentModel/imageBorderStyleButton.ts | 9 +- .../contentModel/imageBorderWidthButton.ts | 22 +- .../contentModel/imageBoxShadowButton.ts | 9 +- .../contentModel/increaseFontSizeButton.ts | 10 +- .../contentModel/increaseIndentButton.ts | 10 +- .../contentModel/insertImageButton.ts | 34 ++-- .../contentModel/insertLinkButton.ts | 80 ++++---- .../contentModel/insertTableButton.ts | 10 +- .../contentModel/italicButton.ts | 10 +- .../contentModel/listStartNumberButton.ts | 50 +++-- .../ribbonButtons/contentModel/ltrButton.ts | 10 +- .../contentModel/moreCommands.ts | 15 ++ .../contentModel/numberedListButton.ts | 11 +- .../ribbonButtons/contentModel/pasteButton.ts | 30 ++- .../ribbonButtons/contentModel/popout.ts | 20 ++ .../ribbonButtons/contentModel/redoButton.ts | 11 +- .../contentModel/removeLinkButton.ts | 10 +- .../ribbonButtons/contentModel/rtlButton.ts | 11 +- .../setBulletedListStyleButton.ts | 13 +- .../contentModel/setHeadingLevelButton.ts | 8 +- .../setNumberedListStyleButton.ts | 13 +- .../contentModel/setTableCellShadeButton.ts | 6 +- .../contentModel/setTableHeaderButton.ts | 14 +- .../contentModel/spaceBeforeAfterButtons.ts | 37 ++-- .../contentModel/spacingButton.ts | 9 +- .../contentModel/strikethroughButton.ts | 11 +- .../contentModel/subscriptButton.ts | 11 +- .../contentModel/superscriptButton.ts | 11 +- .../contentModel/tableBorderApplyButton.ts | 11 +- .../contentModel/tableBorderColorButton.ts | 9 +- .../contentModel/tableBorderStyleButton.ts | 12 +- .../contentModel/tableBorderWidthButton.ts | 12 +- .../contentModel/tableEditButtons.ts | 27 ++- .../contentModel/textColorButton.ts | 6 +- .../contentModel/underlineButton.ts | 11 +- .../ribbonButtons/contentModel/undoButton.ts | 11 +- .../ribbonButtons/contentModel/zoom.ts | 49 +++++ .../lib/editor/StandaloneEditor.ts | 26 +++ .../lib/editor/ContentModelEditor.ts | 26 --- .../lib/editor/IStandaloneEditor.ts | 7 + 69 files changed, 923 insertions(+), 531 deletions(-) create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/export.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/popout.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/zoom.ts diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index 2643eabb305..73561fd54c5 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -11,6 +11,7 @@ import ContentModelRooster from './contentModel/editor/ContentModelRooster'; import ContentModelSnapshotPlugin from './sidePane/snapshot/ContentModelSnapshotPlugin'; import getToggleablePlugins from './getToggleablePlugins'; import MainPaneBase, { MainPaneBaseState } from './MainPaneBase'; +import RibbonPlugin from './ribbonButtons/contentModel/RibbonPlugin'; import SampleEntityPlugin from './sampleEntity/SampleEntityPlugin'; import SidePane from './sidePane/SidePane'; import TitleBar from './titleBar/TitleBar'; @@ -18,7 +19,7 @@ import { arrayPush } from 'roosterjs-editor-dom'; import { ContentModelEditPlugin, EntityDelimiterPlugin } from 'roosterjs-content-model-plugins'; import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; import { ContentModelSegmentFormat, Snapshot } from 'roosterjs-content-model-types'; -import { createEmojiPlugin, createPasteOptionPlugin, RibbonPlugin } from 'roosterjs-react'; +import { createEmojiPlugin, createPasteOptionPlugin } from 'roosterjs-react'; import { EditorPlugin, Snapshots } from 'roosterjs-editor-types'; import { getDarkColor } from 'roosterjs-color-utils'; import { PartialTheme } from '@fluentui/react/lib/Theme'; @@ -185,13 +186,11 @@ class ContentModelEditorMainPane extends MainPaneBase const plugins = [ ...this.toggleablePlugins, - this.contentModelRibbonPlugin, this.contentModelPanePlugin.getInnerRibbonPlugin(), this.contentModelEditPlugin, this.pasteOptionPlugin, this.emojiPlugin, this.entityDelimiterPlugin, - this.formatPainterPlugin, this.sampleEntityPlugin, ]; @@ -243,8 +242,10 @@ class ContentModelEditorMainPane extends MainPaneBase
{this.state.editorCreator && ( extends return this.instance; } + static readonly editorDivId = 'RoosterJsContentDiv'; + constructor(props: {}) { super(props); @@ -58,8 +59,6 @@ export default abstract class MainPaneBase extends abstract renderSidePane(fullWidth: boolean): JSX.Element; - abstract getPlugins(): EditorPlugin[]; - abstract resetEditor(): void; abstract getTheme(isDark: boolean): PartialTheme; diff --git a/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx b/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx index a8dc6efcf46..d31481a5692 100644 --- a/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx +++ b/demo/scripts/controls/contentModel/editor/ContentModelRooster.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; +import { ContentModelEditor, ContentModelEditorOptions } from 'roosterjs-content-model-editor'; import { createUIUtilities, ReactEditorPlugin, UIUtilities } from 'roosterjs-react'; import { divProperties, getNativeProps } from '@fluentui/react/lib/Utilities'; -import { EditorPlugin } from 'roosterjs-content-model-types'; import { useTheme } from '@fluentui/react/lib/Theme'; import { - ContentModelEditor, - ContentModelEditorOptions, - IContentModelEditor, -} from 'roosterjs-content-model-editor'; + EditorPlugin, + IStandaloneEditor, + StandaloneEditorOptions, +} from 'roosterjs-content-model-types'; import type { EditorPlugin as LegacyEditorPlugin } from 'roosterjs-editor-types'; /** @@ -20,10 +20,7 @@ export interface ContentModelRoosterProps * Creator function used for creating the instance of roosterjs editor. * Use this callback when you have your own sub class of roosterjs Editor or force trigging a reset of editor */ - editorCreator?: ( - div: HTMLDivElement, - options: ContentModelEditorOptions - ) => IContentModelEditor; + editorCreator?: (div: HTMLDivElement, options: StandaloneEditorOptions) => IStandaloneEditor; /** * Whether editor should get focus once it is created @@ -39,7 +36,7 @@ export interface ContentModelRoosterProps */ export default function ContentModelRooster(props: ContentModelRoosterProps) { const editorDiv = React.useRef(null); - const editor = React.useRef(null); + const editor = React.useRef(null); const theme = useTheme(); const { focusOnInit, editorCreator, zoomScale, inDarkMode, plugins, legacyPlugins } = props; diff --git a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts index 616096e1d59..d4b4d03f30e 100644 --- a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts +++ b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts @@ -1,73 +1,83 @@ +import MainPaneBase from '../../MainPaneBase'; import { applySegmentFormat, getFormatState } from 'roosterjs-content-model-api'; -import { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; -import { EditorPlugin, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; -import { IContentModelEditor } from 'roosterjs-content-model-editor'; +import { PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { + ContentModelSegmentFormat, + EditorPlugin, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg'); -const FORMATPAINTERCURSOR_STYLE = `;cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`; -const CURSOR_REGEX = /;?\s*cursor:\s*url\(\".*?\"\)[^;]*/gi; - -interface FormatPainterFormatHolder { - format: ContentModelSegmentFormat | null; -} +const FORMATPAINTERCURSOR_STYLE = `cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`; export default class ContentModelFormatPainterPlugin implements EditorPlugin { - private editor: IContentModelEditor | null = null; + private editor: IStandaloneEditor | null = null; + private styleNode: HTMLStyleElement | null = null; + private painterFormat: ContentModelSegmentFormat | null = null; + private static instance: ContentModelFormatPainterPlugin | undefined; + + constructor() { + ContentModelFormatPainterPlugin.instance = this; + } getName() { return 'FormatPainter'; } - initialize(editor: IEditor) { - this.editor = editor as IContentModelEditor; + initialize(editor: IStandaloneEditor) { + this.editor = editor; + + const doc = this.editor.getDocument(); + this.styleNode = doc.createElement('style'); + + doc.head.appendChild(this.styleNode); } dispose() { this.editor = null; + + if (this.styleNode) { + this.styleNode.parentNode?.removeChild(this.styleNode); + this.styleNode = null; + } } onPluginEvent(event: PluginEvent) { if (this.editor && event.eventType == PluginEventType.MouseUp) { - const formatHolder = getFormatHolder(this.editor); + if (this.painterFormat) { + applySegmentFormat(this.editor, this.painterFormat); - if (formatHolder.format) { - applySegmentFormat(this.editor, formatHolder.format); - formatHolder.format = null; - - setFormatPainterCursor(this.editor, false /*isOn*/); + this.setFormatPainterCursor(null); } } } - static startFormatPainter(editor: IContentModelEditor) { - const formatHolder = getFormatHolder(editor); - const format = getSegmentFormat(editor); + private setFormatPainterCursor(format: ContentModelSegmentFormat | null) { + const sheet = this.styleNode.sheet; - if (format) { - formatHolder.format = { ...format }; - setFormatPainterCursor(editor, true /*isOn*/); + if (this.painterFormat) { + for (let i = sheet.cssRules.length - 1; i >= 0; i--) { + sheet.deleteRule(i); + } } - } -} - -function getFormatHolder(editor: IContentModelEditor): FormatPainterFormatHolder { - return editor.getCustomData('__FormatPainterFormat', () => { - return {} as FormatPainterFormatHolder; - }); -} -function setFormatPainterCursor(editor: IContentModelEditor, isOn: boolean) { - let styles = editor.getEditorDomAttribute('style') || ''; - styles = styles.replace(CURSOR_REGEX, ''); + this.painterFormat = format; - if (isOn) { - styles += FORMATPAINTERCURSOR_STYLE; + if (this.painterFormat) { + sheet.insertRule(`#${MainPaneBase.editorDivId} {${FORMATPAINTERCURSOR_STYLE}}`); + } } - editor.setEditorDomAttribute('style', styles); + static startFormatPainter() { + const format = getSegmentFormat(this.instance.editor); + + if (format) { + this.instance.setFormatPainterCursor(format); + } + } } -function getSegmentFormat(editor: IContentModelEditor): ContentModelSegmentFormat { +function getSegmentFormat(editor: IStandaloneEditor): ContentModelSegmentFormat { const formatState = getFormatState(editor); return { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx index fdd99c1f47c..f083a761cc5 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import RibbonPlugin from './RibbonPlugin'; import { alignCenterButton } from './alignCenterButton'; import { alignJustifyButton } from './alignJustifyButton'; import { alignLeftButton } from './alignLeftButton'; @@ -10,14 +12,20 @@ import { bulletedListButton } from './bulletedListButton'; import { changeImageButton } from './changeImageButton'; import { clearFormatButton } from './clearFormatButton'; import { codeButton } from './codeButton'; -import { darkMode } from '../darkMode'; +import { CommandBar, ICommandBarItemProps, ICommandBarProps } from '@fluentui/react/lib/CommandBar'; +import { darkMode } from './darkMode'; import { decreaseFontSizeButton } from './decreaseFontSizeButton'; import { decreaseIndentButton } from './decreaseIndentButton'; -import { exportContent } from '../export'; +import { exportContent } from './export'; +import { FocusZoneDirection } from '@fluentui/react/lib/FocusZone'; import { fontButton } from './fontButton'; import { fontSizeButton } from './fontSizeButton'; import { formatPainterButton } from './formatPainterButton'; +import { FormatState } from 'roosterjs-editor-types'; import { formatTableButton } from './formatTableButton'; +import { getLocalizedString, LocalizedStrings } from 'roosterjs-react'; +import { getObjectKeys } from 'roosterjs-content-model-dom'; +import { IContextualMenuItem, IContextualMenuItemProps } from '@fluentui/react/lib/ContextualMenu'; import { imageBorderColorButton } from './imageBorderColorButton'; import { imageBorderRemoveButton } from './imageBorderRemoveButton'; import { imageBorderStyleButton } from './imageBorderStyleButton'; @@ -28,15 +36,17 @@ import { increaseIndentButton } from './increaseIndentButton'; import { insertImageButton } from './insertImageButton'; import { insertLinkButton } from './insertLinkButton'; import { insertTableButton } from './insertTableButton'; +import { IRenderFunction } from '@fluentui/react/lib/Utilities'; import { italicButton } from './italicButton'; import { listStartNumberButton } from './listStartNumberButton'; import { ltrButton } from './ltrButton'; +import { mergeStyles } from '@fluentui/react/lib/Styling'; +import { moreCommands } from './moreCommands'; import { numberedListButton } from './numberedListButton'; import { pasteButton } from './pasteButton'; -import { popout } from '../popout'; +import { popout } from './popout'; import { redoButton } from './redoButton'; import { removeLinkButton } from './removeLinkButton'; -import { Ribbon, RibbonButton, RibbonPlugin } from 'roosterjs-react'; import { rtlButton } from './rtlButton'; import { setBulletedListStyleButton } from './setBulletedListStyleButton'; import { setHeadingLevelButton } from './setHeadingLevelButton'; @@ -55,7 +65,7 @@ import { tableBorderWidthButton } from './tableBorderWidthButton'; import { textColorButton } from './textColorButton'; import { underlineButton } from './underlineButton'; import { undoButton } from './undoButton'; -import { zoom } from '../zoom'; +import { zoom } from './zoom'; import { tableAlignCellButton, tableAlignTableButton, @@ -65,7 +75,7 @@ import { tableSplitButton, } from './tableEditButtons'; -const buttons = [ +const buttons: ContentModelRibbonButton[] = [ formatPainterButton, boldButton, italicButton, @@ -127,6 +137,172 @@ const buttons = [ pasteButton, ]; +const ribbonClassName = mergeStyles({ + '& .ms-CommandBar': { + padding: '0px', + }, +}); + +const rtlIcon = mergeStyles({ + transform: 'scaleX(-1)', +}); + +interface RibbonProps extends Partial { + /** + * The ribbon plugin used for connect editor and the ribbon + */ + plugin: RibbonPlugin; + + /** + * Buttons in this ribbon + */ + buttons: ContentModelRibbonButton[]; + + /** + * A dictionary of localized strings for all buttons. + * Key of the dictionary is the key of each button, value will be the string or a function to return the string + */ + strings?: LocalizedStrings; +} + +/** + * The format ribbon component of roosterjs-react + * @param props Properties of format ribbon component + * @returns The format ribbon component + */ +function Ribbon(props: RibbonProps) { + const { plugin, buttons, strings, dir } = props; + const [formatState, setFormatState] = React.useState(null); + const isRtl = dir == 'rtl'; + + const onClick = React.useCallback( + (_, item?: IContextualMenuItem) => { + if (item) { + plugin?.onButtonClick(item.data, item.key, strings); + } + }, + [plugin, strings] + ); + + const onHover = React.useCallback( + (button: ContentModelRibbonButton, key: string) => { + plugin.startLivePreview(button, key as T, strings); + }, + [plugin, strings] + ); + + const onDismiss = React.useCallback(() => { + plugin.stopLivePreview(); + }, [plugin]); + + const flipIcon = React.useCallback( + ( + props?: IContextualMenuItemProps, + defaultRender?: (props?: IContextualMenuItemProps) => JSX.Element | null + ): JSX.Element | null => { + if (!defaultRender) { + return null; + } + return {defaultRender(props)}; + }, + [] + ); + + const commandBarItems = React.useMemo((): ICommandBarItemProps[] => { + return buttons.map( + (button): ICommandBarItemProps => { + const selectedItem = + formatState && button.dropDownMenu?.getSelectedItemKey?.(formatState); + const dropDownMenu = button.dropDownMenu; + + const result: ICommandBarItemProps = { + key: button.key, + data: button, + iconProps: { + iconName: button.iconName, + }, + onRenderIcon: isRtl && button.flipWhenRtl ? flipIcon : undefined, + iconOnly: true, + text: getLocalizedString(strings, button.key, button.unlocalizedText), + ariaLabel: getLocalizedString(strings, button.key, button.unlocalizedText), + canCheck: true, + checked: (formatState && button.isChecked?.(formatState)) || false, + disabled: (formatState && button.isDisabled?.(formatState)) || false, + ...(button.commandBarProperties || {}), + }; + + const contextMenuItemRenderer: IRenderFunction = ( + props, + defaultRenderer + ) => + props && defaultRenderer ? ( +
onHover(button, props.key)}> + {defaultRenderer(props)} +
+ ) : null; + + if (dropDownMenu) { + result.subMenuProps = { + shouldFocusOnMount: true, + focusZoneProps: { direction: FocusZoneDirection.bidirectional }, + onMenuDismissed: onDismiss, + onItemClick: onClick, + onRenderContextualMenuItem: dropDownMenu.allowLivePreview + ? contextMenuItemRenderer + : undefined, + items: getObjectKeys(dropDownMenu.items).map(key => ({ + key: key, + text: getLocalizedString( + strings, + key, + dropDownMenu.items[key] + ), + data: button, + canCheck: !!dropDownMenu.getSelectedItemKey, + checked: selectedItem == key || false, + className: dropDownMenu.itemClassName, + onRender: dropDownMenu.itemRender + ? item => dropDownMenu.itemRender!(item, onClick) + : undefined, + })), + ...(dropDownMenu.commandBarSubMenuProperties || {}), + }; + } else { + result.onClick = onClick; + } + + return result; + } + ); + }, [buttons, formatState, isRtl, strings, onClick, onDismiss, onHover]); + + React.useEffect(() => { + const disposer = plugin?.registerFormatChangedCallback(setFormatState); + + return () => { + disposer?.(); + }; + }, [plugin]); + + const moreCommandsBtn = moreCommands as ContentModelRibbonButton; + + return ( + ( + strings, + moreCommandsBtn.key, + moreCommandsBtn.unlocalizedText + ), + ...props?.overflowButtonProps, + }} + /> + ); +} + export default function ContentModelRibbon(props: { ribbonPlugin: RibbonPlugin; isRtl: boolean; @@ -134,7 +310,7 @@ export default function ContentModelRibbon(props: { }) { const { ribbonPlugin, isRtl, isInPopout } = props; const ribbonButtons = React.useMemo(() => { - const result: RibbonButton[] = [...buttons, darkMode, zoom, exportContent]; + const result: ContentModelRibbonButton[] = [...buttons, darkMode, zoom, exportContent]; if (!isInPopout) { result.push(popout); diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts new file mode 100644 index 00000000000..a31a7845057 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonButton.ts @@ -0,0 +1,74 @@ +import { LocalizedStrings, RibbonButtonDropDown, UIUtilities } from 'roosterjs-react'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; +import type { FormatState } from 'roosterjs-editor-types'; +import type { ICommandBarItemProps } from '@fluentui/react/lib/CommandBar'; + +/** + * Represents a button on format ribbon + */ +export default interface ContentModelRibbonButton { + /** + * key of this button, needs to be unique + */ + key: T; + + /** + * Name of button icon. See https://developer.microsoft.com/en-us/fluentui#/styles/web/icons for all icons + */ + iconName: string; + + /** + * True if we need to flip the icon when render in Right-to-left page + */ + flipWhenRtl?: boolean; + + /** + * Text of the button. This text is not localized. To show a localized text, pass a dictionary to Ribbon component via RibbonProps.strings. + */ + unlocalizedText: string; + + /** + * Click handler of this button. + * @param editor the editor instance + * @param key key of the button that is clicked + * @param strings localized strings used by any UI element of this click handler + * @param uiUtilities a utilities object to help render addition UI elements + */ + onClick: ( + editor: IStandaloneEditor, + key: T, + strings: LocalizedStrings | undefined, + uiUtilities: UIUtilities + ) => void; + + /** + * Get if the current button should be checked + * @param formatState The current formatState of editor + * @returns True to show the button in a checked state, otherwise false + * @default False When not specified, it is treated as always returning false + */ + isChecked?: (formatState: FormatState) => boolean; + + /** + * Get if the current button should be disabled + * @param formatState The current formatState of editor + * @returns True to show the button in a disabled state, otherwise false + * @default False When not specified, it is treated as always returning false + */ + isDisabled?: (formatState: FormatState) => boolean; + + /** + * A drop down menu of this button. When set this value, the button will has a "v" icon to let user + * know it will open a drop down menu. And the onClick handler will only be triggered when user click + * a menu item of the drop down. + */ + dropDownMenu?: RibbonButtonDropDown; + + /** + * Use this property to pass in Fluent UI CommandBar property directly. It will overwrite the values of other conflict properties + * + * Do not use ICommandBarItemProps.subMenuProps here since it will be overwritten. + * If need, specify its value using RibbonButton.dropDownMenu.commandBarSubMenuProperties. + */ + commandBarProperties?: Partial; +} diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts index 7dd24709acd..23ee3a4ccb9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts @@ -1,12 +1,13 @@ -import { ContentModelFormatState } from 'roosterjs-content-model-types'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import RibbonPlugin from './RibbonPlugin'; import { FormatState, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { getFormatState } from 'roosterjs-content-model-api'; import { getObjectKeys } from 'roosterjs-editor-dom'; -import { IContentModelEditor } from 'roosterjs-content-model-editor'; -import { LocalizedStrings, RibbonButton, RibbonPlugin, UIUtilities } from 'roosterjs-react'; +import { LocalizedStrings, UIUtilities } from 'roosterjs-react'; +import { ContentModelFormatState, IStandaloneEditor } from 'roosterjs-content-model-types'; export class ContentModelRibbonPlugin implements RibbonPlugin { - private editor: IContentModelEditor | null = null; + private editor: IStandaloneEditor | null = null; private onFormatChanged: ((formatState: FormatState) => void) | null = null; private timer = 0; private formatState: ContentModelFormatState | null = null; @@ -29,7 +30,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * Initialize this plugin * @param editor The editor instance */ - initialize(editor: IContentModelEditor) { + initialize(editor: IStandaloneEditor) { this.editor = editor; } @@ -85,7 +86,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * @param strings The localized string map for this button */ onButtonClick( - button: RibbonButton, + button: ContentModelRibbonButton, key: T, strings?: LocalizedStrings ) { @@ -107,7 +108,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * @param strings The localized string map for this button */ startLivePreview( - button: RibbonButton, + button: ContentModelRibbonButton, key: T, strings?: LocalizedStrings ) { @@ -116,9 +117,9 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { // If editor is already in shadow edit, no need to check again. // And the check result may be incorrect because the content is changed from last shadow edit and the cached selection path won't apply - const range = !isInShadowEdit && this.editor.getSelectionRangeEx(); + const range = !isInShadowEdit && this.editor.getDOMSelection(); - if (isInShadowEdit || (range && !range.areAllCollapsed)) { + if (isInShadowEdit || (range && (range.type != 'range' || !range.range.collapsed))) { this.editor.startShadowEdit(); button.onClick(this.editor, key, strings, this.uiUtilities); } @@ -160,7 +161,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { ) ) { this.formatState = newFormatState; - this.onFormatChanged((newFormatState as any) as FormatState); + this.onFormatChanged(newFormatState); } } } diff --git a/demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts new file mode 100644 index 00000000000..54b732ce848 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/RibbonPlugin.ts @@ -0,0 +1,49 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { EditorPlugin } from 'roosterjs-content-model-types'; +import { LocalizedStrings, UIUtilities } from 'roosterjs-react'; +import type { FormatState } from 'roosterjs-editor-types'; + +/** + * Represents a plugin to connect format ribbon component and the editor + */ +export default interface RibbonPlugin extends EditorPlugin { + /** + * Set the UI utilities objects to this plugin to help render additional UI elements + * @param uiUtilities The UI utilities object to set + */ + setUIUtilities(uiUtilities: UIUtilities): void; + + /** + * Register a callback to be invoked when format state of editor is changed, returns a disposer function. + */ + registerFormatChangedCallback: (callback: (formatState: FormatState) => void) => () => void; + + /** + * When user clicks on a button, call this method to let the plugin to handle this click event + * @param button The button that is clicked + * @param key Key of child menu item that is clicked if any + * @param strings The localized string map for this button + */ + onButtonClick: ( + button: ContentModelRibbonButton, + key: T, + strings?: LocalizedStrings + ) => void; + + /** + * Enter live preview state (shadow edit) of editor if there is a non-collapsed selection + * @param button The button that triggered this action + * @param key Key of the hovered button sub item + * @param strings The localized string map for this button + */ + startLivePreview: ( + button: ContentModelRibbonButton, + key: T, + strings?: LocalizedStrings + ) => void; + + /** + * Leave live preview state (shadow edit) of editor + */ + stopLivePreview: () => void; +} diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts index 968d17e3d01..a2306f0b8bd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts @@ -1,19 +1,17 @@ -import { AlignCenterButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; +import { AlignCenterButtonStringKey } from 'roosterjs-react'; /** * @internal * "Align center" button on the format ribbon */ -export const alignCenterButton: RibbonButton = { +export const alignCenterButton: ContentModelRibbonButton = { key: 'buttonNameAlignCenter', unlocalizedText: 'Align center', iconName: 'AlignCenter', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'center'); - } + setAlignment(editor, 'center'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts index 9b7803f26ff..a93db909a0d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignJustifyButton.ts @@ -1,19 +1,16 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; /** * @internal * "Align justify" button on the format ribbon */ -export const alignJustifyButton: RibbonButton<'buttonNameAlignJustify'> = { +export const alignJustifyButton: ContentModelRibbonButton<'buttonNameAlignJustify'> = { key: 'buttonNameAlignJustify', unlocalizedText: 'Align justify', iconName: 'AlignJustify', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'justify'); - } + setAlignment(editor, 'justify'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts index 55088314ec5..c1b0e014af8 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts @@ -1,19 +1,17 @@ -import { AlignLeftButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; +import { AlignLeftButtonStringKey } from 'roosterjs-react'; /** * @internal * "Align left" button on the format ribbon */ -export const alignLeftButton: RibbonButton = { +export const alignLeftButton: ContentModelRibbonButton = { key: 'buttonNameAlignLeft', unlocalizedText: 'Align left', iconName: 'AlignLeft', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'left'); - } + setAlignment(editor, 'left'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts index bc73fd6dce1..fa3ab23d40d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts @@ -1,19 +1,17 @@ -import { AlignRightButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setAlignment } from 'roosterjs-content-model-api'; +import { AlignRightButtonStringKey } from 'roosterjs-react'; /** * @internal * "Align right" button on the format ribbon */ -export const alignRightButton: RibbonButton = { +export const alignRightButton: ContentModelRibbonButton = { key: 'buttonNameAlignRight', unlocalizedText: 'Align right', iconName: 'AlignRight', onClick: editor => { - if (isContentModelEditor(editor)) { - setAlignment(editor, 'right'); - } + setAlignment(editor, 'right'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts index b3b9e2641ad..36b16cf0e06 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setBackgroundColor } from 'roosterjs-content-model-api'; import { BackgroundColorButtonStringKey, @@ -16,11 +16,11 @@ const originalButton = getButtons([KnownRibbonButtonKey.BackgroundColor])[0] as * @internal * "Background color" button on the format ribbon */ -export const backgroundColorButton: RibbonButton = { +export const backgroundColorButton: ContentModelRibbonButton = { ...originalButton, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameBackgroundColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameBackgroundColor') { setBackgroundColor(editor, getBackgroundColorValue(key).lightModeColor); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts index 583287296be..dfe8d88ca2e 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts @@ -1,20 +1,18 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { QuoteButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleBlockQuote } from 'roosterjs-content-model-api'; +import { QuoteButtonStringKey } from 'roosterjs-react'; /** * @internal * "Block quote" button on the format ribbon */ -export const blockQuoteButton: RibbonButton = { +export const blockQuoteButton: ContentModelRibbonButton = { key: 'buttonNameQuote', unlocalizedText: 'Quote', iconName: 'RightDoubleQuote', isChecked: formatState => !!formatState.isBlockQuote, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleBlockQuote(editor); - } + toggleBlockQuote(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts index 9bcd3597502..9877b554f32 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts @@ -1,20 +1,18 @@ -import { BoldButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { BoldButtonStringKey } from 'roosterjs-react'; import { toggleBold } from 'roosterjs-content-model-api'; /** * @internal * "Bold" button on the format ribbon */ -export const boldButton: RibbonButton = { +export const boldButton: ContentModelRibbonButton = { key: 'buttonNameBold', unlocalizedText: 'Bold', iconName: 'Bold', isChecked: formatState => formatState.isBold, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleBold(editor); - } + toggleBold(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts index 12c2d492889..543a187bff1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts @@ -1,20 +1,18 @@ -import { BulletedListButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleBullet } from 'roosterjs-content-model-api'; +import { BulletedListButtonStringKey } from 'roosterjs-react'; /** * @internal * "Bulleted list" button on the format ribbon */ -export const bulletedListButton: RibbonButton = { +export const bulletedListButton: ContentModelRibbonButton = { key: 'buttonNameBulletedList', unlocalizedText: 'Bulleted list', iconName: 'BulletedList', isChecked: formatState => formatState.isBullet, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleBullet(editor); - } + toggleBullet(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts index d6ab58b8c71..af76da20f48 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts @@ -1,8 +1,7 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { changeImage } from 'roosterjs-content-model-api'; import { createElement } from 'roosterjs-editor-dom'; import { CreateElementData } from 'roosterjs-editor-types'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const FileInput: CreateElementData = { tag: 'input', @@ -17,30 +16,28 @@ const FileInput: CreateElementData = { * @internal * "Change Image" button on the format ribbon */ -export const changeImageButton: RibbonButton<'buttonNameChangeImage'> = { +export const changeImageButton: ContentModelRibbonButton<'buttonNameChangeImage'> = { key: 'buttonNameChangeImage', unlocalizedText: 'Change Image', iconName: 'ImageSearch', isDisabled: formatState => !formatState.canAddImageAltText, onClick: editor => { - if (isContentModelEditor(editor)) { - const document = editor.getDocument(); - const fileInput = createElement(FileInput, document) as HTMLInputElement; - document.body.appendChild(fileInput); + const document = editor.getDocument(); + const fileInput = createElement(FileInput, document) as HTMLInputElement; + document.body.appendChild(fileInput); - fileInput.addEventListener('change', () => { - if (fileInput.files) { - for (let i = 0; i < fileInput.files.length; i++) { - changeImage(editor, fileInput.files[i]); - } + fileInput.addEventListener('change', () => { + if (fileInput.files) { + for (let i = 0; i < fileInput.files.length; i++) { + changeImage(editor, fileInput.files[i]); } - }); - - try { - fileInput.click(); - } finally { - document.body.removeChild(fileInput); } + }); + + try { + fileInput.click(); + } finally { + document.body.removeChild(fileInput); } }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts index cdfc02e6f51..0ca5a9ccec9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts @@ -1,17 +1,15 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { clearFormat } from 'roosterjs-content-model-api'; -import { ClearFormatButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { ClearFormatButtonStringKey } from 'roosterjs-react'; /** * "Clear format" button on the format ribbon */ -export const clearFormatButton: RibbonButton = { +export const clearFormatButton: ContentModelRibbonButton = { key: 'buttonNameClearFormat', unlocalizedText: 'Clear format', iconName: 'ClearFormatting', onClick: editor => { - if (isContentModelEditor(editor)) { - clearFormat(editor); - } + clearFormat(editor); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts index 5c26c62e66c..77c33176be6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts @@ -1,19 +1,17 @@ -import { CodeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleCode } from 'roosterjs-content-model-api'; +import { CodeButtonStringKey } from 'roosterjs-react'; /** * @internal * "Code" button on the format ribbon */ -export const codeButton: RibbonButton = { +export const codeButton: ContentModelRibbonButton = { key: 'buttonNameCode', unlocalizedText: 'Code', iconName: 'Code', isChecked: formatState => !!formatState.isCodeInline, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleCode(editor); - } + toggleCode(editor); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts b/demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts new file mode 100644 index 00000000000..cfda08b909a --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/darkMode.ts @@ -0,0 +1,25 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import MainPaneBase from '../../MainPaneBase'; + +/** + * Key of localized strings of Dark mode button + */ +export type DarkModeButtonStringKey = 'buttonNameDarkMode'; + +/** + * "Dark mode" button on the format ribbon + */ +export const darkMode: ContentModelRibbonButton = { + key: 'buttonNameDarkMode', + unlocalizedText: 'Dark Mode', + iconName: 'ClearNight', + isChecked: formatState => formatState.isDarkMode, + onClick: editor => { + editor.setDarkModeState(!editor.isDarkMode()); + editor.focus(); + + // Let main pane know this state change so that it can be persisted when pop out/pop in + MainPaneBase.getInstance().toggleDarkMode(); + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts index f0d0ac14a1d..b4fa8213d40 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts @@ -1,18 +1,16 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { changeFontSize } from 'roosterjs-content-model-api'; -import { DecreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { DecreaseFontSizeButtonStringKey } from 'roosterjs-react'; /** * @internal * "Decrease font size" button on the format ribbon */ -export const decreaseFontSizeButton: RibbonButton = { +export const decreaseFontSizeButton: ContentModelRibbonButton = { key: 'buttonNameDecreaseFontSize', unlocalizedText: 'Decrease font size', iconName: 'FontDecrease', onClick: editor => { - if (isContentModelEditor(editor)) { - changeFontSize(editor, 'decrease'); - } + changeFontSize(editor, 'decrease'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts index c1d99a80c7a..0825b629428 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts @@ -1,19 +1,17 @@ -import { DecreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setIndentation } from 'roosterjs-content-model-api'; +import { DecreaseIndentButtonStringKey } from 'roosterjs-react'; /** * @internal * "Decrease indent" button on the format ribbon */ -export const decreaseIndentButton: RibbonButton = { +export const decreaseIndentButton: ContentModelRibbonButton = { key: 'buttonNameDecreaseIndent', unlocalizedText: 'Decrease indent', iconName: 'DecreaseIndentLegacy', flipWhenRtl: true, onClick: editor => { - if (isContentModelEditor(editor)) { - setIndentation(editor, 'outdent'); - } + setIndentation(editor, 'outdent'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/export.ts b/demo/scripts/controls/ribbonButtons/contentModel/export.ts new file mode 100644 index 00000000000..3d82b2a3006 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/export.ts @@ -0,0 +1,74 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { cloneModel } from 'roosterjs-content-model-core'; +import { ContentModelEntityFormat } from 'roosterjs-content-model-types'; +import { EntityOperation, PluginEventType } from 'roosterjs-editor-types'; +import { + contentModelToDom, + createModelToDomContext, + parseEntityClassName, +} from 'roosterjs-content-model-dom'; + +/** + * Key of localized strings of Zoom button + */ +export type ExportButtonStringKey = 'buttonNameExport'; + +/** + * "Export content" button on the format ribbon + */ +export const exportContent: ContentModelRibbonButton = { + key: 'buttonNameExport', + unlocalizedText: 'Export', + iconName: 'Export', + flipWhenRtl: true, + onClick: editor => { + // TODO: We need a export function in dev code to handle this feature + const win = editor.getDocument().defaultView.open(); + + editor.formatContentModel(model => { + const clonedModel = cloneModel(model, { + includeCachedElement: (node, type) => { + switch (type) { + case 'cache': + return undefined; + + case 'general': + return node.cloneNode() as HTMLElement; + + case 'entity': + const clonedRoot = node.cloneNode(true) as HTMLElement; + const format: ContentModelEntityFormat = {}; + let isEntity = false; + + clonedRoot.classList.forEach(name => { + isEntity = parseEntityClassName(name, format) || isEntity; + }); + + if (isEntity && format.id && format.entityType) { + editor.triggerPluginEvent(PluginEventType.EntityOperation, { + operation: EntityOperation.ReplaceTemporaryContent, + entity: { + wrapper: clonedRoot, + id: format.id, + type: format.entityType, + isReadonly: !!format.isReadonly, + }, + }); + } + + return clonedRoot; + } + }, + }); + + contentModelToDom( + win.document, + win.document.body, + clonedModel, + createModelToDomContext() + ); + + return false; + }); + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts index 139fec0771d..e2e714b881f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts @@ -1,6 +1,6 @@ -import { FontButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setFontName } from 'roosterjs-content-model-api'; +import { FontButtonStringKey } from 'roosterjs-react'; interface FontName { name: string; @@ -150,7 +150,7 @@ const FirstFontRegex = /^['"]?([^'",]+)/i; * @internal * "Font" button on the format ribbon */ -export const fontButton: RibbonButton = { +export const fontButton: ContentModelRibbonButton = { key: 'buttonNameFont', unlocalizedText: 'Font', iconName: 'Font', @@ -166,8 +166,6 @@ export const fontButton: RibbonButton = { allowLivePreview: true, }, onClick: (editor, font) => { - if (isContentModelEditor(editor)) { - setFontName(editor, font); - } + setFontName(editor, font); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts index 5fc487586ee..6e33042b8a1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts @@ -1,6 +1,6 @@ -import { FontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setFontSize } from 'roosterjs-content-model-api'; +import { FontSizeButtonStringKey } from 'roosterjs-react'; const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; @@ -8,7 +8,7 @@ const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 * @internal * "Font Size" button on the format ribbon */ -export const fontSizeButton: RibbonButton = { +export const fontSizeButton: ContentModelRibbonButton = { key: 'buttonNameFontSize', unlocalizedText: 'Font size', iconName: 'FontSize', @@ -21,8 +21,6 @@ export const fontSizeButton: RibbonButton = { allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setFontSize(editor, size); - } + setFontSize(editor, size); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts index 8b98a9ef676..df5585fd990 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts @@ -1,19 +1,16 @@ import ContentModelFormatPainterPlugin from '../../contentModel/plugins/ContentModelFormatPainterPlugin'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; /** * @internal * "Format Painter" button on the format ribbon */ -export const formatPainterButton: RibbonButton<'formatPainter'> = { +export const formatPainterButton: ContentModelRibbonButton<'formatPainter'> = { key: 'formatPainter', unlocalizedText: 'Format painter', iconName: 'Brush', onClick: editor => { - if (isContentModelEditor(editor)) { - ContentModelFormatPainterPlugin.startFormatPainter(editor); - } + ContentModelFormatPainterPlugin.startFormatPainter(); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts index 9c81a9b9662..ea34aefa2d2 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { formatTable } from 'roosterjs-content-model-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; import { TableBorderFormat } from 'roosterjs-content-model-core'; import { TableMetadataFormat } from 'roosterjs-content-model-types'; @@ -192,7 +191,7 @@ export function createTableFormat( }; } -export const formatTableButton: RibbonButton<'ribbonButtonTableFormat'> = { +export const formatTableButton: ContentModelRibbonButton<'ribbonButtonTableFormat'> = { key: 'ribbonButtonTableFormat', iconName: 'TableComputed', unlocalizedText: 'Format Table', @@ -215,7 +214,7 @@ export const formatTableButton: RibbonButton<'ribbonButtonTableFormat'> = { onClick: (editor, key) => { const format = PREDEFINED_STYLES[key]?.('#ABABAB', '#ABABAB20'); - if (format && isContentModelEditor(editor)) { + if (format) { formatTable(editor, format); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts index a45ea0174dc..39d742f251d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts @@ -1,6 +1,5 @@ -import { getButtons, getTextColorValue, KnownRibbonButtonKey } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { getButtons, getTextColorValue, KnownRibbonButtonKey, RibbonButton } from 'roosterjs-react'; import { setImageBorder } from 'roosterjs-content-model-api'; const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as RibbonButton< @@ -11,14 +10,14 @@ const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as Ribbon * @internal * "Image Border Color" button on the format ribbon */ -export const imageBorderColorButton: RibbonButton<'buttonNameImageBorderColor'> = { +export const imageBorderColorButton: ContentModelRibbonButton<'buttonNameImageBorderColor'> = { ...originalButton, unlocalizedText: 'Image Border Color', iconName: 'Photo2', isDisabled: formatState => !formatState.canAddImageAltText, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameImageBorderColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameImageBorderColor') { setImageBorder(editor, { color: getTextColorValue(key).lightModeColor }, '5px'); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts index 3fe95a2dfcc..da91812e2dd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts @@ -1,19 +1,16 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBorder } from 'roosterjs-content-model-api'; /** * @internal * "Remove Image Border" button on the format ribbon */ -export const imageBorderRemoveButton: RibbonButton<'buttonNameImageBorderRemove'> = { +export const imageBorderRemoveButton: ContentModelRibbonButton<'buttonNameImageBorderRemove'> = { key: 'buttonNameImageBorderRemove', unlocalizedText: 'Remove Image Border', iconName: 'Cancel', isDisabled: formatState => !formatState.canAddImageAltText, onClick: editor => { - if (isContentModelEditor(editor)) { - setImageBorder(editor, null); - } + setImageBorder(editor, null); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts index c39e9ee88d3..19e76fb71be 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts @@ -1,5 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBorder } from 'roosterjs-content-model-api'; const STYLES: Record = { @@ -17,7 +16,7 @@ const STYLES: Record = { * @internal * "Image Border Style" button on the format ribbon */ -export const imageBorderStyleButton: RibbonButton<'buttonNameImageBorderStyle'> = { +export const imageBorderStyleButton: ContentModelRibbonButton<'buttonNameImageBorderStyle'> = { key: 'buttonNameImageBorderStyle', unlocalizedText: 'Image Border Style', iconName: 'BorderDash', @@ -27,9 +26,7 @@ export const imageBorderStyleButton: RibbonButton<'buttonNameImageBorderStyle'> allowLivePreview: true, }, onClick: (editor, style) => { - if (isContentModelEditor(editor)) { - setImageBorder(editor, { style: style }, '5px'); - } + setImageBorder(editor, { style: style }, '5px'); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts index be6086f3fc8..29f7a6522bc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts @@ -1,5 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBorder } from 'roosterjs-content-model-api'; const WIDTH = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; @@ -8,7 +7,7 @@ const WIDTH = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; * @internal * "Image Border Width" button on the format ribbon */ -export const imageBorderWidthButton: RibbonButton<'buttonNameImageBorderWidth'> = { +export const imageBorderWidthButton: ContentModelRibbonButton<'buttonNameImageBorderWidth'> = { key: 'buttonNameImageBorderWidth', unlocalizedText: 'Image Border Width', iconName: 'Photo2', @@ -21,15 +20,14 @@ export const imageBorderWidthButton: RibbonButton<'buttonNameImageBorderWidth'> allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setImageBorder( - editor, - { - width: size, - }, - '5px' - ); - } + setImageBorder( + editor, + { + width: size, + }, + '5px' + ); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts index a1eed8d8f92..d1f936ab1d9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts @@ -1,5 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setImageBoxShadow } from 'roosterjs-content-model-api'; const STYLES_NAMES: Record = { @@ -32,7 +31,7 @@ const STYLES: Record = { * @internal * "Image Shadow" button on the format ribbon */ -export const imageBoxShadowButton: RibbonButton<'buttonNameImageBoxSHadow'> = { +export const imageBoxShadowButton: ContentModelRibbonButton<'buttonNameImageBoxSHadow'> = { key: 'buttonNameImageBoxSHadow', unlocalizedText: 'Image Shadow', iconName: 'Photo2', @@ -42,9 +41,7 @@ export const imageBoxShadowButton: RibbonButton<'buttonNameImageBoxSHadow'> = { allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setImageBoxShadow(editor, STYLES[size], STYLES[size].length ? '4px' : null); - } + setImageBoxShadow(editor, STYLES[size], STYLES[size].length ? '4px' : null); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts index f9308f87024..d0a6e57af79 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts @@ -1,18 +1,16 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { changeFontSize } from 'roosterjs-content-model-api'; -import { IncreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { IncreaseFontSizeButtonStringKey } from 'roosterjs-react'; /** * @internal * "Increase font size" button on the format ribbon */ -export const increaseFontSizeButton: RibbonButton = { +export const increaseFontSizeButton: ContentModelRibbonButton = { key: 'buttonNameIncreaseFontSize', unlocalizedText: 'Increase font size', iconName: 'FontIncrease', onClick: editor => { - if (isContentModelEditor(editor)) { - changeFontSize(editor, 'increase'); - } + changeFontSize(editor, 'increase'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts index bdfefb9cbc6..397b02d56ef 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts @@ -1,19 +1,17 @@ -import { IncreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setIndentation } from 'roosterjs-content-model-api'; +import { IncreaseIndentButtonStringKey } from 'roosterjs-react'; /** * @internal * "Increase indent" button on the format ribbon */ -export const increaseIndentButton: RibbonButton = { +export const increaseIndentButton: ContentModelRibbonButton = { key: 'buttonNameIncreaseIndent', unlocalizedText: 'Increase indent', iconName: 'IncreaseIndentLegacy', flipWhenRtl: true, onClick: editor => { - if (isContentModelEditor(editor)) { - setIndentation(editor, 'indent'); - } + setIndentation(editor, 'indent'); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts index 62a14215b5c..6d88485b47d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts @@ -1,8 +1,8 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { createElement } from 'roosterjs-editor-dom'; import { CreateElementData } from 'roosterjs-editor-types'; import { insertImage } from 'roosterjs-content-model-api'; -import { InsertImageButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { InsertImageButtonStringKey } from 'roosterjs-react'; const FileInput: CreateElementData = { tag: 'input', @@ -17,29 +17,27 @@ const FileInput: CreateElementData = { * @internal * "Insert image" button on the format ribbon */ -export const insertImageButton: RibbonButton = { +export const insertImageButton: ContentModelRibbonButton = { key: 'buttonNameInsertImage', unlocalizedText: 'Insert image', iconName: 'Photo2', onClick: editor => { - if (isContentModelEditor(editor)) { - const document = editor.getDocument(); - const fileInput = createElement(FileInput, document) as HTMLInputElement; - document.body.appendChild(fileInput); + const document = editor.getDocument(); + const fileInput = createElement(FileInput, document) as HTMLInputElement; + document.body.appendChild(fileInput); - fileInput.addEventListener('change', () => { - if (fileInput.files) { - for (let i = 0; i < fileInput.files.length; i++) { - insertImage(editor, fileInput.files[i]); - } + fileInput.addEventListener('change', () => { + if (fileInput.files) { + for (let i = 0; i < fileInput.files.length; i++) { + insertImage(editor, fileInput.files[i]); } - }); - - try { - fileInput.click(); - } finally { - document.body.removeChild(fileInput); } + }); + + try { + fileInput.click(); + } finally { + document.body.removeChild(fileInput); } }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts index b0e6c717754..e380519986b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts @@ -1,60 +1,54 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { adjustLinkSelection, insertLink } from 'roosterjs-content-model-api'; -import { InsertLinkButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { showInputDialog } from 'roosterjs-react/lib/inputDialog'; +import { InsertLinkButtonStringKey } from 'roosterjs-react'; /** * @internal * "Insert link" button on the format ribbon */ -export const insertLinkButton: RibbonButton = { +export const insertLinkButton: ContentModelRibbonButton = { key: 'buttonNameInsertLink', unlocalizedText: 'Insert link', iconName: 'Link', // isDisabled: formatState => !!formatState.isMultilineSelection, onClick: (editor, _, strings, uiUtilities) => { - if (isContentModelEditor(editor)) { - const [displayText, url] = adjustLinkSelection(editor); - const items = { - url: { - autoFocus: true, - labelKey: 'insertLinkDialogUrl' as const, - unlocalizedLabel: 'Web address (URL)', - initValue: url, - }, - displayText: { - labelKey: 'insertLinkDialogDisplayAs' as const, - unlocalizedLabel: 'Display as', - initValue: displayText, - }, - }; + const [displayText, url] = adjustLinkSelection(editor); + const items = { + url: { + autoFocus: true, + labelKey: 'insertLinkDialogUrl' as const, + unlocalizedLabel: 'Web address (URL)', + initValue: url, + }, + displayText: { + labelKey: 'insertLinkDialogDisplayAs' as const, + unlocalizedLabel: 'Display as', + initValue: displayText, + }, + }; - showInputDialog( - uiUtilities, - 'insertLinkTitle', - 'Insert link', - items, - strings, - (itemName, newValue, values) => { - if (itemName == 'url' && values.displayText == values.url) { - values.displayText = newValue; - values.url = newValue; - return values; - } else { - return null; - } + showInputDialog( + uiUtilities, + 'insertLinkTitle', + 'Insert link', + items, + strings, + (itemName, newValue, values) => { + if (itemName == 'url' && values.displayText == values.url) { + values.displayText = newValue; + values.url = newValue; + return values; + } else { + return null; } - ).then(result => { - editor.focus(); + } + ).then(result => { + editor.focus(); - if ( - result && - result.url && - (result.displayText != displayText || result.url != url) - ) { - insertLink(editor, result.url, result.url, result.displayText); - } - }); - } + if (result && result.url && (result.displayText != displayText || result.url != url)) { + insertLink(editor, result.url, result.url, result.displayText); + } + }); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts index 91fe7a4cfb9..d337376696c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts @@ -1,19 +1,17 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { getButtons, KnownRibbonButtonKey } from 'roosterjs-react'; import { insertTable } from 'roosterjs-content-model-api'; import { InsertTableButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; const originalPasteButton: RibbonButton = getButtons([ KnownRibbonButtonKey.InsertTable, ])[0] as RibbonButton; -export const insertTableButton: RibbonButton = { +export const insertTableButton: ContentModelRibbonButton = { ...originalPasteButton, onClick: (editor, key) => { - if (isContentModelEditor(editor)) { - const { row, col } = parseKey(key); - insertTable(editor, col, row); - } + const { row, col } = parseKey(key); + insertTable(editor, col, row); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts index 18b3f5a70cd..dbd15c77388 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts @@ -1,20 +1,18 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { ItalicButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleItalic } from 'roosterjs-content-model-api'; +import { ItalicButtonStringKey } from 'roosterjs-react'; /** * @internal * "Italic" button on the format ribbon */ -export const italicButton: RibbonButton = { +export const italicButton: ContentModelRibbonButton = { key: 'buttonNameItalic', unlocalizedText: 'Italic', iconName: 'Italic', isChecked: formatState => formatState.isItalic, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleItalic(editor); - } + toggleItalic(editor); return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts index cacf17df6c6..c53f3766556 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts @@ -1,13 +1,13 @@ -import showInputDialog from 'roosterjs-react/lib/inputDialog/utils/showInputDialog'; -import { CancelButtonStringKey, OkButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { CancelButtonStringKey, OkButtonStringKey } from 'roosterjs-react'; import { setListStartNumber } from 'roosterjs-content-model-api'; +import { showInputDialog } from 'roosterjs-react/lib/inputDialog'; /** * @internal * "Bulleted list" button on the format ribbon */ -export const listStartNumberButton: RibbonButton< +export const listStartNumberButton: ContentModelRibbonButton< | 'ribbonButtonSetStartNumber' | 'ribbonButtonSetStartNumberTo1' | 'ribbonButtonSetStartNumberCustomize' @@ -25,30 +25,28 @@ export const listStartNumberButton: RibbonButton< iconName: 'NumberSymbol', isDisabled: formatState => !formatState.isNumbering, onClick: (editor, key, strings, uiUtility) => { - if (isContentModelEditor(editor)) { - if (key == 'ribbonButtonSetStartNumberCustomize') { - showInputDialog( - uiUtility, - 'ribbonButtonSetStartNumberCustomize', - 'Start numbering value', - { - startNumber: { - labelKey: null, - unlocalizedLabel: null, - initValue: '1', - }, + if (key == 'ribbonButtonSetStartNumberCustomize') { + showInputDialog( + uiUtility, + 'ribbonButtonSetStartNumberCustomize', + 'Start numbering value', + { + startNumber: { + labelKey: null, + unlocalizedLabel: null, + initValue: '1', }, - strings - ).then(values => { - const newValue = parseInt(values.startNumber); + }, + strings + ).then(values => { + const newValue = parseInt(values.startNumber); - if (newValue > 0) { - setListStartNumber(editor, newValue); - } - }); - } else { - setListStartNumber(editor, 1); - } + if (newValue > 0) { + setListStartNumber(editor, newValue); + } + }); + } else { + setListStartNumber(editor, 1); } return true; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts index ac32f4fdc18..16dae98fb3c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts @@ -1,19 +1,17 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { LtrButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setDirection } from 'roosterjs-content-model-api'; +import { LtrButtonStringKey } from 'roosterjs-react'; /** * @internal * "Left to right" button on the format ribbon */ -export const ltrButton: RibbonButton = { +export const ltrButton: ContentModelRibbonButton = { key: 'buttonNameLtr', unlocalizedText: 'Left to right', iconName: 'BidiLtr', onClick: editor => { - if (isContentModelEditor(editor)) { - setDirection(editor, 'ltr'); - } + setDirection(editor, 'ltr'); return true; }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts b/demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts new file mode 100644 index 00000000000..201fca74c11 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/moreCommands.ts @@ -0,0 +1,15 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { MoreCommandsButtonStringKey } from 'roosterjs-react'; + +/** + * @internal + * "More commands" (overflow) button on the format ribbon + */ +export const moreCommands: ContentModelRibbonButton = { + key: 'buttonNameMoreCommands', + unlocalizedText: 'More commands', + iconName: 'MoreCommands', + onClick: () => { + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts index 45919be26f9..1cff4c628a9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { NumberedListButtonStringKey, RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleNumbering } from 'roosterjs-content-model-api'; +import { NumberedListButtonStringKey } from 'roosterjs-react'; /** * @internal * "Numbering list" button on the format ribbon */ -export const numberedListButton: RibbonButton = { +export const numberedListButton: ContentModelRibbonButton = { key: 'buttonNameNumberedList', unlocalizedText: 'Numbered List', iconName: 'NumberedList', isChecked: formatState => formatState.isNumbering, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleNumbering(editor); - } + toggleNumbering(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts index be62467b902..8225b1a9e3f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/pasteButton.ts @@ -1,30 +1,28 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { extractClipboardItems } from 'roosterjs-editor-dom'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; /** * @internal * "Paste" button on the format ribbon */ -export const pasteButton: RibbonButton<'buttonNamePaste'> = { +export const pasteButton: ContentModelRibbonButton<'buttonNamePaste'> = { key: 'buttonNamePaste', unlocalizedText: 'Paste', iconName: 'Paste', onClick: async editor => { - if (isContentModelEditor(editor)) { - const doc = editor.getDocument(); - const clipboard = doc.defaultView.navigator.clipboard; - if (clipboard && clipboard.read) { - try { - const clipboardItems = await clipboard.read(); - const dataTransferItems = await Promise.all( - createDataTransferItems(clipboardItems) - ); - const clipboardData = await extractClipboardItems(dataTransferItems); - editor.paste(clipboardData); - } catch {} - } + const doc = editor.getDocument(); + const clipboard = doc.defaultView.navigator.clipboard; + if (clipboard && clipboard.read) { + try { + const clipboardItems = await clipboard.read(); + const dataTransferItems = await Promise.all( + createDataTransferItems(clipboardItems) + ); + const clipboardData = await extractClipboardItems(dataTransferItems); + editor.pasteFromClipboard(clipboardData); + } catch {} } + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/popout.ts b/demo/scripts/controls/ribbonButtons/contentModel/popout.ts new file mode 100644 index 00000000000..a10f4dbdf48 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/popout.ts @@ -0,0 +1,20 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import MainPaneBase from '../../MainPaneBase'; + +/** + * Key of localized strings of Popout button + */ +export type PopoutButtonStringKey = 'buttonNamePopout'; + +/** + * "Popout" button on the format ribbon + */ +export const popout: ContentModelRibbonButton = { + key: 'buttonNamePopout', + unlocalizedText: 'Open in a separate window', + iconName: 'OpenInNewWindow', + flipWhenRtl: true, + onClick: _ => { + MainPaneBase.getInstance().popout(); + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts index 856eb6277cf..935f1574bec 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/redoButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { redo } from 'roosterjs-content-model-core'; -import { RedoButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { RedoButtonStringKey } from 'roosterjs-react'; /** * @internal * "Undo" button on the format ribbon */ -export const redoButton: RibbonButton = { +export const redoButton: ContentModelRibbonButton = { key: 'buttonNameRedo', unlocalizedText: 'Redo', iconName: 'Redo', isDisabled: formatState => !formatState.canRedo, onClick: editor => { - if (isContentModelEditor(editor)) { - redo(editor); - } + redo(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts index 3be5b6a0e7f..e9b22cc4493 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts @@ -1,19 +1,17 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { removeLink } from 'roosterjs-content-model-api'; -import { RemoveLinkButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { RemoveLinkButtonStringKey } from 'roosterjs-react'; /** * @internal * "Remove link" button on the format ribbon */ -export const removeLinkButton: RibbonButton = { +export const removeLinkButton: ContentModelRibbonButton = { key: 'buttonNameRemoveLink', unlocalizedText: 'Remove link', iconName: 'RemoveLink', isDisabled: formatState => !formatState.canUnlink, onClick: editor => { - if (isContentModelEditor(editor)) { - removeLink(editor); - } + removeLink(editor); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts index ad786f118fb..880d3e9d582 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts @@ -1,19 +1,18 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, RtlButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { RtlButtonStringKey } from 'roosterjs-react'; import { setDirection } from 'roosterjs-content-model-api'; /** * @internal * "Right to left" button on the format ribbon */ -export const rtlButton: RibbonButton = { +export const rtlButton: ContentModelRibbonButton = { key: 'buttonNameRtl', unlocalizedText: 'Right to left', iconName: 'BidiRtl', onClick: editor => { - if (isContentModelEditor(editor)) { - setDirection(editor, 'rtl'); - } + setDirection(editor, 'rtl'); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts index d95efa096c7..bbcfaf64369 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { BulletListType } from 'roosterjs-content-model-core'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; import { setListStyle } from 'roosterjs-content-model-api'; const dropDownMenuItems = { [BulletListType.Disc]: 'Disc', @@ -14,7 +13,7 @@ const dropDownMenuItems = { [BulletListType.Circle]: 'Circle', }; -export const setBulletedListStyleButton: RibbonButton<'ribbonButtonBulletedListStyle'> = { +export const setBulletedListStyleButton: ContentModelRibbonButton<'ribbonButtonBulletedListStyle'> = { key: 'ribbonButtonBulletedListStyle', dropDownMenu: { items: dropDownMenuItems }, unlocalizedText: 'Set unordered list style', @@ -23,10 +22,8 @@ export const setBulletedListStyleButton: RibbonButton<'ribbonButtonBulletedListS onClick: (editor, key) => { const value = parseInt(key); - if (isContentModelEditor(editor)) { - setListStyle(editor, { - unorderedStyleType: value, - }); - } + setListStyle(editor, { + unorderedStyleType: value, + }); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts index 2b9dd18e30d..d1934298d3a 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setHeadingLevel } from 'roosterjs-content-model-api'; import { getButtons, @@ -20,7 +20,7 @@ const keys: HeadingButtonStringKey[] = [ 'buttonNameHeading6', ]; -export const setHeadingLevelButton: RibbonButton = { +export const setHeadingLevelButton: ContentModelRibbonButton = { dropDownMenu: { ...originalHeadingButton.dropDownMenu, }, @@ -30,8 +30,6 @@ export const setHeadingLevelButton: RibbonButton = { onClick: (editor, key) => { const headingLevel = keys.indexOf(key); - if (isContentModelEditor(editor) && headingLevel >= 0) { - setHeadingLevel(editor, headingLevel as 0 | 1 | 2 | 3 | 4 | 5 | 6); - } + setHeadingLevel(editor, headingLevel as 0 | 1 | 2 | 3 | 4 | 5 | 6); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts index ae195e3fb3a..f1215267f30 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts @@ -1,6 +1,5 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { NumberingListType } from 'roosterjs-content-model-core'; -import { RibbonButton } from 'roosterjs-react'; import { setListStyle } from 'roosterjs-content-model-api'; const dropDownMenuItems = { @@ -26,7 +25,7 @@ const dropDownMenuItems = { [NumberingListType.UpperRomanDash]: 'UpperRomanDash', }; -export const setNumberedListStyleButton: RibbonButton<'ribbonButtonNumberedListStyle'> = { +export const setNumberedListStyleButton: ContentModelRibbonButton<'ribbonButtonNumberedListStyle'> = { key: 'ribbonButtonNumberedListStyle', dropDownMenu: { items: dropDownMenuItems }, unlocalizedText: 'Set ordered list style', @@ -35,10 +34,8 @@ export const setNumberedListStyleButton: RibbonButton<'ribbonButtonNumberedListS onClick: (editor, key) => { const value = parseInt(key); - if (isContentModelEditor(editor)) { - setListStyle(editor, { - orderedStyleType: value, - }); - } + setListStyle(editor, { + orderedStyleType: value, + }); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts index f463e37a3f2..a8ebb0fb2d6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setTableCellShade } from 'roosterjs-content-model-api'; import { BackgroundColorKeys, @@ -12,7 +12,7 @@ const originalBackgroundColorButton: RibbonButton = getButt KnownRibbonButtonKey.BackgroundColor, ])[0] as RibbonButton; -export const setTableCellShadeButton: RibbonButton< +export const setTableCellShadeButton: ContentModelRibbonButton< 'ribbonButtonSetTableCellShade' | BackgroundColorKeys > = { dropDownMenu: { @@ -24,7 +24,7 @@ export const setTableCellShadeButton: RibbonButton< iconName: 'BackgroundColor', isDisabled: formatState => !formatState.isInTable, onClick: (editor, key) => { - if (key != 'ribbonButtonSetTableCellShade' && isContentModelEditor(editor)) { + if (key != 'ribbonButtonSetTableCellShade') { const color = getBackgroundColorValue(key); // Content Model doesn't need dark mode color at this point, so always pass in light mode color diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts index 83795a3336b..ff3b71555cc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts @@ -1,17 +1,13 @@ -import { formatTable } from 'roosterjs-content-model-api'; -import { getFormatState } from 'roosterjs-editor-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { formatTable, getFormatState } from 'roosterjs-content-model-api'; -export const setTableHeaderButton: RibbonButton<'ribbonButtonSetTableHeader'> = { +export const setTableHeaderButton: ContentModelRibbonButton<'ribbonButtonSetTableHeader'> = { key: 'ribbonButtonSetTableHeader', unlocalizedText: 'Toggle table header', iconName: 'Header', isDisabled: formatState => !formatState.isInTable, onClick: editor => { - if (isContentModelEditor(editor)) { - const format = getFormatState(editor); - formatTable(editor, { hasHeaderRow: !format.tableHasHeader }, true /*keepCellShade*/); - } + const format = getFormatState(editor); + formatTable(editor, { hasHeaderRow: !format.tableHasHeader }, true /*keepCellShade*/); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts index 9dd219ddfa5..2cf3d481e2e 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { getFormatState, setParagraphMargin } from 'roosterjs-content-model-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const spaceAfterButtonKey = 'buttonNameSpaceAfter'; const spaceBeforeButtonKey = 'buttonNameSpaceBefore'; @@ -9,20 +8,19 @@ const spaceBeforeButtonKey = 'buttonNameSpaceBefore'; * @internal * "Add space after" button on the format ribbon */ -export const spaceAfterButton: RibbonButton = { +export const spaceAfterButton: ContentModelRibbonButton = { key: spaceAfterButtonKey, unlocalizedText: 'Remove space after', iconName: 'CaretDown8', isChecked: formatState => !formatState.marginBottom || parseInt(formatState.marginBottom) <= 0, onClick: editor => { - if (isContentModelEditor(editor)) { - const marginBottom = getFormatState(editor).marginBottom; - setParagraphMargin( - editor, - undefined /* marginTop */, - parseInt(marginBottom) ? null : '8pt' - ); - } + const marginBottom = getFormatState(editor).marginBottom; + setParagraphMargin( + editor, + undefined /* marginTop */, + parseInt(marginBottom) ? null : '8pt' + ); + return true; }, }; @@ -31,20 +29,19 @@ export const spaceAfterButton: RibbonButton = { * @internal * "Add space before" button on the format ribbon */ -export const spaceBeforeButton: RibbonButton = { +export const spaceBeforeButton: ContentModelRibbonButton = { key: spaceBeforeButtonKey, unlocalizedText: 'Add space before', iconName: 'CaretUp8', isChecked: formatState => parseInt(formatState.marginTop) > 0, onClick: editor => { - if (isContentModelEditor(editor)) { - const marginTop = getFormatState(editor).marginTop; - setParagraphMargin( - editor, - parseInt(marginTop) ? null : '12pt', - undefined /* marginBottom */ - ); - } + const marginTop = getFormatState(editor).marginTop; + setParagraphMargin( + editor, + parseInt(marginTop) ? null : '12pt', + undefined /* marginBottom */ + ); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts index 32f50c7eb6c..cefc667dc5f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts @@ -1,6 +1,5 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setSpacing } from 'roosterjs-content-model-api'; -import type { RibbonButton } from 'roosterjs-react'; const SPACING_OPTIONS = ['1.0', '1.15', '1.5', '2.0']; const NORMAL_SPACING = 1.2; @@ -18,7 +17,7 @@ function findClosest(lineHeight?: string) { * @internal * "Spacing" button on the format ribbon */ -export const spacingButton: RibbonButton = { +export const spacingButton: ContentModelRibbonButton = { key: spacingButtonKey, unlocalizedText: 'Spacing', iconName: 'LineSpacing', @@ -31,8 +30,6 @@ export const spacingButton: RibbonButton = { allowLivePreview: true, }, onClick: (editor, size) => { - if (isContentModelEditor(editor)) { - setSpacing(editor, +size * NORMAL_SPACING); - } + setSpacing(editor, +size * NORMAL_SPACING); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts index 040752c5b5d..b767de2c01b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, StrikethroughButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { StrikethroughButtonStringKey } from 'roosterjs-react'; import { toggleStrikethrough } from 'roosterjs-content-model-api'; /** * @internal * "Strikethrough" button on the format ribbon */ -export const strikethroughButton: RibbonButton = { +export const strikethroughButton: ContentModelRibbonButton = { key: 'buttonNameStrikethrough', unlocalizedText: 'Strikethrough', iconName: 'Strikethrough', isChecked: formatState => formatState.isStrikeThrough, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleStrikethrough(editor); - } + toggleStrikethrough(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts index 4490322311a..12625017db3 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, SubscriptButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { SubscriptButtonStringKey } from 'roosterjs-react'; import { toggleSubscript } from 'roosterjs-content-model-api'; /** * @internal * "Subscript" button on the format ribbon */ -export const subscriptButton: RibbonButton = { +export const subscriptButton: ContentModelRibbonButton = { key: 'buttonNameSubscript', unlocalizedText: 'Subscript', iconName: 'Subscript', isChecked: formatState => formatState.isSubscript, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleSubscript(editor); - } + toggleSubscript(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts index ad2c161e357..e32c0e85cac 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, SuperscriptButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import { SuperscriptButtonStringKey } from 'roosterjs-react'; import { toggleSuperscript } from 'roosterjs-content-model-api'; /** * @internal * "Superscript" button on the format ribbon */ -export const superscriptButton: RibbonButton = { +export const superscriptButton: ContentModelRibbonButton = { key: 'buttonNameSuperscript', unlocalizedText: 'Superscript', iconName: 'Superscript', isChecked: formatState => formatState.isSuperscript, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleSuperscript(editor); - } + toggleSuperscript(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts index 12552ed4c3d..d28572ba532 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts @@ -1,8 +1,7 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; import { applyTableBorderFormat } from 'roosterjs-content-model-api'; import { BorderOperations } from 'roosterjs-content-model-types'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const TABLE_OPERATIONS: Record = { menuNameTableAllBorder: 'allBorders', @@ -15,7 +14,7 @@ const TABLE_OPERATIONS: Record = { menuNameTableOutsideBorder: 'outsideBorders', }; -export const tableBorderApplyButton: RibbonButton<'ribbonButtonTableBorder'> = { +export const tableBorderApplyButton: ContentModelRibbonButton<'ribbonButtonTableBorder'> = { key: 'ribbonButtonTableBorder', iconName: 'TableComputed', unlocalizedText: 'Table Border', @@ -33,9 +32,7 @@ export const tableBorderApplyButton: RibbonButton<'ribbonButtonTableBorder'> = { }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableBorder') { - const border = MainPaneBase.getInstance().getTableBorder(); - applyTableBorderFormat(editor, border, TABLE_OPERATIONS[key]); - } + const border = MainPaneBase.getInstance().getTableBorder(); + applyTableBorderFormat(editor, border, TABLE_OPERATIONS[key]); }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts index 3548f8974e7..44e7f96a934 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderColorButton.ts @@ -1,7 +1,6 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; -import { getButtons, getTextColorValue, KnownRibbonButtonKey } from 'roosterjs-react'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; +import { getButtons, getTextColorValue, KnownRibbonButtonKey, RibbonButton } from 'roosterjs-react'; const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as RibbonButton< 'buttonNameTableBorderColor' @@ -11,14 +10,14 @@ const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as Ribbon * @internal * "Table Border Color" button on the format ribbon */ -export const tableBorderColorButton: RibbonButton<'buttonNameTableBorderColor'> = { +export const tableBorderColorButton: ContentModelRibbonButton<'buttonNameTableBorderColor'> = { ...originalButton, unlocalizedText: 'Table Border Color', iconName: 'ColorSolid', isDisabled: formatState => !formatState.isInTable, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameTableBorderColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameTableBorderColor') { MainPaneBase.getInstance().setTableBorderColor(getTextColorValue(key).lightModeColor); editor.focus(); } diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts index 525c3ba7612..f9e7c410ab4 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderStyleButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const STYLES: Record = { dashed: 'dashed', @@ -17,7 +16,7 @@ const STYLES: Record = { * @internal * "Table Border Style" button on the format ribbon */ -export const tableBorderStyleButton: RibbonButton<'buttonNameTableBorderStyle'> = { +export const tableBorderStyleButton: ContentModelRibbonButton<'buttonNameTableBorderStyle'> = { key: 'buttonNameTableBorderStyle', unlocalizedText: 'Table Border Style', iconName: 'LineStyle', @@ -27,10 +26,9 @@ export const tableBorderStyleButton: RibbonButton<'buttonNameTableBorderStyle'> allowLivePreview: true, }, onClick: (editor, style) => { - if (isContentModelEditor(editor)) { - MainPaneBase.getInstance().setTableBorderStyle(style); - editor.focus(); - } + MainPaneBase.getInstance().setTableBorderStyle(style); + editor.focus(); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts index 89eeb6dc008..d1fefea87be 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderWidthButton.ts @@ -1,6 +1,5 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import MainPaneBase from '../../MainPaneBase'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton } from 'roosterjs-react'; const WIDTH = [0.25, 0.5, 0.75, 1, 1.5, 2.25, 3, 4.5, 6]; @@ -8,7 +7,7 @@ const WIDTH = [0.25, 0.5, 0.75, 1, 1.5, 2.25, 3, 4.5, 6]; * @internal * "Table Border Width" button on the format ribbon */ -export const tableBorderWidthButton: RibbonButton<'buttonNameTableBorderWidth'> = { +export const tableBorderWidthButton: ContentModelRibbonButton<'buttonNameTableBorderWidth'> = { key: 'buttonNameTableBorderWidth', unlocalizedText: 'Table Border Width', iconName: 'LineThickness', @@ -21,10 +20,9 @@ export const tableBorderWidthButton: RibbonButton<'buttonNameTableBorderWidth'> allowLivePreview: true, }, onClick: (editor, width) => { - if (isContentModelEditor(editor)) { - MainPaneBase.getInstance().setTableBorderWidth(width); - editor.focus(); - } + MainPaneBase.getInstance().setTableBorderWidth(width); + editor.focus(); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts index fdf7e39e4f1..f3d2ed1095b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts @@ -1,8 +1,7 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { editTable } from 'roosterjs-content-model-api'; -import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { TableOperation } from 'roosterjs-content-model-types'; import { - RibbonButton, TableEditAlignMenuItemStringKey, TableEditAlignTableMenuItemStringKey, TableEditDeleteMenuItemStringKey, @@ -38,7 +37,7 @@ const TableEditOperationMap: Partial = { key: 'ribbonButtonTableInsert', @@ -54,13 +53,13 @@ export const tableInsertButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableInsert') { + if (key != 'ribbonButtonTableInsert') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableDeleteButton: RibbonButton< +export const tableDeleteButton: ContentModelRibbonButton< 'ribbonButtonTableDelete' | TableEditDeleteMenuItemStringKey > = { key: 'ribbonButtonTableDelete', @@ -75,13 +74,13 @@ export const tableDeleteButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableDelete') { + if (key != 'ribbonButtonTableDelete') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableMergeButton: RibbonButton< +export const tableMergeButton: ContentModelRibbonButton< 'ribbonButtonTableMerge' | TableEditMergeMenuItemStringKey > = { key: 'ribbonButtonTableMerge', @@ -99,13 +98,13 @@ export const tableMergeButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableMerge') { + if (key != 'ribbonButtonTableMerge') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableSplitButton: RibbonButton< +export const tableSplitButton: ContentModelRibbonButton< 'ribbonButtonTableSplit' | TableEditSplitMenuItemStringKey > = { key: 'ribbonButtonTableSplit', @@ -119,13 +118,13 @@ export const tableSplitButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableSplit') { + if (key != 'ribbonButtonTableSplit') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableAlignCellButton: RibbonButton< +export const tableAlignCellButton: ContentModelRibbonButton< 'ribbonButtonTableAlignCell' | TableEditAlignMenuItemStringKey > = { key: 'ribbonButtonTableAlignCell', @@ -144,13 +143,13 @@ export const tableAlignCellButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableAlignCell') { + if (key != 'ribbonButtonTableAlignCell') { editTable(editor, TableEditOperationMap[key]); } }, }; -export const tableAlignTableButton: RibbonButton< +export const tableAlignTableButton: ContentModelRibbonButton< 'ribbonButtonTableAlignTable' | TableEditAlignTableMenuItemStringKey > = { key: 'ribbonButtonTableAlignTable', @@ -165,7 +164,7 @@ export const tableAlignTableButton: RibbonButton< }, }, onClick: (editor, key) => { - if (isContentModelEditor(editor) && key != 'ribbonButtonTableAlignTable') { + if (key != 'ribbonButtonTableAlignTable') { editTable(editor, TableEditOperationMap[key]); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts index 168ad36c22c..5b9ed653586 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts @@ -1,4 +1,4 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { setTextColor } from 'roosterjs-content-model-api'; import { getButtons, @@ -16,11 +16,11 @@ const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as Ribbon * @internal * "Text color" button on the format ribbon */ -export const textColorButton: RibbonButton = { +export const textColorButton: ContentModelRibbonButton = { ...originalButton, onClick: (editor, key) => { // This check will always be true, add it here just to satisfy compiler - if (key != 'buttonNameTextColor' && isContentModelEditor(editor)) { + if (key != 'buttonNameTextColor') { setTextColor(editor, getTextColorValue(key).lightModeColor); } }, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts index 0abfc7ada87..0d8a5ede152 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, UnderlineButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { toggleUnderline } from 'roosterjs-content-model-api'; +import { UnderlineButtonStringKey } from 'roosterjs-react'; /** * @internal * "Underline" button on the format ribbon */ -export const underlineButton: RibbonButton = { +export const underlineButton: ContentModelRibbonButton = { key: 'buttonNameUnderline', unlocalizedText: 'Underline', iconName: 'Underline', isChecked: formatState => formatState.isUnderline, onClick: editor => { - if (isContentModelEditor(editor)) { - toggleUnderline(editor); - } + toggleUnderline(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts index 0ae337f7ce0..2f07a1a560c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/undoButton.ts @@ -1,20 +1,19 @@ -import { isContentModelEditor } from 'roosterjs-content-model-editor'; -import { RibbonButton, UndoButtonStringKey } from 'roosterjs-react'; +import ContentModelRibbonButton from './ContentModelRibbonButton'; import { undo } from 'roosterjs-content-model-core'; +import { UndoButtonStringKey } from 'roosterjs-react'; /** * @internal * "Undo" button on the format ribbon */ -export const undoButton: RibbonButton = { +export const undoButton: ContentModelRibbonButton = { key: 'buttonNameUndo', unlocalizedText: 'Undo', iconName: 'undo', isDisabled: formatState => !formatState.canUndo, onClick: editor => { - if (isContentModelEditor(editor)) { - undo(editor); - } + undo(editor); + return true; }, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/zoom.ts b/demo/scripts/controls/ribbonButtons/contentModel/zoom.ts new file mode 100644 index 00000000000..a4ee19075a7 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/zoom.ts @@ -0,0 +1,49 @@ +import ContentModelRibbonButton from './ContentModelRibbonButton'; +import MainPaneBase from '../../MainPaneBase'; +import { getObjectKeys } from 'roosterjs-editor-dom'; + +const DropDownItems = { + 'zoom50%': '50%', + 'zoom75%': '75%', + 'zoom100%': '100%', + 'zoom150%': '150%', + 'zoom200%': '200%', +}; + +const DropDownValues: { [key in keyof typeof DropDownItems]: number } = { + 'zoom50%': 0.5, + 'zoom75%': 0.75, + 'zoom100%': 1, + 'zoom150%': 1.5, + 'zoom200%': 2, +}; + +/** + * Key of localized strings of Zoom button + */ +export type ZoomButtonStringKey = 'buttonNameZoom'; + +/** + * "Zoom" button on the format ribbon + */ +export const zoom: ContentModelRibbonButton = { + key: 'buttonNameZoom', + unlocalizedText: 'Zoom', + iconName: 'ZoomIn', + dropDownMenu: { + items: DropDownItems, + getSelectedItemKey: formatState => + getObjectKeys(DropDownItems).filter( + key => DropDownValues[key] == formatState.zoomScale + )[0], + }, + onClick: (editor, key) => { + const zoomScale = DropDownValues[key as keyof typeof DropDownItems]; + editor.setZoomScale(zoomScale); + editor.focus(); + + // Let main pane know this state change so that it can be persisted when pop out/pop in + MainPaneBase.getInstance().setScale(zoomScale); + return true; + }, +}; diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts index 12bd22bed16..c4997059129 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -355,6 +355,32 @@ export class StandaloneEditor implements IStandaloneEditor { return this.getCore().zoomScale; } + /** + * Set current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale + * to let editor behave correctly especially for those mouse drag/drop behaviors + * @param scale The new scale number to set. It should be positive number and no greater than 10, otherwise it will be ignored. + */ + setZoomScale(scale: number): void { + const core = this.getCore(); + + if (scale > 0 && scale <= 10) { + const oldValue = core.zoomScale; + core.zoomScale = scale; + + if (oldValue != scale) { + this.triggerPluginEvent( + PluginEventType.ZoomChanged, + { + oldZoomScale: oldValue, + newZoomScale: scale, + }, + true /*broadcast*/ + ); + } + } + } + /** * @returns the current StandaloneEditorCore object * @throws a standard Error if there's no core object diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index fb9427bbb69..a43d84d7f4d 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -965,32 +965,6 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode return this.getContentModelEditorCore().sizeTransformer; } - /** - * Set current zoom scale, default value is 1 - * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale - * to let editor behave correctly especially for those mouse drag/drop behaviors - * @param scale The new scale number to set. It should be positive number and no greater than 10, otherwise it will be ignored. - */ - setZoomScale(scale: number): void { - const core = this.getCore(); - - if (scale > 0 && scale <= 10) { - const oldValue = core.zoomScale; - core.zoomScale = scale; - - if (oldValue != scale) { - this.triggerPluginEvent( - PluginEventType.ZoomChanged, - { - oldZoomScale: oldValue, - newZoomScale: scale, - }, - true /*broadcast*/ - ); - } - } - } - /** * Retrieves the rect of the visible viewport of the editor. */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts index eef3ea1d137..435710594b5 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -144,6 +144,13 @@ export interface IStandaloneEditor { */ getZoomScale(): number; + /** + * Set current zoom scale, default value is 1 + * When editor is put under a zoomed container, need to pass the zoom scale number using EditorOptions.zoomScale + * to let editor behave correctly especially for those mouse drag/drop behaviors + */ + setZoomScale(scale: number): void; + /** * Add a single undo snapshot to undo stack */ From a3065512400650dca1fd6cce083b9d0c5aaa3ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Mon, 8 Jan 2024 19:10:20 -0300 Subject: [PATCH 39/64] delete list --- .../lib/edit/deleteSteps/deleteList.ts | 24 +++ .../lib/edit/keyboardDelete.ts | 9 +- .../test/edit/deleteSteps/deleteListTest.ts | 176 ++++++++++++++++++ .../test/edit/keyboardDeleteTest.ts | 116 +++++++++++- 4 files changed, 314 insertions(+), 11 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts create mode 100644 packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts new file mode 100644 index 00000000000..ebce900c6b8 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts @@ -0,0 +1,24 @@ +import { getClosestAncestorBlockGroupIndex } from 'roosterjs-content-model-core'; +import type { DeleteSelectionStep } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export const deleteList: DeleteSelectionStep = context => { + const { paragraph, marker, path } = context.insertPoint; + + if (context.deleteResult == 'nothingToDelete' || context.deleteResult == 'notDeleted') { + const index = getClosestAncestorBlockGroupIndex(path, ['ListItem']); + const item = path[index]; + if ( + index >= 0 && + paragraph.segments[0] == marker && + item.blockGroupType == 'ListItem' && + (paragraph.segments.length == 1 || + (paragraph.segments.length == 2 && paragraph.segments[1].segmentType == 'Br')) + ) { + item.levels = []; + context.deleteResult = 'singleChar'; + } + } +}; diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts index 7ba0ab3649c..4131ddb5919 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts @@ -1,5 +1,6 @@ import { ChangeSource, deleteSelection, isModifierKey } from 'roosterjs-content-model-core'; import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore'; +import { deleteList } from './deleteSteps/deleteList'; import { isNodeOfType } from 'roosterjs-content-model-dom'; import { handleKeyboardEventResult, @@ -61,7 +62,13 @@ function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelecti const deleteCollapsedSelection = isForward ? forwardDeleteCollapsedSelection : backwardDeleteCollapsedSelection; - return [deleteAllSegmentBeforeStep, deleteWordSelection, deleteCollapsedSelection]; + const deleteListStep = !isForward ? deleteList : null; + return [ + deleteAllSegmentBeforeStep, + deleteWordSelection, + deleteCollapsedSelection, + deleteListStep, + ]; } function shouldDeleteWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts new file mode 100644 index 00000000000..ee12ae7f795 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts @@ -0,0 +1,176 @@ +import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { deleteList } from '../../../lib/edit/deleteSteps/deleteList'; +import { deleteSelection } from 'roosterjs-content-model-core'; +import { normalizeContentModel } from 'roosterjs-content-model-dom'; + +describe('deleteList', () => { + it('deletes the list item when there is only one list item', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }; + const result = deleteSelection(model, [deleteList]); + normalizeContentModel(model); + expect(result.deleteResult).toEqual('singleChar'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + isImplicit: false, + }, + ], + }); + }); + + it('do not delete list with text', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'text', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }; + const result = deleteSelection(model, [deleteList]); + normalizeContentModel(model); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'text', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }); + expect(result.deleteResult).toEqual('notDeleted'); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts index 8f5e7317224..a821d40c414 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts @@ -3,6 +3,7 @@ import * as handleKeyboardEventResult from '../../lib/edit/handleKeyboardEventCo import { ChangeSource } from 'roosterjs-content-model-core'; import { ContentModelDocument, DOMSelection } from 'roosterjs-content-model-types'; import { deleteAllSegmentBefore } from '../../lib/edit/deleteSteps/deleteAllSegmentBefore'; +import { deleteList } from '../../lib/edit/deleteSteps/deleteList'; import { DeleteResult, DeleteSelectionStep } from 'roosterjs-content-model-types'; import { editingTestCommon } from './editingTestCommon'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; @@ -56,6 +57,7 @@ describe('keyboardDelete', () => { collapsed: false, }, }); + const result = keyboardDelete(editor, mockedEvent); expect(result).toBeTrue(); @@ -86,7 +88,7 @@ describe('keyboardDelete', () => { blockGroupType: 'Document', blocks: [], }, - [null!, null!, forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection, null!], 'notDeleted', true, 0 @@ -104,7 +106,7 @@ describe('keyboardDelete', () => { blockGroupType: 'Document', blocks: [], }, - [null!, null!, backwardDeleteCollapsedSelection], + [null!, null!, backwardDeleteCollapsedSelection, deleteList], 'notDeleted', true, 0 @@ -124,7 +126,7 @@ describe('keyboardDelete', () => { blockGroupType: 'Document', blocks: [], }, - [null!, forwardDeleteWordSelection, forwardDeleteCollapsedSelection], + [null!, forwardDeleteWordSelection, forwardDeleteCollapsedSelection, null!], 'notDeleted', true, 0 @@ -144,7 +146,7 @@ describe('keyboardDelete', () => { blockGroupType: 'Document', blocks: [], }, - [null!, backwardDeleteWordSelection, backwardDeleteCollapsedSelection], + [null!, backwardDeleteWordSelection, backwardDeleteCollapsedSelection, deleteList], 'notDeleted', true, 0 @@ -164,7 +166,7 @@ describe('keyboardDelete', () => { blockGroupType: 'Document', blocks: [], }, - [null!, null!, forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection, null!], 'notDeleted', true, 0 @@ -184,7 +186,7 @@ describe('keyboardDelete', () => { blockGroupType: 'Document', blocks: [], }, - [deleteAllSegmentBefore, null!, backwardDeleteCollapsedSelection], + [deleteAllSegmentBefore, null!, backwardDeleteCollapsedSelection, deleteList], 'notDeleted', true, 0 @@ -226,7 +228,7 @@ describe('keyboardDelete', () => { }, ], }, - [null!, null!, forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection, null!], 'notDeleted', true, 0 @@ -268,7 +270,7 @@ describe('keyboardDelete', () => { }, ], }, - [null!, null!, backwardDeleteCollapsedSelection], + [null!, null!, backwardDeleteCollapsedSelection, deleteList], 'notDeleted', true, 0 @@ -320,7 +322,7 @@ describe('keyboardDelete', () => { }, ], }, - [null!, null!, forwardDeleteCollapsedSelection], + [null!, null!, forwardDeleteCollapsedSelection, null!], 'singleChar', false, 1 @@ -372,7 +374,101 @@ describe('keyboardDelete', () => { }, ], }, - [null!, null!, backwardDeleteCollapsedSelection], + [null!, null!, backwardDeleteCollapsedSelection, deleteList], + 'singleChar', + false, + 1 + ); + }); + + it('Backspace on empty list', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }, + 'Backspace', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }, + [null!, null!, backwardDeleteCollapsedSelection, deleteList], 'singleChar', false, 1 From 5cd510f42a04ee3f2aaeee17b40b2251c1fc6e0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:46:55 -0800 Subject: [PATCH 40/64] Bump follow-redirects from 1.14.8 to 1.15.4 (#2319) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.8 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.8...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f8f6f52f911..4cfafcf1341 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2868,9 +2868,9 @@ flatted@^3.2.7: integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== follow-redirects@^1.0.0: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== + version "1.15.4" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" + integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== for-each@^0.3.3: version "0.3.3" From 6ad3fb0349bc2a6e7da97956404f1a8a66bd19b0 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Tue, 9 Jan 2024 14:24:42 -0800 Subject: [PATCH 41/64] Workaround a paste issue (#2321) * Add a workaround for paste link issue * Workaround a paste issue --- .../lib/utils/paste/createPasteFragment.ts | 6 +++++- .../test/utils/paste/createPasteFragmentTest.ts | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts index 4c95cd4c570..b148b33ee30 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts @@ -27,7 +27,11 @@ export function createPasteFragment( img.src = imageDataUri; fragment.appendChild(img); } else if (pasteType != 'asPlainText' && root) { - moveChildNodes(fragment, root); + // This is a temp workaround. We should remove this SPAN later and put pasted content under fragment directly + const span = document.createElement('span'); + + moveChildNodes(span, root); + fragment.appendChild(span); } else if (text) { text.split('\n').forEach((line, index, lines) => { line = line diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts index 0d66cd3ffa7..77252a0eb51 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts @@ -32,7 +32,7 @@ describe('createPasteFragment', () => { } } - it('Empty source, paste image', () => { + xit('Empty source, paste image', () => { const root = document.createElement('div'); root.innerHTML = 'HTML'; @@ -91,7 +91,7 @@ describe('createPasteFragment', () => { ); }); - it('Has url, paste normal, has text', () => { + xit('Has url, paste normal, has text', () => { const root = document.createElement('div'); root.innerHTML = 'HTML'; @@ -131,7 +131,7 @@ describe('createPasteFragment', () => { ); }); - it('Has text, paste normal', () => { + xit('Has text, paste normal', () => { const root = document.createElement('div'); root.innerHTML = 'HTML'; From caae3a34017ecf405ab92ee7b2429b6030ab0573 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 10 Jan 2024 09:33:23 -0800 Subject: [PATCH 42/64] Revert "Workaround a paste issue" (#2323) --- .../lib/utils/paste/createPasteFragment.ts | 6 +----- .../test/utils/paste/createPasteFragmentTest.ts | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts index b148b33ee30..4c95cd4c570 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/createPasteFragment.ts @@ -27,11 +27,7 @@ export function createPasteFragment( img.src = imageDataUri; fragment.appendChild(img); } else if (pasteType != 'asPlainText' && root) { - // This is a temp workaround. We should remove this SPAN later and put pasted content under fragment directly - const span = document.createElement('span'); - - moveChildNodes(span, root); - fragment.appendChild(span); + moveChildNodes(fragment, root); } else if (text) { text.split('\n').forEach((line, index, lines) => { line = line diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts index 77252a0eb51..0d66cd3ffa7 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/createPasteFragmentTest.ts @@ -32,7 +32,7 @@ describe('createPasteFragment', () => { } } - xit('Empty source, paste image', () => { + it('Empty source, paste image', () => { const root = document.createElement('div'); root.innerHTML = 'HTML'; @@ -91,7 +91,7 @@ describe('createPasteFragment', () => { ); }); - xit('Has url, paste normal, has text', () => { + it('Has url, paste normal, has text', () => { const root = document.createElement('div'); root.innerHTML = 'HTML'; @@ -131,7 +131,7 @@ describe('createPasteFragment', () => { ); }); - xit('Has text, paste normal', () => { + it('Has text, paste normal', () => { const root = document.createElement('div'); root.innerHTML = 'HTML'; From 137e875e50e50f73edca24f4afbeaf283debc4c3 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 10 Jan 2024 11:22:57 -0800 Subject: [PATCH 43/64] Standalone Editor: Remove compatible enums from standalone editor (#2296) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * Standalone Editor: Remove compatible enums from standalone editor * improve * fix demo * Fix buttons * fix build * fix build --- .../ribbonButtons/contentModel/export.ts | 2 +- .../lib/publicApi/image/changeImage.ts | 2 +- .../test/publicApi/block/setAlignmentTest.ts | 12 +++---- .../test/publicApi/image/changeImageTest.ts | 14 ++++---- .../corePlugin/ContentModelCopyPastePlugin.ts | 2 +- .../lib/corePlugin/DOMEventPlugin.ts | 10 +++--- .../lib/corePlugin/EntityPlugin.ts | 2 +- .../lib/corePlugin/LifecyclePlugin.ts | 4 +-- .../lib/editor/StandaloneEditor.ts | 5 ++- .../test/coreApi/formatContentModelTest.ts | 2 +- .../test/coreApi/pasteTest.ts | 2 +- .../ContentModelCopyPastePluginTest.ts | 2 +- .../ContentModelFormatPluginTest.ts | 4 +-- .../test/corePlugin/DomEventPluginTest.ts | 26 +++++++-------- .../test/corePlugin/EntityPluginTest.ts | 4 +-- .../test/corePlugin/LifecyclePluginTest.ts | 32 +++++++++---------- .../test/corePlugin/SelectionPluginTest.ts | 6 ++-- .../test/editor/StandaloneEditorTest.ts | 6 ++-- .../generatePasteOptionFromPluginsTest.ts | 2 +- .../lib/editor/ContentModelEditor.ts | 32 +++++++++++++++++-- .../lib/publicTypes/ContentModelEditorCore.ts | 8 ++--- .../lib/editor/IStandaloneEditor.ts | 3 +- 22 files changed, 102 insertions(+), 80 deletions(-) diff --git a/demo/scripts/controls/ribbonButtons/contentModel/export.ts b/demo/scripts/controls/ribbonButtons/contentModel/export.ts index 3d82b2a3006..8c957ca1239 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/export.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/export.ts @@ -45,7 +45,7 @@ export const exportContent: ContentModelRibbonButton = { }); if (isEntity && format.id && format.entityType) { - editor.triggerPluginEvent(PluginEventType.EntityOperation, { + editor.triggerEvent(PluginEventType.EntityOperation, { operation: EntityOperation.ReplaceTemporaryContent, entity: { wrapper: clonedRoot, diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts index 3842d99deee..86b0dc9c9d5 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts @@ -24,7 +24,7 @@ export default function changeImage(editor: IStandaloneEditor, file: File) { image.format.height = ''; image.alt = ''; - editor.triggerPluginEvent(PluginEventType.EditImage, { + editor.triggerEvent(PluginEventType.EditImage, { image: selection.image, previousSrc, newSrc: dataUrl, diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts index ea10eb6806f..27e65109885 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts @@ -417,12 +417,12 @@ describe('setAlignment', () => { describe('setAlignment in table', () => { let editor: IStandaloneEditor; let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; let getVisibleViewport: jasmine.Spy; beforeEach(() => { createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); getVisibleViewport = jasmine.createSpy('getVisibleViewport'); spyOn(normalizeTable, 'normalizeTable'); @@ -432,7 +432,7 @@ describe('setAlignment in table', () => { addUndoSnapshot: (callback: Function) => callback(), createContentModel, isDarkMode: () => false, - triggerPluginEvent, + triggerEvent, getVisibleViewport, } as any) as IStandaloneEditor; }); @@ -823,13 +823,13 @@ describe('setAlignment in list', () => { let editor: IStandaloneEditor; let setContentModel: jasmine.Spy; let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; let getVisibleViewport: jasmine.Spy; beforeEach(() => { setContentModel = jasmine.createSpy('setContentModel'); createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); getVisibleViewport = jasmine.createSpy('getVisibleViewport'); editor = ({ @@ -838,7 +838,7 @@ describe('setAlignment in list', () => { setContentModel, createContentModel, isDarkMode: () => false, - triggerPluginEvent, + triggerEvent, getVisibleViewport, } as any) as IStandaloneEditor; }); diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts index 7a8815de41c..47fac462ce7 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts @@ -18,7 +18,7 @@ describe('changeImage', () => { const testUrl = 'http://test.com/test'; const blob = ({ a: 1 } as any) as File; let imageNode: HTMLImageElement; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; function runTest( model: ContentModelDocument, @@ -29,7 +29,7 @@ describe('changeImage', () => { const getDOMSelection = jasmine .createSpy() .and.returnValues({ type: 'image', image: imageNode }); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent').and.callThrough(); + triggerEvent = jasmine.createSpy('triggerEvent').and.callThrough(); let formatResult: boolean | undefined; const formatContentModel = jasmine @@ -50,7 +50,7 @@ describe('changeImage', () => { isDisposed: () => false, getPendingFormat: () => null as any, getDOMSelection, - triggerPluginEvent, + triggerEvent, formatContentModel, } as any) as IStandaloneEditor; @@ -82,7 +82,7 @@ describe('changeImage', () => { 0 ); - expect(triggerPluginEvent).toHaveBeenCalledTimes(0); + expect(triggerEvent).toHaveBeenCalledTimes(0); }); it('Doc without selection', () => { @@ -116,7 +116,7 @@ describe('changeImage', () => { 0 ); - expect(triggerPluginEvent).toHaveBeenCalledTimes(0); + expect(triggerEvent).toHaveBeenCalledTimes(0); }); it('Doc with selection, but no image', () => { @@ -194,8 +194,8 @@ describe('changeImage', () => { 1 ); - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EditImage, { + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.EditImage, { image: imageNode, newSrc: testUrl, previousSrc: 'test', diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts index 3332db60395..14d66e01c8a 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts @@ -144,7 +144,7 @@ class ContentModelCopyPastePlugin implements PluginWithState { doc?.defaultView?.requestAnimationFrame(() => { if (this.editor) { this.editor.takeSnapshot(); - this.editor.triggerPluginEvent(PluginEventType.ContentChanged, { + this.editor.triggerEvent(PluginEventType.ContentChanged, { source: ChangeSource.Drop, }); } @@ -134,7 +134,7 @@ class DOMEventPlugin implements PluginWithState { }; private onScroll = (e: Event) => { - this.editor?.triggerPluginEvent(PluginEventType.Scroll, { + this.editor?.triggerEvent(PluginEventType.Scroll, { rawEvent: e, scrollContainer: this.state.scrollContainer, }); @@ -175,7 +175,7 @@ class DOMEventPlugin implements PluginWithState { this.state.mouseDownY = event.pageY; } - this.editor.triggerPluginEvent(PluginEventType.MouseDown, { + this.editor.triggerEvent(PluginEventType.MouseDown, { rawEvent: event, }); } @@ -184,7 +184,7 @@ class DOMEventPlugin implements PluginWithState { private onMouseUp = (rawEvent: MouseEvent) => { if (this.editor) { this.removeMouseUpEventListener(); - this.editor.triggerPluginEvent(PluginEventType.MouseUp, { + this.editor.triggerEvent(PluginEventType.MouseUp, { rawEvent, isClicking: this.state.mouseDownX == rawEvent.pageX && @@ -199,7 +199,7 @@ class DOMEventPlugin implements PluginWithState { private onCompositionEnd = (rawEvent: CompositionEvent) => { this.state.isInIME = false; - this.editor?.triggerPluginEvent(PluginEventType.CompositionEnd, { + this.editor?.triggerEvent(PluginEventType.CompositionEnd, { rawEvent, }); }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts index 0759a9cb23e..1ae78b04e38 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts @@ -248,7 +248,7 @@ class EntityPlugin implements PluginWithState { }); return format.id && format.entityType && !format.isFakeEntity - ? editor.triggerPluginEvent(PluginEventType.EntityOperation, { + ? editor.triggerEvent(PluginEventType.EntityOperation, { operation: EntityOperationMap[operation], rawEvent, entity: { diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts index 7e1cb2174e9..433004a5455 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts @@ -92,14 +92,14 @@ class LifecyclePlugin implements PluginWithState { this.adjustColor(); // Let other plugins know that we are ready - this.editor.triggerPluginEvent(PluginEventType.EditorReady, {}, true /*broadcast*/); + this.editor.triggerEvent(PluginEventType.EditorReady, {}, true /*broadcast*/); } /** * Dispose this plugin */ dispose() { - this.editor?.triggerPluginEvent(PluginEventType.BeforeDispose, {}, true /*broadcast*/); + this.editor?.triggerEvent(PluginEventType.BeforeDispose, {}, true /*broadcast*/); if (this.disposer) { this.disposer(); diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts index c4997059129..fa89494de53 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -7,7 +7,6 @@ import type { PluginEventData, PluginEventFromType, } from 'roosterjs-editor-types'; -import type { CompatiblePluginEventType } from 'roosterjs-editor-types/lib/compatibleTypes'; import type { ClipboardData, ContentModelDocument, @@ -212,7 +211,7 @@ export class StandaloneEditor implements IStandaloneEditor { * @returns the event object which is really passed into plugins. Some plugin may modify the event object so * the result of this function provides a chance to read the modified result */ - triggerPluginEvent( + triggerEvent( eventType: T, data: PluginEventData, broadcast: boolean = false @@ -369,7 +368,7 @@ export class StandaloneEditor implements IStandaloneEditor { core.zoomScale = scale; if (oldValue != scale) { - this.triggerPluginEvent( + this.triggerEvent( PluginEventType.ZoomChanged, { oldZoomScale: oldValue, diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts index c616bafc9b2..e6356038da4 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts @@ -37,7 +37,7 @@ describe('formatContentModel', () => { getFocusedPosition = jasmine .createSpy('getFocusedPosition') .and.returnValue({ node: mockedContainer, offset: mockedOffset }); - triggerEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); getDOMSelection = jasmine.createSpy('getDOMSelection').and.returnValue(null); hasFocus = jasmine.createSpy('hasFocus'); diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index f9285159a85..742313df250 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -117,7 +117,7 @@ describe('Paste ', () => { }); spyOn(editor, 'getDocument').and.callThrough(); - spyOn(editor, 'triggerPluginEvent').and.callThrough(); + spyOn(editor, 'triggerEvent').and.callThrough(); }); afterEach(() => { diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index f3d1083b600..15c95e1ab36 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -129,7 +129,7 @@ describe('ContentModelCopyPastePlugin |', () => { domEvents = eventMap; }, createContentModel: (options: any) => createContentModelSpy(options), - triggerPluginEvent(eventType: any, data: any, broadcast: any) { + triggerEvent(eventType: any, data: any, broadcast: any) { triggerPluginEventSpy(eventType, data, broadcast); return data; }, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts index 530209ce7d2..b9c32b8e0dd 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts @@ -102,7 +102,7 @@ describe('ContentModelFormatPlugin', () => { }); it('with pending format and selection, trigger CompositionEnd event', () => { - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + const triggerEvent = jasmine.createSpy('triggerEvent'); const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); const editor = ({ @@ -112,7 +112,7 @@ describe('ContentModelFormatPlugin', () => { }, cacheContentModel: () => {}, isDarkMode: () => false, - triggerPluginEvent, + triggerEvent, getVisibleViewport, } as any) as IStandaloneEditor; const plugin = createContentModelFormatPlugin({}); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts index 673f9684a07..1f515249fe7 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts @@ -179,7 +179,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { let plugin: PluginWithState; let addEventListener: jasmine.Spy; let removeEventListener: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; let eventMap: Record; let scrollContainer: HTMLElement; let onMouseUp: Function; @@ -194,7 +194,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { onMouseUp = handler; }); removeEventListener = jasmine.createSpy('.removeEventListener'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); scrollContainer = { addEventListener: () => {}, removeEventListener: () => {}, @@ -210,7 +210,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { addEventListener, removeEventListener, }), - triggerPluginEvent, + triggerEvent, getEnvironment: () => ({}), attachDomEvent: (map: Record) => { eventMap = map; @@ -262,7 +262,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { onMouseUp(mockedEvent); expect(removeEventListener).toHaveBeenCalled(); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.MouseUp, { + expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.MouseUp, { rawEvent: mockedEvent, isClicking: true, }); @@ -300,7 +300,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { onMouseUp(mockedEvent2); expect(removeEventListener).toHaveBeenCalled(); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.MouseUp, { + expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.MouseUp, { rawEvent: mockedEvent2, isClicking: false, }); @@ -318,7 +318,7 @@ describe('DOMEventPlugin handle other event', () => { let plugin: PluginWithState; let addEventListener: jasmine.Spy; let removeEventListener: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; let eventMap: Record; let scrollContainer: HTMLElement; let addEventListenerSpy: jasmine.Spy; @@ -327,7 +327,7 @@ describe('DOMEventPlugin handle other event', () => { beforeEach(() => { addEventListener = jasmine.createSpy('addEventListener'); removeEventListener = jasmine.createSpy('.removeEventListener'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); addEventListenerSpy = jasmine.createSpy('addEventListener'); scrollContainer = { @@ -353,7 +353,7 @@ describe('DOMEventPlugin handle other event', () => { removeEventListener: () => {}, }, }), - triggerPluginEvent, + triggerEvent, getEnvironment: () => ({}), attachDomEvent: (map: Record) => { eventMap = map; @@ -377,7 +377,7 @@ describe('DOMEventPlugin handle other event', () => { mouseUpEventListerAdded: false, }); - expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); const mockedEvent = 'EVENT' as any; eventMap.compositionend.beforeDispatch(mockedEvent); @@ -388,7 +388,7 @@ describe('DOMEventPlugin handle other event', () => { mouseDownY: null, mouseUpEventListerAdded: false, }); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.CompositionEnd, { + expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.CompositionEnd, { rawEvent: mockedEvent, }); expect(addEventListenerSpy).toHaveBeenCalledTimes(2); @@ -413,7 +413,7 @@ describe('DOMEventPlugin handle other event', () => { mouseUpEventListerAdded: false, }); - expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); expect(preventDefaultSpy).not.toHaveBeenCalled(); }); @@ -436,7 +436,7 @@ describe('DOMEventPlugin handle other event', () => { mouseUpEventListerAdded: false, }); - expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalled(); }); @@ -453,7 +453,7 @@ describe('DOMEventPlugin handle other event', () => { mouseUpEventListerAdded: false, }); expect(takeSnapshotSpy).toHaveBeenCalledWith(); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { + expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { source: ChangeSource.Drop, }); }); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts index 85b44e03d18..0ad21d48c0f 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts @@ -22,7 +22,7 @@ describe('EntityPlugin', () => { beforeEach(() => { createContentModelSpy = jasmine.createSpy('createContentModel'); - triggerPluginEventSpy = jasmine.createSpy('triggerPluginEvent'); + triggerPluginEventSpy = jasmine.createSpy('triggerEvent'); isDarkModeSpy = jasmine.createSpy('isDarkMode'); isNodeInEditorSpy = jasmine.createSpy('isNodeInEditor'); transformColorSpy = spyOn(transformColor, 'transformColor'); @@ -30,7 +30,7 @@ describe('EntityPlugin', () => { editor = { createContentModel: createContentModelSpy, - triggerPluginEvent: triggerPluginEventSpy, + triggerEvent: triggerPluginEventSpy, isDarkMode: isDarkModeSpy, isNodeInEditor: isNodeInEditorSpy, getDarkColorHandler: () => mockedDarkColorHandler, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts index c3afc6b6d80..887cf44db53 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts @@ -6,12 +6,12 @@ describe('LifecyclePlugin', () => { it('init', () => { const div = document.createElement('div'); const plugin = createLifecyclePlugin({}, div); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + const triggerEvent = jasmine.createSpy('triggerEvent'); const state = plugin.getState(); const setContentModelSpy = jasmine.createSpy('setContentModel'); plugin.initialize(({ - triggerPluginEvent, + triggerEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, getDarkColorHandler: () => null, @@ -28,8 +28,8 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeTrue(); expect(div.style.userSelect).toBe('text'); expect(div.innerHTML).toBe(''); - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); expect(setContentModelSpy).toHaveBeenCalledTimes(1); expect(setContentModelSpy).toHaveBeenCalledWith( { @@ -64,12 +64,12 @@ describe('LifecyclePlugin', () => { }, div ); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + const triggerEvent = jasmine.createSpy('triggerEvent'); const state = plugin.getState(); const setContentModelSpy = jasmine.createSpy('setContentModel'); plugin.initialize(({ - triggerPluginEvent, + triggerEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, getDarkColorHandler: () => null, @@ -88,8 +88,8 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeTrue(); expect(div.style.userSelect).toBe('text'); - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); plugin.dispose(); expect(div.isContentEditable).toBeFalse(); @@ -99,11 +99,11 @@ describe('LifecyclePlugin', () => { const div = document.createElement('div'); div.contentEditable = 'true'; const plugin = createLifecyclePlugin({}, div); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + const triggerEvent = jasmine.createSpy('triggerEvent'); const setContentModelSpy = jasmine.createSpy('setContentModel'); plugin.initialize(({ - triggerPluginEvent, + triggerEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, getDarkColorHandler: () => null, @@ -113,8 +113,8 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeTrue(); expect(div.style.userSelect).toBe(''); - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); expect(setContentModelSpy).toHaveBeenCalledTimes(1); expect(setContentModelSpy).toHaveBeenCalledWith( @@ -142,11 +142,11 @@ describe('LifecyclePlugin', () => { const div = document.createElement('div'); div.contentEditable = 'false'; const plugin = createLifecyclePlugin({}, div); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + const triggerEvent = jasmine.createSpy('triggerEvent'); const setContentModelSpy = jasmine.createSpy('setContentModel'); plugin.initialize(({ - triggerPluginEvent, + triggerEvent, setContent: (content: string) => (div.innerHTML = content), getFocusedPosition: () => null, getDarkColorHandler: () => null, @@ -173,8 +173,8 @@ describe('LifecyclePlugin', () => { ); expect(div.isContentEditable).toBeFalse(); expect(div.style.userSelect).toBe(''); - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); plugin.dispose(); expect(div.isContentEditable).toBeFalse(); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts index 8c184a3e202..475638c6851 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts @@ -92,7 +92,7 @@ describe('SelectionPlugin', () => { describe('SelectionPlugin handle onFocus and onBlur event', () => { let plugin: PluginWithState; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; let eventMap: Record; let getElementAtCursorSpy: jasmine.Spy; let createElementSpy: jasmine.Spy; @@ -104,7 +104,7 @@ describe('SelectionPlugin handle onFocus and onBlur event', () => { let editor: IStandaloneEditor; beforeEach(() => { - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); getElementAtCursorSpy = jasmine.createSpy('getElementAtCursor'); createElementSpy = jasmine.createSpy('createElement').and.returnValue(MockedStyleNode); appendChildSpy = jasmine.createSpy('appendChild'); @@ -122,7 +122,7 @@ describe('SelectionPlugin handle onFocus and onBlur event', () => { editor = ({ getDocument: getDocumentSpy, - triggerPluginEvent, + triggerEvent, getEnvironment: () => ({}), attachDomEvent: (map: Record) => { eventMap = map; diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts index 9c7fd7a93e9..569bdf7cb97 100644 --- a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts @@ -418,7 +418,7 @@ describe('StandaloneEditor', () => { expect(() => editor.hasFocus()).toThrow(); }); - it('triggerPluginEvent', () => { + it('triggerEvent', () => { const div = document.createElement('div'); const mockedEventData = { event: 'Mocked', @@ -441,7 +441,7 @@ describe('StandaloneEditor', () => { const editor = new StandaloneEditor(div); const mockedEventType = 'EVENTTYPE' as any; - const result = editor.triggerPluginEvent(mockedEventType, mockedEventData, true); + const result = editor.triggerEvent(mockedEventType, mockedEventData, true); expect(result).toEqual({ eventType: mockedEventType, @@ -460,7 +460,7 @@ describe('StandaloneEditor', () => { editor.dispose(); - expect(() => editor.triggerPluginEvent(mockedEventType, mockedEventData, true)).toThrow(); + expect(() => editor.triggerEvent(mockedEventType, mockedEventData, true)).toThrow(); }); it('attachDomEvent', () => { diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts index 1b0c8eb33ff..f777338e0fe 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts @@ -32,7 +32,7 @@ describe('generatePasteOptionFromPlugins', () => { }; beforeEach(() => { - triggerPluginEventSpy = jasmine.createSpy('triggerPluginEvent'); + triggerPluginEventSpy = jasmine.createSpy('triggerEvent'); core = { api: { triggerEvent: triggerPluginEventSpy, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index a43d84d7f4d..b41295b512c 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -36,6 +36,8 @@ import type { NodePosition, PendableFormatState, PluginEvent, + PluginEventData, + PluginEventFromType, PositionType, Rect, Region, @@ -56,6 +58,7 @@ import type { CompatibleContentPosition, CompatibleExperimentalFeatures, CompatibleGetContentMode, + CompatiblePluginEventType, CompatibleQueryScope, CompatibleRegionType, } from 'roosterjs-editor-types/lib/compatibleTypes'; @@ -290,7 +293,7 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode const core = this.getContentModelEditorCore(); const innerCore = this.getCore(); - return core.api.getContent(core, innerCore, mode); + return core.api.getContent(core, innerCore, mode as GetContentMode); } /** @@ -535,6 +538,27 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode return this.attachDomEvent(eventsMapResult); } + /** + * Trigger an event to be dispatched to all plugins + * @param eventType Type of the event + * @param data data of the event with given type, this is the rest part of PluginEvent with the given type + * @param broadcast indicates if the event needs to be dispatched to all plugins + * True means to all, false means to allow exclusive handling from one plugin unless no one wants that + * @returns the event object which is really passed into plugins. Some plugin may modify the event object so + * the result of this function provides a chance to read the modified result + */ + public triggerPluginEvent( + eventType: T, + data: PluginEventData, + broadcast: boolean = false + ): PluginEventFromType { + return this.triggerEvent( + eventType as PluginEventType, + data, + broadcast + ) as PluginEventFromType; + } + /** * Trigger a ContentChangedEvent * @param source Source of this event, by default is 'SetContent' @@ -945,7 +969,11 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode * @param feature The feature to check */ isFeatureEnabled(feature: ExperimentalFeatures | CompatibleExperimentalFeatures): boolean { - return this.getContentModelEditorCore().experimentalFeatures.indexOf(feature) >= 0; + return ( + this.getContentModelEditorCore().experimentalFeatures.indexOf( + feature as ExperimentalFeatures + ) >= 0 + ); } /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts index 8c88702cd75..7985fe622a2 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelEditorCore.ts @@ -1,9 +1,5 @@ import type { ContentModelCorePluginState } from './ContentModelCorePlugins'; import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; -import type { - CompatibleGetContentMode, - CompatibleExperimentalFeatures, -} from 'roosterjs-editor-types/lib/compatibleTypes'; import type { CustomData, ExperimentalFeatures, @@ -41,7 +37,7 @@ export type SetContent = ( export type GetContent = ( core: ContentModelEditorCore, innerCore: StandaloneEditorCore, - mode: GetContentMode | CompatibleGetContentMode + mode: GetContentMode ) => string; /** @@ -157,7 +153,7 @@ export interface ContentModelEditorCore extends ContentModelCorePluginState { /** * Enabled experimental features */ - readonly experimentalFeatures: (ExperimentalFeatures | CompatibleExperimentalFeatures)[]; + readonly experimentalFeatures: ExperimentalFeatures[]; /** * @deprecated Use zoomScale instead diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts index 435710594b5..05953f038b6 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -3,7 +3,6 @@ import type { ClipboardData } from '../parameter/ClipboardData'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { SnapshotsManager } from '../parameter/SnapshotsManager'; import type { Snapshot } from '../parameter/Snapshot'; -import type { CompatiblePluginEventType } from 'roosterjs-editor-types/lib/compatibleTypes'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { DOMSelection } from '../selection/DOMSelection'; @@ -113,7 +112,7 @@ export interface IStandaloneEditor { * @returns the event object which is really passed into plugins. Some plugin may modify the event object so * the result of this function provides a chance to read the modified result */ - triggerPluginEvent( + triggerEvent( eventType: T, data: PluginEventData, broadcast?: boolean From ff91d1de0e2eb4d8b90eec50d919a00a3082904b Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 11 Jan 2024 08:20:50 -0600 Subject: [PATCH 44/64] Remove Height from Divs on paste #2320 --- ...Parser.ts => containerSizeFormatParser.ts} | 3 +- .../lib/utils/paste/mergePasteContent.ts | 4 +- .../containerSizeFormatParserTest.ts | 61 +++++++++++++++++++ .../containerWidthFormatParserTest.ts | 37 ----------- .../test/utils/paste/mergePasteContentTest.ts | 4 +- 5 files changed, 67 insertions(+), 42 deletions(-) rename packages-content-model/roosterjs-content-model-core/lib/override/{containerWidthFormatParser.ts => containerSizeFormatParser.ts} (76%) create mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts b/packages-content-model/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts similarity index 76% rename from packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts rename to packages-content-model/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts index 6ea362ffbc1..f83c10288f1 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/override/containerWidthFormatParser.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/override/containerSizeFormatParser.ts @@ -3,9 +3,10 @@ import type { FormatParser, SizeFormat } from 'roosterjs-content-model-types'; /** * @internal Do not paste width for Format Containers since it may be generated by browser according to temp div width */ -export const containerWidthFormatParser: FormatParser = (format, element) => { +export const containerSizeFormatParser: FormatParser = (format, element) => { // For pasted content, there may be existing width generated by browser from the temp DIV. So we need to remove it. if (element.tagName == 'DIV') { delete format.width; + delete format.height; } }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts index f63843cbbe6..d46cb78db16 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts @@ -1,4 +1,4 @@ -import { containerWidthFormatParser } from '../../override/containerWidthFormatParser'; +import { containerSizeFormatParser } from '../../override/containerSizeFormatParser'; import { createDomToModelContext, domToContentModel } from 'roosterjs-content-model-dom'; import { createPasteEntityProcessor } from '../../override/pasteEntityProcessor'; import { createPasteGeneralProcessor } from '../../override/pasteGeneralProcessor'; @@ -55,7 +55,7 @@ export function mergePasteContent( display: pasteDisplayFormatParser, }, additionalFormatParsers: { - container: [containerWidthFormatParser], + container: [containerSizeFormatParser], }, }, domToModelOption diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts new file mode 100644 index 00000000000..d8a98670fb2 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/containerSizeFormatParserTest.ts @@ -0,0 +1,61 @@ +import { containerSizeFormatParser } from '../../lib/override/containerSizeFormatParser'; +import { SizeFormat } from 'roosterjs-content-model-types'; + +describe('containerSizeFormatParser', () => { + it('DIV without width', () => { + const div = document.createElement('div'); + const format: SizeFormat = {}; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('DIV with width', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + width: '10px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('SPAN with width & height', () => { + const div = document.createElement('span'); + const format: SizeFormat = { + width: '10px', + height: '10px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({ + height: '10px', + width: '10px', + }); + }); + + it('DIV with height', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + height: '10px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); + + it('SPAN with height', () => { + const div = document.createElement('div'); + const format: SizeFormat = { + height: '10px', + }; + + containerSizeFormatParser(format, div, null!, {}); + + expect(format).toEqual({}); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts deleted file mode 100644 index 14e5e65c9d4..00000000000 --- a/packages-content-model/roosterjs-content-model-core/test/overrides/containerWidthFormatParserTest.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { containerWidthFormatParser } from '../../lib/override/containerWidthFormatParser'; -import { SizeFormat } from 'roosterjs-content-model-types'; - -describe('containerWidthFormatParser', () => { - it('DIV without width', () => { - const div = document.createElement('div'); - const format: SizeFormat = {}; - - containerWidthFormatParser(format, div, null!, {}); - - expect(format).toEqual({}); - }); - - it('DIV with width', () => { - const div = document.createElement('div'); - const format: SizeFormat = { - width: '10px', - }; - - containerWidthFormatParser(format, div, null!, {}); - - expect(format).toEqual({}); - }); - - it('SPAN with width', () => { - const div = document.createElement('span'); - const format: SizeFormat = { - width: '10px', - }; - - containerWidthFormatParser(format, div, null!, {}); - - expect(format).toEqual({ - width: '10px', - }); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts index bb4e68be828..9a97fd4efda 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts @@ -3,7 +3,7 @@ import * as createPasteEntityProcessor from '../../../lib/override/pasteEntityPr import * as createPasteGeneralProcessor from '../../../lib/override/pasteGeneralProcessor'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as mergeModelFile from '../../../lib/publicApi/model/mergeModel'; -import { containerWidthFormatParser } from '../../../lib/override/containerWidthFormatParser'; +import { containerSizeFormatParser } from '../../../lib/override/containerSizeFormatParser'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; import { mergePasteContent } from '../../../lib/utils/paste/mergePasteContent'; import { pasteDisplayFormatParser } from '../../../lib/override/pasteDisplayFormatParser'; @@ -393,7 +393,7 @@ describe('mergePasteContent', () => { display: pasteDisplayFormatParser, }, additionalFormatParsers: { - container: [containerWidthFormatParser], + container: [containerSizeFormatParser], }, }, mockedDefaultDomToModelOptions From 8da9c610c44c4da0b1bb48bdd7391e51bcb0911c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 11 Jan 2024 14:00:53 -0300 Subject: [PATCH 45/64] list with table --- .../lib/edit/deleteSteps/deleteList.ts | 4 +- .../test/edit/deleteSteps/deleteListTest.ts | 152 +++++++++++++++++- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts index ebce900c6b8..5b3d2643bf2 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteList.ts @@ -8,7 +8,7 @@ export const deleteList: DeleteSelectionStep = context => { const { paragraph, marker, path } = context.insertPoint; if (context.deleteResult == 'nothingToDelete' || context.deleteResult == 'notDeleted') { - const index = getClosestAncestorBlockGroupIndex(path, ['ListItem']); + const index = getClosestAncestorBlockGroupIndex(path, ['ListItem', 'TableCell']); const item = path[index]; if ( index >= 0 && @@ -18,7 +18,7 @@ export const deleteList: DeleteSelectionStep = context => { (paragraph.segments.length == 2 && paragraph.segments[1].segmentType == 'Br')) ) { item.levels = []; - context.deleteResult = 'singleChar'; + context.deleteResult = 'range'; } } }; diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts index ee12ae7f795..f5b003a3ea9 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts @@ -49,7 +49,7 @@ describe('deleteList', () => { }; const result = deleteSelection(model, [deleteList]); normalizeContentModel(model); - expect(result.deleteResult).toEqual('singleChar'); + expect(result.deleteResult).toEqual('range'); expect(model).toEqual({ blockGroupType: 'Document', @@ -173,4 +173,154 @@ describe('deleteList', () => { }); expect(result.deleteResult).toEqual('notDeleted'); }); + + it('do not delete list with table', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + borderCollapse: true, + useBorderBox: true, + }, + widths: [120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }; + const result = deleteSelection(model, [deleteList]); + normalizeContentModel(model); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + format: { + listStyleType: '"1. "', + }, + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + segmentFormat: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + borderCollapse: true, + useBorderBox: true, + }, + widths: [120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + ], + levels: [ + { + listType: 'OL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + }, + ], + }); + expect(result.deleteResult).toEqual('notDeleted'); + }); }); From b3bda5ce32443c1f87343912df9b0a51b00c7a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 11 Jan 2024 14:11:32 -0300 Subject: [PATCH 46/64] add test --- .../test/edit/deleteSteps/deleteListTest.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts index f5b003a3ea9..8d7cd3c356e 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteListTest.ts @@ -323,4 +323,137 @@ describe('deleteList', () => { }); expect(result.deleteResult).toEqual('notDeleted'); }); + + it('delete list inside with table cell', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: false, + }, + ], + levels: [ + { + listType: 'UL', + format: { + marginBlockStart: '0px', + marginBlockEnd: '0px', + listStyleType: 'disc', + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + useBorderBox: true, + borderCollapse: true, + }, + widths: [120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + ], + format: {}, + }; + const result = deleteSelection(model, [deleteList]); + normalizeContentModel(model); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: false, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + useBorderBox: true, + borderCollapse: true, + }, + widths: [120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + ], + format: {}, + }); + expect(result.deleteResult).toEqual('range'); + }); }); From 758e22f5796bc25b9c5ea91694f5b173a0357f67 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 11 Jan 2024 09:38:20 -0800 Subject: [PATCH 47/64] Standalone Editor: Create new event types (#2297) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * Standalone Editor: Remove compatible enums from standalone editor * improve * Standalone Editor: Create new event types * Port to new event system * Revert "Port to new event system" This reverts commit 60cf041b3c3334df8a1781e22b2e81adc0775662. * fix demo * Fix buttons * fix build * fix build * fix build --- .../lib/editor/createStandaloneEditorCore.ts | 4 +- .../lib/editor/utils/eventConverter.ts | 518 +++++++ .../test/editor/utils/eventConverterTest.ts | 1228 +++++++++++++++++ .../lib/editor/ContextMenuProvider.ts | 13 + .../lib/editor/StandaloneEditorCore.ts | 4 +- .../lib/event/BasePluginEvent.ts | 31 + .../lib/event/BeforeCutCopyEvent.ts | 21 + .../lib/event/BeforeDisposeEvent.ts | 6 + .../lib/event/BeforeKeyboardEditingEvent.ts | 7 + .../lib/event/BeforePasteEvent.ts | 88 ++ .../lib/event/BeforeSetContentEvent.ts | 12 + .../lib/event/ContentChangedEvent.ts | 72 + .../lib/event/ContentModelBeforePasteEvent.ts | 41 +- .../event/ContentModelContentChangedEvent.ts | 23 +- .../lib/event/ContextMenuEvent.ts | 13 + .../lib/event/EditImageEvent.ts | 29 + .../lib/event/EditorInputEvent.ts | 11 + .../lib/event/EditorReadyEvent.ts | 6 + .../lib/event/EntityOperationEvent.ts | 58 + .../lib/event/ExtractContentWithDomEvent.ts | 15 + .../lib/event/KeyboardEvent.ts | 27 + .../lib/event/MouseEvent.ts | 16 + .../lib/event/PluginEvent.ts | 46 + .../lib/event/PluginEventData.ts | 34 + .../lib/event/PluginEventType.ts | 125 ++ .../lib/event/ScrollEvent.ts | 11 + .../lib/event/SelectionChangedEvent.ts | 12 + .../lib/event/ShadowEditEvent.ts | 11 + .../lib/event/ZoomChangedEvent.ts | 18 + .../lib/index.ts | 50 +- .../lib/parameter/AnnounceData.ts | 44 + .../lib/pluginState/PluginState.ts | 38 + .../StandaloneEditorPluginState.ts | 54 - 33 files changed, 2562 insertions(+), 124 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts create mode 100644 packages-content-model/roosterjs-content-model-editor/test/editor/utils/eventConverterTest.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/editor/ContextMenuProvider.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/BasePluginEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/BeforeCutCopyEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/BeforeDisposeEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/BeforeKeyboardEditingEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/BeforeSetContentEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ContextMenuEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/EditImageEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/EditorInputEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/EditorReadyEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/EntityOperationEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ExtractContentWithDomEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/KeyboardEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/MouseEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/PluginEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/PluginEventData.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/PluginEventType.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ScrollEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ShadowEditEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ZoomChangedEvent.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/AnnounceData.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/pluginState/PluginState.ts delete mode 100644 packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts index 5161773dca2..d2aa0a97f2d 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createStandaloneEditorCore.ts @@ -7,8 +7,8 @@ import { } from './createStandaloneEditorDefaultSettings'; import type { EditorEnvironment, + PluginState, StandaloneEditorCore, - StandaloneEditorCorePluginState, StandaloneEditorCorePlugins, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; @@ -76,7 +76,7 @@ export function defaultTrustHtmlHandler(html: string) { return html; } -function getPluginState(corePlugins: StandaloneEditorCorePlugins): StandaloneEditorCorePluginState { +function getPluginState(corePlugins: StandaloneEditorCorePlugins): PluginState { return { domEvent: corePlugins.domEvent.getState(), copyPaste: corePlugins.copyPaste.getState(), diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts new file mode 100644 index 00000000000..8ab6725c26f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts @@ -0,0 +1,518 @@ +import { convertDomSelectionToRangeEx, convertRangeExToDomSelection } from './selectionConverter'; +import { createDefaultHtmlSanitizerOptions } from 'roosterjs-editor-dom'; +import { + KnownAnnounceStrings as OldKnownAnnounceStrings, + PasteType as OldPasteType, + EntityOperation as OldEntityOperation, + PluginEventType, +} from 'roosterjs-editor-types'; +import type { + PluginEvent as OldEvent, + AnnounceData as OldAnnounceData, +} from 'roosterjs-editor-types'; +import type { + PluginEvent as NewEvent, + PasteType as NewPasteType, + AnnounceData as NewAnnounceData, + KnownAnnounceStrings as NewKnownAnnounceStrings, + EntityOperation as NewEntityOperation, +} from 'roosterjs-content-model-types'; + +const PasteTypeNewToOld: Record = { + asImage: OldPasteType.AsImage, + asPlainText: OldPasteType.AsPlainText, + mergeFormat: OldPasteType.MergeFormat, + normal: OldPasteType.Normal, +}; + +const PasteTypeOldToNew: Record = { + [OldPasteType.AsImage]: 'asImage', + [OldPasteType.AsPlainText]: 'asPlainText', + [OldPasteType.MergeFormat]: 'mergeFormat', + [OldPasteType.Normal]: 'normal', +}; + +const KnownAnnounceStringsOldToNew: Record = { + [OldKnownAnnounceStrings.AnnounceListItemBullet]: 'announceListItemBullet', + [OldKnownAnnounceStrings.AnnounceListItemNumbering]: 'announceListItemNumbering', + [OldKnownAnnounceStrings.AnnounceOnFocusLastCell]: 'announceOnFocusLastCell', +}; + +const KnownAnnounceStringsNewToOld: Record = { + announceListItemBullet: OldKnownAnnounceStrings.AnnounceListItemBullet, + announceListItemNumbering: OldKnownAnnounceStrings.AnnounceListItemNumbering, + announceOnFocusLastCell: OldKnownAnnounceStrings.AnnounceOnFocusLastCell, +}; + +const EntityOperationOldToNew: Record = { + [OldEntityOperation.NewEntity]: 'newEntity', + [OldEntityOperation.Overwrite]: 'overwrite', + [OldEntityOperation.RemoveFromEnd]: 'removeFromEnd', + [OldEntityOperation.RemoveFromStart]: 'removeFromStart', + [OldEntityOperation.ReplaceTemporaryContent]: 'replaceTemporaryContent', + [OldEntityOperation.UpdateEntityState]: 'updateEntityState', + [OldEntityOperation.Click]: 'click', + [OldEntityOperation.ContextMenu]: undefined, + [OldEntityOperation.Escape]: undefined, + [OldEntityOperation.PartialOverwrite]: undefined, + [OldEntityOperation.AddShadowRoot]: undefined, + [OldEntityOperation.RemoveShadowRoot]: undefined, +}; + +const EntityOperationNewToOld: Record = { + newEntity: OldEntityOperation.NewEntity, + overwrite: OldEntityOperation.Overwrite, + removeFromEnd: OldEntityOperation.RemoveFromEnd, + removeFromStart: OldEntityOperation.RemoveFromStart, + replaceTemporaryContent: OldEntityOperation.ReplaceTemporaryContent, + updateEntityState: OldEntityOperation.UpdateEntityState, + click: OldEntityOperation.Click, +}; + +/** + * @internal Convert legacy event object to new event object + */ +export function oldEventToNewEvent( + input: TOldEvent, + refEvent?: NewEvent +): NewEvent | undefined { + switch (input.eventType) { + case PluginEventType.BeforeCutCopy: + return { + eventType: 'beforeCutCopy', + clonedRoot: input.clonedRoot, + eventDataCache: input.eventDataCache, + isCut: input.isCut, + range: input.range, + rawEvent: input.rawEvent, + }; + + case PluginEventType.BeforeDispose: + return { + eventType: 'beforeDispose', + eventDataCache: input.eventDataCache, + }; + + case PluginEventType.BeforeKeyboardEditing: + return { + eventType: 'beforeKeyboardEditing', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case PluginEventType.BeforePaste: + const refBeforePasteEvent = refEvent?.eventType == 'beforePaste' ? refEvent : undefined; + + return { + eventType: 'beforePaste', + clipboardData: input.clipboardData, + customizedMerge: refBeforePasteEvent?.customizedMerge, + domToModelOption: refBeforePasteEvent?.domToModelOption ?? { + additionalAllowedTags: [], + additionalDisallowedTags: [], + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, + }, + eventDataCache: input.eventDataCache, + fragment: input.fragment, + htmlAfter: input.htmlAfter, + htmlAttributes: input.htmlAttributes, + htmlBefore: input.htmlBefore, + pasteType: PasteTypeOldToNew[input.pasteType], + }; + + case PluginEventType.BeforeSetContent: + return { + eventType: 'beforeSetContent', + eventDataCache: input.eventDataCache, + newContent: input.newContent, + }; + + case PluginEventType.CompositionEnd: + return { + eventType: 'compositionEnd', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case PluginEventType.ContentChanged: + const refContentChangedEvent = + refEvent?.eventType == 'contentChanged' ? refEvent : undefined; + return { + eventType: 'contentChanged', + eventDataCache: input.eventDataCache, + changedEntities: refContentChangedEvent?.changedEntities, + contentModel: refContentChangedEvent?.contentModel, + data: input.data, + entityStates: + input.additionalData?.getEntityState?.() ?? + refContentChangedEvent?.entityStates, + selection: refContentChangedEvent?.selection, + source: input.source, + formatApiName: + input.additionalData?.formatApiName ?? refContentChangedEvent?.formatApiName, + announceData: + announceDataOldToNew(input.additionalData?.getAnnounceData?.()) ?? + refContentChangedEvent?.announceData, + }; + + case PluginEventType.ContextMenu: + return { + eventType: 'contextMenu', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + items: input.items, + }; + + case PluginEventType.EditImage: + return { + eventType: 'editImage', + eventDataCache: input.eventDataCache, + image: input.image, + newSrc: input.newSrc, + originalSrc: input.originalSrc, + previousSrc: input.previousSrc, + }; + + case PluginEventType.EditorReady: + return { + eventType: 'editorReady', + eventDataCache: input.eventDataCache, + }; + + case PluginEventType.EnteredShadowEdit: + return { + eventType: 'enteredShadowEdit', + eventDataCache: input.eventDataCache, + }; + + case PluginEventType.EntityOperation: + const operation = EntityOperationOldToNew[input.operation]; + + return operation === undefined + ? undefined + : { + eventType: 'entityOperation', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + entity: input.entity, + operation: operation, + shouldPersist: input.shouldPersist, + state: input.state, + }; + + case PluginEventType.ExtractContentWithDom: + return { + eventType: 'extractContentWithDom', + eventDataCache: input.eventDataCache, + clonedRoot: input.clonedRoot, + }; + + case PluginEventType.Input: + return { + eventType: 'input', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case PluginEventType.KeyDown: + case PluginEventType.KeyPress: + case PluginEventType.KeyUp: + return { + eventType: + input.eventType == PluginEventType.KeyDown + ? 'keyDown' + : input.eventType == PluginEventType.KeyPress + ? 'keyPress' + : 'keyUp', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case PluginEventType.LeavingShadowEdit: + return { + eventType: 'leavingShadowEdit', + eventDataCache: input.eventDataCache, + }; + + case PluginEventType.MouseDown: + return { + eventType: 'mouseDown', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case PluginEventType.MouseUp: + return { + eventType: 'mouseUp', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + isClicking: input.isClicking, + }; + + case PluginEventType.Scroll: + return { + eventType: 'scroll', + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + scrollContainer: input.scrollContainer, + }; + + case PluginEventType.SelectionChanged: + const refSelectionChangedEvent = + refEvent?.eventType == 'selectionChanged' ? refEvent : undefined; + + return { + eventType: 'selectionChanged', + eventDataCache: input.eventDataCache, + newSelection: + refSelectionChangedEvent?.newSelection ?? + convertRangeExToDomSelection(input.selectionRangeEx), + }; + + case PluginEventType.ZoomChanged: + return { + eventType: 'zoomChanged', + eventDataCache: input.eventDataCache, + newZoomScale: input.newZoomScale, + oldZoomScale: input.oldZoomScale, + }; + + default: + return undefined; + } +} + +/** + * @internal Convert new event object to legacy event object + */ +export function newEventToOldEvent(input: NewEvent, refEvent?: OldEvent): OldEvent | undefined { + switch (input.eventType) { + case 'beforeCutCopy': + return { + eventType: PluginEventType.BeforeCutCopy, + clonedRoot: input.clonedRoot, + eventDataCache: input.eventDataCache, + isCut: input.isCut, + range: input.range, + rawEvent: input.rawEvent, + }; + + case 'beforeDispose': + return { + eventType: PluginEventType.BeforeDispose, + eventDataCache: input.eventDataCache, + }; + + case 'beforeKeyboardEditing': + return { + eventType: PluginEventType.BeforeKeyboardEditing, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case 'beforePaste': + const refBeforePasteEvent = + refEvent?.eventType == PluginEventType.BeforePaste ? refEvent : undefined; + + return { + eventType: PluginEventType.BeforePaste, + clipboardData: input.clipboardData, + eventDataCache: input.eventDataCache, + fragment: input.fragment, + htmlAfter: input.htmlAfter, + htmlAttributes: input.htmlAttributes, + htmlBefore: input.htmlBefore, + pasteType: PasteTypeNewToOld[input.pasteType], + sanitizingOption: + refBeforePasteEvent?.sanitizingOption ?? createDefaultHtmlSanitizerOptions(), + }; + + case 'beforeSetContent': + return { + eventType: PluginEventType.BeforeSetContent, + eventDataCache: input.eventDataCache, + newContent: input.newContent, + }; + + case 'compositionEnd': + return { + eventType: PluginEventType.CompositionEnd, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case 'contentChanged': + const entityStates = input.entityStates; + + return { + eventType: PluginEventType.ContentChanged, + eventDataCache: input.eventDataCache, + data: input.data, + source: input.source, + additionalData: { + formatApiName: input.formatApiName, + getAnnounceData: input.announceData + ? () => announceDataNewToOld(input.announceData) + : undefined, + getEntityState: entityStates ? () => entityStates : undefined, + }, + }; + + case 'contextMenu': + return { + eventType: PluginEventType.ContextMenu, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + items: input.items, + }; + + case 'editImage': + return { + eventType: PluginEventType.EditImage, + eventDataCache: input.eventDataCache, + image: input.image, + newSrc: input.newSrc, + originalSrc: input.originalSrc, + previousSrc: input.previousSrc, + }; + + case 'editorReady': + return { + eventType: PluginEventType.EditorReady, + eventDataCache: input.eventDataCache, + }; + + case 'enteredShadowEdit': + const refEnteredShadowEditEvent = + refEvent?.eventType == PluginEventType.EnteredShadowEdit ? refEvent : undefined; + + return { + eventType: PluginEventType.EnteredShadowEdit, + eventDataCache: input.eventDataCache, + fragment: refEnteredShadowEditEvent?.fragment ?? document.createDocumentFragment(), + selectionPath: refEnteredShadowEditEvent?.selectionPath ?? { + end: [], + start: [], + }, + }; + + case 'entityOperation': + return { + eventType: PluginEventType.EntityOperation, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + entity: input.entity, + operation: EntityOperationNewToOld[input.operation], + shouldPersist: input.shouldPersist, + state: input.state, + }; + + case 'extractContentWithDom': + return { + eventType: PluginEventType.ExtractContentWithDom, + eventDataCache: input.eventDataCache, + clonedRoot: input.clonedRoot, + }; + + case 'input': + return { + eventType: PluginEventType.Input, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case 'keyDown': + case 'keyPress': + case 'keyUp': + return { + eventType: + input.eventType == 'keyDown' + ? PluginEventType.KeyDown + : input.eventType == 'keyPress' + ? PluginEventType.KeyPress + : PluginEventType.KeyUp, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case 'leavingShadowEdit': + return { + eventType: PluginEventType.LeavingShadowEdit, + eventDataCache: input.eventDataCache, + }; + + case 'mouseDown': + return { + eventType: PluginEventType.MouseDown, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + }; + + case 'mouseUp': + return { + eventType: PluginEventType.MouseUp, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + isClicking: input.isClicking, + }; + + case 'scroll': + return { + eventType: PluginEventType.Scroll, + eventDataCache: input.eventDataCache, + rawEvent: input.rawEvent, + scrollContainer: input.scrollContainer, + }; + + case 'selectionChanged': + const refSelectionChangedEvent = + refEvent?.eventType == PluginEventType.SelectionChanged ? refEvent : undefined; + + return { + eventType: PluginEventType.SelectionChanged, + eventDataCache: input.eventDataCache, + selectionRangeEx: + refSelectionChangedEvent?.selectionRangeEx ?? + convertDomSelectionToRangeEx(input.newSelection), + }; + + case 'zoomChanged': + return { + eventType: PluginEventType.ZoomChanged, + eventDataCache: input.eventDataCache, + newZoomScale: input.newZoomScale, + oldZoomScale: input.oldZoomScale, + }; + + default: + return undefined; + } +} + +function announceDataOldToNew(data: OldAnnounceData | undefined): NewAnnounceData | undefined { + return data + ? { + defaultStrings: data.defaultStrings + ? KnownAnnounceStringsOldToNew[data.defaultStrings] + : undefined, + formatStrings: data.formatStrings, + text: data.text, + } + : undefined; +} + +function announceDataNewToOld(data: NewAnnounceData | undefined): OldAnnounceData | undefined { + return data + ? { + defaultStrings: data.defaultStrings + ? KnownAnnounceStringsNewToOld[data.defaultStrings] + : undefined, + formatStrings: data.formatStrings, + text: data.text, + } + : undefined; +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/eventConverterTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/utils/eventConverterTest.ts new file mode 100644 index 00000000000..fe0752ef384 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/utils/eventConverterTest.ts @@ -0,0 +1,1228 @@ +import * as selectionConvert from '../../../lib/editor/utils/selectionConverter'; +import { newEventToOldEvent, oldEventToNewEvent } from '../../../lib/editor/utils/eventConverter'; +import { + EntityOperation, + KnownAnnounceStrings, + PasteType, + PluginEventType, +} from 'roosterjs-editor-types'; +import type { ContentChangedEvent, PluginEvent as OldEvent } from 'roosterjs-editor-types'; +import type { PluginEvent as NewEvent } from 'roosterjs-content-model-types'; + +describe('oldEventToNewEvent', () => { + function runTest( + oldEvent: OldEvent, + refEvent: NewEvent | undefined, + expectedResult: NewEvent | undefined + ) { + const result = oldEventToNewEvent(oldEvent, refEvent); + + expect(result).toEqual(expectedResult); + } + + const mockedDataCache = 'CACHE' as any; + const mockedRawEvent = 'EVENT' as any; + + it('BeforeCutCopy', () => { + const mockedRoot = 'ROOT' as any; + const mockedIsCut = 'CUT' as any; + const mockedRange = 'RANGE' as any; + + runTest( + { + eventType: PluginEventType.BeforeCutCopy, + clonedRoot: mockedRoot, + eventDataCache: mockedDataCache, + isCut: mockedIsCut, + range: mockedRange, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'beforeCutCopy', + clonedRoot: mockedRoot, + eventDataCache: mockedDataCache, + isCut: mockedIsCut, + range: mockedRange, + rawEvent: mockedRawEvent, + } + ); + }); + + it('BeforeDispose', () => { + runTest( + { + eventType: PluginEventType.BeforeDispose, + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: 'beforeDispose', + eventDataCache: mockedDataCache, + } + ); + }); + + it('BeforeKeyboardEditing', () => { + runTest( + { + eventType: PluginEventType.BeforeKeyboardEditing, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'beforeKeyboardEditing', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('BeforePaste without ref', () => { + const mockedClipboardData = 'CLIPBOARDDATA' as any; + const mockedFragment = 'FRAGMENT' as any; + const mockedHtmlBefore = 'BEFORE' as any; + const mockedHtmlAfter = 'AFTER' as any; + const mockedHTmlAttributes = 'ATTRIBUTES' as any; + const mockedSanitizeOption = 'OPTION' as any; + + runTest( + { + eventType: PluginEventType.BeforePaste, + eventDataCache: mockedDataCache, + clipboardData: mockedClipboardData, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: PasteType.AsImage, + sanitizingOption: mockedSanitizeOption, + }, + undefined, + { + eventType: 'beforePaste', + clipboardData: mockedClipboardData, + customizedMerge: undefined, + domToModelOption: { + additionalAllowedTags: [], + additionalDisallowedTags: [], + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, + }, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: 'asImage', + } + ); + }); + + it('BeforePaste with ref', () => { + const mockedClipboardData = 'CLIPBOARDDATA' as any; + const mockedFragment = 'FRAGMENT' as any; + const mockedHtmlBefore = 'BEFORE' as any; + const mockedHtmlAfter = 'AFTER' as any; + const mockedHTmlAttributes = 'ATTRIBUTES' as any; + const mockedSanitizeOption = 'OPTION' as any; + const mockedDomToModelOption = 'DOMTOMODEL' as any; + const mockedCustomizedMerge = 'MERGE' as any; + + runTest( + { + eventType: PluginEventType.BeforePaste, + eventDataCache: mockedDataCache, + clipboardData: mockedClipboardData, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: PasteType.AsPlainText, + sanitizingOption: mockedSanitizeOption, + }, + { + eventType: 'beforePaste', + clipboardData: mockedClipboardData, + customizedMerge: mockedCustomizedMerge, + domToModelOption: mockedDomToModelOption, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: 'asImage', + }, + { + eventType: 'beforePaste', + clipboardData: mockedClipboardData, + customizedMerge: mockedCustomizedMerge, + domToModelOption: mockedDomToModelOption, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: 'asPlainText', + } + ); + }); + + it('BeforeSetContent', () => { + const mockedNewContent = 'CONTENT' as any; + + runTest( + { + eventType: PluginEventType.BeforeSetContent, + eventDataCache: mockedDataCache, + newContent: mockedNewContent, + }, + undefined, + { + eventType: 'beforeSetContent', + eventDataCache: mockedDataCache, + newContent: mockedNewContent, + } + ); + }); + + it('CompositionEnd', () => { + runTest( + { + eventType: PluginEventType.CompositionEnd, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'compositionEnd', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('ContentChanged without ref', () => { + const mockedApiName = 'API' as any; + const mockedFormatString = 'STRING' as any; + const mockedText = 'TEXT' as any; + const mockedEntityState = 'STATE' as any; + const mockedData = 'DATA' as any; + const mockedSource = 'SOURCE' as any; + + runTest( + { + eventType: PluginEventType.ContentChanged, + eventDataCache: mockedDataCache, + additionalData: { + formatApiName: mockedApiName, + getAnnounceData: () => ({ + defaultStrings: KnownAnnounceStrings.AnnounceListItemBullet, + formatStrings: mockedFormatString, + text: mockedText, + }), + getEntityState: () => mockedEntityState, + }, + data: mockedData, + source: mockedSource, + }, + undefined, + { + eventType: 'contentChanged', + eventDataCache: mockedDataCache, + changedEntities: undefined, + contentModel: undefined, + data: mockedData, + entityStates: mockedEntityState, + selection: undefined, + source: mockedSource, + formatApiName: mockedApiName, + announceData: { + defaultStrings: 'announceListItemBullet', + formatStrings: mockedFormatString, + text: mockedText, + }, + } + ); + }); + + it('ContentChanged with ref', () => { + const mockedApiName = 'API' as any; + const mockedFormatString = 'STRING' as any; + const mockedText = 'TEXT' as any; + const mockedEntityState = 'STATE' as any; + const mockedData = 'DATA' as any; + const mockedSource = 'SOURCE' as any; + const mockedChangedEntity = 'ENTITY' as any; + const mockedContentModel = 'MODEL' as any; + const mockedSelection = 'SELECTION' as any; + + runTest( + { + eventType: PluginEventType.ContentChanged, + eventDataCache: mockedDataCache, + additionalData: { + formatApiName: mockedApiName, + getAnnounceData: () => ({ + defaultStrings: KnownAnnounceStrings.AnnounceListItemBullet, + formatStrings: mockedFormatString, + text: mockedText, + }), + getEntityState: () => mockedEntityState, + }, + data: mockedData, + source: mockedSource, + }, + { + eventType: 'contentChanged', + changedEntities: mockedChangedEntity, + contentModel: mockedContentModel, + selection: mockedSelection, + source: mockedSource, + }, + { + eventType: 'contentChanged', + eventDataCache: mockedDataCache, + changedEntities: mockedChangedEntity, + contentModel: mockedContentModel, + data: mockedData, + entityStates: mockedEntityState, + selection: mockedSelection, + source: mockedSource, + formatApiName: mockedApiName, + announceData: { + defaultStrings: 'announceListItemBullet', + formatStrings: mockedFormatString, + text: mockedText, + }, + } + ); + }); + + it('ContextMenu', () => { + const mockedItems = 'ITEMS' as any; + + runTest( + { + eventType: PluginEventType.ContextMenu, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + items: mockedItems, + }, + undefined, + { + eventType: 'contextMenu', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + items: mockedItems, + } + ); + }); + + it('EditImage', () => { + const mockedImage = 'IMAGE' as any; + const mockedNewSrc = 'NEWSRC' as any; + const mockedOriginalSrc = 'ORIGINALSRC' as any; + const mockedPreviousSrc = 'PREVIOUSSRC' as any; + + runTest( + { + eventType: PluginEventType.EditImage, + eventDataCache: mockedDataCache, + image: mockedImage, + newSrc: mockedNewSrc, + originalSrc: mockedOriginalSrc, + previousSrc: mockedPreviousSrc, + }, + undefined, + { + eventType: 'editImage', + eventDataCache: mockedDataCache, + image: mockedImage, + newSrc: mockedNewSrc, + originalSrc: mockedOriginalSrc, + previousSrc: mockedPreviousSrc, + } + ); + }); + + it('EditorReady', () => { + runTest( + { + eventType: PluginEventType.EditorReady, + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: 'editorReady', + eventDataCache: mockedDataCache, + } + ); + }); + + it('EnteredShadowEdit', () => { + runTest( + { + eventType: PluginEventType.EnteredShadowEdit, + eventDataCache: mockedDataCache, + fragment: null!, + selectionPath: null!, + }, + undefined, + { + eventType: 'enteredShadowEdit', + eventDataCache: mockedDataCache, + } + ); + }); + + it('EntityOperation', () => { + const mockedEntity = 'Entity' as any; + const mockedShouldPersist = 'PERSIST' as any; + const mockedState = 'STATE' as any; + + runTest( + { + eventType: PluginEventType.EntityOperation, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + entity: mockedEntity, + operation: EntityOperation.NewEntity, + shouldPersist: mockedShouldPersist, + state: mockedState, + }, + undefined, + { + eventType: 'entityOperation', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + entity: mockedEntity, + operation: 'newEntity', + shouldPersist: mockedShouldPersist, + state: mockedState, + } + ); + }); + + it('ExtractContentWithDom', () => { + const mockedClonedRoot = 'ROOT' as any; + + runTest( + { + eventType: PluginEventType.ExtractContentWithDom, + eventDataCache: mockedDataCache, + clonedRoot: mockedClonedRoot, + }, + undefined, + { + eventType: 'extractContentWithDom', + eventDataCache: mockedDataCache, + clonedRoot: mockedClonedRoot, + } + ); + }); + + it('Input', () => { + runTest( + { + eventType: PluginEventType.Input, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'input', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('KeyDown', () => { + runTest( + { + eventType: PluginEventType.KeyDown, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'keyDown', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('KeyPress', () => { + runTest( + { + eventType: PluginEventType.KeyPress, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'keyPress', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('KeyUp', () => { + runTest( + { + eventType: PluginEventType.KeyUp, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'keyUp', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('LeavingShadowEdit', () => { + runTest( + { + eventType: PluginEventType.LeavingShadowEdit, + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: 'leavingShadowEdit', + eventDataCache: mockedDataCache, + } + ); + }); + + it('MouseDown', () => { + runTest( + { + eventType: PluginEventType.MouseDown, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: 'mouseDown', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('MouseUp', () => { + const mockedIsClicking = 'CLICKING' as any; + + runTest( + { + eventType: PluginEventType.MouseUp, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + isClicking: mockedIsClicking, + }, + undefined, + { + eventType: 'mouseUp', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + isClicking: mockedIsClicking, + } + ); + }); + + it('Scroll', () => { + const mockedScrollContainer = 'CONTAINER' as any; + + runTest( + { + eventType: PluginEventType.Scroll, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + scrollContainer: mockedScrollContainer, + }, + undefined, + { + eventType: 'scroll', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + scrollContainer: mockedScrollContainer, + } + ); + }); + + it('SelectionChanged without ref', () => { + const mockedRangeEx = 'RANGEEX' as any; + const mockedNewSelection = 'NEWSELECTION' as any; + spyOn(selectionConvert, 'convertRangeExToDomSelection').and.returnValue(mockedNewSelection); + + runTest( + { + eventType: PluginEventType.SelectionChanged, + eventDataCache: mockedDataCache, + selectionRangeEx: mockedRangeEx, + }, + undefined, + { + eventType: 'selectionChanged', + eventDataCache: mockedDataCache, + newSelection: mockedNewSelection, + } + ); + expect(selectionConvert.convertRangeExToDomSelection).toHaveBeenCalledWith(mockedRangeEx); + }); + + it('SelectionChanged with ref', () => { + const mockedRangeEx = 'RANGEEX' as any; + const mockedNewSelection = 'NEWSELECTION' as any; + spyOn(selectionConvert, 'convertRangeExToDomSelection').and.returnValue(null); + + runTest( + { + eventType: PluginEventType.SelectionChanged, + eventDataCache: mockedDataCache, + selectionRangeEx: mockedRangeEx, + }, + { + eventType: 'selectionChanged', + eventDataCache: mockedDataCache, + newSelection: mockedNewSelection, + }, + { + eventType: 'selectionChanged', + eventDataCache: mockedDataCache, + newSelection: mockedNewSelection, + } + ); + expect(selectionConvert.convertRangeExToDomSelection).not.toHaveBeenCalled(); + }); + + it('ZoomChanged', () => { + const mockedNewZoomScale = 'NEWSCALE' as any; + const mockedOldZoomScale = 'OLDSCALE' as any; + + runTest( + { + eventType: PluginEventType.ZoomChanged, + eventDataCache: mockedDataCache, + newZoomScale: mockedNewZoomScale, + oldZoomScale: mockedOldZoomScale, + }, + undefined, + { + eventType: 'zoomChanged', + eventDataCache: mockedDataCache, + newZoomScale: mockedNewZoomScale, + oldZoomScale: mockedOldZoomScale, + } + ); + }); +}); + +describe('newEventToOldEvent', () => { + function runTest( + newEvent: NewEvent, + refEvent: OldEvent | undefined, + expectedResult: OldEvent | undefined + ) { + const result = newEventToOldEvent(newEvent, refEvent); + + expect(result).toEqual(expectedResult); + + return result; + } + + const mockedRoot = 'ROOT' as any; + const mockedDataCache = 'CACHE' as any; + const mockedIsCut = 'CUT' as any; + const mockedRange = 'RANGE' as any; + const mockedRawEvent = 'EVENT' as any; + + it('BeforeCutCopy', () => { + runTest( + { + eventType: 'beforeCutCopy', + clonedRoot: mockedRoot, + eventDataCache: mockedDataCache, + isCut: mockedIsCut, + range: mockedRange, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.BeforeCutCopy, + clonedRoot: mockedRoot, + eventDataCache: mockedDataCache, + isCut: mockedIsCut, + range: mockedRange, + rawEvent: mockedRawEvent, + } + ); + }); + + it('BeforeDispose', () => { + runTest( + { + eventType: 'beforeDispose', + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: PluginEventType.BeforeDispose, + eventDataCache: mockedDataCache, + } + ); + }); + + it('BeforeKeyboardEditing', () => { + runTest( + { + eventType: 'beforeKeyboardEditing', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.BeforeKeyboardEditing, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('BeforePaste without ref', () => { + const mockedClipboardData = 'CLIPBOARDDATA' as any; + const mockedFragment = 'FRAGMENT' as any; + const mockedHtmlBefore = 'BEFORE' as any; + const mockedHtmlAfter = 'AFTER' as any; + const mockedHTmlAttributes = 'ATTRIBUTES' as any; + const mockedCustomizedMerge = 'MERGE' as any; + const mockedDomToModelOption = 'DOMTOMODEL' as any; + + runTest( + { + eventType: 'beforePaste', + eventDataCache: mockedDataCache, + clipboardData: mockedClipboardData, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: 'asImage', + customizedMerge: mockedCustomizedMerge, + domToModelOption: mockedDomToModelOption, + }, + undefined, + { + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: PasteType.AsImage, + sanitizingOption: { + elementCallbacks: {}, + attributeCallbacks: {}, + cssStyleCallbacks: {}, + additionalTagReplacements: {}, + additionalAllowedAttributes: [], + additionalAllowedCssClasses: [], + additionalDefaultStyleValues: {}, + additionalGlobalStyleNodes: [], + additionalPredefinedCssForElement: {}, + preserveHtmlComments: false, + unknownTagReplacement: null, + }, + } + ); + }); + + it('BeforePaste with ref', () => { + const mockedClipboardData = 'CLIPBOARDDATA' as any; + const mockedFragment = 'FRAGMENT' as any; + const mockedHtmlBefore = 'BEFORE' as any; + const mockedHtmlAfter = 'AFTER' as any; + const mockedHTmlAttributes = 'ATTRIBUTES' as any; + const mockedSanitizeOption = 'OPTION' as any; + const mockedDomToModelOption = 'DOMTOMODEL' as any; + const mockedCustomizedMerge = 'MERGE' as any; + + runTest( + { + eventType: 'beforePaste', + eventDataCache: mockedDataCache, + clipboardData: mockedClipboardData, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: 'asImage', + customizedMerge: mockedCustomizedMerge, + domToModelOption: mockedDomToModelOption, + }, + { + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: PasteType.AsPlainText, + sanitizingOption: mockedSanitizeOption, + }, + { + eventType: PluginEventType.BeforePaste, + clipboardData: mockedClipboardData, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + htmlAfter: mockedHtmlAfter, + htmlBefore: mockedHtmlBefore, + htmlAttributes: mockedHTmlAttributes, + pasteType: PasteType.AsImage, + sanitizingOption: mockedSanitizeOption, + } + ); + }); + + it('BeforeSetContent', () => { + const mockedNewContent = 'CONTENT' as any; + + runTest( + { + eventType: 'beforeSetContent', + eventDataCache: mockedDataCache, + newContent: mockedNewContent, + }, + undefined, + { + eventType: PluginEventType.BeforeSetContent, + eventDataCache: mockedDataCache, + newContent: mockedNewContent, + } + ); + }); + + it('CompositionEnd', () => { + runTest( + { + eventType: 'compositionEnd', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.CompositionEnd, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('ContentChanged without ref', () => { + const mockedApiName = 'API' as any; + const mockedFormatString = 'STRING' as any; + const mockedText = 'TEXT' as any; + const mockedEntityState = 'STATE' as any; + const mockedData = 'DATA' as any; + const mockedSource = 'SOURCE' as any; + + const result = runTest( + { + eventType: 'contentChanged', + eventDataCache: mockedDataCache, + data: mockedData, + entityStates: mockedEntityState, + source: mockedSource, + formatApiName: mockedApiName, + announceData: { + defaultStrings: 'announceListItemBullet', + formatStrings: mockedFormatString, + text: mockedText, + }, + }, + undefined, + { + eventType: PluginEventType.ContentChanged, + eventDataCache: mockedDataCache, + data: mockedData, + source: mockedSource, + additionalData: { + formatApiName: mockedApiName, + getAnnounceData: jasmine.anything() as any, + getEntityState: jasmine.anything() as any, + }, + } + ) as ContentChangedEvent; + + expect(result.additionalData!.getAnnounceData!()).toEqual({ + defaultStrings: KnownAnnounceStrings.AnnounceListItemBullet, + formatStrings: mockedFormatString, + text: mockedText, + }); + expect(result.additionalData!.getEntityState!()).toEqual(mockedEntityState); + }); + + it('ContextMenu', () => { + const mockedItems = 'ITEMS' as any; + + runTest( + { + eventType: 'contextMenu', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + items: mockedItems, + }, + undefined, + { + eventType: PluginEventType.ContextMenu, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + items: mockedItems, + } + ); + }); + + it('EditImage', () => { + const mockedImage = 'IMAGE' as any; + const mockedNewSrc = 'NEWSRC' as any; + const mockedOriginalSrc = 'ORIGINALSRC' as any; + const mockedPreviousSrc = 'PREVIOUSSRC' as any; + + runTest( + { + eventType: 'editImage', + eventDataCache: mockedDataCache, + image: mockedImage, + newSrc: mockedNewSrc, + originalSrc: mockedOriginalSrc, + previousSrc: mockedPreviousSrc, + }, + undefined, + { + eventType: PluginEventType.EditImage, + eventDataCache: mockedDataCache, + image: mockedImage, + newSrc: mockedNewSrc, + originalSrc: mockedOriginalSrc, + previousSrc: mockedPreviousSrc, + } + ); + }); + + it('EditorReady', () => { + runTest( + { + eventType: 'editorReady', + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: PluginEventType.EditorReady, + eventDataCache: mockedDataCache, + } + ); + }); + + it('EnteredShadowEdit without ref', () => { + runTest( + { + eventType: 'enteredShadowEdit', + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: PluginEventType.EnteredShadowEdit, + eventDataCache: mockedDataCache, + fragment: document.createDocumentFragment(), + selectionPath: { + end: [], + start: [], + }, + } + ); + }); + + it('EnteredShadowEdit with ref', () => { + const mockedFragment = 'FRAGMENT' as any; + const mockedSelectionPath = 'PATH' as any; + runTest( + { + eventType: 'enteredShadowEdit', + eventDataCache: mockedDataCache, + }, + { + eventType: PluginEventType.EnteredShadowEdit, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + selectionPath: mockedSelectionPath, + }, + { + eventType: PluginEventType.EnteredShadowEdit, + eventDataCache: mockedDataCache, + fragment: mockedFragment, + selectionPath: mockedSelectionPath, + } + ); + }); + + it('EntityOperation', () => { + const mockedEntity = 'Entity' as any; + const mockedShouldPersist = 'PERSIST' as any; + const mockedState = 'STATE' as any; + + runTest( + { + eventType: 'entityOperation', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + entity: mockedEntity, + operation: 'newEntity', + shouldPersist: mockedShouldPersist, + state: mockedState, + }, + undefined, + { + eventType: PluginEventType.EntityOperation, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + entity: mockedEntity, + operation: EntityOperation.NewEntity, + shouldPersist: mockedShouldPersist, + state: mockedState, + } + ); + }); + + it('ExtractContentWithDom', () => { + const mockedClonedRoot = 'ROOT' as any; + + runTest( + { + eventType: 'extractContentWithDom', + eventDataCache: mockedDataCache, + clonedRoot: mockedClonedRoot, + }, + undefined, + { + eventType: PluginEventType.ExtractContentWithDom, + eventDataCache: mockedDataCache, + clonedRoot: mockedClonedRoot, + } + ); + }); + + it('Input', () => { + runTest( + { + eventType: 'input', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.Input, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('KeyDown', () => { + runTest( + { + eventType: 'keyDown', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.KeyDown, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('KeyPress', () => { + runTest( + { + eventType: 'keyPress', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.KeyPress, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('KeyUp', () => { + runTest( + { + eventType: 'keyUp', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.KeyUp, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('LeavingShadowEdit', () => { + runTest( + { + eventType: 'leavingShadowEdit', + eventDataCache: mockedDataCache, + }, + undefined, + { + eventType: PluginEventType.LeavingShadowEdit, + eventDataCache: mockedDataCache, + } + ); + }); + + it('MouseDown', () => { + runTest( + { + eventType: 'mouseDown', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + }, + undefined, + { + eventType: PluginEventType.MouseDown, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + } + ); + }); + + it('MouseUp', () => { + const mockedIsClicking = 'CLICKING' as any; + + runTest( + { + eventType: 'mouseUp', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + isClicking: mockedIsClicking, + }, + undefined, + { + eventType: PluginEventType.MouseUp, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + isClicking: mockedIsClicking, + } + ); + }); + + it('Scroll', () => { + const mockedScrollContainer = 'CONTAINER' as any; + + runTest( + { + eventType: 'scroll', + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + scrollContainer: mockedScrollContainer, + }, + undefined, + { + eventType: PluginEventType.Scroll, + eventDataCache: mockedDataCache, + rawEvent: mockedRawEvent, + scrollContainer: mockedScrollContainer, + } + ); + }); + + it('SelectionChanged without ref', () => { + const mockedRangeEx = 'RANGEEX' as any; + const mockedNewSelection = 'NEWSELECTION' as any; + spyOn(selectionConvert, 'convertDomSelectionToRangeEx').and.returnValue(mockedRangeEx); + + runTest( + { + eventType: 'selectionChanged', + eventDataCache: mockedDataCache, + newSelection: mockedNewSelection, + }, + undefined, + { + eventType: PluginEventType.SelectionChanged, + eventDataCache: mockedDataCache, + selectionRangeEx: mockedRangeEx, + } + ); + expect(selectionConvert.convertDomSelectionToRangeEx).toHaveBeenCalledWith( + mockedNewSelection + ); + }); + + it('SelectionChanged with ref', () => { + const mockedRangeEx = 'RANGEEX' as any; + const mockedNewSelection = 'NEWSELECTION' as any; + spyOn(selectionConvert, 'convertDomSelectionToRangeEx').and.returnValue(null!); + + runTest( + { + eventType: 'selectionChanged', + eventDataCache: mockedDataCache, + newSelection: mockedNewSelection, + }, + { + eventType: PluginEventType.SelectionChanged, + eventDataCache: mockedDataCache, + selectionRangeEx: mockedRangeEx, + }, + { + eventType: PluginEventType.SelectionChanged, + eventDataCache: mockedDataCache, + selectionRangeEx: mockedRangeEx, + } + ); + expect(selectionConvert.convertDomSelectionToRangeEx).not.toHaveBeenCalled(); + }); + + it('ZoomChanged', () => { + const mockedNewZoomScale = 'NEWSCALE' as any; + const mockedOldZoomScale = 'OLDSCALE' as any; + + runTest( + { + eventType: 'zoomChanged', + eventDataCache: mockedDataCache, + newZoomScale: mockedNewZoomScale, + oldZoomScale: mockedOldZoomScale, + }, + undefined, + { + eventType: PluginEventType.ZoomChanged, + eventDataCache: mockedDataCache, + newZoomScale: mockedNewZoomScale, + oldZoomScale: mockedOldZoomScale, + } + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/ContextMenuProvider.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/ContextMenuProvider.ts new file mode 100644 index 00000000000..a7b37467be3 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/ContextMenuProvider.ts @@ -0,0 +1,13 @@ +import type { EditorPlugin } from './EditorPlugin'; + +/** + * An extended Editor plugin interface which supports providing context menu items + */ +export interface ContextMenuProvider extends EditorPlugin { + /** + * A callback to return context menu items + * @param target Target node that triggered a ContextMenu event + * @returns An array of context menu items, or null means no items needed + */ + getContextMenuItems: (target: Node) => T[] | null; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index e6e3df47df3..185cf7be2f9 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -1,3 +1,4 @@ +import type { PluginState } from '../pluginState/PluginState'; import type { EditorPlugin } from './EditorPlugin'; import type { ClipboardData } from '../parameter/ClipboardData'; import type { PasteType } from '../enum/PasteType'; @@ -11,7 +12,6 @@ import type { TrustedHTMLHandler, } from 'roosterjs-editor-types'; import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { StandaloneEditorCorePluginState } from '../pluginState/StandaloneEditorPluginState'; import type { DOMSelection } from '../selection/DOMSelection'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { DomToModelSettings } from '../context/DomToModelSettings'; @@ -291,7 +291,7 @@ export interface StandaloneCoreApiMap { /** * Represents the core data structure of a Content Model editor */ -export interface StandaloneEditorCore extends StandaloneEditorCorePluginState { +export interface StandaloneEditorCore extends PluginState { /** * The content DIV element of this editor */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/BasePluginEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/BasePluginEvent.ts new file mode 100644 index 00000000000..73f740e2a3e --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/BasePluginEvent.ts @@ -0,0 +1,31 @@ +import type { PluginEventType } from './PluginEventType'; + +/** + * Editor plugin event interface + */ +export interface BasePluginEvent { + /** + * Type of this event + */ + eventType: TPluginEventType; + + /** + * An optional event cache. + * This will be consumed by event cache API to store some expensive calculation result. + * So that for the same event across plugins, the result doesn't need to be calculated again + */ + eventDataCache?: { [key: string]: any }; +} + +/** + * Editor plugin event interface + */ +export interface BasePluginDomEvent< + TPluginEventType extends PluginEventType, + TRawEvent extends Event +> extends BasePluginEvent { + /** + * Raw DOM event + */ + rawEvent: TRawEvent; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/BeforeCutCopyEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeCutCopyEvent.ts new file mode 100644 index 00000000000..3a0f5f25fb8 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeCutCopyEvent.ts @@ -0,0 +1,21 @@ +import type { BasePluginDomEvent } from './BasePluginEvent'; + +/** + * Provides a chance for plugin to change the content before it is copied from editor. + */ +export interface BeforeCutCopyEvent extends BasePluginDomEvent<'beforeCutCopy', ClipboardEvent> { + /** + * An object contains all related data for pasting + */ + clonedRoot: HTMLDivElement; + + /** + * The selection range under cloned root + */ + range: Range; + + /** + * Whether this is a cut event + */ + isCut: boolean; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/BeforeDisposeEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeDisposeEvent.ts new file mode 100644 index 00000000000..a796e0337ee --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeDisposeEvent.ts @@ -0,0 +1,6 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * Provides a chance for plugin to change the content before it is pasted into editor. + */ +export interface BeforeDisposeEvent extends BasePluginEvent<'beforeDispose'> {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/BeforeKeyboardEditingEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeKeyboardEditingEvent.ts new file mode 100644 index 00000000000..48545d258ff --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeKeyboardEditingEvent.ts @@ -0,0 +1,7 @@ +import type { BasePluginDomEvent } from './BasePluginEvent'; + +/** + * Provides a chance for plugin to change the content before it is copied from editor. + */ +export interface BeforeKeyboardEditingEvent + extends BasePluginDomEvent<'beforeKeyboardEditing', KeyboardEvent> {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts new file mode 100644 index 00000000000..58d45aa8a9a --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts @@ -0,0 +1,88 @@ +import type { PasteType } from '../enum/PasteType'; +import type { ClipboardData } from '../parameter/ClipboardData'; +import type { BasePluginEvent } from './BasePluginEvent'; +import type { DomToModelOption } from '../context/DomToModelOption'; +import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { InsertPoint } from '../selection/InsertPoint'; +import type { ValueSanitizer } from '../parameter/ValueSanitizer'; + +/** + * Options for DOM to Content Model conversion for paste only + */ +export interface DomToModelOptionForPaste extends Required { + /** + * Additional allowed HTML tags in lower case. Element with these tags will be preserved + */ + readonly additionalAllowedTags: Lowercase[]; + + /** + * Additional disallowed HTML tags in lower case. Elements with these tags will be dropped + */ + readonly additionalDisallowedTags: Lowercase[]; + + /** + * Additional sanitizers for CSS styles + */ + readonly styleSanitizers: Record; + + /** + * Additional sanitizers for CSS styles + */ + readonly attributeSanitizers: Record; +} + +/** + * A function type used by merging pasted content into current Content Model + * @param target Target Content Model to merge into + * @param source Source Content Model to merge from + * @returns Insert point after merge + */ +export type MergePastedContentFunc = ( + target: ContentModelDocument, + source: ContentModelDocument +) => InsertPoint | null; + +/** + * Data of ContentModelBeforePasteEvent + */ +export interface BeforePasteEvent extends BasePluginEvent<'beforePaste'> { + /** + * An object contains all related data for pasting + */ + readonly clipboardData: ClipboardData; + + /** + * HTML Document Fragment which will be inserted into content + */ + readonly fragment: DocumentFragment; + + /** + * Stripped HTML string before "StartFragment" comment + */ + readonly htmlBefore: string; + + /** + * Stripped HTML string after "EndFragment" comment + */ + readonly htmlAfter: string; + + /** + * Attributes of the root "HTML" tag + */ + readonly htmlAttributes: Record; + + /** + * Paste type option (as plain text, merge format, normal, as image) + */ + readonly pasteType: PasteType; + + /** + * domToModel Options to use when creating the content model from the paste fragment + */ + readonly domToModelOption: DomToModelOptionForPaste; + + /** + * customizedMerge Customized merge function to use when merging the paste fragment into the editor + */ + customizedMerge?: MergePastedContentFunc; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/BeforeSetContentEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeSetContentEvent.ts new file mode 100644 index 00000000000..80a0de7d82f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/BeforeSetContentEvent.ts @@ -0,0 +1,12 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * The event to be triggered before SetContent API is called. + * Handle this event to cache anything you need from editor before it is gone. + */ +export interface BeforeSetContentEvent extends BasePluginEvent<'beforeSetContent'> { + /** + * New content HTML that is about to set to editor + */ + newContent: string; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts new file mode 100644 index 00000000000..bac9d637942 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentChangedEvent.ts @@ -0,0 +1,72 @@ +import type { AnnounceData } from '../parameter/AnnounceData'; +import type { BasePluginEvent } from './BasePluginEvent'; +import type { EntityState } from '../parameter/FormatWithContentModelContext'; +import type { ContentModelEntity } from '../entity/ContentModelEntity'; +import type { EntityRemovalOperation } from '../enum/EntityOperation'; +import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { DOMSelection } from '../selection/DOMSelection'; + +/** + * Represents an entity that has been changed during a content change process + */ +export interface ChangedEntity { + /** + * The changed entity + */ + entity: ContentModelEntity; + + /** + * Operation that causes the change + */ + operation: EntityRemovalOperation | 'newEntity'; + + /** + * @optional Raw DOM event that causes the change + */ + rawEvent?: Event; +} + +/** + * Represents a change to the editor made by another plugin with content model inside + */ +export interface ContentChangedEvent extends BasePluginEvent<'contentChanged'> { + /** + * The content model that is applied which causes this content changed event + */ + readonly contentModel?: ContentModelDocument; + + /** + * Selection range applied to the document + */ + readonly selection?: DOMSelection; + + /** + * Entities got changed (added or removed) during the content change process + */ + readonly changedEntities?: ChangedEntity[]; + + /** + * Entity states related to this event + */ + readonly entityStates?: EntityState[]; + + /** + * Source of the change + */ + readonly source: string; + + /** + * Optional related data + */ + readonly data?: any; + + /** + * Optional property to store the format api name when using ChangeSource.Format + */ + readonly formatApiName?: string; + + /** + * @optional Announce data from this content changed event. + */ + readonly announceData?: AnnounceData; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts index 3a4f8d90711..c41a81ddd1b 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts @@ -1,49 +1,10 @@ -import type { ValueSanitizer } from '../parameter/ValueSanitizer'; -import type { DomToModelOption } from '../context/DomToModelOption'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { InsertPoint } from '../selection/InsertPoint'; +import type { DomToModelOptionForPaste, MergePastedContentFunc } from './BeforePasteEvent'; import type { BeforePasteEvent, BeforePasteEventData, CompatibleBeforePasteEvent, } from 'roosterjs-editor-types'; -/** - * Options for DOM to Content Model conversion for paste only - */ -export interface DomToModelOptionForPaste extends Required { - /** - * Additional allowed HTML tags in lower case. Element with these tags will be preserved - */ - additionalAllowedTags: Lowercase[]; - - /** - * Additional disallowed HTML tags in lower case. Elements with these tags will be dropped - */ - additionalDisallowedTags: Lowercase[]; - - /** - * Additional sanitizers for CSS styles - */ - styleSanitizers: Record; - - /** - * Additional sanitizers for CSS styles - */ - attributeSanitizers: Record; -} - -/** - * A function type used by merging pasted content into current Content Model - * @param target Target Content Model to merge into - * @param source Source Content Model to merge from - * @returns Insert point after merge - */ -export type MergePastedContentFunc = ( - target: ContentModelDocument, - source: ContentModelDocument -) => InsertPoint | null; - /** * Data of ContentModelBeforePasteEvent */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts index 0440e5e05df..6423d386d3f 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts @@ -1,6 +1,5 @@ +import type { ChangedEntity } from './ContentChangedEvent'; import type { EntityState } from '../parameter/FormatWithContentModelContext'; -import type { ContentModelEntity } from '../entity/ContentModelEntity'; -import type { EntityRemovalOperation } from '../enum/EntityOperation'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; import type { @@ -9,26 +8,6 @@ import type { ContentChangedEventData, } from 'roosterjs-editor-types'; -/** - * Represents an entity that has been changed during a content change process - */ -export interface ChangedEntity { - /** - * The changed entity - */ - entity: ContentModelEntity; - - /** - * Operation that causes the change - */ - operation: EntityRemovalOperation | 'newEntity'; - - /** - * @optional Raw DOM event that causes the change - */ - rawEvent?: Event; -} - /** * Data of ContentModelContentChangedEvent */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContextMenuEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContextMenuEvent.ts new file mode 100644 index 00000000000..3f3f1e6e941 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContextMenuEvent.ts @@ -0,0 +1,13 @@ +import type { BasePluginDomEvent } from './BasePluginEvent'; + +/** + * This interface represents a PluginEvent wrapping native ContextMenu event + */ +export interface ContextMenuEvent extends BasePluginDomEvent<'contextMenu', MouseEvent> { + /** + * A callback array to let editor retrieve context menu item related to this event. + * Plugins can add their own getter callback to this array, + * items from each getter will be separated by a splitter item represented by null + */ + items: any[]; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/EditImageEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/EditImageEvent.ts new file mode 100644 index 00000000000..e378e1f4453 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/EditImageEvent.ts @@ -0,0 +1,29 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * Represents an event that will be fired when an inline image is edited by user, and the src + * attribute of the image is about to be changed + */ +export interface EditImageEvent extends BasePluginEvent<'editImage'> { + /** + * The image element that is being changed + */ + readonly image: HTMLImageElement; + + /** + * Original src of the image before all editing in current editor session. + */ + readonly originalSrc: string; + + /** + * Src of the image before current batch of editing + * Plugin can check this value to know which image is not used after the change. + */ + readonly previousSrc: string; + + /** + * New src of the changed image, in DataUri format. + * Plugin can modify this string so that the modified one will be set to the image element + */ + newSrc: string; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/EditorInputEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/EditorInputEvent.ts new file mode 100644 index 00000000000..8a1a5b1e3c4 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/EditorInputEvent.ts @@ -0,0 +1,11 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * This interface represents a PluginEvent wrapping native input / textinput event + */ +export interface EditorInputEvent extends BasePluginEvent<'input'> { + /** + * Raw Input Event + */ + rawEvent: InputEvent; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/EditorReadyEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/EditorReadyEvent.ts new file mode 100644 index 00000000000..611343fe51d --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/EditorReadyEvent.ts @@ -0,0 +1,6 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * Provides a chance for plugin to change the content before it is pasted into editor. + */ +export interface EditorReadyEvent extends BasePluginEvent<'editorReady'> {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/EntityOperationEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/EntityOperationEvent.ts new file mode 100644 index 00000000000..fc3ba4a7062 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/EntityOperationEvent.ts @@ -0,0 +1,58 @@ +import type { BasePluginEvent } from './BasePluginEvent'; +import type { EntityOperation } from '../enum/EntityOperation'; + +/** + * Represents an entity in editor. + */ +export interface Entity { + /** + * Type of this entity. Specified when insert an entity, can be an valid CSS class-like string. + */ + type: string; + /** + * Id of this entity, generated by editor code and will be unique within an editor + */ + id: string; + /** + * The wrapper DOM node of this entity which holds the info CSS classes of this entity + */ + wrapper: HTMLElement; + /** + * Whether this is a readonly entity + */ + isReadonly: boolean; +} + +/** + * Provide a chance for plugins to handle entity related events. + * See enum EntityOperation for more details about each operation + */ +export interface EntityOperationEvent extends BasePluginEvent<'entityOperation'> { + /** + * Operation to this entity + */ + operation: EntityOperation; + + /** + * The entity that editor is operating on + */ + entity: Entity; + + /** + * Optional raw event. Need to do null check before use its value + */ + rawEvent?: Event; + + /** + * For EntityOperation.UpdateEntityState, we use this object to pass the new entity state to plugin. + * For other operation types, it is not used. + */ + state?: string; + + /** + * For EntityOperation.NewEntity, plugin can set this property to true then the entity will be persisted. + * A persisted entity won't be touched during undo/redo, unless it does not exist after undo/redo. + * For other operation types, this value will be ignored. + */ + shouldPersist?: boolean; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ExtractContentWithDomEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ExtractContentWithDomEvent.ts new file mode 100644 index 00000000000..a4cc8e9880c --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ExtractContentWithDomEvent.ts @@ -0,0 +1,15 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * Extract Content with a DOM tree event + * This event is triggered when getContent() is called with triggerExtractContentEvent = true + * Plugin can handle this event to remove the UI only markups to return clean HTML + * by operating on a cloned DOM tree + */ +export interface ExtractContentWithDomEvent extends BasePluginEvent<'extractContentWithDom'> { + /** + * Cloned root element of editor + * Plugin can change this DOM tree to clean up the markups it added before + */ + clonedRoot: HTMLElement; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/KeyboardEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/KeyboardEvent.ts new file mode 100644 index 00000000000..9b69020684b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/KeyboardEvent.ts @@ -0,0 +1,27 @@ +import type { BasePluginDomEvent } from './BasePluginEvent'; + +/** + * This interface represents a PluginEvent wrapping native KeyDown event + */ +export interface KeyDownEvent extends BasePluginDomEvent<'keyDown', KeyboardEvent> { + /** + * Whether this event is handled by edit feature + */ + handledByEditFeature?: boolean; +} + +/** + * This interface represents a PluginEvent wrapping native KeyPress event + */ +export interface KeyPressEvent extends BasePluginDomEvent<'keyPress', KeyboardEvent> {} + +/** + * This interface represents a PluginEvent wrapping native KeyUp event + */ +export interface KeyUpEvent extends BasePluginDomEvent<'keyUp', KeyboardEvent> {} + +/** + * This interface represents a PluginEvent wrapping native CompositionEnd event + */ +export interface CompositionEndEvent + extends BasePluginDomEvent<'compositionEnd', CompositionEvent> {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/MouseEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/MouseEvent.ts new file mode 100644 index 00000000000..f557ced2a5e --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/MouseEvent.ts @@ -0,0 +1,16 @@ +import type { BasePluginDomEvent } from './BasePluginEvent'; + +/** + * This interface represents a PluginEvent wrapping native MouseDown event + */ +export interface MouseDownEvent extends BasePluginDomEvent<'mouseDown', MouseEvent> {} + +/** + * This interface represents a PluginEvent wrapping native MouseUp event + */ +export interface MouseUpEvent extends BasePluginDomEvent<'mouseUp', MouseEvent> { + /** + * Whether this is a mouse click event (mouse up and down on the same position) + */ + isClicking?: boolean; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/PluginEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/PluginEvent.ts new file mode 100644 index 00000000000..b91c898aa4c --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/PluginEvent.ts @@ -0,0 +1,46 @@ +import type { BeforeCutCopyEvent } from './BeforeCutCopyEvent'; +import type { BeforeDisposeEvent } from './BeforeDisposeEvent'; +import type { BeforeKeyboardEditingEvent } from './BeforeKeyboardEditingEvent'; +import type { BeforePasteEvent } from './BeforePasteEvent'; +import type { BeforeSetContentEvent } from './BeforeSetContentEvent'; +import type { CompositionEndEvent, KeyDownEvent, KeyPressEvent, KeyUpEvent } from './KeyboardEvent'; +import type { ContentChangedEvent } from './ContentChangedEvent'; +import type { ContextMenuEvent } from './ContextMenuEvent'; +import type { EditImageEvent } from './EditImageEvent'; +import type { EditorReadyEvent } from './EditorReadyEvent'; +import type { EnterShadowEditEvent, LeaveShadowEditEvent } from './ShadowEditEvent'; +import type { EntityOperationEvent } from './EntityOperationEvent'; +import type { ExtractContentWithDomEvent } from './ExtractContentWithDomEvent'; +import type { EditorInputEvent } from './EditorInputEvent'; +import type { MouseDownEvent, MouseUpEvent } from './MouseEvent'; +import type { ScrollEvent } from './ScrollEvent'; +import type { SelectionChangedEvent } from './SelectionChangedEvent'; +import type { ZoomChangedEvent } from './ZoomChangedEvent'; + +/** + * Editor plugin event interface + */ +export type PluginEvent = + | BeforeCutCopyEvent + | BeforeDisposeEvent + | BeforeKeyboardEditingEvent + | BeforePasteEvent + | BeforeSetContentEvent + | CompositionEndEvent + | ContentChangedEvent + | ContextMenuEvent + | EditImageEvent + | EditorReadyEvent + | EnterShadowEditEvent + | EntityOperationEvent + | ExtractContentWithDomEvent + | EditorInputEvent + | KeyDownEvent + | KeyPressEvent + | KeyUpEvent + | LeaveShadowEditEvent + | MouseDownEvent + | MouseUpEvent + | ScrollEvent + | SelectionChangedEvent + | ZoomChangedEvent; diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/PluginEventData.ts b/packages-content-model/roosterjs-content-model-types/lib/event/PluginEventData.ts new file mode 100644 index 00000000000..b366c06b729 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/PluginEventData.ts @@ -0,0 +1,34 @@ +import type { BasePluginEvent } from './BasePluginEvent'; +import type { PluginEventType } from './PluginEventType'; +import type { PluginEvent } from './PluginEvent'; + +/** + * A type to get specify plugin event type from eventType parameter. + * This type is a middle result and only used by PluginEventFromType type + */ +export type PluginEventFromTypeGeneric< + E extends PluginEvent, + T extends PluginEventType +> = E extends BasePluginEvent ? E : never; + +/** + * A type to get specify plugin event type from eventType parameter. + */ +export type PluginEventFromType = PluginEventFromTypeGeneric< + PluginEvent, + T +>; + +/** + * A type to extract data part of a plugin event type. Data part is the plugin event without eventType field. + * This type is a middle result and only used by PluginEventData type + */ +export type PluginEventDataGeneric< + E extends PluginEvent, + T extends PluginEventType +> = E extends BasePluginEvent ? Pick> : never; + +/** + * A type to extract data part of a plugin event type. Data part is the plugin event without eventType field. + */ +export type PluginEventData = PluginEventDataGeneric; diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/PluginEventType.ts b/packages-content-model/roosterjs-content-model-types/lib/event/PluginEventType.ts new file mode 100644 index 00000000000..f4f1d2df95b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/PluginEventType.ts @@ -0,0 +1,125 @@ +/** + * Type of plugin events + */ +export type PluginEventType = + /** + * HTML KeyDown event + */ + | 'keyDown' + + /** + * HTML KeyPress event + */ + | 'keyPress' + + /** + * HTML KeyUp event + */ + | 'keyUp' + + /** + * HTML Input / TextInput event + */ + | 'input' + + /** + * HTML CompositionEnd event + */ + | 'compositionEnd' + + /** + * HTML MouseDown event + */ + | 'mouseDown' + + /** + * HTML MouseUp event + */ + | 'mouseUp' + + /** + * Content changed event + */ + | 'contentChanged' + + /** + * Extract Content with a DOM tree event + * This event is triggered when getContent() is called with triggerExtractContentEvent = true + * Plugin can handle this event to remove the UI only markups to return clean HTML + * by operating on a cloned DOM tree + */ + | 'extractContentWithDom' + + /** + * Before Paste event, provide a chance to change copied content + */ + | 'beforeCutCopy' + + /** + * Before Paste event, provide a chance to change paste content + */ + | 'beforePaste' + + /** + * Let plugin know editor is ready now + */ + | 'editorReady' + + /** + * Let plugin know editor is about to dispose + */ + | 'beforeDispose' + + /** + * Scroll event triggered by scroll container + */ + | 'scroll' + + /** + * Operating on an entity. See enum EntityOperation for more details about each operation + */ + | 'entityOperation' + + /** + * HTML ContextMenu event + */ + | 'contextMenu' + + /** + * Editor has entered shadow edit mode + */ + | 'enteredShadowEdit' + + /** + * Editor is about to leave shadow edit mode + */ + | 'leavingShadowEdit' + + /** + * Content of image is being changed from client side + */ + | 'editImage' + + /** + * Content of editor is about to be cleared by SetContent API, handle this event to cache anything you need + * before it is gone + */ + | 'beforeSetContent' + + /** + * Zoom scale value is changed, triggered by Editor.setZoomScale() when set a different scale number + */ + | 'zoomChanged' + + /** + * EXPERIMENTAL FEATURE + * Editor changed the selection. + */ + | 'selectionChanged' + + /** + * EXPERIMENTAL FEATURE + * Editor content is about to be changed by keyboard event. + * This is only used by Content Model editing + */ + | 'beforeKeyboardEditing'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ScrollEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ScrollEvent.ts new file mode 100644 index 00000000000..a11f98a4e7d --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ScrollEvent.ts @@ -0,0 +1,11 @@ +import type { BasePluginDomEvent } from './BasePluginEvent'; + +/** + * This interface represents a PluginEvent wrapping native scroll event + */ +export interface ScrollEvent extends BasePluginDomEvent<'scroll', Event> { + /** + * Current scroll container that triggers this scroll event + */ + scrollContainer: HTMLElement; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts new file mode 100644 index 00000000000..d692e5a3873 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts @@ -0,0 +1,12 @@ +import type { BasePluginEvent } from './BasePluginEvent'; +import type { DOMSelection } from '../selection/DOMSelection'; + +/** + * Represents an event that will be fired when the user changed the selection + */ +export interface SelectionChangedEvent extends BasePluginEvent<'selectionChanged'> { + /** + * The new selection after change + */ + newSelection: DOMSelection | null; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ShadowEditEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ShadowEditEvent.ts new file mode 100644 index 00000000000..fb32d1d1eb8 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ShadowEditEvent.ts @@ -0,0 +1,11 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * A plugin triggered right after editor has entered Shadow Edit mode + */ +export interface EnterShadowEditEvent extends BasePluginEvent<'enteredShadowEdit'> {} + +/** + * A plugin triggered right before editor leave Shadow Edit mode + */ +export interface LeaveShadowEditEvent extends BasePluginEvent<'leavingShadowEdit'> {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ZoomChangedEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ZoomChangedEvent.ts new file mode 100644 index 00000000000..9cc66f7e171 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ZoomChangedEvent.ts @@ -0,0 +1,18 @@ +import type { BasePluginEvent } from './BasePluginEvent'; + +/** + * Represents an event object triggered from Editor.setZoomScale() API. + * Plugins can handle this event when they need to do something for zoom changing. + * + */ +export interface ZoomChangedEvent extends BasePluginEvent<'zoomChanged'> { + /** + * Zoom scale value before this change + */ + oldZoomScale: number; + + /** + * Zoom scale value after this change + */ + newZoomScale: number; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 0f894baed90..ad970b46cbb 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -219,9 +219,9 @@ export { export { StandaloneEditorCorePlugins } from './editor/StandaloneEditorCorePlugins'; export { EditorPlugin } from './editor/EditorPlugin'; export { PluginWithState } from './editor/PluginWithState'; +export { ContextMenuProvider } from './editor/ContextMenuProvider'; export { ContentModelCachePluginState } from './pluginState/ContentModelCachePluginState'; -export { StandaloneEditorCorePluginState } from './pluginState/StandaloneEditorPluginState'; export { ContentModelFormatPluginState, PendingFormat, @@ -232,6 +232,14 @@ export { LifecyclePluginState } from './pluginState/LifecyclePluginState'; export { EntityPluginState, KnownEntityItem } from './pluginState/EntityPluginState'; export { SelectionPluginState } from './pluginState/SelectionPluginState'; export { UndoPluginState } from './pluginState/UndoPluginState'; +export { + PluginKey, + KeyOfStatePlugin, + TypeOfStatePlugin, + StatePluginKeys, + GenericPluginState, + PluginState, +} from './pluginState/PluginState'; export { EditorEnvironment } from './parameter/EditorEnvironment'; export { @@ -266,11 +274,10 @@ export { SnapshotsManager } from './parameter/SnapshotsManager'; export { DOMEventHandlerFunction, DOMEventRecord } from './parameter/DOMEventRecord'; export { EdgeLinkPreview } from './parameter/EdgeLinkPreview'; export { ClipboardData } from './parameter/ClipboardData'; +export { AnnounceData, KnownAnnounceStrings } from './parameter/AnnounceData'; export { ValueSanitizer } from './parameter/ValueSanitizer'; export { - MergePastedContentFunc, - DomToModelOptionForPaste, ContentModelBeforePasteEvent, ContentModelBeforePasteEventData, CompatibleContentModelBeforePasteEvent, @@ -279,10 +286,45 @@ export { ContentModelContentChangedEvent, CompatibleContentModelContentChangedEvent, ContentModelContentChangedEventData, - ChangedEntity, } from './event/ContentModelContentChangedEvent'; export { CompatibleContentModelSelectionChangedEvent, ContentModelSelectionChangedEvent, ContentModelSelectionChangedEventData, } from './event/ContentModelSelectionChangedEvent'; +export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent'; +export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent'; +export { BeforeDisposeEvent } from './event/BeforeDisposeEvent'; +export { BeforeKeyboardEditingEvent } from './event/BeforeKeyboardEditingEvent'; +export { + BeforePasteEvent, + DomToModelOptionForPaste, + MergePastedContentFunc, +} from './event/BeforePasteEvent'; +export { BeforeSetContentEvent } from './event/BeforeSetContentEvent'; +export { ContentChangedEvent, ChangedEntity } from './event/ContentChangedEvent'; +export { ContextMenuEvent } from './event/ContextMenuEvent'; +export { EditImageEvent } from './event/EditImageEvent'; +export { EditorReadyEvent } from './event/EditorReadyEvent'; +export { EntityOperationEvent, Entity } from './event/EntityOperationEvent'; +export { ExtractContentWithDomEvent } from './event/ExtractContentWithDomEvent'; +export { EditorInputEvent } from './event/EditorInputEvent'; +export { + KeyDownEvent, + KeyPressEvent, + KeyUpEvent, + CompositionEndEvent, +} from './event/KeyboardEvent'; +export { MouseDownEvent, MouseUpEvent } from './event/MouseEvent'; +export { PluginEvent } from './event/PluginEvent'; +export { + PluginEventData, + PluginEventFromTypeGeneric, + PluginEventFromType, + PluginEventDataGeneric, +} from './event/PluginEventData'; +export { PluginEventType } from './event/PluginEventType'; +export { ScrollEvent } from './event/ScrollEvent'; +export { SelectionChangedEvent } from './event/SelectionChangedEvent'; +export { EnterShadowEditEvent, LeaveShadowEditEvent } from './event/ShadowEditEvent'; +export { ZoomChangedEvent } from './event/ZoomChangedEvent'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/AnnounceData.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/AnnounceData.ts new file mode 100644 index 00000000000..1b92e219e9b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/AnnounceData.ts @@ -0,0 +1,44 @@ +/** + * Known announce strings + */ +export type KnownAnnounceStrings = + /** + * String announced for a list item in a OL List + * @example + * Auto corrected, {0} + * Where {0} is the new list item bullet + */ + | 'announceListItemNumbering' + + /** + * String announced for a list item in a UL List + * @example + * Auto corrected bullet + */ + | 'announceListItemBullet' + + /** + * String announced when cursor is moved to the last cell in a table + */ + | 'announceOnFocusLastCell'; + +/** + * Represents data, that can be used to announce text to screen reader. + */ +export interface AnnounceData { + /** + * @optional Default announce strings built in Rooster + */ + defaultStrings?: KnownAnnounceStrings; + + /** + * @optional string to announce from this Content Changed event, will be the fallback value if default string + * is not provided or if it is not found in the strings map. + */ + text?: string; + + /** + * @optional if provided, will attempt to replace {n} with each of the values inside of the array. + */ + formatStrings?: string[]; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/pluginState/PluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/PluginState.ts new file mode 100644 index 00000000000..879609b30ef --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/pluginState/PluginState.ts @@ -0,0 +1,38 @@ +import type { PluginWithState } from '../editor/PluginWithState'; +import type { StandaloneEditorCorePlugins } from '../editor/StandaloneEditorCorePlugins'; + +/** + * Names of core plugins + */ +export type PluginKey = keyof StandaloneEditorCorePlugins; + +/** + * Names of the core plugins that have plugin state + */ +export type KeyOfStatePlugin< + Key extends PluginKey +> = StandaloneEditorCorePlugins[Key] extends PluginWithState ? Key : never; + +/** + * Get type of a plugin with state + */ +export type TypeOfStatePlugin< + Key extends PluginKey +> = StandaloneEditorCorePlugins[Key] extends PluginWithState ? U : never; + +/** + * All names of plugins with plugin state + */ +export type StatePluginKeys = { [P in Key]: KeyOfStatePlugin

}[Key]; + +/** + * A type map from name of plugin with state to its plugin type + */ +export type GenericPluginState = { + [P in StatePluginKeys]: TypeOfStatePlugin

; +}; + +/** + * Auto-calculated State object type for plugin with states + */ +export type PluginState = GenericPluginState; diff --git a/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts deleted file mode 100644 index fed7e413701..00000000000 --- a/packages-content-model/roosterjs-content-model-types/lib/pluginState/StandaloneEditorPluginState.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { CopyPastePluginState } from './CopyPastePluginState'; -import type { UndoPluginState } from './UndoPluginState'; -import type { SelectionPluginState } from './SelectionPluginState'; -import type { ContentModelCachePluginState } from './ContentModelCachePluginState'; -import type { ContentModelFormatPluginState } from './ContentModelFormatPluginState'; -import type { DOMEventPluginState } from './DOMEventPluginState'; -import type { EntityPluginState } from './EntityPluginState'; -import type { LifecyclePluginState } from './LifecyclePluginState'; - -/** - * Temporary core plugin state for Content Model editor (ported part) - * TODO: Create Content Model plugin state from all core plugins once we have standalone Content Model Editor - */ -export interface StandaloneEditorCorePluginState { - /** - * Plugin state for ContentModelCachePlugin - */ - cache: ContentModelCachePluginState; - - /** - * Plugin state for ContentModelCopyPastePlugin - */ - copyPaste: CopyPastePluginState; - - /** - * Plugin state for ContentModelFormatPlugin - */ - format: ContentModelFormatPluginState; - - /** - * Plugin state for DOMEventPlugin - */ - domEvent: DOMEventPluginState; - - /** - * Plugin state for LifecyclePlugin - */ - lifecycle: LifecyclePluginState; - - /** - * Plugin state for EntityPlugin - */ - entity: EntityPluginState; - - /** - * Plugin state for SelectionPlugin - */ - selection: SelectionPluginState; - - /** - * Plugin state for UndoPlugin - */ - undo: UndoPluginState; -} From e2f8faa3b6fcd4cf120aa4c1170a7cd11dcdfb15 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 11 Jan 2024 22:24:26 -0800 Subject: [PATCH 48/64] Standalone Editor: Port to new event system (#2298) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * Standalone Editor: Remove compatible enums from standalone editor * improve * Standalone Editor: Create new event types * Port to new event system * Revert "Port to new event system" This reverts commit 60cf041b3c3334df8a1781e22b2e81adc0775662. * Port to new event system * Improve * fix build * fix demo * Fix buttons * fix build * fix build * fix build --- demo/scripts/controls/BuildInPluginState.ts | 1 - .../controls/ContentModelEditorMainPane.tsx | 14 +- .../ContentModelFormatPainterPlugin.ts | 4 +- demo/scripts/controls/getToggleablePlugins.ts | 2 - .../contentModel/ContentModelRibbonPlugin.ts | 18 +- .../ribbonButtons/contentModel/export.ts | 5 +- .../ContentModelEditorOptionsPlugin.ts | 1 - .../editorOptions/ContentModelPlugins.tsx | 1 - .../editorOptions/EditorOptionsPlugin.ts | 1 - .../editorOptions/codes/PluginsCode.ts | 2 +- .../eventViewer/ContentModelEventViewPane.tsx | 20 -- .../lib/publicApi/image/changeImage.ts | 3 +- .../test/publicApi/image/changeImageTest.ts | 3 +- .../test/publicApi/link/insertLinkTest.ts | 14 +- .../lib/coreApi/attachDomEvent.ts | 5 +- .../lib/coreApi/formatContentModel.ts | 11 +- .../lib/coreApi/restoreUndoSnapshot.ts | 12 +- .../lib/coreApi/setDOMSelection.ts | 8 +- .../lib/coreApi/switchShadowEdit.ts | 7 +- .../lib/coreApi/triggerEvent.ts | 17 +- .../lib/corePlugin/ContentModelCachePlugin.ts | 17 +- .../corePlugin/ContentModelCopyPastePlugin.ts | 3 +- .../corePlugin/ContentModelFormatPlugin.ts | 13 +- .../lib/corePlugin/DOMEventPlugin.ts | 22 +- .../lib/corePlugin/EntityPlugin.ts | 37 ++-- .../lib/corePlugin/LifecyclePlugin.ts | 9 +- .../lib/corePlugin/SelectionPlugin.ts | 9 +- .../lib/corePlugin/UndoPlugin.ts | 18 +- .../lib/editor/StandaloneEditor.ts | 24 ++- .../paste/generatePasteOptionFromPlugins.ts | 35 +-- .../lib/utils/paste/mergePasteContent.ts | 7 +- .../test/coreApi/attachDomEventTest.ts | 11 +- .../test/coreApi/createContentModelTest.ts | 5 +- .../test/coreApi/createEditorContextTest.ts | 13 +- .../test/coreApi/formatContentModelTest.ts | 65 ++---- .../test/coreApi/pasteTest.ts | 13 +- .../test/coreApi/restoreUndoSnapshotTest.ts | 9 +- .../test/coreApi/setContentModelTest.ts | 5 +- .../test/coreApi/setDOMSelectionTest.ts | 38 ++-- .../test/coreApi/switchShadowEditTest.ts | 17 +- .../test/coreApi/triggerEventTest.ts | 12 +- .../corePlugin/ContentModelCachePluginTest.ts | 45 ++-- .../ContentModelFormatPluginTest.ts | 29 ++- .../test/corePlugin/DomEventPluginTest.ts | 18 +- .../test/corePlugin/EntityPluginTest.ts | 103 +++++---- .../test/corePlugin/LifecyclePluginTest.ts | 10 +- .../test/corePlugin/SelectionPluginTest.ts | 33 ++- .../test/corePlugin/UndoPluginTest.ts | 69 +++--- .../test/editor/StandaloneEditorTest.ts | 5 +- .../generatePasteOptionFromPluginsTest.ts | 47 ++-- .../test/utils/paste/mergePasteContentTest.ts | 7 +- .../lib/coreApi/getContent.ts | 4 +- .../lib/coreApi/setContent.ts | 54 ++++- .../lib/corePlugins/BridgePlugin.ts | 57 +++-- .../corePlugins/EventTypeTranslatePlugin.ts | 51 ----- .../lib/corePlugins/createCorePlugins.ts | 24 --- .../lib/editor/ContentModelEditor.ts | 48 +++-- .../lib/editor/utils/eventConverter.ts | 31 +++ .../lib/editor/utils/selectionConverter.ts | 86 +------- .../lib/index.ts | 5 +- .../publicTypes/ContentModelCorePlugins.ts | 27 +-- .../lib/publicTypes/IContentModelEditor.ts | 7 - .../test/corePlugins/BridgePluginTest.ts | 143 ++++++++++-- .../EventTypeTranslatePluginTest.ts | 56 ----- .../editor/utils/selectionConverterTest.ts | 204 +----------------- .../lib/edit/handleKeyboardEventCommon.ts | 7 +- .../lib/paste/ContentModelPastePlugin.ts | 57 ++--- .../Excel/processPastedContentFromExcel.ts | 4 +- .../processPastedContentFromPowerPoint.ts | 3 +- .../processPastedContentWacComponents.ts | 4 +- .../lib/paste/WordDesktop/getStyleMetadata.ts | 4 +- .../processPastedContentFromWordDesktop.ts | 4 +- .../pasteSourceValidations/getPasteSource.ts | 3 +- .../test/edit/editingTestCommon.ts | 4 +- .../edit/handleKeyboardEventCommonTest.ts | 15 +- .../test/paste/ContentModelPastePluginTest.ts | 60 ++---- .../test/paste/e2e/testUtils.ts | 2 +- .../test/paste/getStyleMetadataTest.ts | 4 +- .../getPasteSourceTest.ts | 3 +- .../processPastedContentFromPowerPointTest.ts | 24 ++- ...processPastedContentFromWordDesktopTest.ts | 4 +- .../lib/editor/EditorPlugin.ts | 2 +- .../lib/editor/IStandaloneEditor.ts | 17 +- .../lib/editor/StandaloneEditorCore.ts | 8 +- .../lib/event/ContentModelBeforePasteEvent.ts | 35 --- .../event/ContentModelContentChangedEvent.ts | 48 ----- .../ContentModelSelectionChangedEvent.ts | 30 --- .../lib/index.ts | 15 -- .../lib/parameter/DOMEventRecord.ts | 2 +- .../lib/createContentModelEditor.ts | 13 +- .../roosterjs-content-model/package.json | 1 - 91 files changed, 755 insertions(+), 1312 deletions(-) delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/corePlugins/EventTypeTranslatePlugin.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/corePlugins/EventTypeTranslatePluginTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts delete mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts delete mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ContentModelSelectionChangedEvent.ts diff --git a/demo/scripts/controls/BuildInPluginState.ts b/demo/scripts/controls/BuildInPluginState.ts index 709a0818ab0..fb61f825b38 100644 --- a/demo/scripts/controls/BuildInPluginState.ts +++ b/demo/scripts/controls/BuildInPluginState.ts @@ -22,7 +22,6 @@ export interface BuildInPluginList { tableEditMenu: boolean; contextMenu: boolean; autoFormat: boolean; - contentModelPaste: boolean; announce: boolean; } diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index 73561fd54c5..210f4b50eee 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -16,7 +16,6 @@ import SampleEntityPlugin from './sampleEntity/SampleEntityPlugin'; import SidePane from './sidePane/SidePane'; import TitleBar from './titleBar/TitleBar'; import { arrayPush } from 'roosterjs-editor-dom'; -import { ContentModelEditPlugin, EntityDelimiterPlugin } from 'roosterjs-content-model-plugins'; import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; import { ContentModelSegmentFormat, Snapshot } from 'roosterjs-content-model-types'; import { createEmojiPlugin, createPasteOptionPlugin } from 'roosterjs-react'; @@ -24,6 +23,11 @@ import { EditorPlugin, Snapshots } from 'roosterjs-editor-types'; import { getDarkColor } from 'roosterjs-color-utils'; import { PartialTheme } from '@fluentui/react/lib/Theme'; import { trustedHTMLHandler } from '../utils/trustedHTMLHandler'; +import { + ContentModelEditPlugin, + ContentModelPastePlugin, + EntityDelimiterPlugin, +} from 'roosterjs-content-model-plugins'; import { ContentModelEditor, ContentModelEditorOptions, @@ -104,6 +108,7 @@ class ContentModelEditorMainPane extends MainPaneBase private entityDelimiterPlugin: EntityDelimiterPlugin; private toggleablePlugins: EditorPlugin[] | null = null; private formatPainterPlugin: ContentModelFormatPainterPlugin; + private pastePlugin: ContentModelPastePlugin; private sampleEntityPlugin: SampleEntityPlugin; private snapshots: Snapshots; @@ -130,6 +135,7 @@ class ContentModelEditorMainPane extends MainPaneBase this.emojiPlugin = createEmojiPlugin(); this.entityDelimiterPlugin = new EntityDelimiterPlugin(); this.formatPainterPlugin = new ContentModelFormatPainterPlugin(); + this.pastePlugin = new ContentModelPastePlugin(); this.sampleEntityPlugin = new SampleEntityPlugin(); this.state = { showSidePane: window.location.hash != '', @@ -245,7 +251,11 @@ class ContentModelEditorMainPane extends MainPaneBase id={MainPaneBase.editorDivId} className={styles.editor} legacyPlugins={allPlugins} - plugins={[this.contentModelRibbonPlugin, this.formatPainterPlugin]} + plugins={[ + this.contentModelRibbonPlugin, + this.formatPainterPlugin, + this.pastePlugin, + ]} defaultSegmentFormat={defaultFormat} inDarkMode={this.state.isDarkMode} getDarkColor={getDarkColor} diff --git a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts index d4b4d03f30e..a5ad17cfb36 100644 --- a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts +++ b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts @@ -1,10 +1,10 @@ import MainPaneBase from '../../MainPaneBase'; import { applySegmentFormat, getFormatState } from 'roosterjs-content-model-api'; -import { PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelSegmentFormat, EditorPlugin, IStandaloneEditor, + PluginEvent, } from 'roosterjs-content-model-types'; const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg'); @@ -43,7 +43,7 @@ export default class ContentModelFormatPainterPlugin implements EditorPlugin { } onPluginEvent(event: PluginEvent) { - if (this.editor && event.eventType == PluginEventType.MouseUp) { + if (this.editor && event.eventType == 'mouseUp') { if (this.painterFormat) { applySegmentFormat(this.editor, this.painterFormat); diff --git a/demo/scripts/controls/getToggleablePlugins.ts b/demo/scripts/controls/getToggleablePlugins.ts index 501dc6d1c28..9a3512a312c 100644 --- a/demo/scripts/controls/getToggleablePlugins.ts +++ b/demo/scripts/controls/getToggleablePlugins.ts @@ -2,7 +2,6 @@ import BuildInPluginState, { BuildInPluginList, UrlPlaceholder } from './BuildIn import { Announce } from 'roosterjs-editor-plugins/lib/Announce'; import { AutoFormat } from 'roosterjs-editor-plugins/lib/AutoFormat'; import { ContentEdit } from 'roosterjs-editor-plugins/lib/ContentEdit'; -import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins'; import { CustomReplace as CustomReplacePlugin } from 'roosterjs-editor-plugins/lib/CustomReplace'; import { CutPasteListChain } from 'roosterjs-editor-plugins/lib/CutPasteListChain'; import { EditorPlugin, KnownAnnounceStrings } from 'roosterjs-editor-types'; @@ -60,7 +59,6 @@ export default function getToggleablePlugins(initState: BuildInPluginState) { ? createTableEditMenuProvider() : null, contextMenu: pluginList.contextMenu ? createContextMenuPlugin() : null, - contentModelPaste: pluginList.contentModelPaste ? new ContentModelPastePlugin() : null, announce: pluginList.announce ? new Announce(getDefaultStringsMap()) : null, }; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts index 23ee3a4ccb9..6e27346c751 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts @@ -1,10 +1,14 @@ import ContentModelRibbonButton from './ContentModelRibbonButton'; import RibbonPlugin from './RibbonPlugin'; -import { FormatState, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { FormatState } from 'roosterjs-editor-types'; import { getFormatState } from 'roosterjs-content-model-api'; import { getObjectKeys } from 'roosterjs-editor-dom'; import { LocalizedStrings, UIUtilities } from 'roosterjs-react'; -import { ContentModelFormatState, IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelFormatState, + IStandaloneEditor, + PluginEvent, +} from 'roosterjs-content-model-types'; export class ContentModelRibbonPlugin implements RibbonPlugin { private editor: IStandaloneEditor | null = null; @@ -47,14 +51,14 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { */ onPluginEvent(event: PluginEvent) { switch (event.eventType) { - case PluginEventType.EditorReady: - case PluginEventType.ContentChanged: - case PluginEventType.ZoomChanged: + case 'editorReady': + case 'contentChanged': + case 'zoomChanged': this.updateFormat(); break; - case PluginEventType.KeyDown: - case PluginEventType.MouseUp: + case 'keyDown': + case 'mouseUp': this.delayUpdate(); break; } diff --git a/demo/scripts/controls/ribbonButtons/contentModel/export.ts b/demo/scripts/controls/ribbonButtons/contentModel/export.ts index 8c957ca1239..b9db78b0149 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/export.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/export.ts @@ -1,7 +1,6 @@ import ContentModelRibbonButton from './ContentModelRibbonButton'; import { cloneModel } from 'roosterjs-content-model-core'; import { ContentModelEntityFormat } from 'roosterjs-content-model-types'; -import { EntityOperation, PluginEventType } from 'roosterjs-editor-types'; import { contentModelToDom, createModelToDomContext, @@ -45,8 +44,8 @@ export const exportContent: ContentModelRibbonButton = { }); if (isEntity && format.id && format.entityType) { - editor.triggerEvent(PluginEventType.EntityOperation, { - operation: EntityOperation.ReplaceTemporaryContent, + editor.triggerEvent('entityOperation', { + operation: 'replaceTemporaryContent', entity: { wrapper: clonedRoot, id: format.id, diff --git a/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts b/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts index 5227a5c0078..dfb958d69c3 100644 --- a/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts +++ b/demo/scripts/controls/sidePane/editorOptions/ContentModelEditorOptionsPlugin.ts @@ -20,7 +20,6 @@ const initialState: BuildInPluginState = { tableEditMenu: true, contextMenu: true, autoFormat: true, - contentModelPaste: true, announce: true, }, contentEditFeatures: getDefaultContentEditFeatureSettings(), diff --git a/demo/scripts/controls/sidePane/editorOptions/ContentModelPlugins.tsx b/demo/scripts/controls/sidePane/editorOptions/ContentModelPlugins.tsx index 5919929d6a9..a1944307c38 100644 --- a/demo/scripts/controls/sidePane/editorOptions/ContentModelPlugins.tsx +++ b/demo/scripts/controls/sidePane/editorOptions/ContentModelPlugins.tsx @@ -70,7 +70,6 @@ export default class ContentModelPlugins extends React.Component

); diff --git a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts index 8c111a1cbca..01c06bc44d0 100644 --- a/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controls/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -20,7 +20,6 @@ const initialState: BuildInPluginState = { tableEditMenu: true, contextMenu: true, autoFormat: true, - contentModelPaste: false, announce: true, }, contentEditFeatures: getDefaultContentEditFeatureSettings(), diff --git a/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts b/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts index 088f787b171..0148327ffff 100644 --- a/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts +++ b/demo/scripts/controls/sidePane/editorOptions/codes/PluginsCode.ts @@ -22,7 +22,7 @@ export default class PluginsCode extends CodeElement { this.plugins = [ pluginList.contentEdit && new ContentEditCode(state.contentEditFeatures), pluginList.hyperlink && new HyperLinkCode(state.linkTitle), - pluginList.contentModelPaste && new ContentModelPasteCode(), + new ContentModelPasteCode(), pluginList.watermark && new WatermarkCode(this.state.watermarkText), pluginList.imageEdit && new ImageEditCode(), pluginList.cutPasteListChain && new CutPasteListChainCode(), diff --git a/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx b/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx index b57357141c0..4fbe6876ec9 100644 --- a/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx +++ b/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { ContentModelContentChangedEvent } from 'roosterjs-content-model-types'; import { EntityOperation, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { SidePaneElementProps } from '../SidePaneElement'; import { @@ -177,25 +176,6 @@ export default class ContentModelEventViewPane extends React.Component< Source= {event.source}, Data= {event.data && event.data.toString && event.data.toString()} - {!!(event as ContentModelContentChangedEvent).contentModel && ( -
- Content Model -
-                                    {JSON.stringify(
-                                        (event as ContentModelContentChangedEvent).contentModel,
-                                        (key, value) =>
-                                            safeInstanceOf(value, 'Node')
-                                                ? Object.prototype.toString.apply(value)
-                                                : key == 'src'
-                                                ? value.length > 100
-                                                    ? value.substring(0, 97) + '...'
-                                                    : value
-                                                : value,
-                                        2
-                                    )}
-                                
-
- )} ); diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts index 86b0dc9c9d5..b2bc15d94f1 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts @@ -1,5 +1,4 @@ import formatImageWithContentModel from '../utils/formatImageWithContentModel'; -import { PluginEventType } from 'roosterjs-editor-types'; import { readFile, updateImageMetadata } from 'roosterjs-content-model-core'; import type { ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; @@ -24,7 +23,7 @@ export default function changeImage(editor: IStandaloneEditor, file: File) { image.format.height = ''; image.alt = ''; - editor.triggerEvent(PluginEventType.EditImage, { + editor.triggerEvent('editImage', { image: selection.image, previousSrc, newSrc: dataUrl, diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts index 47fac462ce7..2f5e29fdea4 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts @@ -1,7 +1,6 @@ import * as readFile from 'roosterjs-content-model-core/lib/publicApi/domUtils/readFile'; import changeImage from '../../../lib/publicApi/image/changeImage'; import { IStandaloneEditor } from 'roosterjs-content-model-types'; -import { PluginEventType } from 'roosterjs-editor-types'; import { ContentModelDocument, ContentModelFormatter, @@ -195,7 +194,7 @@ describe('changeImage', () => { ); expect(triggerEvent).toHaveBeenCalledTimes(1); - expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.EditImage, { + expect(triggerEvent).toHaveBeenCalledWith('editImage', { image: imageNode, newSrc: testUrl, previousSrc: 'test', diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts index 27224bb8001..176da2eee1d 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts @@ -1,8 +1,6 @@ import insertLink from '../../../lib/publicApi/link/insertLink'; -import { ChangeSource } from 'roosterjs-content-model-core'; -import { ContentModelEditor } from 'roosterjs-content-model-editor'; +import { ChangeSource, StandaloneEditor } from 'roosterjs-content-model-core'; import { IStandaloneEditor } from 'roosterjs-content-model-types'; -import { PluginEventType } from 'roosterjs-editor-types'; import { ContentModelDocument, ContentModelLink, @@ -330,8 +328,8 @@ describe('insertLink', () => { getName: () => 'mock', onPluginEvent: onPluginEvent, }; - const editor = new ContentModelEditor(div, { - legacyPlugins: [mockedPlugin], + const editor = new StandaloneEditor(div, { + plugins: [mockedPlugin], }); editor.focus(); @@ -344,12 +342,10 @@ describe('insertLink', () => { expect(a!.outerHTML).toBe('http://test.com'); expect(onPluginEvent).toHaveBeenCalledWith({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.CreateLink, data: a, - additionalData: { - formatApiName: 'insertLink', - }, + formatApiName: 'insertLink', contentModel: jasmine.anything(), selection: jasmine.anything(), changedEntities: [], diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/attachDomEvent.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/attachDomEvent.ts index d716ed00748..833989ebee0 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/attachDomEvent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/attachDomEvent.ts @@ -1,6 +1,5 @@ import { getObjectKeys } from 'roosterjs-content-model-dom'; -import type { AttachDomEvent } from 'roosterjs-content-model-types'; -import type { PluginDomEvent } from 'roosterjs-editor-types'; +import type { AttachDomEvent, PluginEvent } from 'roosterjs-content-model-types'; /** * @internal @@ -22,7 +21,7 @@ export const attachDomEvent: AttachDomEvent = (core, eventMap) => { if (pluginEventType != null) { core.api.triggerEvent( core, - { + { eventType: pluginEventType, rawEvent: event, }, diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts index 01e9dad0e5d..4f3c8bed1ec 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts @@ -1,8 +1,7 @@ import { ChangeSource } from '../constants/ChangeSource'; -import { PluginEventType } from 'roosterjs-editor-types'; import type { ChangedEntity, - ContentModelContentChangedEvent, + ContentChangedEvent, DOMSelection, FormatContentModel, FormatWithContentModelContext, @@ -71,15 +70,13 @@ export const formatContentModel: FormatContentModel = (core, formatter, options) } } - const eventData: ContentModelContentChangedEvent = { - eventType: PluginEventType.ContentChanged, + const eventData: ContentChangedEvent = { + eventType: 'contentChanged', contentModel: clearModelCache ? undefined : model, selection: clearModelCache ? undefined : selection, source: changeSource || ChangeSource.Format, data: getChangeData?.(), - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: getChangedEntities(context, rawEvent), }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot.ts index 45854165dea..da85aa4c9c8 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/restoreUndoSnapshot.ts @@ -1,12 +1,8 @@ import { ChangeSource } from '../constants/ChangeSource'; -import { PluginEventType } from 'roosterjs-editor-types'; import { restoreSnapshotColors } from '../utils/restoreSnapshotColors'; import { restoreSnapshotHTML } from '../utils/restoreSnapshotHTML'; import { restoreSnapshotSelection } from '../utils/restoreSnapshotSelection'; -import type { - ContentModelContentChangedEvent, - RestoreUndoSnapshot, -} from 'roosterjs-content-model-types'; +import type { ContentChangedEvent, RestoreUndoSnapshot } from 'roosterjs-content-model-types'; /** * @internal @@ -18,7 +14,7 @@ export const restoreUndoSnapshot: RestoreUndoSnapshot = (core, snapshot) => { core.api.triggerEvent( core, { - eventType: PluginEventType.BeforeSetContent, + eventType: 'beforeSetContent', newContent: snapshot.html, }, true /*broadcast*/ @@ -31,8 +27,8 @@ export const restoreUndoSnapshot: RestoreUndoSnapshot = (core, snapshot) => { restoreSnapshotSelection(core, snapshot); restoreSnapshotColors(core, snapshot); - const event: ContentModelContentChangedEvent = { - eventType: PluginEventType.ContentChanged, + const event: ContentChangedEvent = { + eventType: 'contentChanged', entityStates: snapshot.entityStates, source: ChangeSource.SetContent, }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts index 37e969891bd..475372cd736 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts @@ -1,9 +1,8 @@ import { addRangeToSelection } from '../corePlugin/utils/addRangeToSelection'; import { isNodeOfType, toArray } from 'roosterjs-content-model-dom'; import { parseTableCells } from '../publicApi/domUtils/tableCellUtils'; -import { PluginEventType } from 'roosterjs-editor-types'; import type { - ContentModelSelectionChangedEvent, + SelectionChangedEvent, SetDOMSelection, TableSelection, } from 'roosterjs-content-model-types'; @@ -85,10 +84,9 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC } if (!skipSelectionChangedEvent) { - const eventData: ContentModelSelectionChangedEvent = { - eventType: PluginEventType.SelectionChanged, + const eventData: SelectionChangedEvent = { + eventType: 'selectionChanged', newSelection: selection, - selectionRangeEx: null, }; core.api.triggerEvent(core, eventData, true /*broadcast*/); diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts index cdc8324d67e..2829c58c888 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts @@ -1,6 +1,5 @@ import { iterateSelections } from '../publicApi/selection/iterateSelections'; import { moveChildNodes } from 'roosterjs-content-model-dom'; -import { PluginEventType } from 'roosterjs-editor-types'; import type { SwitchShadowEdit } from 'roosterjs-content-model-types'; /** @@ -27,9 +26,7 @@ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => { core.api.triggerEvent( core, { - eventType: PluginEventType.EnteredShadowEdit, - fragment, - selectionPath: null, + eventType: 'enteredShadowEdit', }, false /*broadcast*/ ); @@ -47,7 +44,7 @@ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => { core.api.triggerEvent( core, { - eventType: PluginEventType.LeavingShadowEdit, + eventType: 'leavingShadowEdit', }, false /*broadcast*/ ); diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts index 45172495ce9..a4dc7587f09 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/triggerEvent.ts @@ -1,12 +1,15 @@ -import { PluginEventType } from 'roosterjs-editor-types'; -import type { EditorPlugin, TriggerEvent } from 'roosterjs-content-model-types'; -import type { PluginEvent } from 'roosterjs-editor-types'; +import type { + EditorPlugin, + PluginEvent, + PluginEventType, + TriggerEvent, +} from 'roosterjs-content-model-types'; const allowedEventsInShadowEdit: PluginEventType[] = [ - PluginEventType.EditorReady, - PluginEventType.BeforeDispose, - PluginEventType.ExtractContentWithDom, - PluginEventType.ZoomChanged, + 'editorReady', + 'beforeDispose', + 'extractContentWithDom', + 'zoomChanged', ]; /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts index 082de09701a..892a85cb1e5 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts @@ -1,15 +1,14 @@ import { areSameSelection } from './utils/areSameSelection'; import { contentModelDomIndexer } from './utils/contentModelDomIndexer'; import { isCharacterValue } from '../publicApi/domUtils/eventUtils'; -import { PluginEventType } from 'roosterjs-editor-types'; import type { ContentModelCachePluginState, - ContentModelContentChangedEvent, IStandaloneEditor, + KeyDownEvent, + PluginEvent, PluginWithState, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; -import type { PluginEvent, PluginKeyDownEvent } from 'roosterjs-editor-types'; /** * ContentModel cache plugin manages cached Content Model, and refresh the cache when necessary @@ -80,25 +79,25 @@ class ContentModelCachePlugin implements PluginWithState { { [P in keyof HTMLElementEventMap]: DOMEventRecord } > = { // 1. Keyboard event - keypress: this.getEventHandler(PluginEventType.KeyPress), - keydown: this.getEventHandler(PluginEventType.KeyDown), - keyup: this.getEventHandler(PluginEventType.KeyUp), + keypress: this.getEventHandler('keyPress'), + keydown: this.getEventHandler('keyDown'), + keyup: this.getEventHandler('keyUp'), // 2. Mouse event mousedown: { beforeDispatch: this.onMouseDown }, @@ -76,7 +76,7 @@ class DOMEventPlugin implements PluginWithState { drop: { beforeDispatch: this.onDrop }, // 5. Input event - input: this.getEventHandler(PluginEventType.Input), + input: this.getEventHandler('input'), }; this.disposer = this.editor.attachDomEvent(>eventHandlers); @@ -126,7 +126,7 @@ class DOMEventPlugin implements PluginWithState { doc?.defaultView?.requestAnimationFrame(() => { if (this.editor) { this.editor.takeSnapshot(); - this.editor.triggerEvent(PluginEventType.ContentChanged, { + this.editor.triggerEvent('contentChanged', { source: ChangeSource.Drop, }); } @@ -134,7 +134,7 @@ class DOMEventPlugin implements PluginWithState { }; private onScroll = (e: Event) => { - this.editor?.triggerEvent(PluginEventType.Scroll, { + this.editor?.triggerEvent('scroll', { rawEvent: e, scrollContainer: this.state.scrollContainer, }); @@ -142,7 +142,7 @@ class DOMEventPlugin implements PluginWithState { private getEventHandler(eventType: PluginEventType): DOMEventRecord { const beforeDispatch = (event: Event) => - eventType == PluginEventType.Input + eventType == 'input' ? this.onInputEvent(event) : this.onKeyboardEvent(event); @@ -175,7 +175,7 @@ class DOMEventPlugin implements PluginWithState { this.state.mouseDownY = event.pageY; } - this.editor.triggerEvent(PluginEventType.MouseDown, { + this.editor.triggerEvent('mouseDown', { rawEvent: event, }); } @@ -184,7 +184,7 @@ class DOMEventPlugin implements PluginWithState { private onMouseUp = (rawEvent: MouseEvent) => { if (this.editor) { this.removeMouseUpEventListener(); - this.editor.triggerEvent(PluginEventType.MouseUp, { + this.editor.triggerEvent('mouseUp', { rawEvent, isClicking: this.state.mouseDownX == rawEvent.pageX && @@ -199,7 +199,7 @@ class DOMEventPlugin implements PluginWithState { private onCompositionEnd = (rawEvent: CompositionEvent) => { this.state.isInIME = false; - this.editor?.triggerEvent(PluginEventType.CompositionEnd, { + this.editor?.triggerEvent('compositionEnd', { rawEvent, }); }; diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts index 1ae78b04e38..9e18dcb04f9 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts @@ -1,4 +1,3 @@ -import { EntityOperation as LegacyEntityOperation, PluginEventType } from 'roosterjs-editor-types'; import { findAllEntities } from './utils/findAllEntities'; import { transformColor } from '../publicApi/color/transformColor'; import { @@ -11,29 +10,18 @@ import { } from 'roosterjs-content-model-dom'; import type { ChangedEntity, - ContentModelContentChangedEvent, + ContentChangedEvent, ContentModelEntityFormat, EntityOperation, EntityPluginState, IStandaloneEditor, + MouseUpEvent, + PluginEvent, PluginWithState, } from 'roosterjs-content-model-types'; -import type { ContentChangedEvent, PluginEvent, PluginMouseUpEvent } from 'roosterjs-editor-types'; const ENTITY_ID_REGEX = /_(\d{1,8})$/; -// This is only used for compatibility with old editor -// TODO: Remove this map once we have standalone editor -const EntityOperationMap: Record = { - newEntity: LegacyEntityOperation.NewEntity, - overwrite: LegacyEntityOperation.Overwrite, - removeFromEnd: LegacyEntityOperation.RemoveFromEnd, - removeFromStart: LegacyEntityOperation.RemoveFromStart, - replaceTemporaryContent: LegacyEntityOperation.ReplaceTemporaryContent, - updateEntityState: LegacyEntityOperation.UpdateEntityState, - click: LegacyEntityOperation.Click, -}; - /** * Entity Plugin helps handle all operations related to an entity and generate entity specified events */ @@ -87,24 +75,24 @@ class EntityPlugin implements PluginWithState { onPluginEvent(event: PluginEvent) { if (this.editor) { switch (event.eventType) { - case PluginEventType.MouseUp: + case 'mouseUp': this.handleMouseUpEvent(this.editor, event); break; - case PluginEventType.ContentChanged: + case 'contentChanged': this.handleContentChangedEvent(this.editor, event); break; - case PluginEventType.EditorReady: + case 'editorReady': this.handleContentChangedEvent(this.editor); break; - case PluginEventType.ExtractContentWithDom: + case 'extractContentWithDom': this.handleExtractContentWithDomEvent(this.editor, event.clonedRoot); break; } } } - private handleMouseUpEvent(editor: IStandaloneEditor, event: PluginMouseUpEvent) { + private handleMouseUpEvent(editor: IStandaloneEditor, event: MouseUpEvent) { const { rawEvent, isClicking } = event; let node: Node | null = rawEvent.target as Node; @@ -121,10 +109,9 @@ class EntityPlugin implements PluginWithState { } private handleContentChangedEvent(editor: IStandaloneEditor, event?: ContentChangedEvent) { - const cmEvent = event as ContentModelContentChangedEvent | undefined; const modifiedEntities: ChangedEntity[] = - cmEvent?.changedEntities ?? this.getChangedEntities(editor); - const entityStates = cmEvent?.entityStates; + event?.changedEntities ?? this.getChangedEntities(editor); + const entityStates = event?.entityStates; modifiedEntities.forEach(entry => { const { entity, operation, rawEvent } = entry; @@ -248,8 +235,8 @@ class EntityPlugin implements PluginWithState { }); return format.id && format.entityType && !format.isFakeEntity - ? editor.triggerEvent(PluginEventType.EntityOperation, { - operation: EntityOperationMap[operation], + ? editor.triggerEvent('entityOperation', { + operation: operation, rawEvent, entity: { id: format.id, diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts index 433004a5455..0a8ef189fcc 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/LifecyclePlugin.ts @@ -1,5 +1,4 @@ import { ChangeSource } from '../constants/ChangeSource'; -import { PluginEventType } from 'roosterjs-editor-types'; import { createBr, createContentModelDocument, @@ -12,10 +11,10 @@ import type { ContentModelSegmentFormat, IStandaloneEditor, LifecyclePluginState, + PluginEvent, PluginWithState, StandaloneEditorOptions, } from 'roosterjs-content-model-types'; -import type { PluginEvent } from 'roosterjs-editor-types'; const ContentEditableAttributeName = 'contenteditable'; const DefaultTextColor = '#000000'; @@ -92,14 +91,14 @@ class LifecyclePlugin implements PluginWithState { this.adjustColor(); // Let other plugins know that we are ready - this.editor.triggerEvent(PluginEventType.EditorReady, {}, true /*broadcast*/); + this.editor.triggerEvent('editorReady', {}, true /*broadcast*/); } /** * Dispose this plugin */ dispose() { - this.editor?.triggerEvent(PluginEventType.BeforeDispose, {}, true /*broadcast*/); + this.editor?.triggerEvent('beforeDispose', {}, true /*broadcast*/); if (this.disposer) { this.disposer(); @@ -123,7 +122,7 @@ class LifecyclePlugin implements PluginWithState { */ onPluginEvent(event: PluginEvent) { if ( - event.eventType == PluginEventType.ContentChanged && + event.eventType == 'contentChanged' && (event.source == ChangeSource.SwitchToDarkMode || event.source == ChangeSource.SwitchToLightMode) ) { diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts index 28a9957ab04..f06475293d9 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/SelectionPlugin.ts @@ -1,10 +1,9 @@ import { isElementOfType, isNodeOfType, toArray } from 'roosterjs-content-model-dom'; import { isModifierKey } from '../publicApi/domUtils/eventUtils'; -import { PluginEventType } from 'roosterjs-editor-types'; -import type { PluginEvent } from 'roosterjs-editor-types'; import type { DOMSelection, IStandaloneEditor, + PluginEvent, PluginWithState, SelectionPluginState, StandaloneEditorOptions, @@ -94,7 +93,7 @@ class SelectionPlugin implements PluginWithState { let selection: DOMSelection | null; switch (event.eventType) { - case PluginEventType.MouseUp: + case 'mouseUp': if ( (image = this.getClickingImage(event.rawEvent)) && image.isContentEditable && @@ -105,7 +104,7 @@ class SelectionPlugin implements PluginWithState { } break; - case PluginEventType.MouseDown: + case 'mouseDown': selection = this.editor.getDOMSelection(); if ( event.rawEvent.button === MouseRightButton && @@ -121,7 +120,7 @@ class SelectionPlugin implements PluginWithState { } break; - case PluginEventType.KeyDown: + case 'keyDown': const rawEvent = event.rawEvent; const key = rawEvent.key; selection = this.editor.getDOMSelection(); diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts index 9fef80e748b..b357f7101f5 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/UndoPlugin.ts @@ -1,15 +1,15 @@ import { ChangeSource } from '../constants/ChangeSource'; import { createSnapshotsManager } from '../editor/SnapshotsManagerImpl'; import { isCursorMovingKey } from '../publicApi/domUtils/eventUtils'; -import { PluginEventType } from 'roosterjs-editor-types'; import { undo } from '../publicApi/undo/undo'; import type { + ContentChangedEvent, IStandaloneEditor, + PluginEvent, PluginWithState, StandaloneEditorOptions, UndoPluginState, } from 'roosterjs-content-model-types'; -import type { ContentChangedEvent, PluginEvent } from 'roosterjs-editor-types'; const Backspace = 'Backspace'; const Delete = 'Delete'; @@ -73,7 +73,7 @@ class UndoPlugin implements PluginWithState { willHandleEventExclusively(event: PluginEvent) { return ( !!this.editor && - event.eventType == PluginEventType.KeyDown && + event.eventType == 'keyDown' && event.rawEvent.key == Backspace && !event.rawEvent.ctrlKey && this.canUndoAutoComplete(this.editor) @@ -91,7 +91,7 @@ class UndoPlugin implements PluginWithState { } switch (event.eventType) { - case PluginEventType.EditorReady: + case 'editorReady': const manager = this.state.snapshotsManager; const canUndo = manager.hasNewContent || manager.canMove(-1); const canRedo = manager.canMove(1); @@ -102,20 +102,20 @@ class UndoPlugin implements PluginWithState { this.addUndoSnapshot(); } break; - case PluginEventType.KeyDown: + case 'keyDown': this.onKeyDown(this.editor, event.rawEvent); break; - case PluginEventType.KeyPress: + case 'keyPress': this.onKeyPress(this.editor, event.rawEvent); break; - case PluginEventType.CompositionEnd: + case 'compositionEnd': this.clearRedoForInput(); this.addUndoSnapshot(); break; - case PluginEventType.ContentChanged: + case 'contentChanged': this.onContentChanged(event); break; - case PluginEventType.BeforeKeyboardEditing: + case 'beforeKeyboardEditing': this.onBeforeKeyboardEditing(event.rawEvent); break; } diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts index fa89494de53..4d5ea0c5eb1 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -1,12 +1,7 @@ import { ChangeSource } from '../constants/ChangeSource'; import { createStandaloneEditorCore } from './createStandaloneEditorCore'; -import { PluginEventType } from 'roosterjs-editor-types'; import { transformColor } from '../publicApi/color/transformColor'; -import type { - DarkColorHandler, - PluginEventData, - PluginEventFromType, -} from 'roosterjs-editor-types'; +import type { DarkColorHandler, TrustedHTMLHandler } from 'roosterjs-editor-types'; import type { ClipboardData, ContentModelDocument, @@ -21,6 +16,9 @@ import type { ModelToDomOption, OnNodeCreated, PasteType, + PluginEventData, + PluginEventFromType, + PluginEventType, Snapshot, SnapshotsManager, StandaloneEditorCore, @@ -270,7 +268,7 @@ export class StandaloneEditor implements IStandaloneEditor { core.api.triggerEvent( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: isDarkMode ? ChangeSource.SwitchToDarkMode : ChangeSource.SwitchToLightMode, @@ -369,7 +367,7 @@ export class StandaloneEditor implements IStandaloneEditor { if (oldValue != scale) { this.triggerEvent( - PluginEventType.ZoomChanged, + 'zoomChanged', { oldZoomScale: oldValue, newZoomScale: scale, @@ -380,6 +378,16 @@ export class StandaloneEditor implements IStandaloneEditor { } } + /** + * Get a function to convert HTML string to trusted HTML string. + * By default it will just return the input HTML directly. To override this behavior, + * pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + */ + getTrustedHTMLHandler(): TrustedHTMLHandler { + return this.getCore().trustedHTMLHandler; + } + /** * @returns the current StandaloneEditorCore object * @throws a standard Error if there's no core object diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts index a0f1a0c346b..1e230b5da39 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/generatePasteOptionFromPlugins.ts @@ -1,22 +1,12 @@ -import { PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; import type { HtmlFromClipboard } from './retrieveHtmlInfo'; import type { + BeforePasteEvent, ClipboardData, - ContentModelBeforePasteEvent, DomToModelOptionForPaste, PasteType, StandaloneEditorCore, } from 'roosterjs-content-model-types'; -// Map new PasteType to old PasteType -// TODO: We can remove this once we have standalone editor -const PasteTypeMap: Record = { - asImage: OldPasteType.AsImage, - asPlainText: OldPasteType.AsPlainText, - mergeFormat: OldPasteType.MergeFormat, - normal: OldPasteType.Normal, -}; - /** * @internal */ @@ -26,7 +16,7 @@ export function generatePasteOptionFromPlugins( fragment: DocumentFragment, htmlFromClipboard: HtmlFromClipboard, pasteType: PasteType -): ContentModelBeforePasteEvent { +): BeforePasteEvent { const domToModelOption: DomToModelOptionForPaste = { additionalAllowedTags: [], additionalDisallowedTags: [], @@ -37,30 +27,15 @@ export function generatePasteOptionFromPlugins( attributeSanitizers: {}, }; - const event: ContentModelBeforePasteEvent = { - eventType: PluginEventType.BeforePaste, + const event: BeforePasteEvent = { + eventType: 'beforePaste', clipboardData, fragment, htmlBefore: htmlFromClipboard.htmlBefore ?? '', htmlAfter: htmlFromClipboard.htmlAfter ?? '', htmlAttributes: htmlFromClipboard.metadata, - pasteType: PasteTypeMap[pasteType], + pasteType: pasteType, domToModelOption, - - // Deprecated - sanitizingOption: { - elementCallbacks: {}, - attributeCallbacks: {}, - cssStyleCallbacks: {}, - additionalTagReplacements: {}, - additionalAllowedAttributes: [], - additionalAllowedCssClasses: [], - additionalDefaultStyleValues: {}, - additionalGlobalStyleNodes: [], - additionalPredefinedCssForElement: {}, - preserveHtmlComments: false, - unknownTagReplacement: null, - }, }; if (pasteType !== 'asPlainText') { diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts index d46cb78db16..55240901c11 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/paste/mergePasteContent.ts @@ -7,10 +7,9 @@ import { getSelectedSegments } from '../../publicApi/selection/collectSelections import { mergeModel } from '../../publicApi/model/mergeModel'; import { pasteDisplayFormatParser } from '../../override/pasteDisplayFormatParser'; import { pasteTextProcessor } from '../../override/pasteTextProcessor'; -import { PasteType } from 'roosterjs-editor-types'; import type { MergeModelOption } from '../../publicApi/model/mergeModel'; import type { - ContentModelBeforePasteEvent, + BeforePasteEvent, ContentModelDocument, ContentModelSegmentFormat, DomToModelOption, @@ -37,7 +36,7 @@ const EmptySegmentFormat: Required = { export function mergePasteContent( model: ContentModelDocument, context: FormatWithContentModelContext, - eventResult: ContentModelBeforePasteEvent, + eventResult: BeforePasteEvent, defaultDomToModelOptions: DomToModelOption ) { const { fragment, domToModelOption, customizedMerge, pasteType } = eventResult; @@ -65,7 +64,7 @@ export function mergePasteContent( const pasteModel = domToContentModel(fragment, domToModelContext); const mergeOption: MergeModelOption = { - mergeFormat: pasteType == PasteType.MergeFormat ? 'keepSourceEmphasisFormat' : 'none', + mergeFormat: pasteType == 'mergeFormat' ? 'keepSourceEmphasisFormat' : 'none', mergeTable: shouldMergeTable(pasteModel), }; diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/attachDomEventTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/attachDomEventTest.ts index 5db76999f7c..92163c4f6ac 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/attachDomEventTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/attachDomEventTest.ts @@ -1,5 +1,4 @@ import { attachDomEvent } from '../../lib/coreApi/attachDomEvent'; -import { PluginEventType } from 'roosterjs-editor-types'; import { StandaloneEditorCore } from 'roosterjs-content-model-types'; describe('attachDomEvent', () => { @@ -57,7 +56,7 @@ describe('attachDomEvent', () => { core.api.triggerEvent = triggerEventSpy; const disposer = attachDomEvent(core, { - keydown: { pluginEventType: PluginEventType.KeyDown }, + keydown: { pluginEventType: 'keyDown' }, }); const event = document.createEvent('KeyboardEvent'); event.initEvent('keydown'); @@ -66,7 +65,7 @@ describe('attachDomEvent', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: event, }, false @@ -81,7 +80,7 @@ describe('attachDomEvent', () => { core.api.triggerEvent = triggerEventSpy; const disposer = attachDomEvent(core, { keydown: { - pluginEventType: PluginEventType.KeyDown, + pluginEventType: 'keyDown', beforeDispatch: callback, }, }); @@ -93,7 +92,7 @@ describe('attachDomEvent', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: event, }, false @@ -109,7 +108,7 @@ describe('attachDomEvent', () => { const disposer = attachDomEvent(core, { keydown: { - pluginEventType: PluginEventType.KeyDown, + pluginEventType: 'keyDown', beforeDispatch: callback, }, }); diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts index e9810252e3a..49f99ac25cd 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts @@ -2,7 +2,6 @@ import * as cloneModel from '../../lib/publicApi/model/cloneModel'; import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import { createContentModel } from '../../lib/coreApi/createContentModel'; -import { EditorCore } from 'roosterjs-editor-types'; import { StandaloneEditorCore } from 'roosterjs-content-model-types'; const mockedEditorContext = 'EDITORCONTEXT' as any; @@ -13,7 +12,7 @@ const mockedCachedMode = 'CACHEDMODEL' as any; const mockedClonedModel = 'CLONEDMODEL' as any; describe('createContentModel', () => { - let core: StandaloneEditorCore & EditorCore; + let core: StandaloneEditorCore; let createEditorContext: jasmine.Spy; let getDOMSelection: jasmine.Spy; let domToContentModelSpy: jasmine.Spy; @@ -45,7 +44,7 @@ describe('createContentModel', () => { }, lifecycle: {}, domToModelSettings: {}, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; }); it('Reuse model, no cache, no shadow edit', () => { diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts index f9fd8365aac..afb96e7d699 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts @@ -1,5 +1,4 @@ import { createEditorContext } from '../../lib/coreApi/createEditorContext'; -import { EditorCore } from 'roosterjs-editor-types'; import { StandaloneEditorCore } from 'roosterjs-content-model-types'; describe('createEditorContext', () => { @@ -29,7 +28,7 @@ describe('createEditorContext', () => { }, darkColorHandler, cache: {}, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; const context = createEditorContext(core); @@ -72,7 +71,7 @@ describe('createEditorContext', () => { cache: { domIndexer, }, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; const context = createEditorContext(core); @@ -88,7 +87,7 @@ describe('createEditorContext', () => { }); describe('createEditorContext - checkZoomScale', () => { - let core: StandaloneEditorCore & EditorCore; + let core: StandaloneEditorCore; let div: any; let getComputedStyleSpy: jasmine.Spy; let getBoundingClientRectSpy: jasmine.Spy; @@ -118,7 +117,7 @@ describe('createEditorContext - checkZoomScale', () => { }, darkColorHandler, cache: {}, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; }); it('Zoom scale = 1', () => { @@ -180,7 +179,7 @@ describe('createEditorContext - checkZoomScale', () => { }); describe('createEditorContext - checkRootDir', () => { - let core: StandaloneEditorCore & EditorCore; + let core: StandaloneEditorCore; let div: any; let getComputedStyleSpy: jasmine.Spy; let getBoundingClientRectSpy: jasmine.Spy; @@ -210,7 +209,7 @@ describe('createEditorContext - checkRootDir', () => { }, darkColorHandler, cache: {}, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; }); it('LTR CSS', () => { diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts index e6356038da4..b8255fdf113 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts @@ -1,7 +1,6 @@ import * as transformColor from '../../lib/publicApi/color/transformColor'; import { ChangeSource } from '../../lib/constants/ChangeSource'; import { createImage } from 'roosterjs-content-model-dom'; -import { EditorCore, PluginEventType } from 'roosterjs-editor-types'; import { formatContentModel } from '../../lib/coreApi/formatContentModel'; import { ContentModelDocument, @@ -11,7 +10,7 @@ import { } from 'roosterjs-content-model-types'; describe('formatContentModel', () => { - let core: StandaloneEditorCore & EditorCore; + let core: StandaloneEditorCore; let addUndoSnapshot: jasmine.Spy; let createContentModel: jasmine.Spy; let setContentModel: jasmine.Spy; @@ -57,7 +56,7 @@ describe('formatContentModel', () => { undo: { snapshotsManager: {}, }, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; }); describe('Editor has focus', () => { @@ -102,14 +101,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -139,14 +136,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -172,14 +167,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: 'TEST', data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -214,14 +207,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: 'TEST', data: returnData, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -253,14 +244,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -306,14 +295,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [ { entity: entity1, @@ -368,14 +355,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: mockedData, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [ { entity: entity1, @@ -410,14 +395,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -452,14 +435,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: mockedModel, selection: mockedSelection, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true @@ -485,14 +466,12 @@ describe('formatContentModel', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', contentModel: undefined, selection: undefined, source: ChangeSource.Format, data: undefined, - additionalData: { - formatApiName: apiName, - }, + formatApiName: apiName, changedEntities: [], }, true diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts index 742313df250..0e029f60716 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/pasteTest.ts @@ -9,7 +9,6 @@ import * as PPT from 'roosterjs-content-model-plugins/lib/paste/PowerPoint/proce import * as setProcessorF from 'roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; import * as WacComponents from 'roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; import * as WordDesktopFile from 'roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop'; -import { BeforePasteEvent, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; import { ContentModelPastePlugin } from 'roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin'; import { expectEqual, initEditor } from 'roosterjs-content-model-plugins/test/paste/e2e/testUtils'; @@ -22,6 +21,8 @@ import { FormatWithContentModelContext, FormatWithContentModelOptions, IStandaloneEditor, + BeforePasteEvent, + PluginEvent, } from 'roosterjs-content-model-types'; let clipboardData: ClipboardData; @@ -104,7 +105,7 @@ describe('Paste ', () => { context = undefined; editor = new ContentModelEditor(div, { - legacyPlugins: [new ContentModelPastePlugin()], + plugins: [new ContentModelPastePlugin()], coreApiOverride: { focus, createContentModel, @@ -194,7 +195,7 @@ describe('paste with content model & paste plugin', () => { div = document.createElement('div'); document.body.appendChild(div); editor = new ContentModelEditor(div, { - legacyPlugins: [new ContentModelPastePlugin()], + plugins: [new ContentModelPastePlugin()], }); spyOn(addParserF, 'default').and.callThrough(); spyOn(setProcessorF, 'setProcessor').and.callThrough(); @@ -341,13 +342,13 @@ describe('paste with content model & paste plugin', () => { let eventChecker: BeforePasteEvent = {}; editor = new ContentModelEditor(div!, { - legacyPlugins: [ + plugins: [ { initialize: () => {}, dispose: () => {}, getName: () => 'test', onPluginEvent(event: PluginEvent) { - if (event.eventType === PluginEventType.BeforePaste) { + if (event.eventType === 'beforePaste') { eventChecker = event; } }, @@ -360,7 +361,7 @@ describe('paste with content model & paste plugin', () => { expect(eventChecker?.clipboardData).toEqual(clipboardData); expect(eventChecker?.htmlBefore).toBeTruthy(); expect(eventChecker?.htmlAfter).toBeTruthy(); - expect(eventChecker?.pasteType).toEqual(0); + expect(eventChecker?.pasteType).toEqual('normal'); }); }); diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshotTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshotTest.ts index a19166ac39d..81707e39596 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshotTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/restoreUndoSnapshotTest.ts @@ -2,7 +2,6 @@ import * as restoreSnapshotColors from '../../lib/utils/restoreSnapshotColors'; import * as restoreSnapshotHTML from '../../lib/utils/restoreSnapshotHTML'; import * as restoreSnapshotSelection from '../../lib/utils/restoreSnapshotSelection'; import { ChangeSource } from '../../lib/constants/ChangeSource'; -import { PluginEventType } from 'roosterjs-editor-types'; import { restoreUndoSnapshot } from '../../lib/coreApi/restoreUndoSnapshot'; import { Snapshot, StandaloneEditorCore } from 'roosterjs-content-model-types'; @@ -44,7 +43,7 @@ describe('restoreUndoSnapshot', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.BeforeSetContent, + eventType: 'beforeSetContent', newContent: mockedHTML, }, true @@ -52,7 +51,7 @@ describe('restoreUndoSnapshot', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', entityStates: undefined, source: ChangeSource.SetContent, }, @@ -77,7 +76,7 @@ describe('restoreUndoSnapshot', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.BeforeSetContent, + eventType: 'beforeSetContent', newContent: mockedHTML, }, true @@ -85,7 +84,7 @@ describe('restoreUndoSnapshot', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', entityStates: mockedEntityState, source: ChangeSource.SetContent, }, diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts index 0940f354c93..bfacc023283 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts @@ -1,6 +1,5 @@ import * as contentModelToDom from 'roosterjs-content-model-dom/lib/modelToDom/contentModelToDom'; import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext'; -import { EditorCore } from 'roosterjs-editor-types'; import { setContentModel } from '../../lib/coreApi/setContentModel'; import { StandaloneEditorCore } from 'roosterjs-content-model-types'; @@ -15,7 +14,7 @@ const mockedDiv = { ownerDocument: mockedDoc } as any; const mockedConfig = 'CONFIG' as any; describe('setContentModel', () => { - let core: StandaloneEditorCore & EditorCore; + let core: StandaloneEditorCore; let contentModelToDomSpy: jasmine.Spy; let createEditorContext: jasmine.Spy; let createModelToDomContextSpy: jasmine.Spy; @@ -53,7 +52,7 @@ describe('setContentModel', () => { modelToDomSettings: { calculated: mockedConfig, }, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; }); it('no default option, no shadow edit', () => { diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts index 2f86441a183..af10ea66f3a 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/setDOMSelectionTest.ts @@ -1,6 +1,6 @@ import * as addRangeToSelection from '../../lib/corePlugin/utils/addRangeToSelection'; import { createElement } from 'roosterjs-editor-dom'; -import { CreateElementData, PluginEventType } from 'roosterjs-editor-types'; +import { CreateElementData } from 'roosterjs-editor-types'; import { DOMSelection, StandaloneEditorCore } from 'roosterjs-content-model-types'; import { setDOMSelection } from '../../lib/coreApi/setDOMSelection'; @@ -77,8 +77,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: null, }, true @@ -143,8 +142,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -176,8 +174,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -230,8 +227,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -262,8 +258,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -296,8 +291,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -330,8 +324,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -379,8 +372,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -428,8 +420,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -477,8 +468,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -535,8 +525,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true @@ -589,8 +578,7 @@ describe('setDOMSelection', () => { expect(triggerEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, + eventType: 'selectionChanged', newSelection: mockedSelection, }, true diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts index 19fc938f8e6..40c40ba5006 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts @@ -1,5 +1,4 @@ import * as iterateSelections from '../../lib/publicApi/selection/iterateSelections'; -import { EditorCore, PluginEventType } from 'roosterjs-editor-types'; import { StandaloneEditorCore } from 'roosterjs-content-model-types'; import { switchShadowEdit } from '../../lib/coreApi/switchShadowEdit'; @@ -7,7 +6,7 @@ const mockedModel = 'MODEL' as any; const mockedCachedModel = 'CACHEMODEL' as any; describe('switchShadowEdit', () => { - let core: StandaloneEditorCore & EditorCore; + let core: StandaloneEditorCore; let createContentModel: jasmine.Spy; let setContentModel: jasmine.Spy; let getSelectionRange: jasmine.Spy; @@ -29,7 +28,7 @@ describe('switchShadowEdit', () => { lifecycle: {}, contentDiv: document.createElement('div'), cache: {}, - } as any) as StandaloneEditorCore & EditorCore; + } as any) as StandaloneEditorCore; }); describe('was off', () => { @@ -44,9 +43,7 @@ describe('switchShadowEdit', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.EnteredShadowEdit, - fragment: document.createDocumentFragment(), - selectionPath: null, + eventType: 'enteredShadowEdit', }, false ); @@ -65,9 +62,7 @@ describe('switchShadowEdit', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.EnteredShadowEdit, - fragment: document.createDocumentFragment(), - selectionPath: null, + eventType: 'enteredShadowEdit', }, false ); @@ -134,7 +129,7 @@ describe('switchShadowEdit', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.LeavingShadowEdit, + eventType: 'leavingShadowEdit', }, false ); @@ -157,7 +152,7 @@ describe('switchShadowEdit', () => { expect(triggerEvent).toHaveBeenCalledWith( core, { - eventType: PluginEventType.LeavingShadowEdit, + eventType: 'leavingShadowEdit', }, false ); diff --git a/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts index 4952b9810b1..ed9197ee6c7 100644 --- a/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/triggerEventTest.ts @@ -1,5 +1,4 @@ -import { EditorPlugin, StandaloneEditorCore } from 'roosterjs-content-model-types'; -import { PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { EditorPlugin, PluginEvent, StandaloneEditorCore } from 'roosterjs-content-model-types'; import { triggerEvent } from '../../lib/coreApi/triggerEvent'; describe('triggerEvent', () => { @@ -91,7 +90,7 @@ describe('triggerEvent', () => { const onPluginEvent = jasmine.createSpy(); core.plugins.push(createPlugin(onPluginEvent)); - const event = createDefaultEvent(PluginEventType.KeyDown); + const event = createDefaultEvent('keyDown'); core.lifecycle.shadowEditFragment = document.createDocumentFragment(); @@ -104,7 +103,7 @@ describe('triggerEvent', () => { core.plugins.push(createPlugin(onPluginEvent)); - const event = createDefaultEvent(PluginEventType.BeforeDispose); + const event = createDefaultEvent('beforeDispose'); core.lifecycle.shadowEditFragment = document.createDocumentFragment(); triggerEvent(core, event, false); expect(onPluginEvent).toHaveBeenCalled(); @@ -112,10 +111,7 @@ describe('triggerEvent', () => { }); function createDefaultEvent( - type: - | PluginEventType.EditorReady - | PluginEventType.BeforeDispose - | PluginEventType.KeyDown = PluginEventType.BeforeDispose + type: 'editorReady' | 'beforeDispose' | 'keyDown' = 'beforeDispose' ): PluginEvent { return ({ eventType: type }); } diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts index 07528b7c91c..ce777586b4e 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts @@ -1,5 +1,4 @@ import { createContentModelCachePlugin } from '../../lib/corePlugin/ContentModelCachePlugin'; -import { PluginEventType } from 'roosterjs-editor-types'; import { ContentModelCachePluginState, ContentModelDomIndexer, @@ -63,7 +62,7 @@ describe('ContentModelCachePlugin', () => { it('ENTER key', () => { plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Enter', } as any, @@ -78,7 +77,7 @@ describe('ContentModelCachePlugin', () => { it('Other key without selection', () => { plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'B', } as any, @@ -99,7 +98,7 @@ describe('ContentModelCachePlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'B', } as any, @@ -119,7 +118,7 @@ describe('ContentModelCachePlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'B', } as any, @@ -140,7 +139,7 @@ describe('ContentModelCachePlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'ArrowUp', } as any, @@ -162,7 +161,7 @@ describe('ContentModelCachePlugin', () => { } as any; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'B', } as any, @@ -182,7 +181,7 @@ describe('ContentModelCachePlugin', () => { } as any; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'B', } as any, @@ -200,7 +199,7 @@ describe('ContentModelCachePlugin', () => { isInShadowEditSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Enter', } as any, @@ -227,7 +226,7 @@ describe('ContentModelCachePlugin', () => { getDOMSelectionSpy.and.returnValue(selection); plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: null!, }); @@ -249,7 +248,7 @@ describe('ContentModelCachePlugin', () => { getDOMSelectionSpy.and.returnValue(selection); plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: null!, }); @@ -273,7 +272,7 @@ describe('ContentModelCachePlugin', () => { reconcileSelectionSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: null!, }); @@ -295,7 +294,7 @@ describe('ContentModelCachePlugin', () => { getDOMSelectionSpy.and.returnValue(newRangeEx); plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: null!, }); @@ -320,7 +319,7 @@ describe('ContentModelCachePlugin', () => { reconcileSelectionSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: null!, }); @@ -351,8 +350,8 @@ describe('ContentModelCachePlugin', () => { reconcileSelectionSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: selection, + eventType: 'selectionChanged', + newSelection: selection, }); expect(state).toEqual({ @@ -377,8 +376,8 @@ describe('ContentModelCachePlugin', () => { getDOMSelectionSpy.and.returnValue(newRangeEx); plugin.onPluginEvent({ - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: newRangeEx, + eventType: 'selectionChanged', + newSelection: newRangeEx, }); expect(state).toEqual({ @@ -403,8 +402,8 @@ describe('ContentModelCachePlugin', () => { getDOMSelectionSpy.and.returnValue(newRangeEx); plugin.onPluginEvent({ - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: newRangeEx, + eventType: 'selectionChanged', + newSelection: newRangeEx, }); expect(state).toEqual({ @@ -430,7 +429,7 @@ describe('ContentModelCachePlugin', () => { state.cachedSelection = undefined; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: '', }); @@ -454,7 +453,7 @@ describe('ContentModelCachePlugin', () => { reconcileSelectionSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: '', contentModel: model, selection: newRangeEx, @@ -481,7 +480,7 @@ describe('ContentModelCachePlugin', () => { reconcileSelectionSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: '', contentModel: model, selection: newRangeEx, diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts index b9c32b8e0dd..a773e8df4fc 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts @@ -1,7 +1,6 @@ import * as applyPendingFormat from '../../lib/corePlugin/utils/applyPendingFormat'; import { createContentModelFormatPlugin } from '../../lib/corePlugin/ContentModelFormatPlugin'; import { IStandaloneEditor } from 'roosterjs-content-model-types'; -import { PluginEventType } from 'roosterjs-editor-types'; import { addSegment, createContentModelDocument, @@ -26,7 +25,7 @@ describe('ContentModelFormatPlugin', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: ({ key: 'PageUp' } as any) as KeyboardEvent, }); @@ -55,7 +54,7 @@ describe('ContentModelFormatPlugin', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: ({ data: 'a' } as any) as InputEvent, }); @@ -90,7 +89,7 @@ describe('ContentModelFormatPlugin', () => { format: mockedFormat, } as any; plugin.onPluginEvent({ - eventType: PluginEventType.Input, + eventType: 'input', rawEvent: ({ data: 'a', isComposing: true } as any) as InputEvent, }); plugin.dispose(); @@ -124,7 +123,7 @@ describe('ContentModelFormatPlugin', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.CompositionEnd, + eventType: 'compositionEnd', rawEvent: ({ data: 'test' } as any) as CompositionEvent, }); plugin.dispose(); @@ -155,7 +154,7 @@ describe('ContentModelFormatPlugin', () => { } as any; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: ({ which: 17 } as any) as KeyboardEvent, }); plugin.dispose(); @@ -188,7 +187,7 @@ describe('ContentModelFormatPlugin', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: '', }); plugin.dispose(); @@ -217,7 +216,7 @@ describe('ContentModelFormatPlugin', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', rawEvent: ({} as any) as MouseEvent, }); plugin.dispose(); @@ -246,7 +245,7 @@ describe('ContentModelFormatPlugin', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', rawEvent: ({} as any) as MouseEvent, }); plugin.dispose(); @@ -332,7 +331,7 @@ describe('ContentModelFormatPlugin for default format', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -388,7 +387,7 @@ describe('ContentModelFormatPlugin for default format', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -440,7 +439,7 @@ describe('ContentModelFormatPlugin for default format', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -495,7 +494,7 @@ describe('ContentModelFormatPlugin for default format', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -548,7 +547,7 @@ describe('ContentModelFormatPlugin for default format', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -609,7 +608,7 @@ describe('ContentModelFormatPlugin for default format', () => { plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts index 1f515249fe7..ea5590641fd 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/DomEventPluginTest.ts @@ -1,5 +1,5 @@ import * as eventUtils from '../../lib/publicApi/domUtils/eventUtils'; -import { ChangeSource, PluginEventType } from 'roosterjs-editor-types'; +import { ChangeSource } from '../../lib/constants/ChangeSource'; import { createDOMEventPlugin } from '../../lib/corePlugin/DOMEventPlugin'; import { DOMEventPluginState, @@ -122,10 +122,10 @@ describe('DOMEventPlugin verify event handlers while disallow keyboard event pro it('check events are mapped', () => { expect(eventMap).toBeDefined(); - expect(eventMap.keypress.pluginEventType).toBe(PluginEventType.KeyPress); - expect(eventMap.keydown.pluginEventType).toBe(PluginEventType.KeyDown); - expect(eventMap.keyup.pluginEventType).toBe(PluginEventType.KeyUp); - expect(eventMap.input.pluginEventType).toBe(PluginEventType.Input); + expect(eventMap.keypress.pluginEventType).toBe('keyPress'); + expect(eventMap.keydown.pluginEventType).toBe('keyDown'); + expect(eventMap.keyup.pluginEventType).toBe('keyUp'); + expect(eventMap.input.pluginEventType).toBe('input'); expect(eventMap.keypress.beforeDispatch).toBeDefined(); expect(eventMap.keydown.beforeDispatch).toBeDefined(); expect(eventMap.keyup.beforeDispatch).toBeDefined(); @@ -262,7 +262,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { onMouseUp(mockedEvent); expect(removeEventListener).toHaveBeenCalled(); - expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.MouseUp, { + expect(triggerEvent).toHaveBeenCalledWith('mouseUp', { rawEvent: mockedEvent, isClicking: true, }); @@ -300,7 +300,7 @@ describe('DOMEventPlugin handle mouse down and mouse up event', () => { onMouseUp(mockedEvent2); expect(removeEventListener).toHaveBeenCalled(); - expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.MouseUp, { + expect(triggerEvent).toHaveBeenCalledWith('mouseUp', { rawEvent: mockedEvent2, isClicking: false, }); @@ -388,7 +388,7 @@ describe('DOMEventPlugin handle other event', () => { mouseDownY: null, mouseUpEventListerAdded: false, }); - expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.CompositionEnd, { + expect(triggerEvent).toHaveBeenCalledWith('compositionEnd', { rawEvent: mockedEvent, }); expect(addEventListenerSpy).toHaveBeenCalledTimes(2); @@ -453,7 +453,7 @@ describe('DOMEventPlugin handle other event', () => { mouseUpEventListerAdded: false, }); expect(takeSnapshotSpy).toHaveBeenCalledWith(); - expect(triggerEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { + expect(triggerEvent).toHaveBeenCalledWith('contentChanged', { source: ChangeSource.Drop, }); }); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts index 0ad21d48c0f..a3bd9c2d9e7 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts @@ -2,13 +2,12 @@ import * as entityUtils from 'roosterjs-content-model-dom/lib/domUtils/entityUti import * as transformColor from '../../lib/publicApi/color/transformColor'; import { createContentModelDocument, createEntity } from '../../../roosterjs-content-model-dom/lib'; import { createEntityPlugin } from '../../lib/corePlugin/EntityPlugin'; -import { IStandaloneEditor, PluginWithState } from 'roosterjs-content-model-types'; +import { DarkColorHandler } from 'roosterjs-editor-types'; import { - DarkColorHandler, - EntityOperation, EntityPluginState, - PluginEventType, -} from 'roosterjs-editor-types'; + IStandaloneEditor, + PluginWithState, +} from 'roosterjs-content-model-types'; describe('EntityPlugin', () => { let editor: IStandaloneEditor; @@ -52,7 +51,7 @@ describe('EntityPlugin', () => { createContentModelSpy.and.returnValue(createContentModelDocument()); plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); const state = plugin.getState(); @@ -72,7 +71,7 @@ describe('EntityPlugin', () => { createContentModelSpy.and.returnValue(doc); plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); const state = plugin.getState(); @@ -88,8 +87,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: undefined, entity: { id: 'Entity1', @@ -115,7 +114,7 @@ describe('EntityPlugin', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); const state = plugin.getState(); @@ -131,8 +130,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: undefined, entity: { id: 'Entity1', @@ -157,7 +156,7 @@ describe('EntityPlugin', () => { createContentModelSpy.and.returnValue(doc); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', } as any); const state = plugin.getState(); @@ -173,8 +172,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: undefined, entity: { id: 'Entity1', @@ -198,7 +197,7 @@ describe('EntityPlugin', () => { isDarkModeSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', } as any); const state = plugin.getState(); @@ -214,8 +213,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: undefined, entity: { id: 'Entity1', @@ -252,7 +251,7 @@ describe('EntityPlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', } as any); expect(state).toEqual({ @@ -271,8 +270,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(2); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: undefined, entity: { id: 'Entity1', @@ -282,8 +281,8 @@ describe('EntityPlugin', () => { }, state: undefined, }); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.Overwrite, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'overwrite', rawEvent: undefined, entity: { id: 'T2', @@ -311,7 +310,7 @@ describe('EntityPlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', } as any); expect(state).toEqual({ @@ -342,7 +341,7 @@ describe('EntityPlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', } as any); expect(state).toEqual({ @@ -357,8 +356,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: undefined, entity: { id: 'Entity1', @@ -388,7 +387,7 @@ describe('EntityPlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', changedEntities: [ { entity: entity1, @@ -422,8 +421,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(2); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: mockedEvent, entity: { id: 'E2', @@ -433,8 +432,8 @@ describe('EntityPlugin', () => { }, state: undefined, }); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.RemoveFromStart, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'removeFromStart', rawEvent: mockedEvent, entity: { id: 'E1', @@ -463,7 +462,7 @@ describe('EntityPlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', changedEntities: [ { entity: entity2, @@ -491,8 +490,8 @@ describe('EntityPlugin', () => { '
' ); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.NewEntity, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'newEntity', rawEvent: mockedEvent, entity: { id: 'E1_1', @@ -527,7 +526,7 @@ describe('EntityPlugin', () => { }; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', entityStates: [ { id, @@ -537,8 +536,8 @@ describe('EntityPlugin', () => { } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.UpdateEntityState, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'updateEntityState', rawEvent: undefined, entity: { id, @@ -563,7 +562,7 @@ describe('EntityPlugin', () => { isNodeInEditorSpy.and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', rawEvent: mockedEvent, isClicking: true, } as any); @@ -584,14 +583,14 @@ describe('EntityPlugin', () => { spyOn(entityUtils, 'isEntityElement').and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', rawEvent: mockedEvent, isClicking: true, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.Click, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'click', rawEvent: mockedEvent, entity: { id: 'A', @@ -620,14 +619,14 @@ describe('EntityPlugin', () => { spyOn(entityUtils, 'isEntityElement').and.callFake(node => node == mockedNode1); plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', rawEvent: mockedEvent, isClicking: true, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.Click, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'click', rawEvent: mockedEvent, entity: { id: 'A', @@ -652,7 +651,7 @@ describe('EntityPlugin', () => { spyOn(entityUtils, 'isEntityElement').and.returnValue(true); plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', rawEvent: mockedEvent, isClicking: false, } as any); @@ -666,7 +665,7 @@ describe('EntityPlugin', () => { spyOn(entityUtils, 'getAllEntityWrappers').and.returnValue([]); plugin.onPluginEvent({ - eventType: PluginEventType.ExtractContentWithDom, + eventType: 'extractContentWithDom', } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(0); @@ -682,12 +681,12 @@ describe('EntityPlugin', () => { spyOn(entityUtils, 'getAllEntityWrappers').and.returnValue([wrapper1, wrapper2]); plugin.onPluginEvent({ - eventType: PluginEventType.ExtractContentWithDom, + eventType: 'extractContentWithDom', } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(2); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.ReplaceTemporaryContent, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'replaceTemporaryContent', rawEvent: undefined, entity: { id: 'E1', @@ -697,8 +696,8 @@ describe('EntityPlugin', () => { }, state: undefined, }); - expect(triggerPluginEventSpy).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - operation: EntityOperation.ReplaceTemporaryContent, + expect(triggerPluginEventSpy).toHaveBeenCalledWith('entityOperation', { + operation: 'replaceTemporaryContent', rawEvent: undefined, entity: { id: 'E2', diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts index 887cf44db53..833b831da2c 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/LifecyclePluginTest.ts @@ -1,5 +1,5 @@ import { createLifecyclePlugin } from '../../lib/corePlugin/LifecyclePlugin'; -import { DarkColorHandler, PluginEventType } from 'roosterjs-editor-types'; +import { DarkColorHandler } from 'roosterjs-editor-types'; import { IStandaloneEditor } from 'roosterjs-content-model-types'; describe('LifecyclePlugin', () => { @@ -29,7 +29,7 @@ describe('LifecyclePlugin', () => { expect(div.style.userSelect).toBe('text'); expect(div.innerHTML).toBe(''); expect(triggerEvent).toHaveBeenCalledTimes(1); - expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent.calls.argsFor(0)[0]).toBe('editorReady'); expect(setContentModelSpy).toHaveBeenCalledTimes(1); expect(setContentModelSpy).toHaveBeenCalledWith( { @@ -89,7 +89,7 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeTrue(); expect(div.style.userSelect).toBe('text'); expect(triggerEvent).toHaveBeenCalledTimes(1); - expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent.calls.argsFor(0)[0]).toBe('editorReady'); plugin.dispose(); expect(div.isContentEditable).toBeFalse(); @@ -114,7 +114,7 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeTrue(); expect(div.style.userSelect).toBe(''); expect(triggerEvent).toHaveBeenCalledTimes(1); - expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent.calls.argsFor(0)[0]).toBe('editorReady'); expect(setContentModelSpy).toHaveBeenCalledTimes(1); expect(setContentModelSpy).toHaveBeenCalledWith( @@ -174,7 +174,7 @@ describe('LifecyclePlugin', () => { expect(div.isContentEditable).toBeFalse(); expect(div.style.userSelect).toBe(''); expect(triggerEvent).toHaveBeenCalledTimes(1); - expect(triggerEvent.calls.argsFor(0)[0]).toBe(PluginEventType.EditorReady); + expect(triggerEvent.calls.argsFor(0)[0]).toBe('editorReady'); plugin.dispose(); expect(div.isContentEditable).toBeFalse(); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts index 475638c6851..a8920c267ae 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/SelectionPluginTest.ts @@ -1,5 +1,4 @@ import { createSelectionPlugin } from '../../lib/corePlugin/SelectionPlugin'; -import { PluginEventType } from 'roosterjs-editor-types'; import { EditorPlugin, IStandaloneEditor, @@ -209,7 +208,7 @@ describe('SelectionPlugin handle image selection', () => { it('No selection, mouse down to div', () => { const node = document.createElement('div'); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: node, } as any, @@ -239,7 +238,7 @@ describe('SelectionPlugin handle image selection', () => { const node = document.createElement('div'); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: node, } as any, @@ -270,7 +269,7 @@ describe('SelectionPlugin handle image selection', () => { const node = document.createElement('div'); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: node, } as any, @@ -289,7 +288,7 @@ describe('SelectionPlugin handle image selection', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: mockedImage, } as any, @@ -309,7 +308,7 @@ describe('SelectionPlugin handle image selection', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: mockedImage, button: 2, @@ -324,7 +323,7 @@ describe('SelectionPlugin handle image selection', () => { mockedImage.contentEditable = 'true'; plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: mockedImage, button: 2, @@ -338,7 +337,7 @@ describe('SelectionPlugin handle image selection', () => { const node = document.createElement('div'); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', rawEvent: { target: node, button: 2, @@ -354,7 +353,7 @@ describe('SelectionPlugin handle image selection', () => { mockedImage.contentEditable = 'true'; plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', isClicking: true, rawEvent: { target: mockedImage, @@ -374,7 +373,7 @@ describe('SelectionPlugin handle image selection', () => { mockedImage.contentEditable = 'false'; plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', isClicking: true, rawEvent: { target: mockedImage, @@ -390,7 +389,7 @@ describe('SelectionPlugin handle image selection', () => { mockedImage.contentEditable = 'true'; plugin.onPluginEvent({ - eventType: PluginEventType.MouseUp, + eventType: 'mouseUp', isClicking: false, rawEvent: { target: mockedImage, @@ -407,7 +406,7 @@ describe('SelectionPlugin handle image selection', () => { getDOMSelectionSpy.and.returnValue(null); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -423,7 +422,7 @@ describe('SelectionPlugin handle image selection', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -455,7 +454,7 @@ describe('SelectionPlugin handle image selection', () => { createRangeSpy.and.returnValue(mockedRange); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -492,7 +491,7 @@ describe('SelectionPlugin handle image selection', () => { createRangeSpy.and.returnValue(mockedRange); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -530,7 +529,7 @@ describe('SelectionPlugin handle image selection', () => { createRangeSpy.and.returnValue(mockedRange); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); @@ -562,7 +561,7 @@ describe('SelectionPlugin handle image selection', () => { createRangeSpy.and.returnValue(mockedRange); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent, }); diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts index c2a94dcc613..e344d0a491b 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/UndoPluginTest.ts @@ -2,7 +2,6 @@ import * as SnapshotsManagerImpl from '../../lib/editor/SnapshotsManagerImpl'; import * as undo from '../../lib/publicApi/undo/undo'; import { ChangeSource } from '../../lib/constants/ChangeSource'; import { createUndoPlugin } from '../../lib/corePlugin/UndoPlugin'; -import { PluginEventType } from 'roosterjs-editor-types'; import { IStandaloneEditor, PluginWithState, @@ -102,7 +101,7 @@ describe('UndoPlugin', () => { it('Not handled exclusively for EditorReady event', () => { const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); expect(result).toBeFalse(); @@ -112,7 +111,7 @@ describe('UndoPlugin', () => { it('Not handled exclusively for ContentChanged event', () => { const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', } as any); expect(result).toBeFalse(); @@ -122,7 +121,7 @@ describe('UndoPlugin', () => { it('Not handled exclusively for MouseDown event', () => { const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.MouseDown, + eventType: 'mouseDown', } as any); expect(result).toBeFalse(); @@ -132,7 +131,7 @@ describe('UndoPlugin', () => { it('Not handled exclusively for KeyDown event with Enter key', () => { const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Enter', }, @@ -145,7 +144,7 @@ describe('UndoPlugin', () => { it('Not handled exclusively for KeyDown event with Ctrl key', () => { const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', ctrlKey: true, @@ -161,7 +160,7 @@ describe('UndoPlugin', () => { canUndoAutoCompleteSpy.and.returnValue(false); const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', }, @@ -179,7 +178,7 @@ describe('UndoPlugin', () => { }); const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', }, @@ -200,7 +199,7 @@ describe('UndoPlugin', () => { }); const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', }, @@ -228,7 +227,7 @@ describe('UndoPlugin', () => { }); const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', }, @@ -256,7 +255,7 @@ describe('UndoPlugin', () => { }); const result = plugin.willHandleEventExclusively({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', }, @@ -285,7 +284,7 @@ describe('UndoPlugin', () => { plugin.getState().snapshotsManager.hasNewContent = false; plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); @@ -307,7 +306,7 @@ describe('UndoPlugin', () => { plugin.getState().snapshotsManager.hasNewContent = false; plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); expect(takeSnapshotSpy).toHaveBeenCalledTimes(0); @@ -329,7 +328,7 @@ describe('UndoPlugin', () => { plugin.getState().snapshotsManager.hasNewContent = false; plugin.onPluginEvent({ - eventType: PluginEventType.EditorReady, + eventType: 'editorReady', }); expect(takeSnapshotSpy).toHaveBeenCalledTimes(0); @@ -365,7 +364,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', altKey: false, @@ -399,7 +398,7 @@ describe('UndoPlugin', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Backspace', altKey: false, @@ -429,7 +428,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Delete', altKey: false, @@ -459,7 +458,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Up', altKey: false, @@ -492,7 +491,7 @@ describe('UndoPlugin', () => { state.snapshotsManager.hasNewContent = true; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'Up', altKey: false, @@ -526,7 +525,7 @@ describe('UndoPlugin', () => { state.lastKeyPress = 'Backspace'; plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'A', altKey: false, @@ -556,7 +555,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.KeyDown, + eventType: 'keyDown', rawEvent: { key: 'A', altKey: false, @@ -592,7 +591,7 @@ describe('UndoPlugin', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.KeyPress, + eventType: 'keyPress', rawEvent: { key: 'Enter', altKey: false, @@ -627,7 +626,7 @@ describe('UndoPlugin', () => { }); plugin.onPluginEvent({ - eventType: PluginEventType.KeyPress, + eventType: 'keyPress', rawEvent: { key: 'A', altKey: false, @@ -658,7 +657,7 @@ describe('UndoPlugin', () => { state.lastKeyPress = 'A'; plugin.onPluginEvent({ - eventType: PluginEventType.KeyPress, + eventType: 'keyPress', rawEvent: { key: ' ', altKey: false, @@ -689,7 +688,7 @@ describe('UndoPlugin', () => { state.lastKeyPress = ' '; plugin.onPluginEvent({ - eventType: PluginEventType.KeyPress, + eventType: 'keyPress', rawEvent: { key: ' ', altKey: false, @@ -717,7 +716,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.KeyPress, + eventType: 'keyPress', rawEvent: { key: 'Enter', altKey: false, @@ -745,7 +744,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.KeyPress, + eventType: 'keyPress', rawEvent: { key: 'A', altKey: false, @@ -773,7 +772,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.CompositionEnd, + eventType: 'compositionEnd', rawEvent: { key: 'Test', }, @@ -801,7 +800,7 @@ describe('UndoPlugin', () => { state.isRestoring = true; plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: 'Test', } as any); @@ -824,7 +823,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.SwitchToDarkMode, } as any); @@ -847,7 +846,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.SwitchToLightMode, } as any); @@ -870,7 +869,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.Keyboard, } as any); @@ -893,7 +892,7 @@ describe('UndoPlugin', () => { const preventDefaultSpy = jasmine.createSpy('preventDefault'); plugin.onPluginEvent({ - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: 'Test', } as any); @@ -919,7 +918,7 @@ describe('UndoPlugin', () => { state.lastKeyPress = 'A'; plugin.onPluginEvent({ - eventType: PluginEventType.BeforeKeyboardEditing, + eventType: 'beforeKeyboardEditing', rawEvent: { key: 'B', }, @@ -947,7 +946,7 @@ describe('UndoPlugin', () => { state.lastKeyPress = 'A'; plugin.onPluginEvent({ - eventType: PluginEventType.BeforeKeyboardEditing, + eventType: 'beforeKeyboardEditing', rawEvent: { key: 'A', }, diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts index 569bdf7cb97..d5ccaa241e4 100644 --- a/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/editor/StandaloneEditorTest.ts @@ -1,7 +1,6 @@ import * as createStandaloneEditorCore from '../../lib/editor/createStandaloneEditorCore'; import * as transformColor from '../../lib/publicApi/color/transformColor'; import { ChangeSource } from '../../lib/constants/ChangeSource'; -import { PluginEventType } from 'roosterjs-editor-types'; import { StandaloneEditor } from '../../lib/editor/StandaloneEditor'; describe('StandaloneEditor', () => { @@ -698,7 +697,7 @@ describe('StandaloneEditor', () => { expect(triggerEventSpy).toHaveBeenCalledWith( mockedCore, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.SwitchToDarkMode, }, true @@ -718,7 +717,7 @@ describe('StandaloneEditor', () => { expect(triggerEventSpy).toHaveBeenCalledWith( mockedCore, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.SwitchToLightMode, }, true diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts index f777338e0fe..5ad2a7b4304 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/generatePasteOptionFromPluginsTest.ts @@ -1,5 +1,4 @@ import { generatePasteOptionFromPlugins } from '../../../lib/utils/paste/generatePasteOptionFromPlugins'; -import { PasteType, PluginEventType } from 'roosterjs-editor-types'; import { StandaloneEditorCore } from 'roosterjs-content-model-types'; describe('generatePasteOptionFromPlugins', () => { @@ -17,19 +16,6 @@ describe('generatePasteOptionFromPlugins', () => { domToModelOption: 'OptionResult', pasteType: 'TypeResult', } as any; - const sanitizingOption: any = { - elementCallbacks: {}, - attributeCallbacks: {}, - cssStyleCallbacks: {}, - additionalTagReplacements: {}, - additionalAllowedAttributes: [], - additionalAllowedCssClasses: [], - additionalDefaultStyleValues: {}, - additionalGlobalStyleNodes: [], - additionalPredefinedCssForElement: {}, - preserveHtmlComments: false, - unknownTagReplacement: null, - }; beforeEach(() => { triggerPluginEventSpy = jasmine.createSpy('triggerEvent'); @@ -64,22 +50,21 @@ describe('generatePasteOptionFromPlugins', () => { fragment: 'FragmentResult', domToModelOption: 'OptionResult', pasteType: 'TypeResult', - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, - sanitizingOption, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(originalEvent).toEqual({ - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, fragment: mockedFragment, htmlBefore: htmlBefore, htmlAfter: htmlAfter, htmlAttributes: mockedMetadata, - pasteType: PasteType.Normal, + pasteType: 'normal', domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [], @@ -89,12 +74,11 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, - sanitizingOption, }); expect(triggerPluginEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, fragment: 'FragmentResult', htmlBefore: htmlBefore, @@ -102,7 +86,6 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', - sanitizingOption, }, true ); @@ -136,18 +119,17 @@ describe('generatePasteOptionFromPlugins', () => { domToModelOption: 'OptionResult', pasteType: 'TypeResult', customizedMerge: mockedCustomizedMerge, - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, - sanitizingOption, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(triggerPluginEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, fragment: 'FragmentResult', htmlBefore: htmlBefore, @@ -155,7 +137,6 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', - sanitizingOption, customizedMerge: mockedCustomizedMerge, }, true @@ -184,18 +165,17 @@ describe('generatePasteOptionFromPlugins', () => { fragment: 'FragmentResult', domToModelOption: 'OptionResult', pasteType: 'TypeResult', - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, htmlBefore: '', htmlAfter: '', htmlAttributes: mockedMetadata, - sanitizingOption, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(triggerPluginEventSpy).toHaveBeenCalledWith( core, { - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, fragment: 'FragmentResult', htmlBefore: '', @@ -203,18 +183,17 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', - sanitizingOption, }, true ); expect(originalEvent).toEqual({ - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: mockedClipboardData, fragment: mockedFragment, htmlBefore: '', htmlAfter: '', htmlAttributes: mockedMetadata, - pasteType: PasteType.MergeFormat, + pasteType: 'mergeFormat', domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [], @@ -224,7 +203,6 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, - sanitizingOption, }); }); @@ -256,13 +234,12 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, - pasteType: PasteType.AsPlainText, - eventType: PluginEventType.BeforePaste, + pasteType: 'asPlainText', + eventType: 'beforePaste', clipboardData: mockedClipboardData, htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, - sanitizingOption, }); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(0); }); diff --git a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts index 9a97fd4efda..373cc5bc8f8 100644 --- a/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/utils/paste/mergePasteContentTest.ts @@ -8,7 +8,6 @@ import { createContentModelDocument } from 'roosterjs-content-model-dom'; import { mergePasteContent } from '../../../lib/utils/paste/mergePasteContent'; import { pasteDisplayFormatParser } from '../../../lib/override/pasteDisplayFormatParser'; import { pasteTextProcessor } from '../../../lib/override/pasteTextProcessor'; -import { PasteType } from 'roosterjs-editor-types'; import { ContentModelDocument, ContentModelSegmentFormat, @@ -119,7 +118,7 @@ describe('mergePasteContent', () => { spyOn(domToContentModel, 'domToContentModel').and.returnValue(pasteModel); const eventResult = { - pasteType: PasteType.Normal, + pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, } as any; @@ -226,7 +225,7 @@ describe('mergePasteContent', () => { spyOn(domToContentModel, 'domToContentModel').and.returnValue(pasteModel); const eventResult = { - pasteType: PasteType.Normal, + pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, customizedMerge, } as any; @@ -250,7 +249,7 @@ describe('mergePasteContent', () => { spyOn(domToContentModel, 'domToContentModel').and.returnValue(pasteModel); const eventResult = { - pasteType: PasteType.MergeFormat, + pasteType: 'mergeFormat', domToModelOption: { additionalAllowedTags: [] }, } as any; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts index e1994e354cd..3119abb1310 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/getContent.ts @@ -1,4 +1,4 @@ -import { GetContentMode, PluginEventType } from 'roosterjs-editor-types'; +import { GetContentMode } from 'roosterjs-editor-types'; import { transformColor } from 'roosterjs-content-model-core'; import { createRange, @@ -51,7 +51,7 @@ export const getContent: GetContent = (core, innerCore, mode): string => { api.triggerEvent( innerCore, { - eventType: PluginEventType.ExtractContentWithDom, + eventType: 'extractContentWithDom', clonedRoot, }, true /*broadcast*/ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts index 525b5b0c9bf..8f7892d0b81 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/coreApi/setContent.ts @@ -1,10 +1,14 @@ import { ChangeSource, transformColor } from 'roosterjs-content-model-core'; -import { convertMetadataToDOMSelection } from '../editor/utils/selectionConverter'; -import { extractContentMetadata, restoreContentWithEntityPlaceholder } from 'roosterjs-editor-dom'; -import { PluginEventType } from 'roosterjs-editor-types'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; +import { + createRange, + extractContentMetadata, + queryElements, + restoreContentWithEntityPlaceholder, +} from 'roosterjs-editor-dom'; import type { ContentMetadata } from 'roosterjs-editor-types'; import type { SetContent } from '../publicTypes/ContentModelEditorCore'; -import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; +import type { DOMSelection, StandaloneEditorCore } from 'roosterjs-content-model-types'; /** * @internal @@ -31,7 +35,7 @@ export const setContent: SetContent = ( api.triggerEvent( innerCore, { - eventType: PluginEventType.BeforeSetContent, + eventType: 'beforeSetContent', newContent: content, }, true /*broadcast*/ @@ -68,7 +72,7 @@ export const setContent: SetContent = ( api.triggerEvent( innerCore, { - eventType: PluginEventType.ContentChanged, + eventType: 'contentChanged', source: ChangeSource.SetContent, }, false /*broadcast*/ @@ -85,3 +89,41 @@ function selectContentMetadata(core: StandaloneEditorCore, metadata: ContentMeta } } } + +function convertMetadataToDOMSelection( + contentDiv: HTMLElement, + metadata: ContentMetadata | undefined +): DOMSelection | null { + switch (metadata?.type) { + case SelectionRangeTypes.Normal: + return { + type: 'range', + range: createRange(contentDiv, metadata.start, metadata.end), + }; + case SelectionRangeTypes.TableSelection: + const table = queryElements(contentDiv, '#' + metadata.tableId)[0] as HTMLTableElement; + + return table + ? { + type: 'table', + table: table, + firstColumn: metadata.firstCell.x, + firstRow: metadata.firstCell.y, + lastColumn: metadata.lastCell.x, + lastRow: metadata.lastCell.y, + } + : null; + case SelectionRangeTypes.ImageSelection: + const image = queryElements(contentDiv, '#' + metadata.imageId)[0] as HTMLImageElement; + + return image + ? { + type: 'image', + image: image, + } + : null; + + default: + return null; + } +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts index 43759089f85..9f1d76ae781 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/BridgePlugin.ts @@ -1,15 +1,18 @@ import { createContextMenuPlugin } from './ContextMenuPlugin'; import { createEditPlugin } from './EditPlugin'; -import { createEventTypeTranslatePlugin } from './EventTypeTranslatePlugin'; import { createNormalizeTablePlugin } from './NormalizeTablePlugin'; +import { newEventToOldEvent, oldEventToNewEvent } from '../editor/utils/eventConverter'; import { PluginEventType } from 'roosterjs-editor-types'; import type { ContentModelCorePluginState } from '../publicTypes/ContentModelCorePlugins'; import type { ContentModelEditorOptions, IContentModelEditor, } from '../publicTypes/IContentModelEditor'; -import type { EditorPlugin as LegacyEditorPlugin, PluginEvent } from 'roosterjs-editor-types'; -import type { EditorPlugin } from 'roosterjs-content-model-types'; +import type { + EditorPlugin as LegacyEditorPlugin, + PluginEvent as LegacyPluginEvent, +} from 'roosterjs-editor-types'; +import type { EditorPlugin, PluginEvent } from 'roosterjs-content-model-types'; const ExclusivelyHandleEventPluginKey = '__ExclusivelyHandleEventPlugin'; @@ -21,15 +24,14 @@ export class BridgePlugin implements EditorPlugin { private legacyPlugins: LegacyEditorPlugin[]; private corePluginState: ContentModelCorePluginState; private outerEditor: IContentModelEditor | null = null; + private checkExclusivelyHandling: boolean; constructor(options: ContentModelEditorOptions) { - const translatePlugin = createEventTypeTranslatePlugin(); const editPlugin = createEditPlugin(); const contextMenuPlugin = createContextMenuPlugin(options); const normalizeTablePlugin = createNormalizeTablePlugin(); this.legacyPlugins = [ - translatePlugin, editPlugin, ...(options.legacyPlugins ?? []).filter(x => !!x), contextMenuPlugin, @@ -39,6 +41,9 @@ export class BridgePlugin implements EditorPlugin { edit: editPlugin.getState(), contextMenu: contextMenuPlugin.getState(), }; + this.checkExclusivelyHandling = this.legacyPlugins.some( + plugin => plugin.willHandleEventExclusively + ); } /** @@ -92,16 +97,20 @@ export class BridgePlugin implements EditorPlugin { } willHandleEventExclusively(event: PluginEvent) { - for (let i = 0; i < this.legacyPlugins.length; i++) { - const plugin = this.legacyPlugins[i]; + let oldEvent: LegacyPluginEvent | undefined; - if (plugin.willHandleEventExclusively?.(event)) { - if (!event.eventDataCache) { - event.eventDataCache = {}; - } + if (this.checkExclusivelyHandling && (oldEvent = newEventToOldEvent(event))) { + for (let i = 0; i < this.legacyPlugins.length; i++) { + const plugin = this.legacyPlugins[i]; - event.eventDataCache[ExclusivelyHandleEventPluginKey] = plugin; - return true; + if (plugin.willHandleEventExclusively?.(oldEvent)) { + if (!event.eventDataCache) { + event.eventDataCache = {}; + } + + event.eventDataCache[ExclusivelyHandleEventPluginKey] = plugin; + return true; + } } } @@ -109,14 +118,20 @@ export class BridgePlugin implements EditorPlugin { } onPluginEvent(event: PluginEvent) { - const exclusivelyHandleEventPlugin = event.eventDataCache?.[ - ExclusivelyHandleEventPluginKey - ] as EditorPlugin | undefined; - - if (exclusivelyHandleEventPlugin) { - exclusivelyHandleEventPlugin.onPluginEvent?.(event); - } else { - this.legacyPlugins.forEach(plugin => plugin.onPluginEvent?.(event)); + const oldEvent = newEventToOldEvent(event); + + if (oldEvent) { + const exclusivelyHandleEventPlugin = event.eventDataCache?.[ + ExclusivelyHandleEventPluginKey + ] as LegacyEditorPlugin | undefined; + + if (exclusivelyHandleEventPlugin) { + exclusivelyHandleEventPlugin.onPluginEvent?.(oldEvent); + } else { + this.legacyPlugins.forEach(plugin => plugin.onPluginEvent?.(oldEvent)); + } + + Object.assign(event, oldEventToNewEvent(oldEvent, event)); } } } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/EventTypeTranslatePlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/EventTypeTranslatePlugin.ts deleted file mode 100644 index 8c467cc6234..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/EventTypeTranslatePlugin.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { convertDomSelectionToRangeEx } from '../editor/utils/selectionConverter'; -import { PluginEventType } from 'roosterjs-editor-types'; -import type { ContentModelSelectionChangedEvent } from 'roosterjs-content-model-types'; -import type { EditorPlugin, PluginEvent, SelectionChangedEvent } from 'roosterjs-editor-types'; - -/** - * Translate Standalone editor event type to legacy event type - */ -class EventTypeTranslatePlugin implements EditorPlugin { - /** - * Get a friendly name of this plugin - */ - getName() { - return 'EventTypeTranslate'; - } - - /** - * Initialize this plugin. This should only be called from Editor - * @param editor Editor instance - */ - initialize() {} - - /** - * Dispose this plugin - */ - dispose() {} - - onPluginEvent(event: PluginEvent) { - switch (event.eventType) { - case PluginEventType.SelectionChanged: - if (!event.selectionRangeEx && isContentModelSelectionChangedEvent(event)) { - event.selectionRangeEx = convertDomSelectionToRangeEx(event.newSelection); - } - break; - } - } -} - -function isContentModelSelectionChangedEvent( - event: SelectionChangedEvent -): event is ContentModelSelectionChangedEvent { - return !!(event as ContentModelSelectionChangedEvent).newSelection; -} - -/** - * @internal - * Create a new instance of EventTypeTranslatePlugin. - */ -export function createEventTypeTranslatePlugin(): EditorPlugin { - return new EventTypeTranslatePlugin(); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts deleted file mode 100644 index 13cc54a3078..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/corePlugins/createCorePlugins.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createContextMenuPlugin } from './ContextMenuPlugin'; -import { createEditPlugin } from './EditPlugin'; -import { createEventTypeTranslatePlugin } from './EventTypeTranslatePlugin'; -import { createNormalizeTablePlugin } from './NormalizeTablePlugin'; -import type { ContentModelCorePlugins } from '../publicTypes/ContentModelCorePlugins'; -import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; - -/** - * @internal - * Create Core Plugins - * @param options Editor options - */ -export function createCorePlugins(options: ContentModelEditorOptions): ContentModelCorePlugins { - const map = options.corePluginOverride || {}; - - // The order matters, some plugin needs to be put before/after others to make sure event - // can be handled in right order - return { - eventTranslate: map.eventTranslate || createEventTypeTranslatePlugin(), - edit: map.edit || createEditPlugin(), - normalizeTable: map.normalizeTable || createNormalizeTablePlugin(), - contextMenu: map.contextMenu || createContextMenuPlugin(options), - }; -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index b41295b512c..1597e431ed3 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -3,6 +3,11 @@ import { buildRangeEx } from './utils/buildRangeEx'; import { createEditorCore } from './createEditorCore'; import { getObjectKeys } from 'roosterjs-content-model-dom'; import { getPendableFormatState } from './utils/getPendableFormatState'; +import { + newEventToOldEvent, + oldEventToNewEvent, + OldEventTypeToNewEventType, +} from './utils/eventConverter'; import { createModelFromHtml, isBold, @@ -46,7 +51,7 @@ import type { SizeTransformer, StyleBasedFormatState, TableSelection, - TrustedHTMLHandler, + DOMEventHandlerObject, } from 'roosterjs-editor-types'; import { convertDomSelectionToRangeEx, @@ -525,11 +530,18 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode }; if (typeof handlerObj === 'number') { - result.pluginEventType = handlerObj as PluginEventType; + result.pluginEventType = OldEventTypeToNewEventType[handlerObj as PluginEventType]; } else if (typeof handlerObj === 'function') { result.beforeDispatch = handlerObj; } else if (typeof handlerObj === 'object') { - result = handlerObj as DOMEventRecord; + const record = handlerObj as DOMEventHandlerObject; + result = { + beforeDispatch: record.beforeDispatch, + pluginEventType: + typeof record.pluginEventType == 'number' + ? OldEventTypeToNewEventType[record.pluginEventType] + : undefined, + }; } eventsMapResult[key] = result; @@ -552,11 +564,19 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode data: PluginEventData, broadcast: boolean = false ): PluginEventFromType { - return this.triggerEvent( - eventType as PluginEventType, - data, - broadcast - ) as PluginEventFromType; + const oldEvent = { + eventType, + ...data, + } as PluginEvent; + const newEvent = oldEventToNewEvent(oldEvent); + const core = this.getCore(); + + if (newEvent) { + core.api.triggerEvent(core, newEvent, broadcast); + return (newEventToOldEvent(newEvent, oldEvent) ?? oldEvent) as PluginEventFromType; + } else { + return oldEvent as PluginEventFromType; + } } /** @@ -659,7 +679,7 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode data: data, additionalData, }; - core.api.triggerEvent(core, event, true /*broadcast*/); + this.triggerPluginEvent(PluginEventType.ContentChanged, event, true /*broadcast*/); } if (canUndoByBackspace) { @@ -976,16 +996,6 @@ export class ContentModelEditor extends StandaloneEditor implements IContentMode ); } - /** - * Get a function to convert HTML string to trusted HTML string. - * By default it will just return the input HTML directly. To override this behavior, - * pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler - * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types - */ - getTrustedHTMLHandler(): TrustedHTMLHandler { - return this.getCore().trustedHTMLHandler; - } - /** * @deprecated Use getZoomScale() instead */ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts index 8ab6725c26f..bce247dd55f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/eventConverter.ts @@ -16,6 +16,7 @@ import type { AnnounceData as NewAnnounceData, KnownAnnounceStrings as NewKnownAnnounceStrings, EntityOperation as NewEntityOperation, + PluginEventType as NewPluginEventType, } from 'roosterjs-content-model-types'; const PasteTypeNewToOld: Record = { @@ -69,6 +70,36 @@ const EntityOperationNewToOld: Record = click: OldEntityOperation.Click, }; +/** + * @internal + */ +export const OldEventTypeToNewEventType: Record = { + [PluginEventType.BeforeCutCopy]: 'beforeCutCopy', + [PluginEventType.BeforeDispose]: 'beforeDispose', + [PluginEventType.BeforeKeyboardEditing]: 'beforeKeyboardEditing', + [PluginEventType.BeforePaste]: 'beforePaste', + [PluginEventType.BeforeSetContent]: 'beforeSetContent', + [PluginEventType.CompositionEnd]: 'compositionEnd', + [PluginEventType.ContentChanged]: 'contentChanged', + [PluginEventType.ContextMenu]: 'contextMenu', + [PluginEventType.EditImage]: 'editImage', + [PluginEventType.EditorReady]: 'editorReady', + [PluginEventType.EnteredShadowEdit]: 'enteredShadowEdit', + [PluginEventType.EntityOperation]: 'entityOperation', + [PluginEventType.ExtractContentWithDom]: 'extractContentWithDom', + [PluginEventType.Input]: 'input', + [PluginEventType.KeyDown]: 'keyDown', + [PluginEventType.KeyPress]: 'keyPress', + [PluginEventType.KeyUp]: 'keyUp', + [PluginEventType.LeavingShadowEdit]: 'leavingShadowEdit', + [PluginEventType.MouseDown]: 'mouseDown', + [PluginEventType.MouseUp]: 'mouseUp', + [PluginEventType.PendingFormatStateChanged]: undefined, + [PluginEventType.Scroll]: 'scroll', + [PluginEventType.SelectionChanged]: 'selectionChanged', + [PluginEventType.ZoomChanged]: 'zoomChanged', +}; + /** * @internal Convert legacy event object to new event object */ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/selectionConverter.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/selectionConverter.ts index 06773f66d86..287c48e7d8b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/selectionConverter.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/selectionConverter.ts @@ -1,8 +1,8 @@ -import { createRange, getSelectionPath, queryElements } from 'roosterjs-editor-dom'; +import { createRange } from 'roosterjs-editor-dom'; import { createTableRanges } from 'roosterjs-content-model-core'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; import type { DOMSelection } from 'roosterjs-content-model-types'; -import type { ContentMetadata, SelectionRangeEx } from 'roosterjs-editor-types'; +import type { SelectionRangeEx } from 'roosterjs-editor-types'; // In theory, all functions below are not necessary. We keep these functions here only for compatibility with old IEditor interface @@ -84,85 +84,3 @@ export function convertDomSelectionToRangeEx(selection: DOMSelection | null): Se }; } } - -/** - * @internal - */ -export function convertDomSelectionToMetadata( - contentDiv: HTMLElement, - selection: DOMSelection | null -): ContentMetadata | null { - switch (selection?.type) { - case 'table': - return { - type: SelectionRangeTypes.TableSelection, - tableId: selection.table.id, - firstCell: { - x: selection.firstColumn, - y: selection.firstRow, - }, - lastCell: { - x: selection.lastColumn, - y: selection.lastRow, - }, - isDarkMode: false, - }; - case 'image': - return { - type: SelectionRangeTypes.ImageSelection, - imageId: selection.image.id, - isDarkMode: false, - }; - case 'range': - return { - type: SelectionRangeTypes.Normal, - isDarkMode: false, - start: [], - end: [], - ...(getSelectionPath(contentDiv, selection.range) || {}), - }; - default: - return null; - } -} - -/** - * @internal - */ -export function convertMetadataToDOMSelection( - contentDiv: HTMLElement, - metadata: ContentMetadata | undefined -): DOMSelection | null { - switch (metadata?.type) { - case SelectionRangeTypes.Normal: - return { - type: 'range', - range: createRange(contentDiv, metadata.start, metadata.end), - }; - case SelectionRangeTypes.TableSelection: - const table = queryElements(contentDiv, '#' + metadata.tableId)[0] as HTMLTableElement; - - return table - ? { - type: 'table', - table: table, - firstColumn: metadata.firstCell.x, - firstRow: metadata.firstCell.y, - lastColumn: metadata.lastCell.x, - lastRow: metadata.lastCell.y, - } - : null; - case SelectionRangeTypes.ImageSelection: - const image = queryElements(contentDiv, '#' + metadata.imageId)[0] as HTMLImageElement; - - return image - ? { - type: 'image', - image: image, - } - : null; - - default: - return null; - } -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/index.ts b/packages-content-model/roosterjs-content-model-editor/lib/index.ts index 8d68b1d4181..38364003350 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/index.ts @@ -9,10 +9,7 @@ export { } from './publicTypes/ContentModelEditorCore'; export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor'; export { ContextMenuPluginState } from './publicTypes/ContextMenuPluginState'; -export { - ContentModelCorePlugins, - ContentModelCorePluginState, -} from './publicTypes/ContentModelCorePlugins'; +export { ContentModelCorePluginState } from './publicTypes/ContentModelCorePlugins'; export { ContentModelEditor } from './editor/ContentModelEditor'; export { isContentModelEditor } from './editor/isContentModelEditor'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts index b3b29f49ad9..851b170b720 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/ContentModelCorePlugins.ts @@ -1,30 +1,5 @@ import type { ContextMenuPluginState } from './ContextMenuPluginState'; -import type { EditorPlugin, EditPluginState, PluginWithState } from 'roosterjs-editor-types'; - -/** - * An interface for Content Model editor core plugins - */ -export interface ContentModelCorePlugins { - /** - * Translate Standalone editor event type to legacy event type - */ - readonly eventTranslate: EditorPlugin; - - /** - * Edit plugin handles ContentEditFeatures - */ - readonly edit: PluginWithState; - - /** - * NormalizeTable plugin makes sure each table in editor has TBODY/THEAD/TFOOT tag around TR tags - */ - readonly normalizeTable: EditorPlugin; - - /** - * ContextMenu plugin handles Context Menu - */ - readonly contextMenu: PluginWithState; -} +import type { EditPluginState } from 'roosterjs-editor-types'; /** * Core plugin state for Content Model Editor diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts index 7263ce3959e..c15e43ac0a1 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/IContentModelEditor.ts @@ -1,4 +1,3 @@ -import type { ContentModelCorePlugins } from './ContentModelCorePlugins'; import type { ContentModelCoreApiMap } from './ContentModelEditorCore'; import type { EditorPlugin, ExperimentalFeatures, IEditor } from 'roosterjs-editor-types'; import type { StandaloneEditorOptions, IStandaloneEditor } from 'roosterjs-content-model-types'; @@ -19,12 +18,6 @@ export interface ContentModelEditorOptions extends StandaloneEditorOptions { */ initialContent?: string; - /** - * A plugin map to override default core Plugin implementation - * Default value is null - */ - corePluginOverride?: Partial; - /** * A function map to override default core API implementation * Default value is null diff --git a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts index 1ba9a7c5f4d..6c42aeef97a 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/BridgePluginTest.ts @@ -1,6 +1,6 @@ import * as ContextMenuPlugin from '../../lib/corePlugins/ContextMenuPlugin'; import * as EditPlugin from '../../lib/corePlugins/EditPlugin'; -import * as EventTypeTranslatePlugin from '../../lib/corePlugins/EventTypeTranslatePlugin'; +import * as eventConverter from '../../lib/editor/utils/eventConverter'; import * as NormalizeTablePlugin from '../../lib/corePlugins/NormalizeTablePlugin'; import { BridgePlugin } from '../../lib/corePlugins/BridgePlugin'; import { PluginEventType } from 'roosterjs-editor-types'; @@ -18,9 +18,6 @@ describe('BridgePlugin', () => { createMockedPlugin('contextMenu') ); spyOn(EditPlugin, 'createEditPlugin').and.returnValue(createMockedPlugin('edit')); - spyOn(EventTypeTranslatePlugin, 'createEventTypeTranslatePlugin').and.returnValue( - createMockedPlugin('eventTypeTranslate') - ); spyOn(NormalizeTablePlugin, 'createNormalizeTablePlugin').and.returnValue( createMockedPlugin('normalizeTable') ); @@ -109,6 +106,10 @@ describe('BridgePlugin', () => { legacyPlugins: [mockedPlugin1, mockedPlugin2], }); + spyOn(eventConverter, 'newEventToOldEvent').and.callFake(newEvent => { + return ('NEW_' + newEvent) as any; + }); + plugin.setOuterEditor(mockedEditor); const mockedEvent = {} as any; @@ -120,32 +121,140 @@ describe('BridgePlugin', () => { __ExclusivelyHandleEventPlugin: mockedPlugin2, }, }); + expect(eventConverter.newEventToOldEvent).toHaveBeenCalledTimes(1); + expect(eventConverter.newEventToOldEvent).toHaveBeenCalledWith(mockedEvent); + + plugin.dispose(); + }); + + it('onPluginEvent without exclusive handling', () => { + const initializeSpy = jasmine.createSpy('initialize'); + const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1').and.callFake(event => { + event.data = 'plugin1'; + }); + const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2').and.callFake(event => { + event.data = 'plugin2'; + }); + const disposeSpy = jasmine.createSpy('dispose'); + + const mockedPlugin1 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy1, + dispose: disposeSpy, + } as any; + const mockedPlugin2 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy2, + dispose: disposeSpy, + } as any; + + const mockedEditor = 'EDITOR' as any; + const plugin = new BridgePlugin({ + legacyPlugins: [mockedPlugin1, mockedPlugin2], + }); + + spyOn(eventConverter, 'newEventToOldEvent').and.callFake(newEvent => { + return { + eventType: 'old_' + newEvent.eventType, + } as any; + }); + spyOn(eventConverter, 'oldEventToNewEvent').and.callFake((oldEvent: any) => { + return { + eventType: 'new_' + oldEvent.eventType, + data: oldEvent.data, + } as any; + }); + + plugin.setOuterEditor(mockedEditor); + + const mockedEvent = { + eventType: 'newEvent', + } as any; - plugin.initialize(); plugin.onPluginEvent(mockedEvent); expect(onPluginEventSpy1).toHaveBeenCalledTimes(1); - expect(onPluginEventSpy2).toHaveBeenCalledTimes(2); - expect(onPluginEventSpy1).toHaveBeenCalledWith({ - eventType: PluginEventType.EditorReady, + eventType: 'old_newEvent', + data: 'plugin2', }); + expect(onPluginEventSpy2).toHaveBeenCalledTimes(1); expect(onPluginEventSpy2).toHaveBeenCalledWith({ - eventType: PluginEventType.EditorReady, + eventType: 'old_newEvent', + data: 'plugin2', + }); + expect(eventConverter.newEventToOldEvent).toHaveBeenCalledTimes(1); + expect(eventConverter.newEventToOldEvent).toHaveBeenCalledWith(mockedEvent); + expect(eventConverter.oldEventToNewEvent).toHaveBeenCalledTimes(1); + expect(eventConverter.oldEventToNewEvent).toHaveBeenCalledWith( + { + eventType: 'old_newEvent' as any, + data: 'plugin2', + }, + { + eventType: 'new_old_newEvent' as any, + data: 'plugin2', + } + ); + + expect(mockedEvent).toEqual({ + eventType: 'new_old_newEvent', + data: 'plugin2', + }); + + plugin.dispose(); + }); + + it('onPluginEvent with exclusive handling', () => { + const initializeSpy = jasmine.createSpy('initialize'); + const onPluginEventSpy1 = jasmine.createSpy('onPluginEvent1'); + const onPluginEventSpy2 = jasmine.createSpy('onPluginEvent2'); + const disposeSpy = jasmine.createSpy('dispose'); + + const mockedPlugin1 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy1, + dispose: disposeSpy, + } as any; + const mockedPlugin2 = { + initialize: initializeSpy, + onPluginEvent: onPluginEventSpy2, + dispose: disposeSpy, + } as any; + + const mockedEditor = 'EDITOR' as any; + const plugin = new BridgePlugin({ + legacyPlugins: [mockedPlugin1, mockedPlugin2], + }); + + spyOn(eventConverter, 'newEventToOldEvent').and.callFake(newEvent => { + return { + eventType: 'old_' + newEvent.eventType, + eventDataCache: newEvent.eventDataCache, + } as any; }); - expect(onPluginEventSpy2).toHaveBeenCalledWith(mockedEvent); - const mockedEvent2 = { - eventType: 'MockedEvent2', + plugin.setOuterEditor(mockedEditor); + + const mockedEvent = { + eventType: 'newEvent', + eventDataCache: { + ['__ExclusivelyHandleEventPlugin']: mockedPlugin2, + }, } as any; - plugin.onPluginEvent(mockedEvent2); + plugin.onPluginEvent(mockedEvent); - expect(onPluginEventSpy1).toHaveBeenCalledWith(mockedEvent2); - expect(onPluginEventSpy2).toHaveBeenCalledWith(mockedEvent2); - expect(mockedEvent2).toEqual({ - eventType: 'MockedEvent2', + expect(onPluginEventSpy1).toHaveBeenCalledTimes(0); + expect(onPluginEventSpy2).toHaveBeenCalledTimes(1); + expect(onPluginEventSpy2).toHaveBeenCalledWith({ + eventType: 'old_newEvent', + eventDataCache: { + ['__ExclusivelyHandleEventPlugin']: mockedPlugin2, + }, }); + expect(eventConverter.newEventToOldEvent).toHaveBeenCalledTimes(1); + expect(eventConverter.newEventToOldEvent).toHaveBeenCalledWith(mockedEvent); plugin.dispose(); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/EventTypeTranslatePluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/corePlugins/EventTypeTranslatePluginTest.ts deleted file mode 100644 index e916632a838..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/corePlugins/EventTypeTranslatePluginTest.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as selectionConvert from '../../lib/editor/utils/selectionConverter'; -import { createEventTypeTranslatePlugin } from '../../lib/corePlugins/EventTypeTranslatePlugin'; -import { PluginEventType } from 'roosterjs-editor-types'; - -describe('EventTypeTranslatePlugin', () => { - let convertDomSelectionToRangeExSpy: jasmine.Spy; - const mockedDOMSelection = 'DOMSELECTION' as any; - const mockedRangeEx = 'RANGEEX' as any; - - beforeEach(() => { - convertDomSelectionToRangeExSpy = spyOn( - selectionConvert, - 'convertDomSelectionToRangeEx' - ).and.returnValue(mockedRangeEx); - }); - - it('translate a SelectionChanged event without selection', () => { - const plugin = createEventTypeTranslatePlugin(); - - plugin.initialize({} as any); - - const event = { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, - } as any; - - plugin.onPluginEvent(event); - - expect(event).toEqual({ - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, - }); - expect(convertDomSelectionToRangeExSpy).not.toHaveBeenCalled(); - }); - - it('translate a SelectionChanged event', () => { - const plugin = createEventTypeTranslatePlugin(); - - plugin.initialize({} as any); - - const event = { - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: null, - newSelection: mockedDOMSelection, - } as any; - - plugin.onPluginEvent(event); - - expect(event).toEqual({ - eventType: PluginEventType.SelectionChanged, - selectionRangeEx: mockedRangeEx, - newSelection: mockedDOMSelection, - }); - expect(convertDomSelectionToRangeExSpy).toHaveBeenCalledWith(mockedDOMSelection); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/selectionConverterTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/utils/selectionConverterTest.ts index 9735df25736..df95ac3dd7b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/selectionConverterTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/utils/selectionConverterTest.ts @@ -1,13 +1,9 @@ import * as createRange from 'roosterjs-editor-dom/lib/selection/createRange'; -import * as getSelectionPath from 'roosterjs-editor-dom/lib/selection/getSelectionPath'; -import * as queryElements from 'roosterjs-editor-dom/lib/utils/queryElements'; import * as tableCellUtils from 'roosterjs-content-model-core/lib/publicApi/domUtils/tableCellUtils'; -import { ContentMetadata, SelectionRangeTypes } from 'roosterjs-editor-types'; import { DOMSelection } from 'roosterjs-content-model-types'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { - convertDomSelectionToMetadata, convertDomSelectionToRangeEx, - convertMetadataToDOMSelection, convertRangeExToDomSelection, } from '../../../lib/editor/utils/selectionConverter'; @@ -184,201 +180,3 @@ describe('convertDomSelectionToRangeEx', () => { expect(tableCellUtilsSpy).toHaveBeenCalledWith(selection); }); }); - -describe('convertDomSelectionToMetadata', () => { - let getSelectionPathSpy: jasmine.Spy; - - beforeEach(() => { - getSelectionPathSpy = spyOn(getSelectionPath, 'default'); - }); - - it('null selection', () => { - const mockedDiv = 'DIV' as any; - const result = convertDomSelectionToMetadata(mockedDiv, null); - - expect(result).toBeNull(); - expect(getSelectionPathSpy).not.toHaveBeenCalled(); - }); - - it('range selection', () => { - const mockedDiv = 'DIV' as any; - const mockedRange = 'RANGE' as any; - const mockedPathStart = 'START' as any; - const mockedPathEnd = 'END' as any; - - const selection: DOMSelection = { - type: 'range', - range: mockedRange, - }; - const mockedPath = { - start: mockedPathStart, - end: mockedPathEnd, - } as any; - - getSelectionPathSpy.and.returnValue(mockedPath); - const result = convertDomSelectionToMetadata(mockedDiv, selection); - - expect(result).toEqual({ - type: SelectionRangeTypes.Normal, - isDarkMode: false, - start: mockedPathStart, - end: mockedPathEnd, - }); - expect(getSelectionPathSpy).toHaveBeenCalledWith(mockedDiv, mockedRange); - }); - - it('image selection', () => { - const mockedDiv = 'DIV' as any; - const mockedImageId = 'IMAGEID'; - const mockedImage = { - id: mockedImageId, - } as any; - - const selection: DOMSelection = { - type: 'image', - image: mockedImage, - }; - - const result = convertDomSelectionToMetadata(mockedDiv, selection); - - expect(result).toEqual({ - type: SelectionRangeTypes.ImageSelection, - isDarkMode: false, - imageId: mockedImageId, - }); - expect(getSelectionPathSpy).not.toHaveBeenCalled(); - }); - - it('table selection', () => { - const mockedDiv = 'DIV' as any; - const mockedTableId = 'TABLEID'; - const mockedTable = { - id: mockedTableId, - } as any; - - const selection: DOMSelection = { - type: 'table', - table: mockedTable, - firstColumn: 1, - firstRow: 2, - lastColumn: 3, - lastRow: 4, - }; - - const result = convertDomSelectionToMetadata(mockedDiv, selection); - - expect(result).toEqual({ - type: SelectionRangeTypes.TableSelection, - isDarkMode: false, - tableId: mockedTableId, - firstCell: { - x: 1, - y: 2, - }, - lastCell: { - x: 3, - y: 4, - }, - }); - expect(getSelectionPathSpy).not.toHaveBeenCalled(); - }); -}); - -describe('convertMetadataToDOMSelection', () => { - let createRangeSpy = jasmine.createSpy('createRange'); - let queryElementsSpy = jasmine.createSpy('queryElements'); - - beforeEach(() => { - createRangeSpy = spyOn(createRange, 'default'); - queryElementsSpy = spyOn(queryElements, 'default'); - }); - - it('null selection', () => { - const mockedDiv = 'DIV' as any; - const result = convertMetadataToDOMSelection(mockedDiv, undefined); - - expect(result).toBeNull(); - expect(createRangeSpy).not.toHaveBeenCalled(); - expect(queryElementsSpy).not.toHaveBeenCalled(); - }); - - it('range selection', () => { - const mockedDiv = 'DIV' as any; - const mockedStartPath = 'START' as any; - const mockedEndPath = 'END' as any; - const mockedRange = 'RANGE' as any; - const metadata: ContentMetadata = { - type: SelectionRangeTypes.Normal, - isDarkMode: false, - start: mockedStartPath, - end: mockedEndPath, - }; - - createRangeSpy.and.returnValue(mockedRange); - - const result = convertMetadataToDOMSelection(mockedDiv, metadata); - - expect(result).toEqual({ - type: 'range', - range: mockedRange, - }); - expect(createRangeSpy).toHaveBeenCalledWith(mockedDiv, mockedStartPath, mockedEndPath); - expect(queryElementsSpy).not.toHaveBeenCalled(); - }); - - it('image selection', () => { - const mockedDiv = 'DIV' as any; - const mockedImage = 'IMAGE' as any; - const mockedImageId = 'IMAGEID'; - const metadata: ContentMetadata = { - type: SelectionRangeTypes.ImageSelection, - isDarkMode: false, - imageId: mockedImageId, - }; - - queryElementsSpy.and.returnValue([mockedImage]); - - const result = convertMetadataToDOMSelection(mockedDiv, metadata); - - expect(result).toEqual({ - type: 'image', - image: mockedImage, - }); - expect(createRangeSpy).not.toHaveBeenCalled(); - expect(queryElementsSpy).toHaveBeenCalledWith(mockedDiv, '#' + mockedImageId); - }); - - it('table selection', () => { - const mockedDiv = 'DIV' as any; - const mockedTable = 'TABLE' as any; - const mockedTableId = 'TABLEID'; - const metadata: ContentMetadata = { - type: SelectionRangeTypes.TableSelection, - isDarkMode: false, - tableId: mockedTableId, - firstCell: { - x: 1, - y: 2, - }, - lastCell: { - x: 3, - y: 4, - }, - }; - - queryElementsSpy.and.returnValue([mockedTable]); - - const result = convertMetadataToDOMSelection(mockedDiv, metadata); - - expect(result).toEqual({ - type: 'table', - table: mockedTable, - firstColumn: 1, - firstRow: 2, - lastColumn: 3, - lastRow: 4, - }); - expect(createRangeSpy).not.toHaveBeenCalled(); - expect(queryElementsSpy).toHaveBeenCalledWith(mockedDiv, '#' + mockedTableId); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts index da3f811049e..538b7bc9a8e 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts @@ -1,10 +1,9 @@ import { normalizeContentModel } from 'roosterjs-content-model-dom'; -import { PluginEventType } from 'roosterjs-editor-types'; -import type { IContentModelEditor } from 'roosterjs-content-model-editor'; import type { ContentModelDocument, DeleteResult, FormatWithContentModelContext, + IStandaloneEditor, } from 'roosterjs-content-model-types'; /** @@ -12,7 +11,7 @@ import type { * @return True means content is changed, so need to rewrite content model to editor. Otherwise false */ export function handleKeyboardEventResult( - editor: IContentModelEditor, + editor: IStandaloneEditor, model: ContentModelDocument, rawEvent: KeyboardEvent, result: DeleteResult, @@ -47,7 +46,7 @@ export function handleKeyboardEventResult( // Trigger an event to let plugins know the content is about to be changed by Content Model keyboard editing. // So plugins can do proper handling. e.g. UndoPlugin can decide whether take a snapshot before this change happens. - editor.triggerPluginEvent(PluginEventType.BeforeKeyboardEditing, { + editor.triggerEvent('beforeKeyboardEditing', { rawEvent, }); diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts index ed5767a922c..4357e1c5ed4 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts @@ -4,30 +4,19 @@ import { deprecatedBorderColorParser } from './utils/deprecatedColorParser'; import { getPasteSource } from './pasteSourceValidations/getPasteSource'; import { parseLink } from './utils/linkParser'; import { PastePropertyNames } from './pasteSourceValidations/constants'; -import { PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; import { processPastedContentFromExcel } from './Excel/processPastedContentFromExcel'; import { processPastedContentFromPowerPoint } from './PowerPoint/processPastedContentFromPowerPoint'; import { processPastedContentFromWordDesktop } from './WordDesktop/processPastedContentFromWordDesktop'; import { processPastedContentWacComponents } from './WacComponents/processPastedContentWacComponents'; -import type { IContentModelEditor } from 'roosterjs-content-model-editor'; import type { BorderFormat, - ContentModelBeforePasteEvent, ContentModelBlockFormat, ContentModelTableCellFormat, + EditorPlugin, FormatParser, - PasteType, + IStandaloneEditor, + PluginEvent, } from 'roosterjs-content-model-types'; -import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-editor-types'; - -// Map old PasteType to new PasteType -// TODO: We can remove this once we have standalone editor -const PasteTypeMap: Record = { - [OldPasteType.AsImage]: 'asImage', - [OldPasteType.AsPlainText]: 'asPlainText', - [OldPasteType.MergeFormat]: 'mergeFormat', - [OldPasteType.Normal]: 'normal', -}; /** * Paste plugin, handles BeforePaste event and reformat some special content, including: @@ -38,7 +27,7 @@ const PasteTypeMap: Record = { * (This class is still under development, and may still be changed in the future with some breaking changes) */ export class ContentModelPastePlugin implements EditorPlugin { - private editor: IContentModelEditor | null = null; + private editor: IStandaloneEditor | null = null; /** * Construct a new instance of Paste class @@ -60,9 +49,9 @@ export class ContentModelPastePlugin implements EditorPlugin { * editor reference so that it can call to any editor method or format API later. * @param editor The editor object */ - initialize(editor: IEditor) { + initialize(editor: IStandaloneEditor) { // TODO: Later we may need a different interface for Content Model editor plugin - this.editor = editor as IContentModelEditor; + this.editor = editor; } /** @@ -81,55 +70,53 @@ export class ContentModelPastePlugin implements EditorPlugin { * @param event The event to handle: */ onPluginEvent(event: PluginEvent) { - if (!this.editor || event.eventType != PluginEventType.BeforePaste) { + if (!this.editor || event.eventType != 'beforePaste') { return; } - const ev = event as ContentModelBeforePasteEvent; - - if (!ev.domToModelOption) { + if (!event.domToModelOption) { return; } - const pasteSource = getPasteSource(ev, false); - const pasteType = PasteTypeMap[ev.pasteType]; + const pasteSource = getPasteSource(event, false); + const pasteType = event.pasteType; switch (pasteSource) { case 'wordDesktop': - processPastedContentFromWordDesktop(ev, this.editor.getTrustedHTMLHandler()); + processPastedContentFromWordDesktop(event, this.editor.getTrustedHTMLHandler()); break; case 'wacComponents': - processPastedContentWacComponents(ev); + processPastedContentWacComponents(event); break; case 'excelOnline': case 'excelDesktop': if (pasteType === 'normal' || pasteType === 'mergeFormat') { // Handle HTML copied from Excel processPastedContentFromExcel( - ev, + event, this.editor.getTrustedHTMLHandler(), this.allowExcelNoBorderTable ); } break; case 'googleSheets': - ev.domToModelOption.additionalAllowedTags.push( - PastePropertyNames.GOOGLE_SHEET_NODE_NAME + event.domToModelOption.additionalAllowedTags.push( + PastePropertyNames.GOOGLE_SHEET_NODE_NAME as Lowercase ); break; case 'powerPointDesktop': - processPastedContentFromPowerPoint(ev, this.editor.getTrustedHTMLHandler()); + processPastedContentFromPowerPoint(event, this.editor.getTrustedHTMLHandler()); break; } - addParser(ev.domToModelOption, 'link', parseLink); - addParser(ev.domToModelOption, 'tableCell', deprecatedBorderColorParser); - addParser(ev.domToModelOption, 'tableCell', tableBorderParser); - addParser(ev.domToModelOption, 'table', deprecatedBorderColorParser); + addParser(event.domToModelOption, 'link', parseLink); + addParser(event.domToModelOption, 'tableCell', deprecatedBorderColorParser); + addParser(event.domToModelOption, 'tableCell', tableBorderParser); + addParser(event.domToModelOption, 'table', deprecatedBorderColorParser); if (pasteType === 'mergeFormat') { - addParser(ev.domToModelOption, 'block', blockElementParser); - addParser(ev.domToModelOption, 'listLevel', blockElementParser); + addParser(event.domToModelOption, 'block', blockElementParser); + addParser(event.domToModelOption, 'listLevel', blockElementParser); } } } diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts index dcd39ef1e22..443b16da1cb 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts @@ -2,7 +2,7 @@ import addParser from '../utils/addParser'; import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom'; import { setProcessor } from '../utils/setProcessor'; import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; -import type { ContentModelBeforePasteEvent } from 'roosterjs-content-model-types'; +import type { BeforePasteEvent } from 'roosterjs-content-model-types'; const LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i; const LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i; @@ -17,7 +17,7 @@ const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4'; */ export function processPastedContentFromExcel( - event: ContentModelBeforePasteEvent, + event: BeforePasteEvent, trustedHTMLHandler: TrustedHTMLHandler, allowExcelNoBorderTable?: boolean ) { diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts index 668daa5da52..79e685e0885 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts @@ -1,5 +1,6 @@ import { moveChildNodes } from 'roosterjs-content-model-dom'; -import type { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { BeforePasteEvent } from 'roosterjs-content-model-types'; +import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts index 85665ab5b37..783beb96cce 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts @@ -8,7 +8,7 @@ import { WAC_IDENTIFY_SELECTOR, } from './constants'; import type { - ContentModelBeforePasteEvent, + BeforePasteEvent, ContentModelBlockFormat, ContentModelBlockGroup, ContentModelListItemLevelFormat, @@ -189,7 +189,7 @@ const wacCommentParser: FormatParser = ( * We need to remove the display property and margin from all the list item * @param ev ContentModelBeforePasteEvent */ -export function processPastedContentWacComponents(ev: ContentModelBeforePasteEvent) { +export function processPastedContentWacComponents(ev: BeforePasteEvent) { addParser(ev.domToModelOption, 'segment', wacSubSuperParser); addParser(ev.domToModelOption, 'listItemThread', wacListItemParser); addParser(ev.domToModelOption, 'listLevel', wacListLevelParser); diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts index 8d25601a7e7..32a797e9450 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/getStyleMetadata.ts @@ -1,6 +1,6 @@ import { getObjectKeys } from 'roosterjs-content-model-dom'; import type { WordMetadata } from './WordMetadata'; -import type { ContentModelBeforePasteEvent } from 'roosterjs-content-model-types'; +import type { BeforePasteEvent } from 'roosterjs-content-model-types'; const FORMATING_REGEX = /[\n\t'{}"]+/g; @@ -25,7 +25,7 @@ const FORMATING_REGEX = /[\n\t'{}"]+/g; * */ export default function getStyleMetadata( - ev: ContentModelBeforePasteEvent, + ev: BeforePasteEvent, trustedHTMLHandler: (val: string) => string ) { const metadataMap: Map = new Map(); diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index 63e66f2009c..a40515c6c71 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -6,7 +6,7 @@ import { processWordList } from './processWordLists'; import { setProcessor } from '../utils/setProcessor'; import type { WordMetadata } from './WordMetadata'; import type { - ContentModelBeforePasteEvent, + BeforePasteEvent, ContentModelBlockFormat, ContentModelListItemLevelFormat, ContentModelTableFormat, @@ -24,7 +24,7 @@ const DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE = 120; * @param ev ContentModelBeforePasteEvent */ export function processPastedContentFromWordDesktop( - ev: ContentModelBeforePasteEvent, + ev: BeforePasteEvent, trustedHTMLHandler: (text: string) => string ) { const metadataMap: Map = getStyleMetadata(ev, trustedHTMLHandler); diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts index 9f84b0f1125..031a57f338e 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts @@ -5,8 +5,7 @@ import { isGoogleSheetDocument } from './isGoogleSheetDocument'; import { isPowerPointDesktopDocument } from './isPowerPointDesktopDocument'; import { isWordDesktopDocument } from './isWordDesktopDocument'; import { shouldConvertToSingleImage } from './shouldConvertToSingleImage'; -import type { ClipboardData } from 'roosterjs-content-model-types'; -import type { BeforePasteEvent } from 'roosterjs-editor-types'; +import type { BeforePasteEvent, ClipboardData } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/editingTestCommon.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/editingTestCommon.ts index 6d637e5f62f..5a9ed6d7635 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/editingTestCommon.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/editingTestCommon.ts @@ -12,7 +12,7 @@ export function editingTestCommon( result: ContentModelDocument, calledTimes: number ) { - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + const triggerEvent = jasmine.createSpy('triggerEvent'); let formatResult: boolean | undefined; @@ -29,7 +29,7 @@ export function editingTestCommon( }); const editor = ({ - triggerPluginEvent, + triggerEvent, getEnvironment: () => ({}), formatContentModel, } as any) as IContentModelEditor; diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts index cde253d34d3..d5e793e7de8 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts @@ -1,7 +1,6 @@ import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import { FormatWithContentModelContext } from 'roosterjs-content-model-types'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; -import { PluginEventType } from 'roosterjs-editor-types'; import { handleKeyboardEventResult, shouldDeleteAllSegmentsBefore, @@ -14,20 +13,20 @@ describe('handleKeyboardEventResult', () => { let cacheContentModel: jasmine.Spy; let preventDefault: jasmine.Spy; let triggerContentChangedEvent: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; + let triggerEvent: jasmine.Spy; let addUndoSnapshot: jasmine.Spy; beforeEach(() => { cacheContentModel = jasmine.createSpy('cacheContentModel'); preventDefault = jasmine.createSpy('preventDefault'); triggerContentChangedEvent = jasmine.createSpy('triggerContentChangedEvent'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + triggerEvent = jasmine.createSpy('triggerEvent'); addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); mockedEditor = ({ cacheContentModel, triggerContentChangedEvent, - triggerPluginEvent, + triggerEvent, addUndoSnapshot, } as any) as IContentModelEditor; mockedEvent = ({ @@ -59,7 +58,7 @@ describe('handleKeyboardEventResult', () => { expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(mockedModel); expect(triggerContentChangedEvent).not.toHaveBeenCalled(); expect(cacheContentModel).not.toHaveBeenCalled(); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.BeforeKeyboardEditing, { + expect(triggerEvent).toHaveBeenCalledWith('beforeKeyboardEditing', { rawEvent: mockedEvent, }); expect(context.skipUndoSnapshot).toBeTrue(); @@ -86,7 +85,7 @@ describe('handleKeyboardEventResult', () => { expect(triggerContentChangedEvent).not.toHaveBeenCalled(); expect(normalizeContentModel.normalizeContentModel).not.toHaveBeenCalled(); expect(cacheContentModel).not.toHaveBeenCalledWith(null); - expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); expect(context.skipUndoSnapshot).toBeTrue(); expect(context.clearModelCache).toBeTruthy(); }); @@ -111,7 +110,7 @@ describe('handleKeyboardEventResult', () => { expect(triggerContentChangedEvent).not.toHaveBeenCalled(); expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(mockedModel); expect(cacheContentModel).not.toHaveBeenCalled(); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.BeforeKeyboardEditing, { + expect(triggerEvent).toHaveBeenCalledWith('beforeKeyboardEditing', { rawEvent: mockedEvent, }); expect(context.skipUndoSnapshot).toBeFalse(); @@ -138,7 +137,7 @@ describe('handleKeyboardEventResult', () => { expect(triggerContentChangedEvent).not.toHaveBeenCalled(); expect(normalizeContentModel.normalizeContentModel).not.toHaveBeenCalled(); expect(cacheContentModel).not.toHaveBeenCalled(); - expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); expect(context.skipUndoSnapshot).toBeTrue(); expect(context.clearModelCache).toBeFalsy(); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index 957efc9e6d0..b412e21695f 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -4,11 +4,10 @@ import * as getPasteSource from '../../lib/paste/pasteSourceValidations/getPaste import * as PowerPointFile from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; import * as setProcessor from '../../lib/paste/utils/setProcessor'; import * as WacFile from '../../lib/paste/WacComponents/processPastedContentWacComponents'; -import { ContentModelBeforePasteEvent } from 'roosterjs-content-model-types'; +import { BeforePasteEvent } from 'roosterjs-content-model-types'; import { ContentModelPastePlugin } from '../../lib/paste/ContentModelPastePlugin'; import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { PastePropertyNames } from '../../lib/paste/pasteSourceValidations/constants'; -import { PasteType, PluginEventType } from 'roosterjs-editor-types'; const trustedHTMLHandler = (val: string) => val; const DEFAULT_TIMES_ADD_PARSER_CALLED = 4; @@ -24,27 +23,7 @@ describe('Content Model Paste Plugin Test', () => { spyOn(setProcessor, 'setProcessor').and.callThrough(); }); - let event: ContentModelBeforePasteEvent = ({ - clipboardData: {}, - fragment: document.createDocumentFragment(), - sanitizingOption: { - elementCallbacks: {}, - attributeCallbacks: {}, - cssStyleCallbacks: {}, - additionalTagReplacements: {}, - additionalAllowedAttributes: [], - additionalAllowedCssClasses: [], - additionalDefaultStyleValues: {}, - additionalGlobalStyleNodes: [], - additionalPredefinedCssForElement: {}, - preserveHtmlComments: false, - unknownTagReplacement: null, - }, - htmlBefore: '', - htmlAfter: '', - htmlAttributes: {}, - domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [] }, - }); + let event: BeforePasteEvent; describe('onPluginEvent', () => { let plugin = new ContentModelPastePlugin(); @@ -52,28 +31,21 @@ describe('Content Model Paste Plugin Test', () => { beforeEach(() => { plugin = new ContentModelPastePlugin(); - event = ({ - eventType: PluginEventType.BeforePaste, - domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [] }, - sanitizingOption: { - elementCallbacks: {}, - attributeCallbacks: {}, - cssStyleCallbacks: {}, - additionalTagReplacements: {}, - additionalAllowedAttributes: [], - additionalAllowedCssClasses: [], - additionalDefaultStyleValues: {}, - additionalGlobalStyleNodes: [], - additionalPredefinedCssForElement: {}, - preserveHtmlComments: false, - unknownTagReplacement: null, - }, - pasteType: PasteType.Normal, + event = { + eventType: 'beforePaste', clipboardData: { html: '', }, fragment: document.createDocumentFragment(), - }); + htmlBefore: '', + htmlAfter: '', + htmlAttributes: {}, + pasteType: 'normal', + domToModelOption: { + additionalAllowedTags: [], + additionalDisallowedTags: [], + } as any, + }; }); it('WordDesktop', () => { @@ -90,7 +62,7 @@ describe('Content Model Paste Plugin Test', () => { spyOn(getPasteSource, 'getPasteSource').and.returnValue('excelDesktop'); spyOn(ExcelFile, 'processPastedContentFromExcel').and.callThrough(); - (event).pasteType = PasteType.MergeFormat; + (event).pasteType = 'mergeFormat'; plugin.initialize(editor); plugin.onPluginEvent(event); @@ -107,7 +79,7 @@ describe('Content Model Paste Plugin Test', () => { spyOn(getPasteSource, 'getPasteSource').and.returnValue('excelDesktop'); spyOn(ExcelFile, 'processPastedContentFromExcel').and.callThrough(); - (event).pasteType = PasteType.AsImage; + (event).pasteType = 'asImage'; plugin.initialize(editor); plugin.onPluginEvent(event); @@ -198,7 +170,7 @@ describe('Content Model Paste Plugin Test', () => { expect(addParser.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(0); expect(event.domToModelOption.additionalAllowedTags).toEqual([ - PastePropertyNames.GOOGLE_SHEET_NODE_NAME, + PastePropertyNames.GOOGLE_SHEET_NODE_NAME as Lowercase, ]); }); }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts index 1c0bb630ea2..3d03d83a631 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts @@ -13,7 +13,7 @@ export function initEditor(id: string): IContentModelEditor { document.body.insertBefore(node, document.body.childNodes[0]); let options: ContentModelEditorOptions = { - legacyPlugins: [new ContentModelPastePlugin()], + plugins: [new ContentModelPastePlugin()], coreApiOverride: { getVisibleViewport: () => { return { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts index c62b569af11..4067fbc000d 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/getStyleMetadataTest.ts @@ -1,9 +1,9 @@ import getStyleMetadata from '../../lib/paste/WordDesktop/getStyleMetadata'; -import { ContentModelBeforePasteEvent } from 'roosterjs-content-model-types'; +import { BeforePasteEvent } from 'roosterjs-content-model-types'; describe('getStyleMetadata', () => { it('Extract metadata from style element', () => { - const event = ({ + const event = ({ htmlBefore: '', }); diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts index e5cafe15069..e00e653cdaf 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts @@ -1,5 +1,4 @@ -import { BeforePasteEvent } from 'roosterjs-editor-types'; -import { ClipboardData } from 'roosterjs-content-model-types'; +import { BeforePasteEvent, ClipboardData } from 'roosterjs-content-model-types'; import { getPasteSource } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; import { PastePropertyNames } from '../../../lib/paste/pasteSourceValidations/constants'; import { diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts index 5fcc1d540c7..de5ede5bd72 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts @@ -1,24 +1,26 @@ import * as moveChildNodes from 'roosterjs-content-model-dom/lib/domUtils/moveChildNodes'; -import { createDefaultHtmlSanitizerOptions } from 'roosterjs-editor-dom'; import { processPastedContentFromPowerPoint } from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; -import { - BeforePasteEvent, - PasteType, - PluginEventType, - TrustedHTMLHandler, -} from 'roosterjs-editor-types'; -import type { ClipboardData } from 'roosterjs-content-model-types'; +import { TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { BeforePasteEvent, ClipboardData } from 'roosterjs-content-model-types'; const getPasteEvent = (): BeforePasteEvent => { return { - eventType: PluginEventType.BeforePaste, + eventType: 'beforePaste', clipboardData: {}, fragment: document.createDocumentFragment(), - sanitizingOption: createDefaultHtmlSanitizerOptions(), htmlBefore: '', htmlAfter: '', htmlAttributes: {}, - pasteType: PasteType.Normal, + pasteType: 'normal', + domToModelOption: { + additionalAllowedTags: [], + additionalDisallowedTags: [], + additionalFormatParsers: {}, + formatParserOverride: {}, + processorOverride: {}, + styleSanitizers: {}, + attributeSanitizers: {}, + }, }; }; diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index b0b8a367f1c..47fd3f62e40 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -1,9 +1,9 @@ import * as getStyleMetadata from '../../lib/paste/WordDesktop/getStyleMetadata'; -import { ClipboardData, ContentModelBeforePasteEvent } from 'roosterjs-content-model-types'; import { expectEqual } from './e2e/testUtils'; import { PluginEventType } from 'roosterjs-editor-types'; import { processPastedContentFromWordDesktop } from '../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { WordMetadata } from '../../lib/paste/WordDesktop/WordMetadata'; +import { BeforePasteEvent, ClipboardData } from 'roosterjs-content-model-types'; import { createDomToModelContext, domToContentModel, @@ -5149,7 +5149,7 @@ export function createBeforePasteEventMock(fragment: DocumentFragment, htmlBefor htmlAfter: '', htmlAttributes: {}, domToModelOption: { additionalAllowedTags: [], additionalDisallowedTags: [] }, - } as any) as ContentModelBeforePasteEvent; + } as any) as BeforePasteEvent; } function createListElementFromWord( diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts index 063811e4c1d..0da11b5b855 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/EditorPlugin.ts @@ -1,5 +1,5 @@ +import type { PluginEvent } from '../event/PluginEvent'; import type { IStandaloneEditor } from './IStandaloneEditor'; -import type { PluginEvent } from 'roosterjs-editor-types'; /** * Interface of an editor plugin diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts index 05953f038b6..448df5007a4 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -1,3 +1,5 @@ +import type { PluginEventData, PluginEventFromType } from '../event/PluginEventData'; +import type { PluginEventType } from '../event/PluginEventType'; import type { PasteType } from '../enum/PasteType'; import type { ClipboardData } from '../parameter/ClipboardData'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; @@ -14,12 +16,7 @@ import type { ContentModelFormatter, FormatWithContentModelOptions, } from '../parameter/FormatWithContentModelOptions'; -import type { - DarkColorHandler, - PluginEventData, - PluginEventFromType, - PluginEventType, -} from 'roosterjs-editor-types'; +import type { DarkColorHandler, TrustedHTMLHandler } from 'roosterjs-editor-types'; /** * An interface of standalone Content Model editor. @@ -220,4 +217,12 @@ export interface IStandaloneEditor { * @returns true if focus is in editor, otherwise false */ hasFocus(): boolean; + + /** + * Get a function to convert HTML string to trusted HTML string. + * By default it will just return the input HTML directly. To override this behavior, + * pass your own trusted HTML handler to EditorOptions.trustedHTMLHandler + * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types + */ + getTrustedHTMLHandler(): TrustedHTMLHandler; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index 185cf7be2f9..709b65bfbdb 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -1,3 +1,4 @@ +import type { PluginEvent } from '../event/PluginEvent'; import type { PluginState } from '../pluginState/PluginState'; import type { EditorPlugin } from './EditorPlugin'; import type { ClipboardData } from '../parameter/ClipboardData'; @@ -5,12 +6,7 @@ import type { PasteType } from '../enum/PasteType'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { Snapshot } from '../parameter/Snapshot'; import type { EntityState } from '../parameter/FormatWithContentModelContext'; -import type { - DarkColorHandler, - PluginEvent, - Rect, - TrustedHTMLHandler, -} from 'roosterjs-editor-types'; +import type { DarkColorHandler, Rect, TrustedHTMLHandler } from 'roosterjs-editor-types'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; import type { DomToModelOption } from '../context/DomToModelOption'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts deleted file mode 100644 index c41a81ddd1b..00000000000 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { DomToModelOptionForPaste, MergePastedContentFunc } from './BeforePasteEvent'; -import type { - BeforePasteEvent, - BeforePasteEventData, - CompatibleBeforePasteEvent, -} from 'roosterjs-editor-types'; - -/** - * Data of ContentModelBeforePasteEvent - */ -export interface ContentModelBeforePasteEventData extends BeforePasteEventData { - /** - * domToModel Options to use when creating the content model from the paste fragment - */ - domToModelOption: DomToModelOptionForPaste; - - /** - * customizedMerge Customized merge function to use when merging the paste fragment into the editor - */ - customizedMerge?: MergePastedContentFunc; -} - -/** - * Provides a chance for plugin to change the content before it is pasted into editor. - */ -export interface ContentModelBeforePasteEvent - extends ContentModelBeforePasteEventData, - BeforePasteEvent {} - -/** - * Provides a chance for plugin to change the content before it is pasted into editor. - */ -export interface CompatibleContentModelBeforePasteEvent - extends ContentModelBeforePasteEventData, - CompatibleBeforePasteEvent {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts deleted file mode 100644 index 6423d386d3f..00000000000 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { ChangedEntity } from './ContentChangedEvent'; -import type { EntityState } from '../parameter/FormatWithContentModelContext'; -import type { ContentModelDocument } from '../group/ContentModelDocument'; -import type { DOMSelection } from '../selection/DOMSelection'; -import type { - CompatibleContentChangedEvent, - ContentChangedEvent, - ContentChangedEventData, -} from 'roosterjs-editor-types'; - -/** - * Data of ContentModelContentChangedEvent - */ -export interface ContentModelContentChangedEventData extends ContentChangedEventData { - /** - * The content model that is applied which causes this content changed event - */ - readonly contentModel?: ContentModelDocument; - - /** - * Selection range applied to the document - */ - readonly selection?: DOMSelection; - - /** - * Entities got changed (added or removed) during the content change process - */ - readonly changedEntities?: ChangedEntity[]; - - /** - * Entity states related to this event - */ - readonly entityStates?: EntityState[]; -} - -/** - * Represents a change to the editor made by another plugin with content model inside - */ -export interface ContentModelContentChangedEvent - extends ContentChangedEvent, - ContentModelContentChangedEventData {} - -/** - * Represents a change to the editor made by another plugin with content model inside - */ -export interface CompatibleContentModelContentChangedEvent - extends CompatibleContentChangedEvent, - ContentModelContentChangedEventData {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelSelectionChangedEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelSelectionChangedEvent.ts deleted file mode 100644 index d573bc77ba7..00000000000 --- a/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelSelectionChangedEvent.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { DOMSelection } from '../selection/DOMSelection'; -import type { - CompatibleSelectionChangedEvent, - SelectionChangedEvent, - SelectionChangedEventData, -} from 'roosterjs-editor-types'; - -/** - * Data of ContentModelSelectionChangedEvent - */ -export interface ContentModelSelectionChangedEventData extends SelectionChangedEventData { - /** - * The new selection after change - */ - newSelection: DOMSelection | null; -} - -/** - * Represents an event that will be fired when the user changed the selection - */ -export interface ContentModelSelectionChangedEvent - extends ContentModelSelectionChangedEventData, - SelectionChangedEvent {} - -/** - * Represents an event that will be fired when the user changed the selection - */ -export interface CompatibleContentModelSelectionChangedEvent - extends ContentModelSelectionChangedEventData, - CompatibleSelectionChangedEvent {} diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index ad970b46cbb..39bf456ce4b 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -277,21 +277,6 @@ export { ClipboardData } from './parameter/ClipboardData'; export { AnnounceData, KnownAnnounceStrings } from './parameter/AnnounceData'; export { ValueSanitizer } from './parameter/ValueSanitizer'; -export { - ContentModelBeforePasteEvent, - ContentModelBeforePasteEventData, - CompatibleContentModelBeforePasteEvent, -} from './event/ContentModelBeforePasteEvent'; -export { - ContentModelContentChangedEvent, - CompatibleContentModelContentChangedEvent, - ContentModelContentChangedEventData, -} from './event/ContentModelContentChangedEvent'; -export { - CompatibleContentModelSelectionChangedEvent, - ContentModelSelectionChangedEvent, - ContentModelSelectionChangedEventData, -} from './event/ContentModelSelectionChangedEvent'; export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent'; export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent'; export { BeforeDisposeEvent } from './event/BeforeDisposeEvent'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMEventRecord.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMEventRecord.ts index b3359566e6c..460f3260325 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMEventRecord.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/DOMEventRecord.ts @@ -1,4 +1,4 @@ -import type { PluginEventType } from 'roosterjs-editor-types'; +import type { PluginEventType } from '../event/PluginEventType'; /** * Handler function type of DOM event diff --git a/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts b/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts index 4c8242a6639..fc4437d9380 100644 --- a/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts @@ -4,11 +4,11 @@ import { ContentModelPastePlugin, EntityDelimiterPlugin, } from 'roosterjs-content-model-plugins'; -import type { EditorPlugin } from 'roosterjs-editor-types'; import type { ContentModelEditorOptions, IContentModelEditor, } from 'roosterjs-content-model-editor'; +import type { EditorPlugin } from 'roosterjs-content-model-types'; /** * Create a Content Model Editor using the given options @@ -23,15 +23,12 @@ export function createContentModelEditor( additionalPlugins?: EditorPlugin[], initialContent?: string ): IContentModelEditor { - const plugins = additionalPlugins ? [...additionalPlugins] : []; - plugins.push( - new ContentModelPastePlugin(), - new ContentModelEditPlugin(), - new EntityDelimiterPlugin() - ); + const legacyPlugins = [new ContentModelEditPlugin(), new EntityDelimiterPlugin()]; + const plugins = [new ContentModelPastePlugin(), ...(additionalPlugins ?? [])]; const options: ContentModelEditorOptions = { - legacyPlugins: plugins, + legacyPlugins: legacyPlugins, + plugins: plugins, initialContent: initialContent, defaultSegmentFormat: { fontFamily: 'Calibri,Arial,Helvetica,sans-serif', diff --git a/packages-content-model/roosterjs-content-model/package.json b/packages-content-model/roosterjs-content-model/package.json index 76bfe86013d..f821aa513a7 100644 --- a/packages-content-model/roosterjs-content-model/package.json +++ b/packages-content-model/roosterjs-content-model/package.json @@ -3,7 +3,6 @@ "description": "Content Model for roosterjs (Under development)", "dependencies": { "tslib": "^2.3.1", - "roosterjs-editor-types": "", "roosterjs-content-model-types": "", "roosterjs-content-model-dom": "", "roosterjs-content-model-core": "", From 2c4ff2dbe4bc55f29a0f029a7ffad752e971e26a Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Fri, 12 Jan 2024 15:15:59 -0600 Subject: [PATCH 49/64] Table fidelity Improvement: Add each border radius when the shorthand css is not provided and `borderCollapse: separate` support (#2325) * Add border radius props * fix * fix test * Change test name * address comment --- .../common/borderFormatHandler.ts | 23 +++++- .../table/tableSpacingFormatHandler.ts | 9 +++ .../common/borderFormatHandlerTest.ts | 73 +++++++++++++++++++ .../table/tableSpacingFormatHandlerTest.ts | 10 ++- .../lib/format/formatParts/BorderFormat.ts | 20 +++++ .../lib/format/formatParts/SpacingFormat.ts | 5 ++ 6 files changed, 138 insertions(+), 2 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts index d03028de2cd..dd52529281b 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/common/borderFormatHandler.ts @@ -19,6 +19,15 @@ const BorderWidthKeys: (keyof CSSStyleDeclaration)[] = [ 'borderLeftWidth', ]; +const BorderRadiusKeys: (keyof BorderFormat & keyof CSSStyleDeclaration)[] = [ + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', +]; + +const AllKeys = BorderKeys.concat(BorderRadiusKeys); + /** * @internal */ @@ -42,15 +51,27 @@ export const borderFormatHandler: FormatHandler = { if (borderRadius) { format.borderRadius = borderRadius; + } else { + BorderRadiusKeys.forEach(key => { + const value = element.style[key]; + + if (value) { + format[key] = value; + } + }); } }, apply: (format, element) => { - BorderKeys.forEach(key => { + AllKeys.forEach(key => { const value = format[key]; if (value) { element.style[key] = value; } }); + + if (format.borderRadius) { + element.style.borderRadius = format.borderRadius; + } }, }; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts index df66ef61867..2165edeb33a 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/table/tableSpacingFormatHandler.ts @@ -2,6 +2,7 @@ import type { FormatHandler } from '../FormatHandler'; import type { SpacingFormat } from 'roosterjs-content-model-types'; const BorderCollapsed = 'collapse'; +const BorderSeparate = 'separate'; const CellPadding = 'cellPadding'; /** @@ -17,12 +18,20 @@ export const tableSpacingFormatHandler: FormatHandler = { format.borderCollapse = true; } } + + if (element.style.borderCollapse == BorderSeparate) { + format.borderSeparate = true; + } }, apply: (format, element) => { if (format.borderCollapse) { element.style.borderCollapse = BorderCollapsed; element.style.borderSpacing = '0'; element.style.boxSizing = 'border-box'; + } else if (format.borderSeparate) { + element.style.borderCollapse = BorderSeparate; + element.style.borderSpacing = '0'; + element.style.boxSizing = 'border-box'; } }, }; diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts index 17fb9bb5e9b..fbf9e8aae58 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/common/borderFormatHandlerTest.ts @@ -91,6 +91,59 @@ describe('borderFormatHandler.parse', () => { borderRadius: '10px', }); }); + + it('Has border radius and independant corner radius, but prefer shorthand css', () => { + div.style.borderTopRightRadius = '7px'; + div.style.borderBottomLeftRadius = '7px'; + div.style.borderBottomRightRadius = '7px'; + div.style.borderRadius = '10px'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderRadius: '10px', + }); + }); + + it('Has border borderTopLeftRadius', () => { + div.style.borderTopLeftRadius = '10px'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderTopLeftRadius: '10px', + }); + }); + + it('Has border borderTopRightRadius', () => { + div.style.borderTopRightRadius = '10px'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderTopRightRadius: '10px', + }); + }); + + it('Has border borderBottomLeftRadius', () => { + div.style.borderBottomLeftRadius = '10px'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderBottomLeftRadius: '10px', + }); + }); + + it('Has border borderBottomRightRadius', () => { + div.style.borderBottomRightRadius = '10px'; + + borderFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + borderBottomRightRadius: '10px', + }); + }); }); describe('borderFormatHandler.apply', () => { @@ -125,4 +178,24 @@ describe('borderFormatHandler.apply', () => { expect(div.outerHTML).toEqual('
'); }); + + itChromeOnly('Use independant border radius 1', () => { + format.borderBottomLeftRadius = '2px'; + format.borderBottomRightRadius = '3px'; + format.borderTopRightRadius = '3px'; + + borderFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toEqual( + '
' + ); + }); + + it('border radius', () => { + format.borderRadius = '50%'; + + borderFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toEqual('
'); + }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts index 4af7f3d2985..8ccee8fdb0e 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/table/tableSpacingFormatHandlerTest.ts @@ -28,7 +28,7 @@ describe('tableSpacingFormatHandler.parse', () => { it('Non-collapsed border', () => { div.style.borderCollapse = 'separate'; tableSpacingFormatHandler.parse(format, div, context, {}); - expect(format).toEqual({}); + expect(format).toEqual({ borderSeparate: true }); }); it('Set border collapsed if element contains cellpadding attribute', () => { @@ -61,4 +61,12 @@ describe('tableSpacingFormatHandler.apply', () => { '
' ); }); + + it('Separated border', () => { + format.borderSeparate = true; + tableSpacingFormatHandler.apply(format, div, context); + expect(div.outerHTML).toEqual( + '
' + ); + }); }); diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/BorderFormat.ts b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/BorderFormat.ts index 8d0b501cdfd..6be7a80e55d 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/BorderFormat.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/BorderFormat.ts @@ -26,4 +26,24 @@ export type BorderFormat = { * Radius to be applied in all borders corners */ borderRadius?: string; + + /** + * Radius to be applied in top left corner + */ + borderTopLeftRadius?: string; + + /** + * Radius to be applied in top right corner + */ + borderTopRightRadius?: string; + + /** + * Radius to be applied in bottom left corner + */ + borderBottomLeftRadius?: string; + + /** + * Radius to be applied in bottom right corner + */ + borderBottomRightRadius?: string; }; diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/SpacingFormat.ts b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/SpacingFormat.ts index d6f5d98f808..386140103b7 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/SpacingFormat.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/SpacingFormat.ts @@ -6,4 +6,9 @@ export type SpacingFormat = { * Whether borders of cells are collapsed together */ borderCollapse?: boolean; + + /** + * Whether borders of cells are separated + */ + borderSeparate?: boolean; }; From f588961f8cad2c789cdb275599a4faf1738bb958 Mon Sep 17 00:00:00 2001 From: florian-msft <87671048+florian-msft@users.noreply.github.com> Date: Sun, 14 Jan 2024 07:26:42 +0100 Subject: [PATCH 50/64] Allow TableResize's onShowHelperElement callback to get affected element (#2315) Co-authored-by: Jiuqing Song --- .../lib/plugins/TableResize/TableResize.ts | 3 ++- .../lib/plugins/TableResize/editors/CellResizer.ts | 5 +++-- .../lib/plugins/TableResize/editors/TableEditor.ts | 3 ++- .../lib/plugins/TableResize/editors/TableInserter.ts | 5 +++-- .../lib/plugins/TableResize/editors/TableResizer.ts | 5 +++-- .../lib/plugins/TableResize/editors/TableSelector.ts | 5 +++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts index dc9cea12d9b..7ef777d5d1b 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/TableResize.ts @@ -32,7 +32,8 @@ export default class TableResize implements EditorPlugin { constructor( private onShowHelperElement?: ( elementData: CreateElementData, - helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector', + tableOrTd: HTMLTableElement | HTMLTableCellElement ) => void, private anchorContainerSelector?: string ) {} diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts index 1ee83681d32..19940c83dc4 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/CellResizer.ts @@ -19,7 +19,8 @@ export default function createCellResizer( onEnd: () => false, onShowHelperElement?: ( elementData: CreateElementData, - helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector', + td: HTMLTableCellElement ) => void, anchorContainer?: HTMLElement ): TableEditFeature | null { @@ -29,7 +30,7 @@ export default function createCellResizer( style: `position: fixed; cursor: ${isHorizontal ? 'row' : 'col'}-resize; user-select: none`, }; - onShowHelperElement?.(createElementData, 'CellResizer'); + onShowHelperElement?.(createElementData, 'CellResizer', td); const div = createElement(createElementData, document) as HTMLDivElement; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts index 736ba599f7a..9779cb548bf 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableEditor.ts @@ -80,7 +80,8 @@ export default class TableEditor { private onChanged: () => void, private onShowHelperElement?: ( elementData: CreateElementData, - helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector', + tableOrTd: HTMLTableElement | HTMLTableCellElement ) => void, private anchorContainer?: HTMLElement, private contentDiv?: EventTarget | null diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts index c940bd15555..5404892e1b7 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableInserter.ts @@ -21,7 +21,8 @@ export default function createTableInserter( getOnMouseOut: (feature: HTMLElement) => (ev: MouseEvent) => void, onShowHelperElement?: ( elementData: CreateElementData, - helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector', + td: HTMLTableCellElement ) => void, anchorContainer?: HTMLElement ): TableEditFeature | null { @@ -41,7 +42,7 @@ export default function createTableInserter( editor.getDefaultFormat().backgroundColor || 'white' ); - onShowHelperElement?.(createElementData, 'TableInserter'); + onShowHelperElement?.(createElementData, 'TableInserter', td); const div = createElement(createElementData, document) as HTMLDivElement; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts index 3a0468c073b..368a9b62f1f 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableResizer.ts @@ -23,7 +23,8 @@ export default function createTableResizer( onEnd: () => false, onShowHelperElement?: ( elementData: CreateElementData, - helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector', + table: HTMLTableElement ) => void, contentDiv?: EventTarget | null, anchorContainer?: HTMLElement @@ -44,7 +45,7 @@ export default function createTableResizer( }-resize; user-select: none; border: 1px solid #808080`, }; - onShowHelperElement?.(createElementData, 'TableResizer'); + onShowHelperElement?.(createElementData, 'TableResizer', table); const div = createElement(createElementData, document) as HTMLDivElement; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts index a425af0f7a4..f7c32389661 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/TableResize/editors/TableSelector.ts @@ -22,7 +22,8 @@ export default function createTableSelector( getOnMouseOut: (feature: HTMLElement) => (ev: MouseEvent) => void, onShowHelperElement?: ( elementData: CreateElementData, - helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector' + helperType: 'CellResizer' | 'TableInserter' | 'TableResizer' | 'TableSelector', + table: HTMLTableElement ) => void, contentDiv?: EventTarget | null, anchorContainer?: HTMLElement @@ -40,7 +41,7 @@ export default function createTableSelector( style: 'position: fixed; cursor: all-scroll; user-select: none; border: 1px solid #808080', }; - onShowHelperElement?.(createElementData, 'TableSelector'); + onShowHelperElement?.(createElementData, 'TableSelector', table); const div = createElement(createElementData, document) as HTMLDivElement; From 4808c0bf2ed2dfa68d52d17ede2ebf063cb72f61 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 15 Jan 2024 20:26:21 -0800 Subject: [PATCH 51/64] Standalone Editor step 5: Create demo page for Standalone Editor (part 1) (#2295) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * Standalone Editor step 5 * fix demo * Fix buttons * fix build --- .../controls/ContentModelEditorMainPane.tsx | 2 +- demo/scripts/controls/MainPane.tsx | 2 +- .../controls/StandaloneEditorMainPane.scss | 88 ++++++ .../controls/StandaloneEditorMainPane.tsx | 285 ++++++++++++++++++ demo/scripts/controls/sidePane/SidePane.tsx | 9 +- .../controls/sidePane/StandaloneSidePane.scss | 69 +++++ .../controls/theme/standaloneEditorTheme.scss | 27 ++ .../controls/titleBar/StandaloneTitleBar.scss | 58 ++++ demo/scripts/controls/titleBar/TitleBar.tsx | 12 +- demo/scripts/index.ts | 3 + 10 files changed, 548 insertions(+), 7 deletions(-) create mode 100644 demo/scripts/controls/StandaloneEditorMainPane.scss create mode 100644 demo/scripts/controls/StandaloneEditorMainPane.tsx create mode 100644 demo/scripts/controls/sidePane/StandaloneSidePane.scss create mode 100644 demo/scripts/controls/theme/standaloneEditorTheme.scss create mode 100644 demo/scripts/controls/titleBar/StandaloneTitleBar.scss diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index 210f4b50eee..e2247991259 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -178,7 +178,7 @@ class ContentModelEditorMainPane extends MainPaneBase { IStandaloneEditor; +} + +class ContentModelEditorMainPane extends MainPaneBase { + private formatStatePlugin: ContentModelFormatStatePlugin; + private editorOptionPlugin: ContentModelEditorOptionsPlugin; + private eventViewPlugin: ContentModelEventViewPlugin; + private apiPlaygroundPlugin: ApiPlaygroundPlugin; + private contentModelPanePlugin: ContentModelPanePlugin; + private contentModelEditPlugin: ContentModelEditPlugin; + private contentModelRibbonPlugin: RibbonPlugin; + private pasteOptionPlugin: EditorPlugin; + private emojiPlugin: EditorPlugin; + private snapshotPlugin: ContentModelSnapshotPlugin; + private entityDelimiterPlugin: EntityDelimiterPlugin; + private toggleablePlugins: EditorPlugin[] | null = null; + private formatPainterPlugin: ContentModelFormatPainterPlugin; + private sampleEntityPlugin: SampleEntityPlugin; + private snapshots: Snapshots; + + constructor(props: {}) { + super(props); + + this.snapshots = { + snapshots: [], + totalSize: 0, + currentIndex: -1, + autoCompleteIndex: -1, + maxSize: 1e7, + }; + + this.formatStatePlugin = new ContentModelFormatStatePlugin(); + this.editorOptionPlugin = new ContentModelEditorOptionsPlugin(); + this.eventViewPlugin = new ContentModelEventViewPlugin(); + this.apiPlaygroundPlugin = new ApiPlaygroundPlugin(); + this.snapshotPlugin = new ContentModelSnapshotPlugin(this.snapshots); + this.contentModelPanePlugin = new ContentModelPanePlugin(); + this.contentModelEditPlugin = new ContentModelEditPlugin(); + this.contentModelRibbonPlugin = new ContentModelRibbonPlugin(); + this.pasteOptionPlugin = createPasteOptionPlugin(); + this.emojiPlugin = createEmojiPlugin(); + this.entityDelimiterPlugin = new EntityDelimiterPlugin(); + this.formatPainterPlugin = new ContentModelFormatPainterPlugin(); + this.sampleEntityPlugin = new SampleEntityPlugin(); + this.state = { + showSidePane: window.location.hash != '', + popoutWindow: null, + initState: this.editorOptionPlugin.getBuildInPluginState(), + scale: 1, + isDarkMode: this.themeMatch?.matches || false, + editorCreator: null, + isRtl: false, + tableBorderFormat: { + width: '1px', + style: 'solid', + color: '#ABABAB', + }, + }; + } + + getStyles(): Record { + return styles; + } + + renderTitleBar() { + return ; + } + + renderRibbon(isPopout: boolean) { + return ( + + ); + } + + renderSidePane(fullWidth: boolean) { + const styles = this.getStyles(); + + return ( + + ); + } + + getPlugins() { + this.toggleablePlugins = + this.toggleablePlugins || getToggleablePlugins(this.state.initState); + + const plugins = [ + ...this.toggleablePlugins, + this.contentModelPanePlugin.getInnerRibbonPlugin(), + this.contentModelEditPlugin, + this.pasteOptionPlugin, + this.emojiPlugin, + this.entityDelimiterPlugin, + this.sampleEntityPlugin, + ]; + + if (this.state.showSidePane || this.state.popoutWindow) { + arrayPush(plugins, this.getSidePanePlugins()); + } + + plugins.push(this.updateContentPlugin); + + return plugins; + } + + resetEditor() { + this.toggleablePlugins = null; + this.setState({ + editorCreator: (div: HTMLDivElement, options: StandaloneEditorOptions) => + new StandaloneEditor(div, { + ...options, + cacheModel: this.state.initState.cacheModel, + }), + }); + } + + renderEditor() { + const styles = this.getStyles(); + const allPlugins = this.getPlugins(); + const editorStyles = { + transform: `scale(${this.state.scale})`, + transformOrigin: this.state.isRtl ? 'right top' : 'left top', + height: `calc(${100 / this.state.scale}%)`, + width: `calc(${100 / this.state.scale}%)`, + }; + const format = this.state.initState.defaultFormat; + const defaultFormat: ContentModelSegmentFormat = { + fontWeight: format.bold ? 'bold' : undefined, + italic: format.italic || undefined, + underline: format.underline || undefined, + fontFamily: format.fontFamily || undefined, + fontSize: format.fontSize || undefined, + textColor: format.textColors?.lightModeColor || format.textColor || undefined, + backgroundColor: + format.backgroundColors?.lightModeColor || format.backgroundColor || undefined, + }; + + this.updateContentPlugin.forceUpdate(); + + return ( +
+
+ {this.state.editorCreator && ( + + )} +
+
+ ); + } + + getTheme(isDark: boolean): PartialTheme { + return isDark ? DarkTheme : LightTheme; + } + + private getSidePanePlugins() { + return [ + this.formatStatePlugin, + this.editorOptionPlugin, + this.eventViewPlugin, + this.apiPlaygroundPlugin, + this.snapshotPlugin, + this.contentModelPanePlugin, + ]; + } +} + +export function mount(parent: HTMLElement) { + ReactDOM.render(, parent); +} diff --git a/demo/scripts/controls/sidePane/SidePane.tsx b/demo/scripts/controls/sidePane/SidePane.tsx index 3459b1ab1bd..8449b230a27 100644 --- a/demo/scripts/controls/sidePane/SidePane.tsx +++ b/demo/scripts/controls/sidePane/SidePane.tsx @@ -3,10 +3,11 @@ import SidePanePlugin from '../SidePanePlugin'; const classicStyles = require('./SidePane.scss'); const contentModelStyles = require('./ContentModelSidePane.scss'); +const standaloneModelStyles = require('./StandaloneSidePane.scss'); export interface SidePaneProps { plugins: SidePanePlugin[]; - isContentModelDemo: boolean; + mode: 'classical' | 'contentModel' | 'standalone'; className?: string; } @@ -96,6 +97,10 @@ export default class SidePane extends React.Component { render() { const { mode, className: baseClassName } = this.props; - const styles = mode == 'contentModel' ? contentModelStyles : classicalStyles; + const styles = + mode == 'contentModel' + ? contentModelStyles + : mode == 'standalone' + ? standaloneStyles + : classicalStyles; const className = styles.titleBar + ' ' + (baseClassName || ''); const titleText = mode == 'contentModel' ? 'RoosterJs Content Model Demo Site' : mode == 'classical' ? 'RoosterJs Demo Site' - : 'RoosterJs Adapter Demo Site'; + : 'RoosterJs Standalone Demo Site'; const switchLink = mode == 'contentModel' ? ( diff --git a/demo/scripts/index.ts b/demo/scripts/index.ts index 269728b04f8..9e007bdd317 100644 --- a/demo/scripts/index.ts +++ b/demo/scripts/index.ts @@ -1,10 +1,13 @@ import { mount as mountClassicalEditorMainPane } from './controls/MainPane'; import { mount as mountContentModelEditorMainPane } from './controls/ContentModelEditorMainPane'; +import { mount as mountStandaloneEditorMainPane } from './controls/StandaloneEditorMainPane'; const search = document.location.search.substring(1).split('&'); if (search.some(x => x == 'cm=1')) { mountContentModelEditorMainPane(document.getElementById('mainPane')); +} else if (search.some(x => x == 'cm=2')) { + mountStandaloneEditorMainPane(document.getElementById('mainPane')); } else { mountClassicalEditorMainPane(document.getElementById('mainPane')); } From 31e88dd56901190ca853fb0cc2c1b9b0dbecd4dd Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 15 Jan 2024 20:31:07 -0800 Subject: [PATCH 52/64] Standalone Editor: Decouple TrustHTMLHandler and Rect (#2307) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * Standalone Editor: Remove compatible enums from standalone editor * improve * Standalone Editor: Create new event types * Port to new event system * Revert "Port to new event system" This reverts commit 60cf041b3c3334df8a1781e22b2e81adc0775662. * Port to new event system * Improve * Standalone Editor: Decouple TrustHTMLHandler and Rect * fix build * fix demo * Fix buttons * fix build * fix build * fix build --- .../lib/coreApi/getVisibleViewport.ts | 3 +-- .../lib/coreApi/paste.ts | 2 +- .../lib/editor/StandaloneEditor.ts | 3 ++- .../publicApi/model/createModelFromHtml.ts | 2 +- .../lib/editor/ContentModelEditor.ts | 3 +-- .../Excel/processPastedContentFromExcel.ts | 3 +-- .../processPastedContentFromPowerPoint.ts | 3 +-- .../processPastedContentFromPowerPointTest.ts | 7 ++++-- .../lib/editor/IStandaloneEditor.ts | 3 ++- .../lib/editor/StandaloneEditorCore.ts | 4 +++- .../lib/editor/StandaloneEditorOptions.ts | 2 +- .../lib/index.ts | 2 ++ .../lib/parameter/Rect.ts | 24 +++++++++++++++++++ .../lib/parameter/TrustedHTMLHandler.ts | 4 ++++ 14 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/Rect.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts index de6dc3d30c3..deadbe89f1a 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/getVisibleViewport.ts @@ -1,5 +1,4 @@ -import type { Rect } from 'roosterjs-editor-types'; -import type { GetVisibleViewport } from 'roosterjs-content-model-types'; +import type { GetVisibleViewport, Rect } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts index 1d0505b1bd0..1e2849b50bd 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/paste.ts @@ -11,8 +11,8 @@ import type { ClipboardData, Paste, StandaloneEditorCore, + TrustedHTMLHandler, } from 'roosterjs-content-model-types'; -import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; const CloneOption: CloneModelOptions = { includeCachedElement: (node, type) => (type == 'cache' ? undefined : node), diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts index 4d5ea0c5eb1..8e19642d1b5 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/StandaloneEditor.ts @@ -1,7 +1,7 @@ import { ChangeSource } from '../constants/ChangeSource'; import { createStandaloneEditorCore } from './createStandaloneEditorCore'; import { transformColor } from '../publicApi/color/transformColor'; -import type { DarkColorHandler, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { DarkColorHandler } from 'roosterjs-editor-types'; import type { ClipboardData, ContentModelDocument, @@ -23,6 +23,7 @@ import type { SnapshotsManager, StandaloneEditorCore, StandaloneEditorOptions, + TrustedHTMLHandler, } from 'roosterjs-content-model-types'; /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/createModelFromHtml.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/createModelFromHtml.ts index e289cb0fb52..fe5fd1d8e4e 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/createModelFromHtml.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/createModelFromHtml.ts @@ -3,8 +3,8 @@ import type { ContentModelDocument, ContentModelSegmentFormat, DomToModelOption, + TrustedHTMLHandler, } from 'roosterjs-content-model-types'; -import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; /** * Create Content Model from HTML string diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts index 1597e431ed3..0808c1bf05a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/ContentModelEditor.ts @@ -44,7 +44,6 @@ import type { PluginEventData, PluginEventFromType, PositionType, - Rect, Region, SelectionPath, SelectionRangeEx, @@ -90,7 +89,7 @@ import type { ContentModelEditorOptions, IContentModelEditor, } from '../publicTypes/IContentModelEditor'; -import type { DOMEventRecord } from 'roosterjs-content-model-types'; +import type { DOMEventRecord, Rect } from 'roosterjs-content-model-types'; /** * Editor for Content Model. diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts index 443b16da1cb..47339f76d24 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts @@ -1,8 +1,7 @@ import addParser from '../utils/addParser'; import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom'; import { setProcessor } from '../utils/setProcessor'; -import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; -import type { BeforePasteEvent } from 'roosterjs-content-model-types'; +import type { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-content-model-types'; const LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i; const LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i; diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts index 79e685e0885..dad6ea6d2f5 100644 --- a/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts @@ -1,6 +1,5 @@ import { moveChildNodes } from 'roosterjs-content-model-dom'; -import type { BeforePasteEvent } from 'roosterjs-content-model-types'; -import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { BeforePasteEvent, TrustedHTMLHandler } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts index de5ede5bd72..ed7bbef3432 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts @@ -1,7 +1,10 @@ import * as moveChildNodes from 'roosterjs-content-model-dom/lib/domUtils/moveChildNodes'; import { processPastedContentFromPowerPoint } from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; -import { TrustedHTMLHandler } from 'roosterjs-editor-types'; -import type { BeforePasteEvent, ClipboardData } from 'roosterjs-content-model-types'; +import type { + BeforePasteEvent, + ClipboardData, + TrustedHTMLHandler, +} from 'roosterjs-content-model-types'; const getPasteEvent = (): BeforePasteEvent => { return { diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts index 448df5007a4..18d4568d885 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -16,7 +16,8 @@ import type { ContentModelFormatter, FormatWithContentModelOptions, } from '../parameter/FormatWithContentModelOptions'; -import type { DarkColorHandler, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { DarkColorHandler } from 'roosterjs-editor-types'; +import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; /** * An interface of standalone Content Model editor. diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts index 709b65bfbdb..8cccb30575b 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -6,7 +6,7 @@ import type { PasteType } from '../enum/PasteType'; import type { DOMEventRecord } from '../parameter/DOMEventRecord'; import type { Snapshot } from '../parameter/Snapshot'; import type { EntityState } from '../parameter/FormatWithContentModelContext'; -import type { DarkColorHandler, Rect, TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { DarkColorHandler } from 'roosterjs-editor-types'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { DOMSelection } from '../selection/DOMSelection'; import type { DomToModelOption } from '../context/DomToModelOption'; @@ -15,6 +15,8 @@ import type { EditorContext } from '../context/EditorContext'; import type { EditorEnvironment } from '../parameter/EditorEnvironment'; import type { ModelToDomOption } from '../context/ModelToDomOption'; import type { ModelToDomSettings, OnNodeCreated } from '../context/ModelToDomSettings'; +import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; +import type { Rect } from '../parameter/Rect'; import type { ContentModelFormatter, FormatWithContentModelOptions, diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts index 8e2432b4cef..1fd189cf949 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts @@ -1,11 +1,11 @@ import type { EditorPlugin } from './EditorPlugin'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; import type { StandaloneCoreApiMap } from './StandaloneEditorCore'; -import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; import type { DomToModelOption } from '../context/DomToModelOption'; import type { ModelToDomOption } from '../context/ModelToDomOption'; import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { SnapshotsManager } from '../parameter/SnapshotsManager'; +import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; /** * Options for Content Model editor diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 39bf456ce4b..32e6c90e64e 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -275,6 +275,8 @@ export { DOMEventHandlerFunction, DOMEventRecord } from './parameter/DOMEventRec export { EdgeLinkPreview } from './parameter/EdgeLinkPreview'; export { ClipboardData } from './parameter/ClipboardData'; export { AnnounceData, KnownAnnounceStrings } from './parameter/AnnounceData'; +export { TrustedHTMLHandler } from './parameter/TrustedHTMLHandler'; +export { Rect } from './parameter/Rect'; export { ValueSanitizer } from './parameter/ValueSanitizer'; export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/Rect.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/Rect.ts new file mode 100644 index 00000000000..dbc8ae8d3c3 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/Rect.ts @@ -0,0 +1,24 @@ +/** + * This represents a rect inside editor + */ +export interface Rect { + /** + * Top + */ + top: number; + + /** + * Bottom + */ + bottom: number; + + /** + * Left + */ + left: number; + + /** + * Right + */ + right: number; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts new file mode 100644 index 00000000000..05785843669 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/TrustedHTMLHandler.ts @@ -0,0 +1,4 @@ +/** + * A handler type to convert HTML string to a trust HTML string + */ +export type TrustedHTMLHandler = (html: string) => string; From fa0be458c8df7041ce1ea8fae7602f25ff49e6e3 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Tue, 16 Jan 2024 08:54:51 -0600 Subject: [PATCH 53/64] Content Model Fidelity: Add TextColor Parser to tables (#2338) * init undefined * fix test --- .../lib/formatHandlers/defaultFormatHandlers.ts | 1 + .../test/paste/e2e/cmPasteTest.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts index 0e0a3bb08c7..fc90fb10a50 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts @@ -166,6 +166,7 @@ export const defaultFormatKeysPerCategory: { 'margin', 'size', 'tableLayout', + 'textColor', ], tableBorder: ['borderBox', 'tableSpacing'], tableCellBorder: ['borderBox'], diff --git a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts index fe25a51a933..82becb46925 100644 --- a/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts @@ -203,6 +203,7 @@ describe(ID, () => { width: '170pt', useBorderBox: true, borderCollapse: true, + textColor: 'rgb(0, 0, 0)', }, widths: jasmine.anything(), dataset: {}, From 3bdf97eed4e0583f8ef935b85b6ddcca70b36fc8 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 17 Jan 2024 09:58:50 -0800 Subject: [PATCH 54/64] Standalone Editor: Improve Undo (#2308) * Standalone Editor step 2 * Standalone Editor step 3 * improve * Standalone Editor step 4 * Standalone Editor: Remove compatible enums from standalone editor * improve * Standalone Editor: Create new event types * Port to new event system * Revert "Port to new event system" This reverts commit 60cf041b3c3334df8a1781e22b2e81adc0775662. * Port to new event system * Improve * Standalone Editor: Decouple TrustHTMLHandler and Rect * fix build * Standalone editor: Improve Undo * fix build * improve * fix demo * Fix buttons * fix build * fix build * fix build * Improve * fix build --- .../controls/ContentModelEditorMainPane.tsx | 8 +- .../controls/StandaloneEditorMainPane.tsx | 2 +- .../snapshot/ContentModelSnapshotPane.tsx | 69 ++++++++++++--- .../snapshot/ContentModelSnapshotPlugin.tsx | 84 ++----------------- .../lib/coreApi/addUndoSnapshot.ts | 6 +- .../lib/corePlugin/UndoPlugin.ts | 2 +- .../lib/editor/SnapshotsManagerImpl.ts | 15 +++- .../lib/editor/StandaloneEditor.ts | 4 +- .../roosterjs-content-model-core/lib/index.ts | 1 - .../test/coreApi/addUndoSnapshotTest.ts | 31 ++++++- .../test/corePlugin/UndoPluginTest.ts | 10 ++- .../test/editor/SnapshotsManagerImplTest.ts | 73 ++++++++++++++++ .../test/editor/StandaloneEditorTest.ts | 8 +- .../lib/editor/IStandaloneEditor.ts | 2 +- .../lib/editor/StandaloneEditorCore.ts | 2 +- .../lib/editor/StandaloneEditorOptions.ts | 7 +- .../lib/parameter/Snapshot.ts | 5 ++ 17 files changed, 212 insertions(+), 117 deletions(-) diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index e2247991259..8f3d5c5dd68 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -17,9 +17,9 @@ import SidePane from './sidePane/SidePane'; import TitleBar from './titleBar/TitleBar'; import { arrayPush } from 'roosterjs-editor-dom'; import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; -import { ContentModelSegmentFormat, Snapshot } from 'roosterjs-content-model-types'; +import { ContentModelSegmentFormat, Snapshots } from 'roosterjs-content-model-types'; import { createEmojiPlugin, createPasteOptionPlugin } from 'roosterjs-react'; -import { EditorPlugin, Snapshots } from 'roosterjs-editor-types'; +import { EditorPlugin } from 'roosterjs-editor-types'; import { getDarkColor } from 'roosterjs-color-utils'; import { PartialTheme } from '@fluentui/react/lib/Theme'; import { trustedHTMLHandler } from '../utils/trustedHTMLHandler'; @@ -110,7 +110,7 @@ class ContentModelEditorMainPane extends MainPaneBase private formatPainterPlugin: ContentModelFormatPainterPlugin; private pastePlugin: ContentModelPastePlugin; private sampleEntityPlugin: SampleEntityPlugin; - private snapshots: Snapshots; + private snapshots: Snapshots; constructor(props: {}) { super(props); @@ -260,7 +260,7 @@ class ContentModelEditorMainPane extends MainPaneBase inDarkMode={this.state.isDarkMode} getDarkColor={getDarkColor} experimentalFeatures={this.state.initState.experimentalFeatures} - snapshotsManager={this.snapshotPlugin.getSnapshotsManager()} + snapshots={this.snapshotPlugin.getSnapshots()} trustedHTMLHandler={trustedHTMLHandler} zoomScale={this.state.scale} initialContent={this.content} diff --git a/demo/scripts/controls/StandaloneEditorMainPane.tsx b/demo/scripts/controls/StandaloneEditorMainPane.tsx index 908e0cebc2d..26705c0eb67 100644 --- a/demo/scripts/controls/StandaloneEditorMainPane.tsx +++ b/demo/scripts/controls/StandaloneEditorMainPane.tsx @@ -251,7 +251,7 @@ class ContentModelEditorMainPane extends MainPaneBase inDarkMode={this.state.isDarkMode} getDarkColor={getDarkColor} experimentalFeatures={this.state.initState.experimentalFeatures} - snapshotsManager={this.snapshotPlugin.getSnapshotsManager()} + snapshots={this.snapshotPlugin.getSnapshots()} trustedHTMLHandler={trustedHTMLHandler} zoomScale={this.state.scale} initialContent={this.content} diff --git a/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx b/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx index 810eb298cec..984d1a3ab91 100644 --- a/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx +++ b/demo/scripts/controls/sidePane/snapshot/ContentModelSnapshotPane.tsx @@ -38,7 +38,7 @@ export default class ContentModelSnapshotPane extends React.Component< render() { return ( -
+

Undo Snapshots

{this.state.snapshots.map(this.renderItem)} @@ -47,6 +47,7 @@ export default class ContentModelSnapshotPane extends React.Component<
{' '} +
HTML: