From bbf6a433d2c18937525b596a5ec33579674521aa Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 10 Nov 2023 12:20:44 -0800 Subject: [PATCH] Bump version to 8.59.0 (#2203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * margin in lists * remove code * type * refactor * fixes * remove parameter * refactor * remove file change * Use Content Model to handle Delete/Backspace key in more cases (#2162) * Do not set focus when quite shadow edit (#2163) * Fix #237217 (#2164) * Fix #237735 (#2165) * Fix #236416 (#2166) * wip * fixes * Fix #2160 (#2167) * Fix #2160 * fix build * test * remove test * remove test * remove test * Rearrange parameters of iterateSelections (#2180) * Rearrange parameters of iterateSelections * improve * Standalone editor step 0: Create a copy of Editor class (#2175) * Standalone editor step 0: Create a copy of Editor class * remove unnecessary change * improve * Fix #2061, apply pending format on Android (#2172) * Fix #2061 * Fix for Android * Fix #2080 (#2173) * Standalone editor Step 0.5: Create test page for adapter editor (#2176) * Standalone editor step 0: Create a copy of Editor class * Standalone editor Step 0.5: Create test page for adapter editor * Add Content Model functionality to AdapterEditor * remove unnecessary change * remove unnecessary change * improve * toggleListType * type * Fix Mouseout behavior to hide table editors (#2181) * init * init * fix build * Add comment * Fix apply Table Inside borders operation and Demo site (#2184) * fix cases * fix demo * Add image size checkmarks to context menu (#2168) * Allow a max error un percentage size check * Allow a checkmark to be shown in ctx menu * Add checkmark to image sizes * Add missing type * Fix types * Add comment * Attempt to fix image selection * Revert unneeded changes in image selection * Use attr as backup in resize calc * Rely entirely on image selection * Revert changes to domeventplugin * Copy changes into content model adapter * Revert previous standalone editor change (#2189) * Revert previous standalone change * fix build * Move formatWithContentModel to be a core API (#2185) * Content Model: Allow clear cache from formatContentModel (#2186) * Move formatWithContentModel to be a core API * Content Model: Allow clear cache from formatContentModel * Content Model: Potential perf improvement in getFormatState (#2187) * Content Model: Move pending format into editor core (#2188) * Move formatWithContentModel to be a core API * Content Model: Allow clear cache from formatContentModel * Content Model: Move pending format into editor core * Convert DeleteResult from const enum to string literal type (#2191) * Move paste plugin to roosterjs-content-model-plugins package (#2192) * image selection plugin * check image parent node * Adding module entry to package.json (#2197) * Move ContentModelEdit plugin to plugins package (#2195) * Move ContentModelEdit plugin to plugins package * improve --------- Co-authored-by: Bryan Valverde U * Move type files to roosterjs-content-model-types package (#2196) * Move ContentModelEdit plugin to plugins package * Move types to roosterjs-content-model-types package * improve * Improve * improve * Improve * improve * fix build * fix build * Move core API to roosterjs-content-model-core package (#2198) * Move ContentModelEdit plugin to plugins package * Move types to roosterjs-content-model-types package * improve * Improve * improve * Improve * improve * Move core API to core package * fix build * improve * fix build * fix build * Move corePlugins to roosterjs-content-model-core package (#2199) * Move ContentModelEdit plugin to plugins package * Move types to roosterjs-content-model-types package * improve * Improve * improve * Improve * improve * Move core API to core package * fix build * improve * Move corePlugins to roosterjs-content-model-core package * fix build * improve * fix build * fix build * Move format API to roosterjs-content-model-api package (#2200) * Move ContentModelEdit plugin to plugins package * Move types to roosterjs-content-model-types package * improve * Improve * improve * Improve * improve * Move core API to core package * fix build * improve * Move corePlugins to roosterjs-content-model-core package * fix build * improve * fix build * Move format API to roosterjs-content-model-api package * fix build * Improve --------- Co-authored-by: JĂșlia Roldi Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> Co-authored-by: Bryan Valverde U Co-authored-by: Andres-CT98 <107568016+Andres-CT98@users.noreply.github.com> Co-authored-by: Ian Elizondo Co-authored-by: Keven Arroyo --- .../controls/ContentModelEditorMainPane.tsx | 24 +- demo/scripts/controls/MainPane.tsx | 2 +- demo/scripts/controls/MainPaneBase.tsx | 2 +- .../formatPart/BorderFormatRenderers.ts | 4 +- .../model/ContentModelDocumentView.tsx | 2 +- .../model/ContentModelFormatContainerView.tsx | 2 +- .../model/ContentModelGeneralView.tsx | 2 +- .../model/ContentModelImageView.tsx | 2 +- .../model/ContentModelListItemView.tsx | 2 +- .../model/ContentModelListLevelView.tsx | 2 +- .../model/ContentModelParagraphView.tsx | 2 +- .../model/ContentModelTableCellView.tsx | 3 +- .../model/ContentModelTableRowView.tsx | 2 +- .../model/ContentModelTableView.tsx | 3 +- .../ContentModelFormatPainterPlugin.ts | 7 +- demo/scripts/controls/getToggleablePlugins.ts | 2 +- .../contentModel/ContentModelRibbonPlugin.ts | 8 +- .../contentModel/alignCenterButton.ts | 3 +- .../contentModel/alignLeftButton.ts | 3 +- .../contentModel/alignRightButton.ts | 3 +- .../contentModel/backgroundColorButton.ts | 3 +- .../contentModel/blockQuoteButton.ts | 3 +- .../ribbonButtons/contentModel/boldButton.ts | 3 +- .../contentModel/bulletedListButton.ts | 3 +- .../contentModel/changeImageButton.ts | 3 +- .../contentModel/clearFormatButton.ts | 3 +- .../ribbonButtons/contentModel/codeButton.ts | 3 +- .../contentModel/decreaseFontSizeButton.ts | 3 +- .../contentModel/decreaseIndentButton.ts | 3 +- .../ribbonButtons/contentModel/fontButton.ts | 3 +- .../contentModel/fontSizeButton.ts | 3 +- .../contentModel/formatTableButton.ts | 7 +- .../contentModel/imageBorderColorButton.ts | 3 +- .../contentModel/imageBorderRemoveButton.ts | 3 +- .../contentModel/imageBorderStyleButton.ts | 3 +- .../contentModel/imageBorderWidthButton.ts | 3 +- .../contentModel/imageBoxShadowButton.ts | 3 +- .../contentModel/increaseFontSizeButton.ts | 3 +- .../contentModel/increaseIndentButton.ts | 3 +- .../contentModel/insertImageButton.ts | 3 +- .../contentModel/insertLinkButton.ts | 7 +- .../contentModel/insertTableButton.ts | 3 +- .../contentModel/italicButton.ts | 3 +- .../contentModel/listStartNumberButton.ts | 3 +- .../ribbonButtons/contentModel/ltrButton.ts | 3 +- .../contentModel/numberedListButton.ts | 3 +- .../contentModel/removeLinkButton.ts | 3 +- .../ribbonButtons/contentModel/rtlButton.ts | 3 +- .../setBulletedListStyleButton.ts | 3 +- .../contentModel/setHeadingLevelButton.ts | 3 +- .../setNumberedListStyleButton.ts | 3 +- .../contentModel/setTableCellShadeButton.ts | 3 +- .../contentModel/setTableHeaderButton.ts | 3 +- .../contentModel/spaceBeforeAfterButtons.ts | 7 +- .../contentModel/spacingButton.ts | 3 +- .../contentModel/strikethroughButton.ts | 3 +- .../contentModel/subscriptButton.ts | 3 +- .../contentModel/superscriptButton.ts | 3 +- .../contentModel/tableBorderApplyButton.ts | 22 +- .../contentModel/tableEditButtons.ts | 4 +- .../contentModel/textColorButton.ts | 3 +- .../contentModel/underlineButton.ts | 3 +- .../insertEntity/InsertEntityPane.tsx | 8 +- .../eventViewer/ContentModelEventViewPane.tsx | 2 +- .../ContentModelFormatStatePlugin.ts | 3 +- demo/scripts/controls/titleBar/TitleBar.tsx | 34 +- demo/scripts/tsconfig.json | 18 + .../roosterjs-content-model-api/lib/index.ts | 45 + .../lib/modelApi/block/setModelAlignment.ts | 9 +- .../lib/modelApi/block/setModelDirection.ts | 3 +- .../lib/modelApi/block/setModelIndentation.ts | 3 +- .../modelApi/block/toggleModelBlockQuote.ts | 5 +- .../lib/modelApi/common/clearModelFormat.ts | 16 +- .../common/retrieveModelFormatState.ts | 16 +- .../lib/modelApi/common/wrapBlock.ts | 0 .../lib/modelApi}/domUtils/readFile.ts | 0 .../lib/modelApi/entity/insertEntityModel.ts | 17 +- .../modelApi/image/applyImageBorderFormat.ts | 5 +- .../list/findListItemsInSameThread.ts | 0 .../lib/modelApi/list/setListType.ts | 5 +- .../selection/adjustSegmentSelection.ts | 3 +- .../modelApi/selection/adjustWordSelection.ts | 5 +- .../selection/collapseTableSelection.ts | 0 .../lib/modelApi/table/alignTable.ts | 3 +- .../lib/modelApi/table/alignTableCell.ts | 7 +- .../lib/modelApi/table/canMergeCells.ts | 0 .../modelApi/table/createTableStructure.ts | 0 .../lib/modelApi/table/deleteTable.ts | 0 .../lib/modelApi/table/deleteTableColumn.ts | 0 .../lib/modelApi/table/deleteTableRow.ts | 0 .../table/ensureFocusableParagraphForTable.ts | 0 .../lib/modelApi/table/getSelectedCells.ts | 0 .../lib/modelApi/table/insertTableColumn.ts | 6 +- .../lib/modelApi/table/insertTableRow.ts | 6 +- .../lib/modelApi/table/mergeTableCells.ts | 0 .../lib/modelApi/table/mergeTableColumn.ts | 6 +- .../lib/modelApi/table/mergeTableRow.ts | 3 +- .../table/splitTableCellHorizontally.ts | 0 .../table/splitTableCellVertically.ts | 0 .../lib/publicApi/block/setAlignment.ts | 9 +- .../lib/publicApi/block/setDirection.ts | 15 + .../lib/publicApi/block/setHeadingLevel.ts | 8 +- .../lib/publicApi/block/setIndentation.ts | 15 +- .../lib/publicApi/block/setParagraphMargin.ts | 4 +- .../lib/publicApi/block/setSpacing.ts | 4 +- .../lib/publicApi/block/toggleBlockQuote.ts | 21 +- .../lib/publicApi/entity/insertEntity.ts | 25 +- .../lib/publicApi/format/clearFormat.ts | 36 + .../lib/publicApi/format/getFormatState.ts | 62 +- .../publicApi/image/adjustImageSelection.ts | 31 + .../lib/publicApi/image/changeImage.ts | 9 +- .../lib/publicApi/image/insertImage.ts | 42 + .../lib/publicApi/image/setImageAltText.ts | 5 +- .../lib/publicApi/image/setImageBorder.ts | 6 +- .../lib/publicApi/image/setImageBoxShadow.ts | 5 +- .../lib/publicApi/link/adjustLinkSelection.ts | 44 + .../lib/publicApi/link/insertLink.ts | 20 +- .../lib/publicApi/link/removeLink.ts | 41 + .../lib/publicApi/list/setListStartNumber.ts | 29 + .../lib/publicApi/list/setListStyle.ts | 38 + .../lib/publicApi/list/toggleBullet.ts | 23 + .../lib/publicApi/list/toggleNumbering.ts | 23 + .../publicApi/segment/applySegmentFormat.ts | 5 +- .../publicApi/segment/changeCapitalization.ts | 4 +- .../lib/publicApi/segment/changeFontSize.ts | 7 +- .../publicApi/segment/setBackgroundColor.ts | 7 +- .../lib/publicApi/segment/setFontName.ts | 4 +- .../lib/publicApi/segment/setFontSize.ts | 4 +- .../lib/publicApi/segment/setTextColor.ts | 4 +- .../lib/publicApi/segment/toggleBold.ts | 4 +- .../lib/publicApi/segment/toggleCode.ts | 5 +- .../lib/publicApi/segment/toggleItalic.ts | 4 +- .../publicApi/segment/toggleStrikethrough.ts | 4 +- .../lib/publicApi/segment/toggleSubscript.ts | 4 +- .../publicApi/segment/toggleSuperscript.ts | 4 +- .../lib/publicApi/segment/toggleUnderline.ts | 4 +- .../selection/hasSelectionInBlock.ts | 0 .../selection/hasSelectionInBlockGroup.ts | 0 .../selection/hasSelectionInSegment.ts | 0 .../publicApi/table/applyTableBorderFormat.ts | 427 +++++ .../lib/publicApi/table/editTable.ts | 133 ++ .../lib/publicApi/table/formatTable.ts | 47 + .../lib/publicApi/table/insertTable.ts | 63 + .../lib/publicApi/table/setTableCellShade.ts | 41 + .../utils/formatImageWithContentModel.ts | 5 +- .../utils/formatParagraphWithContentModel.ts | 25 + .../utils/formatSegmentWithContentModel.ts | 89 + .../roosterjs-content-model-api/package.json | 14 + .../modelApi/block/setModelAlignmentTest.ts | 0 .../modelApi/block/setModelDirectionTest.ts | 0 .../modelApi/block/setModelIndentationTest.ts | 0 .../block/toggleModelBlockQuoteTest.ts | 0 .../modelApi/common/clearModelFormatTest.ts | 0 .../common/retrieveModelFormatStateTest.ts | 56 +- .../test/modelApi/common/wrapBlockTest.ts | 0 .../modelApi/entity/insertEntityModelTest.ts | 3 +- .../image/applyImageBorderFormatTest.ts | 3 +- .../list/findListItemsInSameThreadTest.ts | 0 .../test/modelApi/list/setListTypeTest.ts | 18 + .../selection/adjustSegmentSelectionTest.ts | 0 .../selection/adjustWordSelectionTest.ts | 0 .../selection/collapseTableSelectionTest.ts | 0 .../test/modelApi/table/alignTableCellTest.ts | 10 +- .../test/modelApi/table/alignTableTest.ts | 0 .../test/modelApi/table/canMergeCellsTest.ts | 0 .../table/createTableStructureTest.ts | 0 .../modelApi/table/deleteTableColumnTest.ts | 0 .../test/modelApi/table/deleteTableRowTest.ts | 0 .../test/modelApi/table/deleteTableTest.ts | 0 .../ensureFocusableParagraphForTableTest.ts | 0 .../modelApi/table/getSelectedCellsTest.ts | 0 .../modelApi/table/insertTableColumnTest.ts | 0 .../test/modelApi/table/insertTableRowTest.ts | 0 .../modelApi/table/mergeTableCellsTest.ts | 0 .../modelApi/table/mergeTableColumnTest.ts | 0 .../test/modelApi/table/mergeTableRowTest.ts | 0 .../table/splitTableCellHorizontallyTest.ts | 0 .../table/splitTableCellVerticallyTest.ts | 0 .../publicApi/block/paragraphTestCommon.ts | 38 + .../test/publicApi/block/setAlignmentTest.ts | 78 +- .../test/publicApi/block/setDirectionTest.ts | 0 .../publicApi/block/setHeadingLevelTest.ts | 0 .../publicApi/block/setIndentationTest.ts | 74 + .../publicApi/block/setParagraphMarginTest.ts | 0 .../test/publicApi/block/setSpacingTest.ts | 0 .../publicApi/block/toggleBlockQuoteTest.ts | 46 +- .../test/publicApi/entity/insertEntityTest.ts | 46 +- .../test/publicApi/format/clearFormatTest.ts | 31 +- .../publicApi/format/getFormatStateTest.ts | 25 +- .../image/adjustImageSelectionTest.ts | 0 .../test/publicApi/image/changeImageTest.ts | 76 +- .../test/publicApi/image/insertImageTest.ts | 50 +- .../publicApi/image/setImageAltTextTest.ts | 0 .../publicApi/image/setImageBorderTest.ts | 3 +- .../publicApi/image/setImageBoxShadowTest.ts | 0 .../publicApi/link/adjustLinkSelectionTest.ts | 57 +- .../test/publicApi/link/insertLinkTest.ts | 68 +- .../test/publicApi/link/removeLinkTest.ts | 53 +- .../publicApi/list/setListStartNumberTest.ts | 36 +- .../test/publicApi/list/setListStyleTest.ts | 14 +- .../test/publicApi/list/toggleBulletTest.ts | 60 + .../publicApi/list/toggleNumberingTest.ts | 58 + .../segment/applySegmentFormatTest.ts | 0 .../segment/changeCapitalizationTest.ts | 0 .../publicApi/segment/changeFontSizeTest.ts | 94 +- .../publicApi/segment/segmentTestCommon.ts | 39 + .../segment/setBackgroundColorTest.ts | 0 .../test/publicApi/segment/setFontNameTest.ts | 0 .../test/publicApi/segment/setFontSizeTest.ts | 0 .../publicApi/segment/setTextColorTest.ts | 0 .../test/publicApi/segment/toggleBoldTest.ts | 0 .../test/publicApi/segment/toggleCodeTest.ts | 0 .../publicApi/segment/toggleItalicTest.ts | 0 .../segment/toggleStrikethroughTest.ts | 0 .../publicApi/segment/toggleSubscriptTest.ts | 0 .../segment/toggleSuperscriptTest.ts | 0 .../publicApi/segment/toggleUnderlineTest.ts | 0 .../selection/hasSelectionInBlockTest.ts | 0 .../selection/hasSelectionInSegmentTest.ts | 0 .../table/applyTableBorderFormatTest.ts | 71 +- .../publicApi/table/setTableCellShadeTest.ts | 63 +- .../utils/formatImageWithContentModelTest.ts | 55 +- .../formatParagraphWithContentModelTest.ts | 86 +- .../formatSegmentWithContentModelTest.ts | 116 +- .../lib/constants/ChangeSource.ts | 59 + .../lib}/coreApi/createContentModel.ts | 12 +- .../lib}/coreApi/createEditorContext.ts | 3 +- .../lib/coreApi/formatContentModel.ts | 174 ++ .../lib}/coreApi/getDOMSelection.ts | 9 +- .../lib}/coreApi/setContentModel.ts | 2 +- .../lib}/coreApi/setDOMSelection.ts | 2 +- .../lib}/coreApi/switchShadowEdit.ts | 17 +- .../corePlugin}/ContentModelCachePlugin.ts | 23 +- .../ContentModelCopyPastePlugin.ts | 50 +- .../corePlugin}/ContentModelFormatPlugin.ts | 64 +- .../ContentModelTypeInContainerPlugin.ts | 2 +- .../corePlugin/utils}/addRangeToSelection.ts | 0 .../corePlugin/utils}/applyDefaultFormat.ts | 81 +- .../corePlugin/utils}/applyPendingFormat.ts | 30 +- .../lib/corePlugin/utils/areSameSelection.ts} | 2 +- .../utils/contentModelDomIndexer.ts | 2 +- .../editor/createContentModelEditorCore.ts | 67 + .../editor/promoteToContentModelEditorCore.ts | 87 + .../roosterjs-content-model-core/lib/index.ts | 50 + .../lib}/metadata/definitionCreators.ts | 0 .../lib}/metadata/updateImageMetadata.ts | 0 .../lib}/metadata/updateListMetadata.ts | 0 .../lib}/metadata/updateTableCellMetadata.ts | 0 .../lib}/metadata/updateTableMetadata.ts | 0 .../modelApi/edit}/deleteExpandedSelection.ts | 49 +- .../lib/modelApi/edit}/deleteSingleChar.ts | 0 .../lib/override}/tablePreProcessor.ts | 2 +- .../lib/publicApi}/domUtils/borderValues.ts | 2 +- .../lib/publicApi}/domUtils/eventUtils.ts | 2 - .../lib/publicApi}/domUtils/stringUtil.ts | 3 - .../lib/publicApi/model}/cloneModel.ts | 13 +- .../getClosestAncestorBlockGroupIndex.ts | 7 +- .../publicApi/model}/isBlockGroupOfType.ts | 4 +- .../lib/publicApi/model}/mergeModel.ts | 10 +- .../lib/publicApi/model}/paste.ts | 50 +- .../publicApi}/selection/collectSelections.ts | 50 +- .../lib/publicApi/selection}/deleteBlock.ts | 12 +- .../lib/publicApi/selection}/deleteSegment.ts | 16 +- .../publicApi/selection}/deleteSelection.ts | 21 +- .../selection/getSelectionRootNode.ts | 6 +- .../publicApi}/selection/iterateSelections.ts | 26 +- .../lib/publicApi}/selection/setSelection.ts | 5 +- .../lib/publicApi}/table/applyTableFormat.ts | 11 +- .../lib/publicApi}/table/normalizeTable.ts | 10 +- .../table/setTableCellBackgroundColor.ts | 8 +- .../roosterjs-content-model-core/package.json | 14 + .../test}/coreApi/createContentModelTest.ts | 11 +- .../test}/coreApi/createEditorContextTest.ts | 17 +- .../test/coreApi/formatContentModelTest.ts | 700 +++++++ .../test}/coreApi/setContentModelTest.ts | 9 +- .../test}/coreApi/switchShadowEditTest.ts | 16 +- .../ContentModelCachePluginTest.ts | 16 +- .../ContentModelCopyPastePluginTest.ts | 124 +- .../ContentModelFormatPluginTest.ts | 478 ++--- .../utils/applyDefaultFormatTest.ts | 408 +++++ .../utils}/applyPendingFormatTest.ts | 142 +- .../corePlugin/utils}/areSameRangeExTest.ts | 6 +- .../utils/contentModelDomIndexerTest.ts | 4 +- .../createContentModelEditorCoreTest.ts | 237 +++ .../promoteToContentModelEditorCoreTest.ts | 180 ++ .../handleListItemWithMetadataTest.ts | 2 +- .../metadata/handleListWithMetadataTest.ts | 2 +- .../test}/metadata/updateImageMetadataTest.ts | 2 +- .../test}/metadata/updateListMetadataTest.ts | 2 +- .../metadata/updateTableCellMetadataTest.ts | 2 +- .../test}/metadata/updateTableMetadataTest.ts | 2 +- .../modelApi/edit}/deleteSingleCharTest.ts | 2 +- .../test}/overrides/tablePreProcessorTest.ts | 2 +- .../publicApi}/domUtils/borderValuesTest.ts | 5 +- .../test/publicApi/model}/cloneModelTest.ts | 2 +- .../getClosestAncestorBlockGroupIndexTest.ts | 2 +- .../model}/isBlockGroupOfTypeTest.ts | 2 +- .../test/publicApi/model}/mergeModelTest.ts | 8 +- .../test/publicApi/model}/pasteTest.ts | 138 +- .../selection/collectSelectionsTest.ts | 22 +- .../selection/deleteSelectionTest.ts | 965 ++++++++++ .../selection/getSelectedSegmentsTest.ts | 16 +- .../selection/getSelectionRootNodeTest.ts | 41 + .../selection/iterateSelectionsTest.ts | 266 +-- .../publicApi}/selection/setSelectionTest.ts | 2 +- .../publicApi}/table/applyTableFormatTest.ts | 2 +- .../publicApi}/table/normalizeTableTest.ts | 2 +- .../table/setTableCellBackgroundColorTest.ts | 2 +- .../block/marginFormatHandler.ts | 20 + .../block/marginFormatHandlerTest.ts | 70 + .../lib/editor/ContentModelEditor.ts | 31 +- .../editor/createContentModelEditorCore.ts | 147 -- .../lib/index.ts | 127 +- .../edit/utils/DeleteSelectionStep.ts | 43 - .../modelApi/edit/utils/createInsertPoint.ts | 24 - .../lib/modelApi/format/pendingFormat.ts | 79 - .../lib/publicApi/block/setDirection.ts | 14 - .../lib/publicApi/format/clearFormat.ts | 30 - .../publicApi/image/adjustImageSelection.ts | 31 - .../lib/publicApi/image/insertImage.ts | 38 - .../lib/publicApi/link/adjustLinkSelection.ts | 41 - .../lib/publicApi/link/removeLink.ts | 37 - .../lib/publicApi/list/setListStartNumber.ts | 25 - .../lib/publicApi/list/setListStyle.ts | 36 - .../lib/publicApi/list/toggleBullet.ts | 17 - .../lib/publicApi/list/toggleNumbering.ts | 17 - .../selection/getSelectedSegments.ts | 12 - .../publicApi/table/applyTableBorderFormat.ts | 340 ---- .../lib/publicApi/table/editTable.ts | 128 -- .../lib/publicApi/table/formatTable.ts | 42 - .../lib/publicApi/table/insertTable.ts | 59 - .../lib/publicApi/table/setTableCellShade.ts | 35 - .../utils/formatParagraphWithContentModel.ts | 28 - .../utils/formatSegmentWithContentModel.ts | 87 - .../publicApi/utils/formatWithContentModel.ts | 156 -- .../lib/publicTypes/ContentModelEditorCore.ts | 123 +- .../lib/publicTypes/IContentModelEditor.ts | 79 +- .../event/ContentModelContentChangedEvent.ts | 95 - .../FormatWithContentModelContext.ts | 148 -- .../ContentModelFormatPluginState.ts | 11 - .../package.json | 1 + .../test/editor/ContentModelEditorTest.ts | 29 + .../createContentModelEditorCoreTest.ts | 463 ----- .../test/modelApi/format/pendingFormatTest.ts | 173 -- .../publicApi/block/paragraphTestCommon.ts | 40 - .../publicApi/block/setIndentationTest.ts | 46 - .../publicApi/editing/editingTestCommon.ts | 52 - .../test/publicApi/list/toggleBulletTest.ts | 47 - .../publicApi/list/toggleNumberingTest.ts | 47 - .../publicApi/segment/segmentTestCommon.ts | 45 - .../utils/formatWithContentModelTest.ts | 342 ---- .../lib/edit}/ContentModelEditPlugin.ts | 17 +- .../deleteSteps/deleteAllSegmentBefore.ts | 7 +- .../deleteSteps/deleteCollapsedSelection.ts | 29 +- .../edit/deleteSteps/deleteWordSelection.ts | 14 +- .../lib/edit}/handleKeyboardEventCommon.ts | 26 +- .../lib/edit}/keyboardDelete.ts | 30 +- .../lib/edit/utils}/getLeafSiblingBlock.ts | 0 .../lib/index.ts | 2 + .../lib/paste}/ContentModelPastePlugin.ts | 8 +- .../Excel/processPastedContentFromExcel.ts | 2 +- .../processPastedContentFromPowerPoint.ts | 0 .../processPastedContentWacComponents.ts | 2 +- .../processPastedContentFromWordDesktop.ts | 2 +- .../paste}/WordDesktop/processWordComments.ts | 0 .../paste}/WordDesktop/processWordLists.ts | 0 .../pasteSourceValidations/constants.ts | 0 .../documentContainWacElements.ts | 0 .../pasteSourceValidations/getPasteSource.ts | 0 .../isExcelDesktopDocument.ts | 0 .../isExcelOnlineDocument.ts | 0 .../isGoogleSheetDocument.ts | 0 .../isPowerPointDesktopDocument.ts | 0 .../isWordDesktopDocument.ts | 0 .../shouldConvertToSingleImage.ts | 0 .../lib/paste}/utils/addParser.ts | 0 .../lib/paste}/utils/deprecatedColorParser.ts | 0 .../lib/paste}/utils/getStyles.ts | 0 .../lib/paste}/utils/linkParser.ts | 0 .../lib/paste}/utils/setProcessor.ts | 0 .../package.json | 15 + .../test/edit}/ContentModelEditPluginTest.ts | 8 +- .../deleteCollapsedSelectionTest.ts} | 1617 ++--------------- .../deleteSteps/deleteWordSelectionTest.ts | 413 +++++ .../test/edit/editingTestCommon.ts | 42 + .../edit}/handleKeyboardEventCommonTest.ts | 27 +- .../test/edit}/keyboardDeleteTest.ts | 93 +- .../edit/utils}/getLeafSiblingBlockTest.ts | 2 +- .../paste}/ContentModelPastePluginTest.ts | 22 +- .../test}/paste/deprecatedColorParserTest.ts | 2 +- .../paste/e2e/cmPasteFromExcelOnlineTest.ts | 6 +- .../test}/paste/e2e/cmPasteFromExcelTest.ts | 6 +- .../test}/paste/e2e/cmPasteFromWacTest.ts | 6 +- .../test}/paste/e2e/cmPasteFromWordTest.ts | 7 +- .../test}/paste/e2e/cmPasteTest.ts | 6 +- .../test}/paste/e2e/testUtils.ts | 12 +- .../test}/paste/linkParserTest.ts | 2 +- .../documentContainWacElementsTest.ts | 4 +- .../getPasteSourceTest.ts | 4 +- .../isExcelDesktopDocumentTest.ts | 4 +- .../isExcelOnlineDocumentTest.ts | 4 +- .../isGoogleSheetDocumentTest.ts | 6 +- .../isPowerPointDesktopDocumentTest.ts | 4 +- .../isWordDesktopDocumentTest.ts | 4 +- .../pasteSourceValidations/pasteTestUtils.ts | 0 .../shouldConvertToSingleImageTest.ts | 4 +- .../processPastedContentFromExcelTest.ts | 4 +- .../processPastedContentFromPowerPointTest.ts | 2 +- .../paste/processPastedContentFromWacTest.ts | 5 +- ...processPastedContentFromWordDesktopTest.ts | 7 +- .../test}/paste/utils/getStylesTest.ts | 2 +- .../lib/editor/IStandaloneEditor.ts | 139 ++ .../lib/editor/StandaloneEditorCore.ts | 178 ++ .../lib/editor/StandaloneEditorOptions.ts | 22 + .../lib}/enum/BorderOperations.ts | 0 .../lib/enum/DeleteResult.ts | 23 + .../lib/enum/EntityOperation.ts | 52 + .../lib/enum/InsertEntityPosition.ts | 8 + .../lib/enum}/PasteType.ts | 0 .../lib/enum}/TableOperation.ts | 0 .../event/ContentModelBeforePasteEvent.ts | 5 +- .../event/ContentModelContentChangedEvent.ts | 36 + .../lib/format/formatParts/MarginFormat.ts | 10 + .../lib/index.ts | 75 + .../lib/parameter}/Border.ts | 0 .../lib/parameter}/ContentModelFormatState.ts | 2 +- .../lib/parameter/DeleteSelectionStep.ts | 56 + .../lib/parameter/EditorEnvironment.ts | 14 + .../FormatWithContentModelContext.ts | 66 + .../FormatWithContentModelOptions.ts | 52 + .../lib/parameter}/ImageFormatState.ts | 0 .../lib}/parameter/InsertEntityOptions.ts | 9 - .../ContentModelCachePluginState.ts | 8 +- .../ContentModelFormatPluginState.ts | 36 + .../pluginState/ContentModelPluginState.ts | 0 .../lib}/selection/InsertPoint.ts | 8 +- .../lib}/selection/TableSelectionContext.ts | 2 +- .../package.json | 4 +- .../lib/createContentModelEditor.ts | 10 +- .../roosterjs-content-model/lib/index.ts | 3 + .../roosterjs-content-model/package.json | 5 +- .../menus/createImageEditMenuProvider.tsx | 61 +- .../lib/contextMenu/types/ContextMenuItem.ts | 8 + .../utils/createContextMenuProvider.ts | 14 +- .../lib/utils/toggleListType.ts | 13 + .../test/format/setIndentationTest.ts | 26 + .../test/utils/toggleListTypeTest.ts | 20 + .../lib/corePlugins/ImageSelection.ts | 18 +- .../roosterjs-editor-dom/lib/list/VList.ts | 11 +- .../lib/list/VListItem.ts | 8 + .../test/list/VListTest.ts | 52 + .../lib/plugins/ImageEdit/api/isResizedTo.ts | 13 +- .../lib/plugins/Picker/PickerPlugin.ts | 16 +- .../lib/plugins/TableResize/TableResize.ts | 30 +- .../test/TableResize/tableResizeTest.ts | 264 +++ tools/buildTools/normalize.js | 1 + versions.json | 6 +- 457 files changed, 8591 insertions(+), 6939 deletions(-) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/index.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/block/setModelAlignment.ts (85%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/block/setModelDirection.ts (94%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/block/setModelIndentation.ts (93%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/block/toggleModelBlockQuote.ts (92%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/common/clearModelFormat.ts (92%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/common/retrieveModelFormatState.ts (94%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/common/wrapBlock.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib => roosterjs-content-model-api/lib/modelApi}/domUtils/readFile.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/entity/insertEntityModel.ts (84%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/image/applyImageBorderFormat.ts (89%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/list/findListItemsInSameThread.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/list/setListType.ts (95%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/selection/adjustSegmentSelection.ts (93%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/selection/adjustWordSelection.ts (95%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/selection/collapseTableSelection.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/alignTable.ts (64%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/alignTableCell.ts (89%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/canMergeCells.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/createTableStructure.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/deleteTable.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/deleteTableColumn.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/deleteTableRow.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/ensureFocusableParagraphForTable.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/getSelectedCells.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/insertTableColumn.ts (85%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/insertTableRow.ts (83%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/mergeTableCells.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/mergeTableColumn.ts (91%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/mergeTableRow.ts (91%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/splitTableCellHorizontally.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/modelApi/table/splitTableCellVertically.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/block/setAlignment.ts (54%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setDirection.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/block/setHeadingLevel.ts (90%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/block/setIndentation.ts (69%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/block/setParagraphMargin.ts (90%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/block/setSpacing.ts (78%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/block/toggleBlockQuote.ts (69%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/entity/insertEntity.ts (86%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/format/getFormatState.ts (58%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/image/changeImage.ts (75%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/image/insertImage.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/image/setImageAltText.ts (63%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/image/setImageBorder.ts (77%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/image/setImageBoxShadow.ts (84%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/link/insertLink.ts (88%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStartNumber.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStyle.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleBullet.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleNumbering.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/applySegmentFormat.ts (84%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/changeCapitalization.ts (95%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/changeFontSize.ts (93%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/setBackgroundColor.ts (85%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/setFontName.ts (77%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/setFontSize.ts (88%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/setTextColor.ts (83%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleBold.ts (84%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleCode.ts (76%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleItalic.ts (72%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleStrikethrough.ts (72%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleSubscript.ts (75%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleSuperscript.ts (75%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/segment/toggleUnderline.ts (78%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/selection/hasSelectionInBlock.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/selection/hasSelectionInBlockGroup.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/selection/hasSelectionInSegment.ts (100%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/table/editTable.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/table/insertTable.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/lib/publicApi/utils/formatImageWithContentModel.ts (74%) create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts create mode 100644 packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts create mode 100644 packages-content-model/roosterjs-content-model-api/package.json rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/block/setModelAlignmentTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/block/setModelDirectionTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/block/setModelIndentationTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/block/toggleModelBlockQuoteTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/common/clearModelFormatTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/common/retrieveModelFormatStateTest.ts (93%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/common/wrapBlockTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/entity/insertEntityModelTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/image/applyImageBorderFormatTest.ts (96%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/list/findListItemsInSameThreadTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/list/setListTypeTest.ts (95%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/selection/adjustSegmentSelectionTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/selection/adjustWordSelectionTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/selection/collapseTableSelectionTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/alignTableCellTest.ts (97%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/alignTableTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/canMergeCellsTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/createTableStructureTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/deleteTableColumnTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/deleteTableRowTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/deleteTableTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/ensureFocusableParagraphForTableTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/getSelectedCellsTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/insertTableColumnTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/insertTableRowTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/mergeTableCellsTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/mergeTableColumnTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/mergeTableRowTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/splitTableCellHorizontallyTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/modelApi/table/splitTableCellVerticallyTest.ts (100%) create mode 100644 packages-content-model/roosterjs-content-model-api/test/publicApi/block/paragraphTestCommon.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/block/setAlignmentTest.ts (94%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/block/setDirectionTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/block/setHeadingLevelTest.ts (100%) create mode 100644 packages-content-model/roosterjs-content-model-api/test/publicApi/block/setIndentationTest.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/block/setParagraphMarginTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/block/setSpacingTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/block/toggleBlockQuoteTest.ts (54%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/entity/insertEntityTest.ts (84%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/format/clearFormatTest.ts (55%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/format/getFormatStateTest.ts (94%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/image/adjustImageSelectionTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/image/changeImageTest.ts (71%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/image/insertImageTest.ts (80%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/image/setImageAltTextTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/image/setImageBorderTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/image/setImageBoxShadowTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/link/adjustLinkSelectionTest.ts (89%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/link/insertLinkTest.ts (86%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/link/removeLinkTest.ts (85%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/list/setListStartNumberTest.ts (90%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/list/setListStyleTest.ts (97%) create mode 100644 packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleBulletTest.ts create mode 100644 packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleNumberingTest.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/applySegmentFormatTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/changeCapitalizationTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/changeFontSizeTest.ts (86%) create mode 100644 packages-content-model/roosterjs-content-model-api/test/publicApi/segment/segmentTestCommon.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/setBackgroundColorTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/setFontNameTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/setFontSizeTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/setTextColorTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleBoldTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleCodeTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleItalicTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleStrikethroughTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleSubscriptTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleSuperscriptTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/segment/toggleUnderlineTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/selection/hasSelectionInBlockTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/selection/hasSelectionInSegmentTest.ts (100%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/table/applyTableBorderFormatTest.ts (97%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/table/setTableCellShadeTest.ts (91%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/utils/formatImageWithContentModelTest.ts (79%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/utils/formatParagraphWithContentModelTest.ts (50%) rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-api}/test/publicApi/utils/formatSegmentWithContentModelTest.ts (77%) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/constants/ChangeSource.ts rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib}/coreApi/createContentModel.ts (86%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib}/coreApi/createEditorContext.ts (88%) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib}/coreApi/getDOMSelection.ts (81%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib}/coreApi/setContentModel.ts (94%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib}/coreApi/setDOMSelection.ts (94%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib}/coreApi/switchShadowEdit.ts (79%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/corePlugins => roosterjs-content-model-core/lib/corePlugin}/ContentModelCachePlugin.ts (88%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/corePlugins => roosterjs-content-model-core/lib/corePlugin}/ContentModelCopyPastePlugin.ts (84%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/corePlugins => roosterjs-content-model-core/lib/corePlugin}/ContentModelFormatPlugin.ts (65%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/corePlugins => roosterjs-content-model-core/lib/corePlugin}/ContentModelTypeInContainerPlugin.ts (91%) rename packages-content-model/{roosterjs-content-model-editor/lib/domUtils => roosterjs-content-model-core/lib/corePlugin/utils}/addRangeToSelection.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicApi/format => roosterjs-content-model-core/lib/corePlugin/utils}/applyDefaultFormat.ts (57%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicApi/format => roosterjs-content-model-core/lib/corePlugin/utils}/applyPendingFormat.ts (76%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/selection/areSameRangeEx.ts => roosterjs-content-model-core/lib/corePlugin/utils/areSameSelection.ts} (92%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor => roosterjs-content-model-core/lib/corePlugin}/utils/contentModelDomIndexer.ts (99%) create mode 100644 packages-content-model/roosterjs-content-model-core/lib/editor/createContentModelEditorCore.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/editor/promoteToContentModelEditorCore.ts create mode 100644 packages-content-model/roosterjs-content-model-core/lib/index.ts rename packages-content-model/{roosterjs-content-model-editor/lib/domUtils => roosterjs-content-model-core/lib}/metadata/definitionCreators.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/domUtils => roosterjs-content-model-core/lib}/metadata/updateImageMetadata.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/domUtils => roosterjs-content-model-core/lib}/metadata/updateListMetadata.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/domUtils => roosterjs-content-model-core/lib}/metadata/updateTableCellMetadata.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/domUtils => roosterjs-content-model-core/lib}/metadata/updateTableMetadata.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/edit/utils => roosterjs-content-model-core/lib/modelApi/edit}/deleteExpandedSelection.ts (80%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/edit/utils => roosterjs-content-model-core/lib/modelApi/edit}/deleteSingleChar.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/overrides => roosterjs-content-model-core/lib/override}/tablePreProcessor.ts (92%) rename packages-content-model/{roosterjs-content-model-editor/lib => roosterjs-content-model-core/lib/publicApi}/domUtils/borderValues.ts (95%) rename packages-content-model/{roosterjs-content-model-editor/lib => roosterjs-content-model-core/lib/publicApi}/domUtils/eventUtils.ts (97%) rename packages-content-model/{roosterjs-content-model-editor/lib => roosterjs-content-model-core/lib/publicApi}/domUtils/stringUtil.ts (96%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/common => roosterjs-content-model-core/lib/publicApi/model}/cloneModel.ts (95%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/common => roosterjs-content-model-core/lib/publicApi/model}/getClosestAncestorBlockGroupIndex.ts (76%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/common => roosterjs-content-model-core/lib/publicApi/model}/isBlockGroupOfType.ts (74%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/common => roosterjs-content-model-core/lib/publicApi/model}/mergeModel.ts (97%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicApi/utils => roosterjs-content-model-core/lib/publicApi/model}/paste.ts (83%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/selection/collectSelections.ts (78%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/edit/utils => roosterjs-content-model-core/lib/publicApi/selection}/deleteBlock.ts (76%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/edit/utils => roosterjs-content-model-core/lib/publicApi/selection}/deleteSegment.ts (81%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/edit => roosterjs-content-model-core/lib/publicApi/selection}/deleteSelection.ts (67%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/selection/getSelectionRootNode.ts (59%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/selection/iterateSelections.ts (93%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/selection/setSelection.ts (92%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/table/applyTableFormat.ts (93%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/table/normalizeTable.ts (91%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-core/lib/publicApi}/table/setTableCellBackgroundColor.ts (89%) create mode 100644 packages-content-model/roosterjs-content-model-core/package.json rename packages-content-model/{roosterjs-content-model-editor/test/editor => roosterjs-content-model-core/test}/coreApi/createContentModelTest.ts (94%) rename packages-content-model/{roosterjs-content-model-editor/test/editor => roosterjs-content-model-core/test}/coreApi/createEditorContextTest.ts (92%) create mode 100644 packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts rename packages-content-model/{roosterjs-content-model-editor/test/editor => roosterjs-content-model-core/test}/coreApi/setContentModelTest.ts (93%) rename packages-content-model/{roosterjs-content-model-editor/test/editor => roosterjs-content-model-core/test}/coreApi/switchShadowEditTest.ts (90%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-core/test/corePlugin}/ContentModelCachePluginTest.ts (96%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-core/test/corePlugin}/ContentModelCopyPastePluginTest.ts (86%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/corePlugins => roosterjs-content-model-core/test/corePlugin}/ContentModelFormatPluginTest.ts (52%) create mode 100644 packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyDefaultFormatTest.ts rename packages-content-model/{roosterjs-content-model-editor/test/publicApi/format => roosterjs-content-model-core/test/corePlugin/utils}/applyPendingFormatTest.ts (71%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/selection => roosterjs-content-model-core/test/corePlugin/utils}/areSameRangeExTest.ts (97%) rename packages-content-model/{roosterjs-content-model-editor/test/editor => roosterjs-content-model-core/test/corePlugin}/utils/contentModelDomIndexerTest.ts (99%) create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/createContentModelEditorCoreTest.ts create mode 100644 packages-content-model/roosterjs-content-model-core/test/editor/promoteToContentModelEditorCoreTest.ts rename packages-content-model/{roosterjs-content-model-editor/test/domUtils => roosterjs-content-model-core/test}/metadata/handleListItemWithMetadataTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/domUtils => roosterjs-content-model-core/test}/metadata/handleListWithMetadataTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/domUtils => roosterjs-content-model-core/test}/metadata/updateImageMetadataTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/domUtils => roosterjs-content-model-core/test}/metadata/updateListMetadataTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/domUtils => roosterjs-content-model-core/test}/metadata/updateTableCellMetadataTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/domUtils => roosterjs-content-model-core/test}/metadata/updateTableMetadataTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/edit/utils => roosterjs-content-model-core/test/modelApi/edit}/deleteSingleCharTest.ts (94%) rename packages-content-model/{roosterjs-content-model-editor/test/editor => roosterjs-content-model-core/test}/overrides/tablePreProcessorTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test => roosterjs-content-model-core/test/publicApi}/domUtils/borderValuesTest.ts (94%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/common => roosterjs-content-model-core/test/publicApi/model}/cloneModelTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/common => roosterjs-content-model-core/test/publicApi/model}/getClosestAncestorBlockGroupIndexTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/common => roosterjs-content-model-core/test/publicApi/model}/isBlockGroupOfTypeTest.ts (92%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/common => roosterjs-content-model-core/test/publicApi/model}/mergeModelTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/publicApi/utils => roosterjs-content-model-core/test/publicApi/model}/pasteTest.ts (88%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi => roosterjs-content-model-core/test/publicApi}/selection/collectSelectionsTest.ts (98%) create mode 100644 packages-content-model/roosterjs-content-model-core/test/publicApi/selection/deleteSelectionTest.ts rename packages-content-model/{roosterjs-content-model-editor => roosterjs-content-model-core}/test/publicApi/selection/getSelectedSegmentsTest.ts (94%) create mode 100644 packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectionRootNodeTest.ts rename packages-content-model/{roosterjs-content-model-editor/test/modelApi => roosterjs-content-model-core/test/publicApi}/selection/iterateSelectionsTest.ts (83%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi => roosterjs-content-model-core/test/publicApi}/selection/setSelectionTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi => roosterjs-content-model-core/test/publicApi}/table/applyTableFormatTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi => roosterjs-content-model-core/test/publicApi}/table/normalizeTableTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi => roosterjs-content-model-core/test/publicApi}/table/setTableCellBackgroundColorTest.ts (98%) delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/DeleteSelectionStep.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/createInsertPoint.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/modelApi/format/pendingFormat.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setDirection.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/clearFormat.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/adjustImageSelection.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/adjustLinkSelection.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/removeLink.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStartNumber.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStyle.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleBullet.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleNumbering.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/getSelectedSegments.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/applyTableBorderFormat.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/editTable.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/formatTable.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/insertTable.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/setTableCellShade.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatParagraphWithContentModel.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatSegmentWithContentModel.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelContentChangedEvent.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelFormatPluginState.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/modelApi/format/pendingFormatTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setIndentationTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts delete mode 100644 packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts rename packages-content-model/{roosterjs-content-model-editor/lib/editor/corePlugins => roosterjs-content-model-plugins/lib/edit}/ContentModelEditPlugin.ts (83%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-plugins/lib}/edit/deleteSteps/deleteAllSegmentBefore.ts (63%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-plugins/lib}/edit/deleteSteps/deleteCollapsedSelection.ts (79%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi => roosterjs-content-model-plugins/lib}/edit/deleteSteps/deleteWordSelection.ts (92%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/utils => roosterjs-content-model-plugins/lib/edit}/handleKeyboardEventCommon.ts (75%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicApi/editing => roosterjs-content-model-plugins/lib/edit}/keyboardDelete.ts (70%) rename packages-content-model/{roosterjs-content-model-editor/lib/modelApi/block => roosterjs-content-model-plugins/lib/edit/utils}/getLeafSiblingBlock.ts (100%) create mode 100644 packages-content-model/roosterjs-content-model-plugins/lib/index.ts rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/ContentModelPastePlugin.ts (95%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/Excel/processPastedContentFromExcel.ts (96%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/PowerPoint/processPastedContentFromPowerPoint.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/WacComponents/processPastedContentWacComponents.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/WordDesktop/processPastedContentFromWordDesktop.ts (97%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/WordDesktop/processWordComments.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/WordDesktop/processWordLists.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/constants.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/documentContainWacElements.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/getPasteSource.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/isExcelDesktopDocument.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/isExcelOnlineDocument.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/isGoogleSheetDocument.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/isPowerPointDesktopDocument.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/isWordDesktopDocument.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/pasteSourceValidations/shouldConvertToSingleImage.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/utils/addParser.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/utils/deprecatedColorParser.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/utils/getStyles.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/utils/linkParser.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/editor/plugins/PastePlugin => roosterjs-content-model-plugins/lib/paste}/utils/setProcessor.ts (100%) create mode 100644 packages-content-model/roosterjs-content-model-plugins/package.json rename packages-content-model/{roosterjs-content-model-editor/test/editor/corePlugins => roosterjs-content-model-plugins/test/edit}/ContentModelEditPluginTest.ts (92%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts => roosterjs-content-model-plugins/test/edit/deleteSteps/deleteCollapsedSelectionTest.ts} (69%) create mode 100644 packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteWordSelectionTest.ts create mode 100644 packages-content-model/roosterjs-content-model-plugins/test/edit/editingTestCommon.ts rename packages-content-model/{roosterjs-content-model-editor/test/editor/utils => roosterjs-content-model-plugins/test/edit}/handleKeyboardEventCommonTest.ts (91%) rename packages-content-model/{roosterjs-content-model-editor/test/publicApi/editing => roosterjs-content-model-plugins/test/edit}/keyboardDeleteTest.ts (84%) rename packages-content-model/{roosterjs-content-model-editor/test/modelApi/block => roosterjs-content-model-plugins/test/edit/utils}/getLeafSiblingBlockTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test/paste}/ContentModelPastePluginTest.ts (88%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/deprecatedColorParserTest.ts (88%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/e2e/cmPasteFromExcelOnlineTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/e2e/cmPasteFromExcelTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/e2e/cmPasteFromWacTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/e2e/cmPasteFromWordTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/e2e/cmPasteTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/e2e/testUtils.ts (70%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/linkParserTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/documentContainWacElementsTest.ts (91%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/getPasteSourceTest.ts (94%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts (83%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts (79%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts (71%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts (68%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts (82%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/pasteTestUtils.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts (82%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/processPastedContentFromExcelTest.ts (98%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/processPastedContentFromPowerPointTest.ts (96%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/processPastedContentFromWacTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/processPastedContentFromWordDesktopTest.ts (99%) rename packages-content-model/{roosterjs-content-model-editor/test/editor/plugins => roosterjs-content-model-plugins/test}/paste/utils/getStylesTest.ts (92%) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/enum/BorderOperations.ts (100%) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/enum/DeleteResult.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/enum/EntityOperation.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/enum/InsertEntityPosition.ts rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes/parameter => roosterjs-content-model-types/lib/enum}/PasteType.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes/parameter => roosterjs-content-model-types/lib/enum}/TableOperation.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/event/ContentModelBeforePasteEvent.ts (85%) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes/interface => roosterjs-content-model-types/lib/parameter}/Border.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes/format/formatState => roosterjs-content-model-types/lib/parameter}/ContentModelFormatState.ts (97%) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/EditorEnvironment.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelContext.ts create mode 100644 packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelOptions.ts rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes/format/formatState => roosterjs-content-model-types/lib/parameter}/ImageFormatState.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/parameter/InsertEntityOptions.ts (54%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/pluginState/ContentModelCachePluginState.ts (70%) create mode 100644 packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelFormatPluginState.ts rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/pluginState/ContentModelPluginState.ts (100%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/selection/InsertPoint.ts (71%) rename packages-content-model/{roosterjs-content-model-editor/lib/publicTypes => roosterjs-content-model-types/lib}/selection/TableSelectionContext.ts (86%) diff --git a/demo/scripts/controls/ContentModelEditorMainPane.tsx b/demo/scripts/controls/ContentModelEditorMainPane.tsx index d57e1da3d26..704414e9081 100644 --- a/demo/scripts/controls/ContentModelEditorMainPane.tsx +++ b/demo/scripts/controls/ContentModelEditorMainPane.tsx @@ -15,15 +15,11 @@ import SnapshotPlugin from './sidePane/snapshot/SnapshotPlugin'; import TitleBar from './titleBar/TitleBar'; import { arrayPush } from 'roosterjs-editor-dom'; import { ContentModelEditor } from 'roosterjs-content-model-editor'; +import { ContentModelEditPlugin } from 'roosterjs-content-model-plugins'; import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; +import { createEmojiPlugin, createPasteOptionPlugin, RibbonPlugin } from 'roosterjs-react'; import { EditorOptions, EditorPlugin } from 'roosterjs-editor-types'; import { PartialTheme } from '@fluentui/react/lib/Theme'; -import { - createRibbonPlugin, - RibbonPlugin, - createPasteOptionPlugin, - createEmojiPlugin, -} from 'roosterjs-react'; const styles = require('./ContentModelEditorMainPane.scss'); @@ -86,8 +82,8 @@ class ContentModelEditorMainPane extends MainPaneBase { private editorOptionPlugin: ContentModelEditorOptionsPlugin; private eventViewPlugin: ContentModelEventViewPlugin; private apiPlaygroundPlugin: ApiPlaygroundPlugin; - private ContentModelPanePlugin: ContentModelPanePlugin; - private ribbonPlugin: RibbonPlugin; + private contentModelPanePlugin: ContentModelPanePlugin; + private contentModelEditPlugin: ContentModelEditPlugin; private contentModelRibbonPlugin: RibbonPlugin; private pasteOptionPlugin: EditorPlugin; private emojiPlugin: EditorPlugin; @@ -103,8 +99,8 @@ class ContentModelEditorMainPane extends MainPaneBase { this.eventViewPlugin = new ContentModelEventViewPlugin(); this.apiPlaygroundPlugin = new ApiPlaygroundPlugin(); this.snapshotPlugin = new SnapshotPlugin(); - this.ContentModelPanePlugin = new ContentModelPanePlugin(); - this.ribbonPlugin = createRibbonPlugin(); + this.contentModelPanePlugin = new ContentModelPanePlugin(); + this.contentModelEditPlugin = new ContentModelEditPlugin(); this.contentModelRibbonPlugin = new ContentModelRibbonPlugin(); this.pasteOptionPlugin = createPasteOptionPlugin(); this.emojiPlugin = createEmojiPlugin(); @@ -131,7 +127,7 @@ class ContentModelEditorMainPane extends MainPaneBase { } renderTitleBar() { - return ; + return ; } renderRibbon(isPopout: boolean) { @@ -165,9 +161,9 @@ class ContentModelEditorMainPane extends MainPaneBase { const plugins = [ ...this.toggleablePlugins, - this.ribbonPlugin, this.contentModelRibbonPlugin, - this.ContentModelPanePlugin.getInnerRibbonPlugin(), + this.contentModelPanePlugin.getInnerRibbonPlugin(), + this.contentModelEditPlugin, this.pasteOptionPlugin, this.emojiPlugin, this.formatPainterPlugin, @@ -205,7 +201,7 @@ class ContentModelEditorMainPane extends MainPaneBase { this.eventViewPlugin, this.apiPlaygroundPlugin, this.snapshotPlugin, - this.ContentModelPanePlugin, + this.contentModelPanePlugin, ]; } } diff --git a/demo/scripts/controls/MainPane.tsx b/demo/scripts/controls/MainPane.tsx index 6b0eada1569..ec18675e5b2 100644 --- a/demo/scripts/controls/MainPane.tsx +++ b/demo/scripts/controls/MainPane.tsx @@ -143,7 +143,7 @@ class MainPane extends MainPaneBase { } renderTitleBar() { - return ; + return ; } renderRibbon(isPopout: boolean) { diff --git a/demo/scripts/controls/MainPaneBase.tsx b/demo/scripts/controls/MainPaneBase.tsx index 014a79f758d..f68d5acf7fb 100644 --- a/demo/scripts/controls/MainPaneBase.tsx +++ b/demo/scripts/controls/MainPaneBase.tsx @@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom'; import BuildInPluginState from './BuildInPluginState'; import SidePane from './sidePane/SidePane'; import SnapshotPlugin from './sidePane/snapshot/SnapshotPlugin'; -import { Border } from 'roosterjs-content-model-editor'; +import { Border } from 'roosterjs-content-model-types'; import { EditorOptions, EditorPlugin, IEditor } from 'roosterjs-editor-types'; import { getDarkColor } from 'roosterjs-color-utils'; import { PartialTheme, ThemeProvider } from '@fluentui/react/lib/Theme'; diff --git a/demo/scripts/controls/contentModel/components/format/formatPart/BorderFormatRenderers.ts b/demo/scripts/controls/contentModel/components/format/formatPart/BorderFormatRenderers.ts index 40148b7f1e5..14091d480e0 100644 --- a/demo/scripts/controls/contentModel/components/format/formatPart/BorderFormatRenderers.ts +++ b/demo/scripts/controls/contentModel/components/format/formatPart/BorderFormatRenderers.ts @@ -1,8 +1,8 @@ +import { BorderFormat } from 'roosterjs-content-model-types'; +import { combineBorderValue, extractBorderValues } from 'roosterjs-content-model-core'; import { createDropDownFormatRenderer } from '../utils/createDropDownFormatRenderer'; import { createTextFormatRenderer } from '../utils/createTextFormatRenderer'; import { FormatRenderer } from '../utils/FormatRenderer'; -import { BorderFormat } from 'roosterjs-content-model-types'; -import { combineBorderValue, extractBorderValues } from 'roosterjs-content-model-editor'; type BorderStyle = | 'dashed' diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx index 6fbe45c1889..cb2729be51d 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelDocumentView.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { BlockGroupContentView } from './BlockGroupContentView'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { ContentModelView } from '../ContentModelView'; -import { hasSelectionInBlockGroup } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlockGroup } from 'roosterjs-content-model-api'; const styles = require('./ContentModelDocumentView.scss'); diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelFormatContainerView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelFormatContainerView.tsx index 646f71b63da..e7d93eba233 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelFormatContainerView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelFormatContainerView.tsx @@ -5,7 +5,7 @@ import { ContentModelView } from '../ContentModelView'; import { DisplayFormatRenderer } from '../format/formatPart/DisplayFormatRenderer'; import { FormatRenderer } from '../format/utils/FormatRenderer'; import { FormatView } from '../format/FormatView'; -import { hasSelectionInBlock } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlock } from 'roosterjs-content-model-api'; import { SegmentFormatView } from '../format/SegmentFormatView'; import { SizeFormatRenderers } from '../format/formatPart/SizeFormatRenderers'; import { diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelGeneralView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelGeneralView.tsx index bf957e34a1e..d42b2ef1eb0 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelGeneralView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelGeneralView.tsx @@ -3,7 +3,7 @@ import { BlockGroupContentView } from './BlockGroupContentView'; import { ContentModelCodeView } from './ContentModelCodeView'; import { ContentModelLinkView } from './ContentModelLinkView'; import { ContentModelView } from '../ContentModelView'; -import { hasSelectionInBlock } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlock } from 'roosterjs-content-model-api'; import { SegmentFormatView } from '../format/SegmentFormatView'; import { ContentModelGeneralBlock, diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx index 948a6390a1a..4d2db4664d1 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx @@ -13,7 +13,7 @@ import { MetadataView } from '../format/MetadataView'; import { PaddingFormatRenderer } from '../format/formatPart/PaddingFormatRenderer'; import { SegmentFormatView } from '../format/SegmentFormatView'; import { SizeFormatRenderers } from '../format/formatPart/SizeFormatRenderers'; -import { updateImageMetadata } from 'roosterjs-content-model-editor'; +import { updateImageMetadata } from 'roosterjs-content-model-core'; import { useProperty } from '../../hooks/useProperty'; const styles = require('./ContentModelImageView.scss'); diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelListItemView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelListItemView.tsx index b453f77ae99..4d75adf63a1 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelListItemView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelListItemView.tsx @@ -7,7 +7,7 @@ import { FontFamilyFormatRenderer } from '../format/formatPart/FontFamilyFormatR import { FontSizeFormatRenderer } from '../format/formatPart/FontSizeFormatRenderer'; import { FormatRenderer } from '../format/utils/FormatRenderer'; import { FormatView } from '../format/FormatView'; -import { hasSelectionInBlockGroup } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlockGroup } from 'roosterjs-content-model-api'; import { LineHeightFormatRenderer } from '../format/formatPart/LineHeightFormatRenderer'; import { MarginFormatRenderer } from '../format/formatPart/MarginFormatRenderer'; import { TextAlignFormatRenderer } from '../format/formatPart/TextAlignFormatRenderer'; diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelListLevelView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelListLevelView.tsx index 96f9eb7f9db..e0c3523979a 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelListLevelView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelListLevelView.tsx @@ -10,7 +10,7 @@ import { MarginFormatRenderer } from '../format/formatPart/MarginFormatRenderer' import { MetadataView } from '../format/MetadataView'; import { PaddingFormatRenderer } from '../format/formatPart/PaddingFormatRenderer'; import { TextAlignFormatRenderer } from '../format/formatPart/TextAlignFormatRenderer'; -import { updateListMetadata } from 'roosterjs-content-model-editor'; +import { updateListMetadata } from 'roosterjs-content-model-core'; import { useProperty } from '../../hooks/useProperty'; import { ContentModelListItemLevelFormat, diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelParagraphView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelParagraphView.tsx index e5c7907f3f6..10fd06f9ef5 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelParagraphView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelParagraphView.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { BlockFormatView } from '../format/BlockFormatView'; import { ContentModelSegmentView } from './ContentModelSegmentView'; import { ContentModelView } from '../ContentModelView'; -import { hasSelectionInBlock } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlock } from 'roosterjs-content-model-api'; import { SegmentFormatView } from '../format/SegmentFormatView'; import { useProperty } from '../../hooks/useProperty'; import { diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx index c620b954528..065c8ead528 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx @@ -8,7 +8,7 @@ import { ContentModelView } from '../ContentModelView'; import { DirectionFormatRenderer } from '../format/formatPart/DirectionFormatRenderer'; import { FormatRenderer } from '../format/utils/FormatRenderer'; import { FormatView } from '../format/FormatView'; -import { hasSelectionInBlockGroup, updateTableCellMetadata } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlockGroup } from 'roosterjs-content-model-api'; import { HtmlAlignFormatRenderer } from '../format/formatPart/HtmlAlignFormatRenderer'; import { MetadataView } from '../format/MetadataView'; import { PaddingFormatRenderer } from '../format/formatPart/PaddingFormatRenderer'; @@ -16,6 +16,7 @@ import { SizeFormatRenderers } from '../format/formatPart/SizeFormatRenderers'; import { TableCellMetadataFormatRenders } from '../format/formatPart/TableCellMetadataFormatRenders'; import { TextAlignFormatRenderer } from '../format/formatPart/TextAlignFormatRenderer'; import { TextColorFormatRenderer } from '../format/formatPart/TextColorFormatRenderer'; +import { updateTableCellMetadata } from 'roosterjs-content-model-core'; import { useProperty } from '../../hooks/useProperty'; import { VerticalAlignFormatRenderer } from '../format/formatPart/VerticalAlignFormatRenderer'; import { WordBreakFormatRenderer } from '../format/formatPart/WordBreakFormatRenderer'; diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelTableRowView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableRowView.tsx index fbd8a002d6e..7a70f087476 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableRowView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableRowView.tsx @@ -5,7 +5,7 @@ import { ContentModelBlockGroupView } from './ContentModelBlockGroupView'; import { ContentModelView } from '../ContentModelView'; import { FormatRenderer } from '../format/utils/FormatRenderer'; import { FormatView } from '../format/FormatView'; -import { hasSelectionInBlockGroup } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlockGroup } from 'roosterjs-content-model-api'; import { useProperty } from '../../hooks/useProperty'; const styles = require('./ContentModelTableRowView.scss'); diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx index bbee53af187..744d9ae4db2 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx @@ -8,13 +8,14 @@ import { ContentModelView } from '../ContentModelView'; import { DisplayFormatRenderer } from '../format/formatPart/DisplayFormatRenderer'; import { FormatRenderer } from '../format/utils/FormatRenderer'; import { FormatView } from '../format/FormatView'; -import { hasSelectionInBlock, updateTableMetadata } from 'roosterjs-content-model-editor'; +import { hasSelectionInBlock } from 'roosterjs-content-model-api'; import { IdFormatRenderer } from '../format/formatPart/IdFormatRenderer'; import { MarginFormatRenderer } from '../format/formatPart/MarginFormatRenderer'; import { MetadataView } from '../format/MetadataView'; import { SpacingFormatRenderer } from '../format/formatPart/SpacingFormatRenderer'; import { TableLayoutFormatRenderer } from '../format/formatPart/TableLayoutFormatRenderer'; import { TableMetadataFormatRenders } from '../format/formatPart/TableMetadataFormatRenders'; +import { updateTableMetadata } from 'roosterjs-content-model-core'; import { useProperty } from '../../hooks/useProperty'; const styles = require('./ContentModelTableView.scss'); diff --git a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts index e9f2fee28a9..616096e1d59 100644 --- a/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts +++ b/demo/scripts/controls/contentModel/plugins/ContentModelFormatPainterPlugin.ts @@ -1,10 +1,7 @@ +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 { - applySegmentFormat, - getFormatState, - IContentModelEditor, -} from 'roosterjs-content-model-editor'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg'); const FORMATPAINTERCURSOR_STYLE = `;cursor: url("${FORMATPAINTERCURSOR_SVG}") 8.5 16, auto`; diff --git a/demo/scripts/controls/getToggleablePlugins.ts b/demo/scripts/controls/getToggleablePlugins.ts index 0f0b9157812..501dc6d1c28 100644 --- a/demo/scripts/controls/getToggleablePlugins.ts +++ b/demo/scripts/controls/getToggleablePlugins.ts @@ -2,7 +2,7 @@ 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-editor'; +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'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts index 7c7cbc5e9d3..7dd24709acd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts @@ -1,11 +1,9 @@ +import { ContentModelFormatState } from 'roosterjs-content-model-types'; 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 { - ContentModelFormatState, - getFormatState, - IContentModelEditor, -} from 'roosterjs-content-model-editor'; export class ContentModelRibbonPlugin implements RibbonPlugin { private editor: IContentModelEditor | null = null; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts index 7fb8b8bdd65..968d17e3d01 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts @@ -1,5 +1,6 @@ import { AlignCenterButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setAlignment } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setAlignment } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts index b2f53aef58d..55088314ec5 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts @@ -1,5 +1,6 @@ import { AlignLeftButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setAlignment } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setAlignment } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts index 8412d73bfda..bc73fd6dce1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts @@ -1,5 +1,6 @@ import { AlignRightButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setAlignment } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setAlignment } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts index 3fa9846098b..b3b9e2641ad 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts @@ -1,4 +1,5 @@ -import { isContentModelEditor, setBackgroundColor } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setBackgroundColor } from 'roosterjs-content-model-api'; import { BackgroundColorButtonStringKey, getBackgroundColorValue, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts index 656a4f25848..583287296be 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleBlockQuote } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { QuoteButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { toggleBlockQuote } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts index 52cda87d295..9bcd3597502 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts @@ -1,5 +1,6 @@ import { BoldButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, toggleBold } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { toggleBold } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts index 9d9c2c35607..12c2d492889 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts @@ -1,5 +1,6 @@ import { BulletedListButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, toggleBullet } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { toggleBullet } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts index 620cc60b64e..d6ab58b8c71 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts @@ -1,6 +1,7 @@ -import { changeImage, isContentModelEditor } from 'roosterjs-content-model-editor'; +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 = { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts index 5e8709dab20..cdfc02e6f51 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts @@ -1,5 +1,6 @@ -import { clearFormat, isContentModelEditor } from 'roosterjs-content-model-editor'; +import { clearFormat } from 'roosterjs-content-model-api'; import { ClearFormatButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; /** * "Clear format" button on the format ribbon diff --git a/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts index e3bafe5629f..5c26c62e66c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/codeButton.ts @@ -1,5 +1,6 @@ import { CodeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, toggleCode } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { toggleCode } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts index 0e1c9409275..f0d0ac14a1d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts @@ -1,5 +1,6 @@ -import { changeFontSize, isContentModelEditor } from 'roosterjs-content-model-editor'; +import { changeFontSize } from 'roosterjs-content-model-api'; import { DecreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts index ab5cd8c029b..c1d99a80c7a 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts @@ -1,5 +1,6 @@ import { DecreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setIndentation } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setIndentation } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts index 885a805b06d..139fec0771d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts @@ -1,5 +1,6 @@ import { FontButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setFontName } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setFontName } from 'roosterjs-content-model-api'; interface FontName { name: string; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts index e8c37f02703..5fc487586ee 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts @@ -1,5 +1,6 @@ import { FontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setFontSize } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setFontSize } from 'roosterjs-content-model-api'; const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts index 8abf48a67e6..e0b0a2236fb 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts @@ -1,4 +1,5 @@ -import { formatTable, isContentModelEditor } from 'roosterjs-content-model-editor'; +import { formatTable } from 'roosterjs-content-model-api'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; import { TableBorderFormat, TableMetadataFormat } from 'roosterjs-content-model-types'; @@ -67,12 +68,12 @@ const PREDEFINED_STYLES: Record< color /**topBorder */, color /**bottomBorder */, color /** verticalColors*/, - false /** bandedRows */, + true /** bandedRows */, false /** bandedColumns */, false /** headerRow */, false /** firstColumn */, TableBorderFormat.FIRST_COLUMN_HEADER_EXTERNAL /** tableBorderFormat */, - null /** bgColorEven */, + '#B0B0B0' /** bgColorEven */, lightColor /** bgColorOdd */, color /** headerRowColor */ ), diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts index 3236dd65263..a45ea0174dc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts @@ -1,6 +1,7 @@ import { getButtons, getTextColorValue, KnownRibbonButtonKey } from 'roosterjs-react'; -import { isContentModelEditor, setImageBorder } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; +import { setImageBorder } from 'roosterjs-content-model-api'; const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as RibbonButton< 'buttonNameImageBorderColor' diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts index 29c8b7cd4af..3fe95a2dfcc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderRemoveButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, setImageBorder } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; +import { setImageBorder } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts index 4d0e559f067..c39e9ee88d3 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, setImageBorder } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; +import { setImageBorder } from 'roosterjs-content-model-api'; const STYLES: Record = { dashed: 'dashed', diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts index fda580b2411..be6086f3fc8 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, setImageBorder } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; +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]; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts index b5a093174a5..a1eed8d8f92 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, setImageBoxShadow } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; +import { setImageBoxShadow } from 'roosterjs-content-model-api'; const STYLES_NAMES: Record = { noShadow: 'noShadow', diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts index 80eb5fb5428..f9308f87024 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts @@ -1,5 +1,6 @@ -import { changeFontSize, isContentModelEditor } from 'roosterjs-content-model-editor'; +import { changeFontSize } from 'roosterjs-content-model-api'; import { IncreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts index 972f35be14d..bdfefb9cbc6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts @@ -1,5 +1,6 @@ import { IncreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setIndentation } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setIndentation } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts index e37d31f7db3..62a14215b5c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts @@ -1,7 +1,8 @@ import { createElement } from 'roosterjs-editor-dom'; import { CreateElementData } from 'roosterjs-editor-types'; -import { insertImage, isContentModelEditor } from 'roosterjs-content-model-editor'; +import { insertImage } from 'roosterjs-content-model-api'; import { InsertImageButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; const FileInput: CreateElementData = { tag: 'input', diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts index e8f971511dc..b0e6c717754 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts @@ -1,10 +1,7 @@ +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 { - adjustLinkSelection, - insertLink, - isContentModelEditor, -} from 'roosterjs-content-model-editor'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts index c10fd5028d8..91fe7a4cfb9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts @@ -1,6 +1,7 @@ import { getButtons, KnownRibbonButtonKey } from 'roosterjs-react'; -import { insertTable, isContentModelEditor } from 'roosterjs-content-model-editor'; +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, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts index 0ed0d8f3993..18b3f5a70cd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleItalic } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { ItalicButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { toggleItalic } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts index 7e186693445..cacf17df6c6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts @@ -1,6 +1,7 @@ import showInputDialog from 'roosterjs-react/lib/inputDialog/utils/showInputDialog'; import { CancelButtonStringKey, OkButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setListStartNumber } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setListStartNumber } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts index 602bb3e7028..ac32f4fdc18 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts @@ -1,5 +1,6 @@ +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { LtrButtonStringKey, RibbonButton } from 'roosterjs-react'; -import { isContentModelEditor, setDirection } from 'roosterjs-content-model-editor'; +import { setDirection } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts index 555b8d54a08..45919be26f9 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleNumbering } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { NumberedListButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { toggleNumbering } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts index 08a176b9ef0..3be5b6a0e7f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts @@ -1,4 +1,5 @@ -import { isContentModelEditor, removeLink } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { removeLink } from 'roosterjs-content-model-api'; import { RemoveLinkButtonStringKey, RibbonButton } from 'roosterjs-react'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts index a115c24ae7f..ad786f118fb 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, setDirection } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton, RtlButtonStringKey } from 'roosterjs-react'; +import { setDirection } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts index b00646a4e9f..abbb0052cfe 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts @@ -1,6 +1,7 @@ import { BulletListType } from 'roosterjs-content-model-types'; -import { isContentModelEditor, setListStyle } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; +import { setListStyle } from 'roosterjs-content-model-api'; const dropDownMenuItems = { [BulletListType.Disc]: 'Disc', [BulletListType.Dash]: 'Dash', diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts index 03c44f0dbb7..2b9dd18e30d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setHeadingLevelButton.ts @@ -1,4 +1,5 @@ -import { isContentModelEditor, setHeadingLevel } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setHeadingLevel } from 'roosterjs-content-model-api'; import { getButtons, HeadingButtonStringKey, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts index b732f4f8761..87fe7369ef7 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts @@ -1,6 +1,7 @@ -import { isContentModelEditor, setListStyle } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { NumberingListType } from 'roosterjs-content-model-types'; import { RibbonButton } from 'roosterjs-react'; +import { setListStyle } from 'roosterjs-content-model-api'; const dropDownMenuItems = { [NumberingListType.Decimal]: 'Decimal', diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts index 0541618a119..f463e37a3f2 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts @@ -1,4 +1,5 @@ -import { isContentModelEditor, setTableCellShade } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setTableCellShade } from 'roosterjs-content-model-api'; import { BackgroundColorKeys, getBackgroundColorValue, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts index 557a5776baf..83795a3336b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts @@ -1,5 +1,6 @@ -import { formatTable, isContentModelEditor } from 'roosterjs-content-model-editor'; +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'; export const setTableHeaderButton: RibbonButton<'ribbonButtonSetTableHeader'> = { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts index 15cbd8e42a9..9dd219ddfa5 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts @@ -1,9 +1,6 @@ +import { getFormatState, setParagraphMargin } from 'roosterjs-content-model-api'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton } from 'roosterjs-react'; -import { - getFormatState, - isContentModelEditor, - setParagraphMargin, -} from 'roosterjs-content-model-editor'; const spaceAfterButtonKey = 'buttonNameSpaceAfter'; const spaceBeforeButtonKey = 'buttonNameSpaceBefore'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts index b1666224b0e..32f50c7eb6c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts @@ -1,4 +1,5 @@ -import { isContentModelEditor, setSpacing } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setSpacing } from 'roosterjs-content-model-api'; import type { RibbonButton } from 'roosterjs-react'; const SPACING_OPTIONS = ['1.0', '1.15', '1.5', '2.0']; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts index 49457b725d1..040752c5b5d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleStrikethrough } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton, StrikethroughButtonStringKey } from 'roosterjs-react'; +import { toggleStrikethrough } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts index 50529a5dac3..4490322311a 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleSubscript } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton, SubscriptButtonStringKey } from 'roosterjs-react'; +import { toggleSubscript } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts index f14c3d30a37..ad2c161e357 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleSuperscript } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton, SuperscriptButtonStringKey } from 'roosterjs-react'; +import { toggleSuperscript } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts index 67b0ffa649d..12552ed4c3d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableBorderApplyButton.ts @@ -1,16 +1,18 @@ import MainPaneBase from '../../MainPaneBase'; -import { applyTableBorderFormat, isContentModelEditor } from 'roosterjs-content-model-editor'; +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 = { - menuNameTableAllBorder: 'AllBorders', - menuNameTableNoBorder: 'NoBorders', - menuNameTableLeftBorder: 'LeftBorders', - menuNameTableRightBorder: 'RightBorders', - menuNameTableTopBorder: 'TopBorders', - menuNameTableBottomBorder: 'BottomBorders', - menuNameTableInsideBorder: 'InsideBorders', - menuNameTableOutsideBorder: 'OutsideBorders', +const TABLE_OPERATIONS: Record = { + menuNameTableAllBorder: 'allBorders', + menuNameTableNoBorder: 'noBorders', + menuNameTableLeftBorder: 'leftBorders', + menuNameTableRightBorder: 'rightBorders', + menuNameTableTopBorder: 'topBorders', + menuNameTableBottomBorder: 'bottomBorders', + menuNameTableInsideBorder: 'insideBorders', + menuNameTableOutsideBorder: 'outsideBorders', }; export const tableBorderApplyButton: RibbonButton<'ribbonButtonTableBorder'> = { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts index 851ae1d33e3..fdf7e39e4f1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts @@ -1,4 +1,6 @@ -import { editTable, isContentModelEditor, TableOperation } from 'roosterjs-content-model-editor'; +import { editTable } from 'roosterjs-content-model-api'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { TableOperation } from 'roosterjs-content-model-types'; import { RibbonButton, TableEditAlignMenuItemStringKey, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts index 0f9a8b8fbf1..168ad36c22c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts @@ -1,4 +1,5 @@ -import { isContentModelEditor, setTextColor } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; +import { setTextColor } from 'roosterjs-content-model-api'; import { getButtons, getTextColorValue, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts index 33a77e8e815..0abfc7ada87 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts @@ -1,5 +1,6 @@ -import { isContentModelEditor, toggleUnderline } from 'roosterjs-content-model-editor'; +import { isContentModelEditor } from 'roosterjs-content-model-editor'; import { RibbonButton, UnderlineButtonStringKey } from 'roosterjs-react'; +import { toggleUnderline } from 'roosterjs-content-model-api'; /** * @internal diff --git a/demo/scripts/controls/sidePane/contentModelApiPlayground/insertEntity/InsertEntityPane.tsx b/demo/scripts/controls/sidePane/contentModelApiPlayground/insertEntity/InsertEntityPane.tsx index 5fbebf4588d..47bba88757b 100644 --- a/demo/scripts/controls/sidePane/contentModelApiPlayground/insertEntity/InsertEntityPane.tsx +++ b/demo/scripts/controls/sidePane/contentModelApiPlayground/insertEntity/InsertEntityPane.tsx @@ -2,12 +2,10 @@ import * as React from 'react'; import ApiPaneProps from '../ApiPaneProps'; import { Entity } from 'roosterjs-editor-types'; import { getEntityFromElement, getEntitySelector } from 'roosterjs-editor-dom'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; +import { insertEntity } from 'roosterjs-content-model-api'; +import { InsertEntityOptions } from 'roosterjs-content-model-types'; import { trustedHTMLHandler } from '../../../../utils/trustedHTMLHandler'; -import { - IContentModelEditor, - insertEntity, - InsertEntityOptions, -} from 'roosterjs-content-model-editor'; const styles = require('./InsertEntityPane.scss'); diff --git a/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx b/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx index 1e552ff9937..b57357141c0 100644 --- a/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx +++ b/demo/scripts/controls/sidePane/eventViewer/ContentModelEventViewPane.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { ContentModelContentChangedEvent } from 'roosterjs-content-model-editor'; +import { ContentModelContentChangedEvent } from 'roosterjs-content-model-types'; import { EntityOperation, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { SidePaneElementProps } from '../SidePaneElement'; import { diff --git a/demo/scripts/controls/sidePane/formatState/ContentModelFormatStatePlugin.ts b/demo/scripts/controls/sidePane/formatState/ContentModelFormatStatePlugin.ts index 8028e309225..1cec23429bf 100644 --- a/demo/scripts/controls/sidePane/formatState/ContentModelFormatStatePlugin.ts +++ b/demo/scripts/controls/sidePane/formatState/ContentModelFormatStatePlugin.ts @@ -1,7 +1,8 @@ import FormatStatePlugin from './FormatStatePlugin'; import { FormatState } from 'roosterjs-editor-types'; -import { getFormatState, IContentModelEditor } from 'roosterjs-content-model-editor'; +import { getFormatState } from 'roosterjs-content-model-api'; import { getPositionRect } from 'roosterjs-editor-dom'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; export default class ContentModelFormatStatePlugin extends FormatStatePlugin { protected getFormatState() { diff --git a/demo/scripts/controls/titleBar/TitleBar.tsx b/demo/scripts/controls/titleBar/TitleBar.tsx index bf7437018c6..7f22a783018 100644 --- a/demo/scripts/controls/titleBar/TitleBar.tsx +++ b/demo/scripts/controls/titleBar/TitleBar.tsx @@ -7,26 +7,30 @@ const github = require('./iconmonstr-github-1.svg'); export interface TitleBarProps { className?: string; - isContentModelPane: boolean; + mode: 'classical' | 'contentModel'; } export default class TitleBar extends React.Component { render() { - const { isContentModelPane, className: baseClassName } = this.props; - const styles = isContentModelPane ? contentModelStyles : classicalStyles; + const { mode, className: baseClassName } = this.props; + const styles = mode == 'contentModel' ? contentModelStyles : classicalStyles; const className = styles.titleBar + ' ' + (baseClassName || ''); - const titleText = isContentModelPane - ? 'RoosterJs Content Model Demo Site' - : 'RoosterJs Demo Site'; - const switchLink = isContentModelPane ? ( - - Switch to classical demo - - ) : ( - - Switch to Content Model demo - - ); + const titleText = + mode == 'contentModel' + ? 'RoosterJs Content Model Demo Site' + : mode == 'classical' + ? 'RoosterJs Demo Site' + : 'RoosterJs Adapter Demo Site'; + const switchLink = + mode == 'contentModel' ? ( + + Switch to classical demo + + ) : ( + + Switch to Content Model demo + + ); return (
diff --git a/demo/scripts/tsconfig.json b/demo/scripts/tsconfig.json index 8fe669a06a5..be116dab8c8 100644 --- a/demo/scripts/tsconfig.json +++ b/demo/scripts/tsconfig.json @@ -37,12 +37,30 @@ "roosterjs-content-model-dom/lib/*": [ "packages-content-model/roosterjs-content-model-dom/lib/*" ], + "roosterjs-content-model-core": [ + "packages-content-model/roosterjs-content-model-core/lib/index" + ], + "roosterjs-content-model-core/lib/*": [ + "packages-content-model/roosterjs-content-model-core/lib/*" + ], + "roosterjs-content-model-api": [ + "packages-content-model/roosterjs-content-model-api/lib/index" + ], + "roosterjs-content-model-api/lib/*": [ + "packages-content-model/roosterjs-content-model-api/lib/*" + ], "roosterjs-content-model-editor": [ "packages-content-model/roosterjs-content-model-editor/lib/index" ], "roosterjs-content-model-editor/lib/*": [ "packages-content-model/roosterjs-content-model-editor/lib/*" ], + "roosterjs-content-model-plugins": [ + "packages-content-model/roosterjs-content-model-plugins/lib/index" + ], + "roosterjs-content-model-plugins/lib/*": [ + "packages-content-model/roosterjs-content-model-plugins/lib/*" + ], "roosterjs-react": ["packages-ui/roosterjs-react/lib/index"], "roosterjs-react/lib/*": ["packages-ui/roosterjs-react/lib/*"] } diff --git a/packages-content-model/roosterjs-content-model-api/lib/index.ts b/packages-content-model/roosterjs-content-model-api/lib/index.ts new file mode 100644 index 00000000000..1db5ab16114 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/index.ts @@ -0,0 +1,45 @@ +export { default as insertTable } from './publicApi/table/insertTable'; +export { default as formatTable } from './publicApi/table/formatTable'; +export { default as setTableCellShade } from './publicApi/table/setTableCellShade'; +export { default as editTable } from './publicApi/table/editTable'; +export { default as applyTableBorderFormat } from './publicApi/table/applyTableBorderFormat'; +export { default as toggleBullet } from './publicApi/list/toggleBullet'; +export { default as toggleNumbering } from './publicApi/list/toggleNumbering'; +export { default as toggleBold } from './publicApi/segment/toggleBold'; +export { default as toggleItalic } from './publicApi/segment/toggleItalic'; +export { default as toggleUnderline } from './publicApi/segment/toggleUnderline'; +export { default as toggleStrikethrough } from './publicApi/segment/toggleStrikethrough'; +export { default as toggleSubscript } from './publicApi/segment/toggleSubscript'; +export { default as toggleSuperscript } from './publicApi/segment/toggleSuperscript'; +export { default as setBackgroundColor } from './publicApi/segment/setBackgroundColor'; +export { default as setFontName } from './publicApi/segment/setFontName'; +export { default as setFontSize } from './publicApi/segment/setFontSize'; +export { default as setTextColor } from './publicApi/segment/setTextColor'; +export { default as changeFontSize } from './publicApi/segment/changeFontSize'; +export { default as applySegmentFormat } from './publicApi/segment/applySegmentFormat'; +export { default as changeCapitalization } from './publicApi/segment/changeCapitalization'; +export { default as insertImage } from './publicApi/image/insertImage'; +export { default as setListStyle } from './publicApi/list/setListStyle'; +export { default as setListStartNumber } from './publicApi/list/setListStartNumber'; +export { default as hasSelectionInBlock } from './publicApi/selection/hasSelectionInBlock'; +export { default as hasSelectionInSegment } from './publicApi/selection/hasSelectionInSegment'; +export { default as hasSelectionInBlockGroup } from './publicApi/selection/hasSelectionInBlockGroup'; +export { default as setIndentation } from './publicApi/block/setIndentation'; +export { default as setAlignment } from './publicApi/block/setAlignment'; +export { default as setDirection } from './publicApi/block/setDirection'; +export { default as setHeadingLevel } from './publicApi/block/setHeadingLevel'; +export { default as toggleBlockQuote } from './publicApi/block/toggleBlockQuote'; +export { default as setSpacing } from './publicApi/block/setSpacing'; +export { default as setImageBorder } from './publicApi/image/setImageBorder'; +export { default as setImageBoxShadow } from './publicApi/image/setImageBoxShadow'; +export { default as changeImage } from './publicApi/image/changeImage'; +export { default as getFormatState } from './publicApi/format/getFormatState'; +export { default as clearFormat } from './publicApi/format/clearFormat'; +export { default as insertLink } from './publicApi/link/insertLink'; +export { default as removeLink } from './publicApi/link/removeLink'; +export { default as adjustLinkSelection } from './publicApi/link/adjustLinkSelection'; +export { default as setImageAltText } from './publicApi/image/setImageAltText'; +export { default as adjustImageSelection } from './publicApi/image/adjustImageSelection'; +export { default as setParagraphMargin } from './publicApi/block/setParagraphMargin'; +export { default as toggleCode } from './publicApi/segment/toggleCode'; +export { default as insertEntity } from './publicApi/entity/insertEntity'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelAlignment.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts similarity index 85% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelAlignment.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts index 416f8dd6a57..7195148b4f1 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelAlignment.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelAlignment.ts @@ -1,7 +1,10 @@ import { alignTable } from '../table/alignTable'; -import { getOperationalBlocks } from '../selection/collectSelections'; -import type { TableAlignOperation } from '../../publicTypes/parameter/TableOperation'; -import type { ContentModelDocument, ContentModelListItem } from 'roosterjs-content-model-types'; +import { getOperationalBlocks } from 'roosterjs-content-model-core'; +import type { + ContentModelDocument, + ContentModelListItem, + TableAlignOperation, +} from 'roosterjs-content-model-types'; const ResultMap: Record< 'left' | 'center' | 'right', diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelDirection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelDirection.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts index f8d6f9497a2..3a984275972 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelDirection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts @@ -1,6 +1,5 @@ import { findListItemsInSameThread } from '../list/findListItemsInSameThread'; -import { getOperationalBlocks } from '../selection/collectSelections'; -import { isBlockGroupOfType } from '../common/isBlockGroupOfType'; +import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-core'; import type { ContentModelBlockFormat, ContentModelDocument, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelIndentation.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelIndentation.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts index 4f5b1776a2b..97fa93cc7a8 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/setModelIndentation.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -1,6 +1,5 @@ import { createListLevel, parseValueWithUnit } from 'roosterjs-content-model-dom'; -import { getOperationalBlocks } from '../selection/collectSelections'; -import { isBlockGroupOfType } from '../common/isBlockGroupOfType'; +import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-core'; import type { ContentModelDocument, ContentModelListItem, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/toggleModelBlockQuote.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/toggleModelBlockQuote.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts index 5bef7411040..f3f25a977f4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/toggleModelBlockQuote.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/block/toggleModelBlockQuote.ts @@ -1,8 +1,7 @@ import { areSameFormats, createFormatContainer, unwrapBlock } from 'roosterjs-content-model-dom'; -import { getOperationalBlocks } from '../selection/collectSelections'; -import { isBlockGroupOfType } from '../common/isBlockGroupOfType'; +import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-core'; import { wrapBlockStep1, wrapBlockStep2 } from '../common/wrapBlock'; -import type { OperationalBlocks } from '../selection/collectSelections'; +import type { OperationalBlocks } from 'roosterjs-content-model-core'; import type { WrapBlockStep1Result } from '../common/wrapBlock'; import type { ContentModelBlock, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/clearModelFormat.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/clearModelFormat.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts index 7228bf4ccf4..3cfdf9ce3f1 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/clearModelFormat.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts @@ -1,11 +1,12 @@ import { adjustWordSelection } from '../selection/adjustWordSelection'; -import { applyTableFormat } from '../table/applyTableFormat'; import { createFormatContainer } from 'roosterjs-content-model-dom'; -import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; -import { iterateSelections } from '../selection/iterateSelections'; -import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; -import { updateTableMetadata } from '../../domUtils/metadata/updateTableMetadata'; -import type { TableSelectionContext } from '../../publicTypes/selection/TableSelectionContext'; +import { + iterateSelections, + applyTableFormat, + getClosestAncestorBlockGroupIndex, + updateTableCellMetadata, + updateTableMetadata, +} from 'roosterjs-content-model-core'; import type { ContentModelBlock, ContentModelBlockGroup, @@ -16,6 +17,7 @@ import type { ContentModelSegmentFormat, ContentModelTable, Selectable, + TableSelectionContext, } from 'roosterjs-content-model-types'; /** @@ -28,7 +30,7 @@ export function clearModelFormat( tablesToClear: [ContentModelTable, boolean][] ) { iterateSelections( - [model], + model, (path, tableContext, block, segments) => { if (segments) { segmentsToClear.push(...segments); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/retrieveModelFormatState.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/retrieveModelFormatState.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts index f9eb9a7c448..ff557da027d 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/retrieveModelFormatState.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/retrieveModelFormatState.ts @@ -1,11 +1,12 @@ -import { extractBorderValues } from '../../domUtils/borderValues'; -import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; import { isBold } from '../../publicApi/segment/toggleBold'; -import { iterateSelections } from '../selection/iterateSelections'; -import { updateTableMetadata } from '../../domUtils/metadata/updateTableMetadata'; -import type { ContentModelFormatState } from '../../publicTypes/format/formatState/ContentModelFormatState'; -import type { TableSelectionContext } from '../../publicTypes/selection/TableSelectionContext'; +import { + extractBorderValues, + getClosestAncestorBlockGroupIndex, + iterateSelections, + updateTableMetadata, +} from 'roosterjs-content-model-core'; import type { + ContentModelFormatState, ContentModelBlock, ContentModelBlockGroup, ContentModelDocument, @@ -14,6 +15,7 @@ import type { ContentModelListItem, ContentModelParagraph, ContentModelSegmentFormat, + TableSelectionContext, } from 'roosterjs-content-model-types'; /** @@ -31,7 +33,7 @@ export function retrieveModelFormatState( let isFirstSegment = true; iterateSelections( - [model], + model, (path, tableContext, block, segments) => { // Structure formats retrieveStructureFormat(formatState, path, isFirst); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/wrapBlock.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/common/wrapBlock.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/wrapBlock.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/common/wrapBlock.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/readFile.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/domUtils/readFile.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/readFile.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/domUtils/readFile.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/entity/insertEntityModel.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/entity/insertEntityModel.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts index d6874fe26b0..5a52bfb959e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/entity/insertEntityModel.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/entity/insertEntityModel.ts @@ -1,22 +1,23 @@ -import { DeleteResult } from '../edit/utils/DeleteSelectionStep'; -import { deleteSelection } from '../edit/deleteSelection'; -import { getClosestAncestorBlockGroupIndex } from '../common/getClosestAncestorBlockGroupIndex'; -import { setSelection } from '../selection/setSelection'; +import { + deleteSelection, + getClosestAncestorBlockGroupIndex, + setSelection, +} from 'roosterjs-content-model-core'; import { createBr, createParagraph, createSelectionMarker, normalizeContentModel, } from 'roosterjs-content-model-dom'; -import type { DeleteSelectionResult } from '../edit/utils/DeleteSelectionStep'; -import type { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext'; -import type { InsertEntityPosition } from '../../publicTypes/parameter/InsertEntityOptions'; import type { ContentModelBlock, ContentModelBlockGroup, ContentModelDocument, ContentModelEntity, ContentModelParagraph, + DeleteSelectionResult, + FormatWithContentModelContext, + InsertEntityPosition, } from 'roosterjs-content-model-types'; /** @@ -40,7 +41,7 @@ export function insertEntityModel( } else if ((deleteResult = deleteSelection(model, [], context)).insertPoint) { const { marker, paragraph, path } = deleteResult.insertPoint; - if (deleteResult.deleteResult == DeleteResult.Range) { + if (deleteResult.deleteResult == 'range') { normalizeContentModel(model); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/image/applyImageBorderFormat.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/image/applyImageBorderFormat.ts similarity index 89% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/image/applyImageBorderFormat.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/image/applyImageBorderFormat.ts index d2d8aa8abb5..6d7ef1a5898 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/image/applyImageBorderFormat.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/image/applyImageBorderFormat.ts @@ -1,7 +1,6 @@ -import { extractBorderValues } from '../../domUtils/borderValues'; +import { extractBorderValues } from 'roosterjs-content-model-core'; import { parseValueWithUnit } from 'roosterjs-content-model-dom'; -import type { Border } from '../../publicTypes/interface/Border'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; +import type { Border, ContentModelImage } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/list/findListItemsInSameThread.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/list/findListItemsInSameThread.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/list/findListItemsInSameThread.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/list/setListType.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/list/setListType.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/list/setListType.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/list/setListType.ts index f29470f2ec3..6e83bedbf44 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/list/setListType.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/list/setListType.ts @@ -1,5 +1,4 @@ -import { getOperationalBlocks } from '../selection/collectSelections'; -import { isBlockGroupOfType } from '../common/isBlockGroupOfType'; +import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-core'; import { createListItem, createListLevel, @@ -60,6 +59,8 @@ export function setListType(model: ContentModelDocument, listType: 'OL' | 'UL') direction: block.format.direction, textAlign: block.format.textAlign, marginTop: hasIgnoredParagraphBefore ? '0' : undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }), ], // For list bullet, we only want to carry over these formats from segments: diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/adjustSegmentSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/adjustSegmentSelection.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts index 1af7eb1ccbc..40a73ae3468 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/adjustSegmentSelection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustSegmentSelection.ts @@ -1,5 +1,4 @@ -import { getSelectedParagraphs } from './collectSelections'; -import { setSelection } from './setSelection'; +import { getSelectedParagraphs, setSelection } from 'roosterjs-content-model-core'; import type { ContentModelDocument, ContentModelSegment } from 'roosterjs-content-model-types'; /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/adjustWordSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/adjustWordSelection.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts index 0a2035f99b8..a80dbf0c3b1 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/adjustWordSelection.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/adjustWordSelection.ts @@ -1,6 +1,5 @@ import { createText } from 'roosterjs-content-model-dom'; -import { isPunctuation, isSpace } from '../../domUtils/stringUtil'; -import { iterateSelections } from '../../modelApi/selection/iterateSelections'; +import { isPunctuation, isSpace, iterateSelections } from 'roosterjs-content-model-core'; import type { ContentModelDocument, ContentModelParagraph, @@ -17,7 +16,7 @@ export function adjustWordSelection( ): ContentModelSegment[] { let markerBlock: ContentModelParagraph | undefined; - iterateSelections([model], (path, tableContext, block, segments) => { + iterateSelections(model, (_, __, block, segments) => { //Find the block with the selection marker if (block?.blockType == 'Paragraph' && segments?.length == 1 && segments[0] == marker) { markerBlock = block; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collapseTableSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/collapseTableSelection.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collapseTableSelection.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/selection/collapseTableSelection.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/alignTable.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts similarity index 64% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/alignTable.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts index ccf78d77a4a..1d8be6d468e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/alignTable.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/table/alignTable.ts @@ -1,5 +1,4 @@ -import type { TableAlignOperation } from '../../publicTypes/parameter/TableOperation'; -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import type { ContentModelTable, TableAlignOperation } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/alignTableCell.ts b/packages-content-model/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts similarity index 89% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/alignTableCell.ts rename to packages-content-model/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts index 93b972ef13e..296ccb034d5 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/alignTableCell.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/modelApi/table/alignTableCell.ts @@ -1,10 +1,11 @@ import { getSelectedCells } from './getSelectedCells'; -import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; +import { updateTableCellMetadata } from 'roosterjs-content-model-core'; import type { + ContentModelTable, + ContentModelTableCell, TableCellHorizontalAlignOperation, TableCellVerticalAlignOperation, -} from '../../publicTypes/parameter/TableOperation'; -import type { ContentModelTable, ContentModelTableCell } from 'roosterjs-content-model-types'; +} from 'roosterjs-content-model-types'; const TextAlignValueMap: Partial setModelAlignment(model, alignment)); + editor.formatContentModel(model => setModelAlignment(model, alignment), { + apiName: 'setAlignment', + }); } diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setDirection.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setDirection.ts new file mode 100644 index 00000000000..2ec3396557a --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setDirection.ts @@ -0,0 +1,15 @@ +import { setModelDirection } from '../../modelApi/block/setModelDirection'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Set text direction of selected paragraphs (Left to right or Right to left) + * @param editor The editor to set alignment + * @param direction Direction value: ltr (Left to right) or rtl (Right to left) + */ +export default function setDirection(editor: IStandaloneEditor, direction: 'ltr' | 'rtl') { + editor.focus(); + + editor.formatContentModel(model => setModelDirection(model, direction), { + apiName: 'setDirection', + }); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setHeadingLevel.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setHeadingLevel.ts similarity index 90% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setHeadingLevel.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setHeadingLevel.ts index 0a172bb41d1..d77e53f82d6 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setHeadingLevel.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setHeadingLevel.ts @@ -1,6 +1,8 @@ import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; -import type { ContentModelParagraphDecorator } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { + ContentModelParagraphDecorator, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; type HeadingLevelTags = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; @@ -19,7 +21,7 @@ const HeaderFontSizes: Record = { * @param headingLevel Level of heading, from 1 to 6. Set to 0 means set it back to a regular paragraph */ export default function setHeadingLevel( - editor: IContentModelEditor, + editor: IStandaloneEditor, headingLevel: 0 | 1 | 2 | 3 | 4 | 5 | 6 ) { editor.focus(); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setIndentation.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setIndentation.ts similarity index 69% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setIndentation.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setIndentation.ts index 6c01898cec3..24266a90f78 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setIndentation.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setIndentation.ts @@ -1,7 +1,6 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; import { normalizeContentModel } from 'roosterjs-content-model-dom'; import { setModelIndentation } from '../../modelApi/block/setModelIndentation'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Indent or outdent to selected paragraphs @@ -10,26 +9,26 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param length The length of pixel to indent/outdent @default 40 */ export default function setIndentation( - editor: IContentModelEditor, + editor: IStandaloneEditor, indentation: 'indent' | 'outdent', length?: number ) { editor.focus(); - formatWithContentModel( - editor, - 'setIndentation', - model => { + editor.formatContentModel( + (model, context) => { const result = setModelIndentation(model, indentation, length); if (result) { normalizeContentModel(model); } + context.newPendingFormat = 'preserve'; + return result; }, { - preservePendingFormat: true, + apiName: 'setIndentation', } ); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setParagraphMargin.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setParagraphMargin.ts similarity index 90% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setParagraphMargin.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setParagraphMargin.ts index 903f8cae4e9..a2f85af4d5f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setParagraphMargin.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setParagraphMargin.ts @@ -1,6 +1,6 @@ import { createParagraphDecorator } from 'roosterjs-content-model-dom'; import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggles the current block(s) margin properties. @@ -10,7 +10,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param marginBottom value for bottom margin */ export default function setParagraphMargin( - editor: IContentModelEditor, + editor: IStandaloneEditor, marginTop?: string | null, marginBottom?: string | null ) { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setSpacing.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setSpacing.ts similarity index 78% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setSpacing.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setSpacing.ts index f5c36d46448..dd204e6d253 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setSpacing.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/setSpacing.ts @@ -1,12 +1,12 @@ import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Sets current selected block(s) line-height property and wipes such property from child segments * @param editor The editor to operate on * @param spacing Unitless/px value to set line height */ -export default function setSpacing(editor: IContentModelEditor, spacing: number | string) { +export default function setSpacing(editor: IStandaloneEditor, spacing: number | string) { editor.focus(); formatParagraphWithContentModel(editor, 'setSpacing', paragraph => { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/toggleBlockQuote.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/toggleBlockQuote.ts similarity index 69% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/toggleBlockQuote.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/block/toggleBlockQuote.ts index d3f4a69c6ef..ec3ac86d7a5 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/toggleBlockQuote.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/block/toggleBlockQuote.ts @@ -1,7 +1,8 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; import { toggleModelBlockQuote } from '../../modelApi/block/toggleModelBlockQuote'; -import type { ContentModelFormatContainerFormat } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { + ContentModelFormatContainerFormat, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; const DefaultQuoteFormat: ContentModelFormatContainerFormat = { borderLeft: '3px solid rgb(200, 200, 200)', // TODO: Support RTL @@ -23,7 +24,7 @@ const BuildInQuoteFormat: ContentModelFormatContainerFormat = { * @param quoteFormat @optional Block format for the new quote object */ export default function toggleBlockQuote( - editor: IContentModelEditor, + editor: IStandaloneEditor, quoteFormat: ContentModelFormatContainerFormat = DefaultQuoteFormat ) { const fullQuoteFormat = { @@ -33,12 +34,14 @@ export default function toggleBlockQuote( editor.focus(); - formatWithContentModel( - editor, - 'toggleBlockQuote', - model => toggleModelBlockQuote(model, fullQuoteFormat), + editor.formatContentModel( + (model, context) => { + context.newPendingFormat = 'preserve'; + + return toggleModelBlockQuote(model, fullQuoteFormat); + }, { - preservePendingFormat: true, + apiName: 'toggleBlockQuote', } ); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts similarity index 86% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts index 79415d37529..7b7b4701d5d 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts @@ -1,14 +1,14 @@ -import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent'; +import { ChangeSource } from 'roosterjs-content-model-core'; import { createEntity, normalizeContentModel } from 'roosterjs-content-model-dom'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; import { insertEntityModel } from '../../modelApi/entity/insertEntityModel'; -import type { ContentModelEntity, DOMSelection } from 'roosterjs-content-model-types'; -import type { Entity } from 'roosterjs-editor-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import type { - InsertEntityOptions, + ContentModelEntity, + DOMSelection, InsertEntityPosition, -} from '../../publicTypes/parameter/InsertEntityOptions'; + InsertEntityOptions, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; +import type { Entity } from 'roosterjs-editor-types'; const BlockEntityTag = 'div'; const InlineEntityTag = 'span'; @@ -25,7 +25,7 @@ const InlineEntityTag = 'span'; * @param options Move options to insert. See InsertEntityOptions */ export default function insertEntity( - editor: IContentModelEditor, + editor: IStandaloneEditor, type: string, isBlock: boolean, position: 'focus' | 'begin' | 'end' | DOMSelection, @@ -44,7 +44,7 @@ export default function insertEntity( * @param options Move options to insert. See InsertEntityOptions */ export default function insertEntity( - editor: IContentModelEditor, + editor: IStandaloneEditor, type: string, isBlock: true, position: InsertEntityPosition | DOMSelection, @@ -52,7 +52,7 @@ export default function insertEntity( ): ContentModelEntity | null; export default function insertEntity( - editor: IContentModelEditor, + editor: IStandaloneEditor, type: string, isBlock: boolean, position?: InsertEntityPosition | DOMSelection, @@ -70,9 +70,7 @@ export default function insertEntity( const entityModel = createEntity(wrapper, true /*isReadonly*/, undefined /*format*/, type); - formatWithContentModel( - editor, - 'insertEntity', + editor.formatContentModel( (model, context) => { insertEntityModel( model, @@ -104,6 +102,7 @@ export default function insertEntity( return entity; }, + apiName: 'insertEntity', } ); diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts new file mode 100644 index 00000000000..a890b26dc60 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts @@ -0,0 +1,36 @@ +import { clearModelFormat } from '../../modelApi/common/clearModelFormat'; +import { normalizeContentModel } from 'roosterjs-content-model-dom'; +import type { + IStandaloneEditor, + ContentModelBlock, + ContentModelBlockGroup, + ContentModelSegment, + ContentModelTable, +} from 'roosterjs-content-model-types'; + +/** + * Clear format of selection + * @param editor The editor to clear format from + */ +export default function clearFormat(editor: IStandaloneEditor) { + editor.focus(); + + editor.formatContentModel( + model => { + const blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][] = []; + const segmentsToClear: ContentModelSegment[] = []; + const tablesToClear: [ContentModelTable, boolean][] = []; + + clearModelFormat(model, blocksToClear, segmentsToClear, tablesToClear); + + normalizeContentModel(model); + + return ( + blocksToClear.length > 0 || segmentsToClear.length > 0 || tablesToClear.length > 0 + ); + }, + { + apiName: 'clearFormat', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/format/getFormatState.ts similarity index 58% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/format/getFormatState.ts index 4e2b4b64cc4..188c656e011 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/getFormatState.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/format/getFormatState.ts @@ -1,9 +1,12 @@ -import { getPendingFormat } from '../../modelApi/format/pendingFormat'; -import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode'; +import { getSelectionRootNode } from 'roosterjs-content-model-core'; import { retrieveModelFormatState } from '../../modelApi/common/retrieveModelFormatState'; -import type { ContentModelBlockGroup, DomToModelContext } from 'roosterjs-content-model-types'; -import type { ContentModelFormatState } from '../../publicTypes/format/formatState/ContentModelFormatState'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { + IStandaloneEditor, + ContentModelBlockGroup, + ContentModelFormatState, + DomToModelContext, +} from 'roosterjs-content-model-types'; + import { getRegularSelectionOffsets, handleRegularSelection, @@ -15,8 +18,8 @@ import { * Get current format state * @param editor The editor to get format from */ -export default function getFormatState(editor: IContentModelEditor): ContentModelFormatState { - const pendingFormat = getPendingFormat(editor); +export default function getFormatState(editor: IStandaloneEditor): ContentModelFormatState { + const pendingFormat = editor.getPendingFormat(); const model = editor.createContentModel({ processorOverride: { child: reducedModelChildProcessor, @@ -57,37 +60,34 @@ export function reducedModelChildProcessor( parent: ParentNode, context: FormatStateContext ) { - const selectionRootNode = getSelectionRootNode(context.selection); - - if (selectionRootNode) { - if (!context.nodeStack) { - context.nodeStack = createNodeStack(parent, selectionRootNode); - } + if (!context.nodeStack) { + const selectionRootNode = getSelectionRootNode(context.selection); + context.nodeStack = selectionRootNode ? createNodeStack(parent, selectionRootNode) : []; + } - const stackChild = context.nodeStack.pop(); + const stackChild = context.nodeStack.pop(); - if (stackChild) { - const [nodeStartOffset, nodeEndOffset] = getRegularSelectionOffsets(context, parent); + if (stackChild) { + const [nodeStartOffset, nodeEndOffset] = getRegularSelectionOffsets(context, parent); - // If selection is not on this node, skip getting node index to save some time since we don't need it here - const index = - nodeStartOffset >= 0 || nodeEndOffset >= 0 ? getChildIndex(parent, stackChild) : -1; + // If selection is not on this node, skip getting node index to save some time since we don't need it here + const index = + nodeStartOffset >= 0 || nodeEndOffset >= 0 ? getChildIndex(parent, stackChild) : -1; - if (index >= 0) { - handleRegularSelection(index, context, group, nodeStartOffset, nodeEndOffset); - } + if (index >= 0) { + handleRegularSelection(index, context, group, nodeStartOffset, nodeEndOffset); + } - processChildNode(group, stackChild, context); + processChildNode(group, stackChild, context); - if (index >= 0) { - handleRegularSelection(index + 1, context, group, nodeStartOffset, nodeEndOffset); - } - } else { - // No child node from node stack, that means we have reached the deepest node of selection. - // Now we can use default child processor to perform full sub tree scanning for content model, - // So that all selected node will be included. - context.defaultElementProcessors.child(group, parent, context); + if (index >= 0) { + handleRegularSelection(index + 1, context, group, nodeStartOffset, nodeEndOffset); } + } else { + // No child node from node stack, that means we have reached the deepest node of selection. + // Now we can use default child processor to perform full sub tree scanning for content model, + // So that all selected node will be included. + context.defaultElementProcessors.child(group, parent, context); } } diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts new file mode 100644 index 00000000000..83e7e76c6a1 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/adjustImageSelection.ts @@ -0,0 +1,31 @@ +import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; +import type { ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Adjust selection to make sure select an image if any + * @return Content Model Image object if an image is select, or null + */ +export default function adjustImageSelection(editor: IStandaloneEditor): ContentModelImage | null { + let image: ContentModelImage | null = null; + + editor.formatContentModel( + model => + adjustSegmentSelection( + model, + target => { + if (target.isSelected && target.segmentType == 'Image') { + image = target; + return true; + } else { + return false; + } + }, + (target, ref) => target == ref + ), + { + apiName: 'adjustImageSelection', + } + ); + + return image; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts similarity index 75% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts index 20187fd008b..ff9e109db3e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/changeImage.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/changeImage.ts @@ -1,16 +1,15 @@ import formatImageWithContentModel from '../utils/formatImageWithContentModel'; import { PluginEventType } from 'roosterjs-editor-types'; -import { readFile } from '../../domUtils/readFile'; -import { updateImageMetadata } from '../../domUtils/metadata/updateImageMetadata'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { readFile } from '../../modelApi/domUtils/readFile'; +import { updateImageMetadata } from 'roosterjs-content-model-core'; +import type { ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Change the selected image src * @param editor The editor instance * @param file The image file */ -export default function changeImage(editor: IContentModelEditor, file: File) { +export default function changeImage(editor: IStandaloneEditor, file: File) { editor.focus(); const selection = editor.getDOMSelection(); diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/insertImage.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/insertImage.ts new file mode 100644 index 00000000000..d1ad6da48c6 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/insertImage.ts @@ -0,0 +1,42 @@ +import { addSegment, createContentModelDocument, createImage } from 'roosterjs-content-model-dom'; +import { mergeModel } from 'roosterjs-content-model-core'; +import { readFile } from '../../modelApi/domUtils/readFile'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Insert an image into current selected position + * @param editor The editor to operate on + * @param file Image Blob file or source string + */ +export default function insertImage(editor: IStandaloneEditor, imageFileOrSrc: File | string) { + editor.focus(); + + if (typeof imageFileOrSrc == 'string') { + insertImageWithSrc(editor, imageFileOrSrc); + } else { + readFile(imageFileOrSrc, dataUrl => { + if (dataUrl && !editor.isDisposed()) { + insertImageWithSrc(editor, dataUrl); + } + }); + } +} + +function insertImageWithSrc(editor: IStandaloneEditor, src: string) { + editor.formatContentModel( + (model, context) => { + const image = createImage(src, { backgroundColor: '' }); + const doc = createContentModelDocument(); + + addSegment(doc, image); + mergeModel(model, doc, context, { + mergeFormat: 'mergeAll', + }); + + return true; + }, + { + apiName: 'insertImage', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageAltText.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageAltText.ts similarity index 63% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageAltText.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageAltText.ts index fc40ca1c4ad..c21995e2d3f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageAltText.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageAltText.ts @@ -1,6 +1,5 @@ import formatImageWithContentModel from '../utils/formatImageWithContentModel'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Set image alt text for all selected images at selection. If no images is contained @@ -8,7 +7,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param editor The editor instance * @param altText The image alt text */ -export default function setImageAltText(editor: IContentModelEditor, altText: string) { +export default function setImageAltText(editor: IStandaloneEditor, altText: string) { editor.focus(); formatImageWithContentModel(editor, 'setImageAltText', (image: ContentModelImage) => { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageBorder.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageBorder.ts similarity index 77% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageBorder.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageBorder.ts index 9774b47a1e6..71b2d5a0306 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageBorder.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageBorder.ts @@ -1,8 +1,6 @@ import applyImageBorderFormat from '../../modelApi/image/applyImageBorderFormat'; import formatImageWithContentModel from '../utils/formatImageWithContentModel'; -import type { Border } from '../../publicTypes/interface/Border'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { Border, ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Set image border style for all selected images at selection. @@ -12,7 +10,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param borderRadius the border radius value, if undefined, the border radius will keep the actual value */ export default function setImageBorder( - editor: IContentModelEditor, + editor: IStandaloneEditor, border: Border | null, borderRadius?: string ) { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageBoxShadow.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageBoxShadow.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageBoxShadow.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageBoxShadow.ts index 014221dce4c..2f50789079b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/setImageBoxShadow.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/image/setImageBoxShadow.ts @@ -1,6 +1,5 @@ import formatImageWithContentModel from '../utils/formatImageWithContentModel'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Set image box shadow for all selected images at selection. @@ -9,7 +8,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param margin The image margin for all sides (eg. "4px"), null to remove margin */ export default function setImageBoxShadow( - editor: IContentModelEditor, + editor: IStandaloneEditor, boxShadow: string, margin?: string | null ) { diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts new file mode 100644 index 00000000000..c0e1dc63610 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/adjustLinkSelection.ts @@ -0,0 +1,44 @@ +import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; +import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; +import { getSelectedSegments, setSelection } from 'roosterjs-content-model-core'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Adjust selection to make sure select a hyperlink if any, or a word if original selection is collapsed + * @return A combination of existing link display text and url if any. If there is no existing link, return selected text and null + */ +export default function adjustLinkSelection(editor: IStandaloneEditor): [string, string | null] { + let text = ''; + let url: string | null = null; + + editor.formatContentModel( + model => { + let changed = adjustSegmentSelection( + model, + target => !!target.isSelected && !!target.link, + (target, ref) => !!target.link && target.link.format.href == ref.link!.format.href + ); + let segments = getSelectedSegments(model, false /*includingFormatHolder*/); + const firstSegment = segments[0]; + + if (segments.length == 1 && firstSegment.segmentType == 'SelectionMarker') { + segments = adjustWordSelection(model, firstSegment); + + if (segments.length > 1) { + changed = true; + setSelection(model, segments[0], segments[segments.length - 1]); + } + } + + text = segments.map(x => (x.segmentType == 'Text' ? x.text : '')).join(''); + url = segments[0]?.link?.format.href || null; + + return changed; + }, + { + apiName: 'adjustLinkSelection', + } + ); + + return [text, url]; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/insertLink.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/insertLink.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts index fba7dad76fe..3d29e419aae 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/insertLink.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/insertLink.ts @@ -1,11 +1,6 @@ -import getSelectedSegments from '../selection/getSelectedSegments'; -import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getPendingFormat } from '../../modelApi/format/pendingFormat'; +import { ChangeSource, getSelectedSegments, mergeModel } from 'roosterjs-content-model-core'; import { HtmlSanitizer, matchLink } from 'roosterjs-editor-dom'; -import { mergeModel } from '../../modelApi/common/mergeModel'; -import type { ContentModelLink } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { ContentModelLink, IStandaloneEditor } from 'roosterjs-content-model-types'; import { addLink, addSegment, @@ -34,7 +29,7 @@ const FTP_REGEX = /^ftp\./i; * If not specified and there wasn't a link, the link url will be used as display text. */ export default function insertLink( - editor: IContentModelEditor, + editor: IStandaloneEditor, link: string, anchorTitle?: string, displayText?: string, @@ -49,9 +44,7 @@ export default function insertLink( const links: ContentModelLink[] = []; let anchorNode: Node | undefined; - formatWithContentModel( - editor, - 'insertLink', + editor.formatContentModel( (model, context) => { const segments = getSelectedSegments(model, false /*includingFormatHolder*/); const originalText = segments @@ -80,8 +73,8 @@ export default function insertLink( (!!text && text != originalText) ) { const segment = createText(text || (linkData ? linkData.originalUrl : url), { - ...(segments[0]?.format || {}), - ...(getPendingFormat(editor) || {}), + ...segments[0]?.format, + ...editor.getPendingFormat(), }); const doc = createContentModelDocument(); const link = createLink(linkUrl, anchorTitle, target); @@ -108,6 +101,7 @@ export default function insertLink( } }, getChangeData: () => anchorNode, + apiName: 'insertLink', } ); } diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts new file mode 100644 index 00000000000..b6c7f38abb6 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/link/removeLink.ts @@ -0,0 +1,41 @@ +import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; +import { getSelectedSegments } from 'roosterjs-content-model-core'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Remove link at selection. If no links at selection, do nothing. + * If selection contains multiple links, all of the link styles will be removed. + * If only part of a link is selected, the whole link style will be removed. + * @param editor The editor instance + */ +export default function removeLink(editor: IStandaloneEditor) { + editor.focus(); + + editor.formatContentModel( + model => { + adjustSegmentSelection( + model, + target => !!target.isSelected && !!target.link, + (target, ref) => + target.isSelected || // Expand the selection to any link that is involved. So we can remove multiple links together + (!!target.link && target.link.format.href == ref.link!.format.href) + ); + + const segments = getSelectedSegments(model, false /*includingFormatHolder*/); + let isChanged = false; + + segments.forEach(segment => { + if (segment.link) { + isChanged = true; + + delete segment.link; + } + }); + + return isChanged; + }, + { + apiName: 'removeLink', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStartNumber.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStartNumber.ts new file mode 100644 index 00000000000..1ce023a263a --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStartNumber.ts @@ -0,0 +1,29 @@ +import { getFirstSelectedListItem } from 'roosterjs-content-model-core'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Set start number of a list item + * @param editor The editor to operate on + * @param value The number to set to, must be equal or greater than 1 + */ +export default function setListStartNumber(editor: IStandaloneEditor, value: number) { + editor.focus(); + + editor.formatContentModel( + model => { + const listItem = getFirstSelectedListItem(model); + const level = listItem?.levels[listItem?.levels.length - 1]; + + if (level) { + level.format.startNumberOverride = value; + + return true; + } else { + return false; + } + }, + { + apiName: 'setListStartNumber', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStyle.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStyle.ts new file mode 100644 index 00000000000..1a4c075e19f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/setListStyle.ts @@ -0,0 +1,38 @@ +import { findListItemsInSameThread } from '../../modelApi/list/findListItemsInSameThread'; +import { getFirstSelectedListItem, updateListMetadata } from 'roosterjs-content-model-core'; +import type { IStandaloneEditor, ListMetadataFormat } from 'roosterjs-content-model-types'; + +/** + * Set style of list items with in same thread of current item + * @param editor The editor to operate on + * @param style The target list item style to set + */ +export default function setListStyle(editor: IStandaloneEditor, style: ListMetadataFormat) { + editor.focus(); + + editor.formatContentModel( + model => { + const listItem = getFirstSelectedListItem(model); + + if (listItem) { + const listItems = findListItemsInSameThread(model, listItem); + const levelIndex = listItem.levels.length - 1; + + listItems.forEach(listItem => { + const level = listItem.levels[levelIndex]; + + if (level) { + updateListMetadata(level, metadata => Object.assign({}, metadata, style)); + } + }); + + return true; + } else { + return false; + } + }, + { + apiName: 'setListStyle', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleBullet.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleBullet.ts new file mode 100644 index 00000000000..7729eade0c6 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleBullet.ts @@ -0,0 +1,23 @@ +import { setListType } from '../../modelApi/list/setListType'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Toggle bullet list type + * - When there are some blocks not in bullet list, set all blocks to the given type + * - When all blocks are already in bullet list, turn off / outdent there list type + * @param editor The editor to operate on + */ +export default function toggleBullet(editor: IStandaloneEditor) { + editor.focus(); + + editor.formatContentModel( + (model, context) => { + context.newPendingFormat = 'preserve'; + + return setListType(model, 'UL'); + }, + { + apiName: 'toggleBullet', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleNumbering.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleNumbering.ts new file mode 100644 index 00000000000..b98c6df7b2f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/list/toggleNumbering.ts @@ -0,0 +1,23 @@ +import { setListType } from '../../modelApi/list/setListType'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Toggle numbering list type + * - When there are some blocks not in numbering list, set all blocks to the given type + * - When all blocks are already in numbering list, turn off / outdent there list type + * @param editor The editor to operate on + */ +export default function toggleNumbering(editor: IStandaloneEditor) { + editor.focus(); + + editor.formatContentModel( + (model, context) => { + context.newPendingFormat = 'preserve'; + + return setListType(model, 'OL'); + }, + { + apiName: 'toggleNumbering', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/applySegmentFormat.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/applySegmentFormat.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts index d4efff86879..a3cf335538a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/applySegmentFormat.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/applySegmentFormat.ts @@ -1,6 +1,5 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { ContentModelSegmentFormat, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Bulk apply segment format to all selected content. This is usually used for format painter. @@ -8,7 +7,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param newFormat The segment format to apply */ export default function applySegmentFormat( - editor: IContentModelEditor, + editor: IStandaloneEditor, newFormat: ContentModelSegmentFormat ) { formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/changeCapitalization.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/changeCapitalization.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/changeCapitalization.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/changeCapitalization.ts index e0f11220813..207efe20aae 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/changeCapitalization.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/changeCapitalization.ts @@ -1,5 +1,5 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Change the capitalization of text in the selection @@ -10,7 +10,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * Default is the host environment’s current locale. */ export default function changeCapitalization( - editor: IContentModelEditor, + editor: IStandaloneEditor, capitalization: 'sentence' | 'lowerCase' | 'upperCase' | 'capitalize', language?: string ) { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/changeFontSize.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/changeFontSize.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts index 1d9481c4f27..fcfba224fd0 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/changeFontSize.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/changeFontSize.ts @@ -4,8 +4,8 @@ import { setFontSizeInternal } from './setFontSize'; import type { ContentModelParagraph, ContentModelSegmentFormat, + IStandaloneEditor, } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Default font size sequence, in pt. Suggest editor UI use this sequence as your font size list, @@ -21,10 +21,7 @@ const MAX_FONT_SIZE = 1000; * @param change Whether increase or decrease font size * @param fontSizes A sorted font size array, in pt. Default value is FONT_SIZES */ -export default function changeFontSize( - editor: IContentModelEditor, - change: 'increase' | 'decrease' -) { +export default function changeFontSize(editor: IStandaloneEditor, change: 'increase' | 'decrease') { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setBackgroundColor.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts similarity index 85% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setBackgroundColor.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts index 3f8e769b69d..099c2d82ac6 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setBackgroundColor.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setBackgroundColor.ts @@ -1,8 +1,7 @@ import { createSelectionMarker } from 'roosterjs-content-model-dom'; import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { setSelection } from '../../modelApi/selection/setSelection'; -import type { ContentModelParagraph } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { setSelection } from 'roosterjs-content-model-core'; +import type { ContentModelParagraph, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Set background color @@ -10,7 +9,7 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param backgroundColor The color to set. Pass null to remove existing color. */ export default function setBackgroundColor( - editor: IContentModelEditor, + editor: IStandaloneEditor, backgroundColor: string | null ) { editor.focus(); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setFontName.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setFontName.ts similarity index 77% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setFontName.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setFontName.ts index 64b8014f39f..bef80eaed7e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setFontName.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setFontName.ts @@ -1,12 +1,12 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Set font name * @param editor The editor to operate on * @param fontName The font name to set */ -export default function setFontName(editor: IContentModelEditor, fontName: string) { +export default function setFontName(editor: IStandaloneEditor, fontName: string) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setFontSize.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setFontSize.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts index e958825158f..f39104ecd1a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setFontSize.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setFontSize.ts @@ -2,15 +2,15 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContent import type { ContentModelParagraph, ContentModelSegmentFormat, + IStandaloneEditor, } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Set font size * @param editor The editor to operate on * @param fontSize The font size to set */ -export default function setFontSize(editor: IContentModelEditor, fontSize: string) { +export default function setFontSize(editor: IStandaloneEditor, fontSize: string) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setTextColor.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setTextColor.ts similarity index 83% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setTextColor.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setTextColor.ts index 60d51fcaae3..edf70181d89 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/setTextColor.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/setTextColor.ts @@ -1,12 +1,12 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Set text color * @param editor The editor to operate on * @param textColor The text color to set. Pass null to remove existing color. */ -export default function setTextColor(editor: IContentModelEditor, textColor: string | null) { +export default function setTextColor(editor: IStandaloneEditor, textColor: string | null) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleBold.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleBold.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleBold.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleBold.ts index f31e6a73981..f20eae3930a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleBold.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleBold.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggle bold style * @param editor The editor to operate on */ -export default function toggleBold(editor: IContentModelEditor) { +export default function toggleBold(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleCode.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleCode.ts similarity index 76% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleCode.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleCode.ts index 05a9d49f62f..4269bcd762a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleCode.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleCode.ts @@ -1,7 +1,6 @@ import { addCode } from 'roosterjs-content-model-dom'; import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { ContentModelCode } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { ContentModelCode, IStandaloneEditor } from 'roosterjs-content-model-types'; const DefaultCode: ContentModelCode = { format: { @@ -13,7 +12,7 @@ const DefaultCode: ContentModelCode = { * Toggle italic style * @param editor The editor to operate on */ -export default function toggleCode(editor: IContentModelEditor) { +export default function toggleCode(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleItalic.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleItalic.ts similarity index 72% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleItalic.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleItalic.ts index 70a8d718153..b3c25de6524 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleItalic.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleItalic.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggle italic style * @param editor The editor to operate on */ -export default function toggleItalic(editor: IContentModelEditor) { +export default function toggleItalic(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleStrikethrough.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleStrikethrough.ts similarity index 72% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleStrikethrough.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleStrikethrough.ts index 075eb2ebefa..93e2df84d2c 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleStrikethrough.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleStrikethrough.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggle strikethrough style * @param editor The editor to operate on */ -export default function toggleStrikethrough(editor: IContentModelEditor) { +export default function toggleStrikethrough(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleSubscript.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleSubscript.ts similarity index 75% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleSubscript.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleSubscript.ts index 8e1d177a21d..28c9b6da37b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleSubscript.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleSubscript.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggle subscript style * @param editor The editor to operate on */ -export default function toggleSubscript(editor: IContentModelEditor) { +export default function toggleSubscript(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleSuperscript.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleSuperscript.ts similarity index 75% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleSuperscript.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleSuperscript.ts index 716838ae7da..2e84d4d5059 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleSuperscript.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleSuperscript.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggle superscript style * @param editor The editor to operate on */ -export default function toggleSuperscript(editor: IContentModelEditor) { +export default function toggleSuperscript(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleUnderline.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts similarity index 78% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleUnderline.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts index 6fdd7cd2aec..8614fdd7b1d 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/segment/toggleUnderline.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/segment/toggleUnderline.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; /** * Toggle underline style * @param editor The editor to operate on */ -export default function toggleUnderline(editor: IContentModelEditor) { +export default function toggleUnderline(editor: IStandaloneEditor) { editor.focus(); formatSegmentWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/hasSelectionInBlock.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/selection/hasSelectionInBlock.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/hasSelectionInBlock.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/selection/hasSelectionInBlock.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/hasSelectionInBlockGroup.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/selection/hasSelectionInBlockGroup.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/hasSelectionInBlockGroup.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/selection/hasSelectionInBlockGroup.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/hasSelectionInSegment.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/selection/hasSelectionInSegment.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/hasSelectionInSegment.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/selection/hasSelectionInSegment.ts 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 new file mode 100644 index 00000000000..7d73af2f1c2 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/applyTableBorderFormat.ts @@ -0,0 +1,427 @@ +import { getSelectedCells } from '../../modelApi/table/getSelectedCells'; +import { parseValueWithUnit } from 'roosterjs-content-model-dom'; +import { + extractBorderValues, + getFirstSelectedTable, + updateTableCellMetadata, +} from 'roosterjs-content-model-core'; +import type { + IStandaloneEditor, + Border, + ContentModelTable, + ContentModelTableCell, + BorderOperations, +} from 'roosterjs-content-model-types'; +import type { TableSelectionCoordinates } from '../../modelApi/table/getSelectedCells'; + +/** + * @internal + * Border positions + */ +type BorderPositions = 'borderTop' | 'borderBottom' | 'borderLeft' | 'borderRight'; + +/** + * @internal + * Perimeter of the table selection + * Used to determine where to apply border to the cells adjacent to the selection. + */ +type Perimeter = { + Top: boolean; + Bottom: boolean; + Left: boolean; + Right: boolean; +}; + +/** + * Operations to apply border + * @param editor The editor instance + * @param border The border to apply + * @param operation The operation to apply + */ +export default function applyTableBorderFormat( + editor: IStandaloneEditor, + border: Border, + operation: BorderOperations +) { + editor.formatContentModel( + model => { + const [tableModel] = getFirstSelectedTable(model); + + if (tableModel) { + const sel = getSelectedCells(tableModel); + const perimeter: Perimeter = { + Top: false, + Bottom: false, + Left: false, + Right: false, + }; + + // Create border format with table format as backup + let borderFormat = ''; + const format = tableModel.format; + const { width, style, color } = border; + const extractedBorder = extractBorderValues(format.borderTop); + const borderColor = extractedBorder.color; + const borderWidth = extractedBorder.width; + const borderStyle = extractedBorder.style; + + if (width) { + borderFormat = parseValueWithUnit(width) + 'px'; + } else if (borderWidth) { + borderFormat = borderWidth; + } else { + borderFormat = '1px'; + } + + if (style) { + borderFormat = `${borderFormat} ${style}`; + } else if (borderStyle) { + borderFormat = `${borderFormat} ${borderStyle}`; + } else { + borderFormat = `${borderFormat} solid`; + } + + if (color) { + borderFormat = `${borderFormat} ${color}`; + } else if (borderColor) { + borderFormat = `${borderFormat} ${borderColor}`; + } + + if (sel) { + const operations: BorderOperations[] = [operation]; + while (operations.length) { + switch (operations.pop()) { + case 'noBorders': + // Do All borders but with empty border format + borderFormat = ''; + operations.push('allBorders'); + break; + case 'allBorders': + const allBorders: BorderPositions[] = [ + 'borderTop', + 'borderBottom', + 'borderLeft', + 'borderRight', + ]; + for ( + let rowIndex = sel.firstRow; + rowIndex <= sel.lastRow; + rowIndex++ + ) { + for ( + let colIndex = sel.firstCol; + colIndex <= sel.lastCol; + colIndex++ + ) { + const cell = tableModel.rows[rowIndex].cells[colIndex]; + // Format cells - All borders + applyBorderFormat(cell, borderFormat, allBorders); + } + } + + // Format perimeter + perimeter.Top = true; + perimeter.Bottom = true; + perimeter.Left = true; + perimeter.Right = true; + break; + case 'leftBorders': + const leftBorder: BorderPositions[] = ['borderLeft']; + for ( + let rowIndex = sel.firstRow; + rowIndex <= sel.lastRow; + rowIndex++ + ) { + const cell = tableModel.rows[rowIndex].cells[sel.firstCol]; + // Format cells - Left border + applyBorderFormat(cell, borderFormat, leftBorder); + } + + // Format perimeter + perimeter.Left = true; + break; + case 'rightBorders': + const rightBorder: BorderPositions[] = ['borderRight']; + for ( + let rowIndex = sel.firstRow; + rowIndex <= sel.lastRow; + rowIndex++ + ) { + const cell = tableModel.rows[rowIndex].cells[sel.lastCol]; + // Format cells - Right border + applyBorderFormat(cell, borderFormat, rightBorder); + } + + // Format perimeter + perimeter.Right = true; + break; + case 'topBorders': + const topBorder: BorderPositions[] = ['borderTop']; + for ( + let colIndex = sel.firstCol; + colIndex <= sel.lastCol; + colIndex++ + ) { + const cell = tableModel.rows[sel.firstRow].cells[colIndex]; + // Format cells - Top border + applyBorderFormat(cell, borderFormat, topBorder); + } + + // Format perimeter + perimeter.Top = true; + break; + case 'bottomBorders': + const bottomBorder: BorderPositions[] = ['borderBottom']; + for ( + let colIndex = sel.firstCol; + colIndex <= sel.lastCol; + colIndex++ + ) { + const cell = tableModel.rows[sel.lastRow].cells[colIndex]; + // Format cells - Bottom border + applyBorderFormat(cell, borderFormat, bottomBorder); + } + + // Format perimeter + perimeter.Bottom = true; + break; + case 'insideBorders': + // Format cells - Inside borders + const singleCol = sel.lastCol == sel.firstCol; + const singleRow = sel.lastRow == sel.firstRow; + // Single cell selection + if (singleCol && singleRow) { + break; + } + // Single column selection + if (singleCol) { + applyBorderFormat( + tableModel.rows[sel.firstRow].cells[sel.firstCol], + borderFormat, + ['borderBottom'] + ); + for ( + let rowIndex = sel.firstRow + 1; + rowIndex <= sel.lastRow - 1; + rowIndex++ + ) { + const cell = tableModel.rows[rowIndex].cells[sel.firstCol]; + applyBorderFormat(cell, borderFormat, [ + 'borderTop', + 'borderBottom', + ]); + } + applyBorderFormat( + tableModel.rows[sel.lastRow].cells[sel.firstCol], + borderFormat, + ['borderTop'] + ); + break; + } + // Single row selection + if (singleRow) { + applyBorderFormat( + tableModel.rows[sel.firstRow].cells[sel.firstCol], + borderFormat, + ['borderRight'] + ); + for ( + let colIndex = sel.firstCol + 1; + colIndex <= sel.lastCol - 1; + colIndex++ + ) { + const cell = tableModel.rows[sel.firstRow].cells[colIndex]; + applyBorderFormat(cell, borderFormat, [ + 'borderLeft', + 'borderRight', + ]); + } + applyBorderFormat( + tableModel.rows[sel.firstRow].cells[sel.lastCol], + borderFormat, + ['borderLeft'] + ); + break; + } + + // For multiple rows and columns selections + // Top left cell + applyBorderFormat( + tableModel.rows[sel.firstRow].cells[sel.firstCol], + borderFormat, + ['borderBottom', 'borderRight'] + ); + // Top right cell + applyBorderFormat( + tableModel.rows[sel.firstRow].cells[sel.lastCol], + borderFormat, + ['borderBottom', 'borderLeft'] + ); + // Bottom left cell + applyBorderFormat( + tableModel.rows[sel.lastRow].cells[sel.firstCol], + borderFormat, + ['borderTop', 'borderRight'] + ); + // Bottom right cell + applyBorderFormat( + tableModel.rows[sel.lastRow].cells[sel.lastCol], + borderFormat, + ['borderTop', 'borderLeft'] + ); + // First row + for ( + let colIndex = sel.firstCol + 1; + colIndex < sel.lastCol; + colIndex++ + ) { + const cell = tableModel.rows[sel.firstRow].cells[colIndex]; + applyBorderFormat(cell, borderFormat, [ + 'borderBottom', + 'borderLeft', + 'borderRight', + ]); + } + // Last row + for ( + let colIndex = sel.firstCol + 1; + colIndex < sel.lastCol; + colIndex++ + ) { + const cell = tableModel.rows[sel.lastRow].cells[colIndex]; + applyBorderFormat(cell, borderFormat, [ + 'borderTop', + 'borderLeft', + 'borderRight', + ]); + } + // First column + for ( + let rowIndex = sel.firstRow + 1; + rowIndex < sel.lastRow; + rowIndex++ + ) { + const cell = tableModel.rows[rowIndex].cells[sel.firstCol]; + applyBorderFormat(cell, borderFormat, [ + 'borderTop', + 'borderBottom', + 'borderRight', + ]); + } + // Last column + for ( + let rowIndex = sel.firstRow + 1; + rowIndex < sel.lastRow; + rowIndex++ + ) { + const cell = tableModel.rows[rowIndex].cells[sel.lastCol]; + applyBorderFormat(cell, borderFormat, [ + 'borderTop', + 'borderBottom', + 'borderLeft', + ]); + } + // Inner cells + sel.firstCol++; + sel.firstRow++; + sel.lastCol--; + sel.lastRow--; + operations.push('allBorders'); + break; + case 'outsideBorders': + // Format cells - Outside borders + operations.push('topBorders'); + operations.push('bottomBorders'); + operations.push('leftBorders'); + operations.push('rightBorders'); + break; + default: + break; + } + } + + //Format perimeter if necessary or possible + modifyPerimeter(tableModel, sel, borderFormat, perimeter); + } + + return true; + } else { + return false; + } + }, + { + apiName: 'tableBorder', + } + ); +} + +/** + * @internal + * Apply border format to a cell + * @param cell The cell to apply border format + * @param borderFormat The border format to apply + * @param positions The positions to apply + */ +function applyBorderFormat( + cell: ContentModelTableCell, + borderFormat: string, + positions: BorderPositions[] +) { + positions.forEach(pos => { + cell.format[pos] = borderFormat; + }); + + updateTableCellMetadata(cell, metadata => { + metadata = metadata || {}; + metadata.borderOverride = true; + return metadata; + }); + + // Cell was modified, so delete cached element + delete cell.cachedElement; +} + +/** + * @internal + * Modify the perimeter of the table selection + * @param tableModel The table model + * @param sel The table selection + * @param borderFormat The border format to apply + * If borderFormat is empty, the border will be removed + * @param perimeter Where in the perimeter to apply + */ +function modifyPerimeter( + tableModel: ContentModelTable, + sel: TableSelectionCoordinates, + borderFormat: string, + perimeter: Perimeter +) { + // Top of selection + if (perimeter.Top && sel.firstRow - 1 >= 0) { + for (let colIndex = sel.firstCol; colIndex <= sel.lastCol; colIndex++) { + const cell = tableModel.rows[sel.firstRow - 1].cells[colIndex]; + applyBorderFormat(cell, borderFormat, ['borderBottom']); + } + } + // Bottom of selection + if (perimeter.Bottom && sel.lastRow + 1 < tableModel.rows.length) { + for (let colIndex = sel.firstCol; colIndex <= sel.lastCol; colIndex++) { + const cell = tableModel.rows[sel.lastRow + 1].cells[colIndex]; + applyBorderFormat(cell, borderFormat, ['borderTop']); + } + } + // Left of selection + if (perimeter.Left && sel.firstCol - 1 >= 0) { + for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { + const cell = tableModel.rows[rowIndex].cells[sel.firstCol - 1]; + applyBorderFormat(cell, borderFormat, ['borderRight']); + } + } + // Right of selection + if (perimeter.Right && sel.lastCol + 1 < tableModel.rows[0].cells.length) { + for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { + const cell = tableModel.rows[rowIndex].cells[sel.lastCol + 1]; + applyBorderFormat(cell, borderFormat, ['borderLeft']); + } + } +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/editTable.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/editTable.ts new file mode 100644 index 00000000000..1ffadcc164f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/editTable.ts @@ -0,0 +1,133 @@ +import hasSelectionInBlock from '../selection/hasSelectionInBlock'; +import { alignTable } from '../../modelApi/table/alignTable'; +import { deleteTable } from '../../modelApi/table/deleteTable'; +import { deleteTableColumn } from '../../modelApi/table/deleteTableColumn'; +import { deleteTableRow } from '../../modelApi/table/deleteTableRow'; +import { ensureFocusableParagraphForTable } from '../../modelApi/table/ensureFocusableParagraphForTable'; +import { insertTableColumn } from '../../modelApi/table/insertTableColumn'; +import { insertTableRow } from '../../modelApi/table/insertTableRow'; +import { mergeTableCells } from '../../modelApi/table/mergeTableCells'; +import { mergeTableColumn } from '../../modelApi/table/mergeTableColumn'; +import { mergeTableRow } from '../../modelApi/table/mergeTableRow'; +import { splitTableCellHorizontally } from '../../modelApi/table/splitTableCellHorizontally'; +import { splitTableCellVertically } from '../../modelApi/table/splitTableCellVertically'; +import { + applyTableFormat, + getFirstSelectedTable, + normalizeTable, + setSelection, +} from 'roosterjs-content-model-core'; +import type { TableOperation, IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + alignTableCellHorizontally, + alignTableCellVertically, +} from '../../modelApi/table/alignTableCell'; +import { + createSelectionMarker, + hasMetadata, + setParagraphNotImplicit, +} from 'roosterjs-content-model-dom'; + +/** + * Format current focused table with the given format + * @param editor The editor instance + * @param operation The table operation to apply + */ +export default function editTable(editor: IStandaloneEditor, operation: TableOperation) { + editor.focus(); + + editor.formatContentModel( + model => { + const [tableModel, path] = getFirstSelectedTable(model); + + if (tableModel) { + switch (operation) { + case 'alignCellLeft': + case 'alignCellCenter': + case 'alignCellRight': + alignTableCellHorizontally(tableModel, operation); + break; + case 'alignCellTop': + case 'alignCellMiddle': + case 'alignCellBottom': + alignTableCellVertically(tableModel, operation); + break; + case 'alignCenter': + case 'alignLeft': + case 'alignRight': + alignTable(tableModel, operation); + break; + + case 'deleteColumn': + deleteTableColumn(tableModel); + break; + + case 'deleteRow': + deleteTableRow(tableModel); + break; + + case 'deleteTable': + deleteTable(tableModel); + break; + + case 'insertAbove': + case 'insertBelow': + insertTableRow(tableModel, operation); + break; + + case 'insertLeft': + case 'insertRight': + insertTableColumn(tableModel, operation); + break; + + case 'mergeAbove': + case 'mergeBelow': + mergeTableRow(tableModel, operation); + break; + + case 'mergeCells': + mergeTableCells(tableModel); + break; + + case 'mergeLeft': + case 'mergeRight': + mergeTableColumn(tableModel, operation); + break; + + case 'splitHorizontally': + splitTableCellHorizontally(tableModel); + break; + + case 'splitVertically': + splitTableCellVertically(tableModel); + break; + } + + if (!hasSelectionInBlock(tableModel)) { + const paragraph = ensureFocusableParagraphForTable(model, path, tableModel); + + if (paragraph) { + const marker = createSelectionMarker(model.format); + + paragraph.segments.unshift(marker); + setParagraphNotImplicit(paragraph); + setSelection(model, marker); + } + } + + normalizeTable(tableModel, model.format); + + if (hasMetadata(tableModel)) { + applyTableFormat(tableModel, undefined /*newFormat*/, true /*keepCellShade*/); + } + + return true; + } else { + return false; + } + }, + { + apiName: 'editTable', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts new file mode 100644 index 00000000000..411f229bedb --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/formatTable.ts @@ -0,0 +1,47 @@ +import { + applyTableFormat, + getFirstSelectedTable, + updateTableCellMetadata, +} from 'roosterjs-content-model-core'; +import type { IStandaloneEditor, TableMetadataFormat } from 'roosterjs-content-model-types'; + +/** + * Format current focused table with the given format + * @param editor The editor instance + * @param format The table format to apply + * @param keepCellShade Whether keep existing shade color when apply format if there is a manually set shade color + */ +export default function formatTable( + editor: IStandaloneEditor, + format: TableMetadataFormat, + keepCellShade?: boolean +) { + editor.focus(); + + editor.formatContentModel( + model => { + const [tableModel] = getFirstSelectedTable(model); + + if (tableModel) { + // Wipe border metadata + tableModel.rows.forEach(row => { + row.cells.forEach(cell => { + updateTableCellMetadata(cell, metadata => { + if (metadata) { + delete metadata.borderOverride; + } + return metadata; + }); + }); + }); + applyTableFormat(tableModel, format, keepCellShade); + return true; + } else { + return false; + } + }, + { + apiName: 'formatTable', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/insertTable.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/insertTable.ts new file mode 100644 index 00000000000..0128b7913a2 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/insertTable.ts @@ -0,0 +1,63 @@ +import { createContentModelDocument, createSelectionMarker } from 'roosterjs-content-model-dom'; +import { createTableStructure } from '../../modelApi/table/createTableStructure'; +import { + applyTableFormat, + deleteSelection, + mergeModel, + normalizeTable, + setSelection, +} from 'roosterjs-content-model-core'; +import type { IStandaloneEditor, TableMetadataFormat } from 'roosterjs-content-model-types'; + +/** + * Insert table into editor at current selection + * @param editor The editor instance + * @param columns Number of columns in table, it also controls the default table cell width: + * if columns <= 4, width = 120px; if columns <= 6, width = 100px; else width = 70px + * @param rows Number of rows in table + * @param format (Optional) The table format. If not passed, the default format will be applied: + * background color: #FFF; border color: #ABABAB + */ +export default function insertTable( + editor: IStandaloneEditor, + columns: number, + rows: number, + format?: Partial +) { + editor.focus(); + + editor.formatContentModel( + (model, context) => { + const insertPosition = deleteSelection(model, [], context).insertPoint; + + if (insertPosition) { + const doc = createContentModelDocument(); + const table = createTableStructure(doc, columns, rows); + + normalizeTable(table, editor.getPendingFormat() || insertPosition.marker.format); + // Assign default vertical align + format = format || { verticalAlign: 'top' }; + applyTableFormat(table, format); + mergeModel(model, doc, context, { + insertPosition, + mergeFormat: 'mergeAll', + }); + + const firstBlock = table.rows[0]?.cells[0]?.blocks[0]; + + if (firstBlock?.blockType == 'Paragraph') { + const marker = createSelectionMarker(firstBlock.segments[0]?.format); + firstBlock.segments.unshift(marker); + setSelection(model, marker); + } + + return true; + } else { + return false; + } + }, + { + apiName: 'insertTable', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts new file mode 100644 index 00000000000..46a284f4d3a --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/table/setTableCellShade.ts @@ -0,0 +1,41 @@ +import hasSelectionInBlockGroup from '../selection/hasSelectionInBlockGroup'; +import { + getFirstSelectedTable, + normalizeTable, + setTableCellBackgroundColor, +} from 'roosterjs-content-model-core'; +import type { IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * Set table cell shade color + * @param editor The editor instance + * @param color The color to set. Pass null to remove existing shade color + */ +export default function setTableCellShade(editor: IStandaloneEditor, color: string | null) { + editor.focus(); + + editor.formatContentModel( + model => { + const [table] = getFirstSelectedTable(model); + + if (table) { + normalizeTable(table); + + table.rows.forEach(row => + row.cells.forEach(cell => { + if (hasSelectionInBlockGroup(cell)) { + setTableCellBackgroundColor(cell, color, true /*isColorOverride*/); + } + }) + ); + + return true; + } else { + return false; + } + }, + { + apiName: 'setTableCellShade', + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatImageWithContentModel.ts similarity index 74% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts rename to packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatImageWithContentModel.ts index a0f5e4ceb05..df35d9b8e74 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatImageWithContentModel.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatImageWithContentModel.ts @@ -1,12 +1,11 @@ import { formatSegmentWithContentModel } from './formatSegmentWithContentModel'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { ContentModelImage, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * @internal */ export default function formatImageWithContentModel( - editor: IContentModelEditor, + editor: IStandaloneEditor, apiName: string, callback: (segment: ContentModelImage) => void ) { diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts new file mode 100644 index 00000000000..a54e2e8deca --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatParagraphWithContentModel.ts @@ -0,0 +1,25 @@ +import { getSelectedParagraphs } from 'roosterjs-content-model-core'; +import type { ContentModelParagraph, IStandaloneEditor } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function formatParagraphWithContentModel( + editor: IStandaloneEditor, + apiName: string, + setStyleCallback: (paragraph: ContentModelParagraph) => void +) { + editor.formatContentModel( + (model, context) => { + const paragraphs = getSelectedParagraphs(model); + + paragraphs.forEach(setStyleCallback); + context.newPendingFormat = 'preserve'; + + return paragraphs.length > 0; + }, + { + apiName, + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts new file mode 100644 index 00000000000..30dd6a92c00 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/utils/formatSegmentWithContentModel.ts @@ -0,0 +1,89 @@ +import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; +import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-core'; +import type { + ContentModelDocument, + ContentModelParagraph, + ContentModelSegment, + ContentModelSegmentFormat, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; +/** + * @internal + */ +export function formatSegmentWithContentModel( + editor: IStandaloneEditor, + apiName: string, + toggleStyleCallback: ( + format: ContentModelSegmentFormat, + isTuringOn: boolean, + segment: ContentModelSegment | null, + paragraph: ContentModelParagraph | null + ) => void, + segmentHasStyleCallback?: ( + format: ContentModelSegmentFormat, + segment: ContentModelSegment | null, + paragraph: ContentModelParagraph | null + ) => boolean, + includingFormatHolder?: boolean, + afterFormatCallback?: (model: ContentModelDocument) => void +) { + editor.formatContentModel( + (model, context) => { + let segmentAndParagraphs = getSelectedSegmentsAndParagraphs( + model, + !!includingFormatHolder + ); + const pendingFormat = editor.getPendingFormat(); + let isCollapsedSelection = + segmentAndParagraphs.length == 1 && + segmentAndParagraphs[0][0].segmentType == 'SelectionMarker'; + + if (isCollapsedSelection) { + const para = segmentAndParagraphs[0][1]; + + segmentAndParagraphs = adjustWordSelection( + model, + segmentAndParagraphs[0][0] + ).map(x => [x, para]); + + if (segmentAndParagraphs.length > 1) { + isCollapsedSelection = false; + } + } + + const formatsAndSegments: [ + ContentModelSegmentFormat, + ContentModelSegment | null, + ContentModelParagraph | null + ][] = pendingFormat + ? [[pendingFormat, null, null]] + : segmentAndParagraphs.map(item => [item[0].format, item[0], item[1]]); + + const isTurningOff = segmentHasStyleCallback + ? formatsAndSegments.every(([format, segment, paragraph]) => + segmentHasStyleCallback(format, segment, paragraph) + ) + : false; + + formatsAndSegments.forEach(([format, segment, paragraph]) => + toggleStyleCallback(format, !isTurningOff, segment, paragraph) + ); + + afterFormatCallback?.(model); + + if (!pendingFormat && isCollapsedSelection) { + context.newPendingFormat = segmentAndParagraphs[0][0].format; + } + + if (isCollapsedSelection) { + editor.focus(); + return false; + } else { + return formatsAndSegments.length > 0; + } + }, + { + apiName, + } + ); +} diff --git a/packages-content-model/roosterjs-content-model-api/package.json b/packages-content-model/roosterjs-content-model-api/package.json new file mode 100644 index 00000000000..d4f7558e6ae --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/package.json @@ -0,0 +1,14 @@ +{ + "name": "roosterjs-content-model-api", + "description": "Content Model for roosterjs (Under development)", + "dependencies": { + "tslib": "^2.3.1", + "roosterjs-editor-types": "", + "roosterjs-editor-dom": "", + "roosterjs-content-model-core": "", + "roosterjs-content-model-dom": "", + "roosterjs-content-model-types": "" + }, + "version": "0.0.0", + "main": "./lib/index.ts" +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/block/setModelAlignmentTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/block/setModelAlignmentTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelAlignmentTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/block/setModelDirectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/block/setModelDirectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelDirectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/block/setModelIndentationTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/block/setModelIndentationTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/block/toggleModelBlockQuoteTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/block/toggleModelBlockQuoteTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/block/toggleModelBlockQuoteTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/block/toggleModelBlockQuoteTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/clearModelFormatTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/clearModelFormatTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/retrieveModelFormatStateTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/retrieveModelFormatStateTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts index 296db0ff587..45d9439fe76 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/retrieveModelFormatStateTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/common/retrieveModelFormatStateTest.ts @@ -1,7 +1,6 @@ -import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelections'; -import { applyTableFormat } from '../../../lib/modelApi/table/applyTableFormat'; -import { ContentModelFormatState } from '../../../lib/publicTypes/format/formatState/ContentModelFormatState'; -import { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; +import * as iterateSelections from 'roosterjs-content-model-core/lib/publicApi/selection/iterateSelections'; +import { applyTableFormat } from 'roosterjs-content-model-core'; +import { ContentModelFormatState, ContentModelSegmentFormat } from 'roosterjs-content-model-types'; import { retrieveModelFormatState } from '../../../lib/modelApi/common/retrieveModelFormatState'; import { addCode, @@ -64,7 +63,7 @@ describe('retrieveModelFormatState', () => { const marker = createSelectionMarker(segmentFormat); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [marker]); + callback([path], undefined, para, [marker]); return false; }); @@ -82,7 +81,7 @@ describe('retrieveModelFormatState', () => { addCode(marker, { format: { fontFamily: 'monospace' } }); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [marker]); + callback([path], undefined, para, [marker]); return false; }); @@ -150,7 +149,7 @@ describe('retrieveModelFormatState', () => { const marker = createSelectionMarker(segmentFormat); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [marker]); + callback([path], undefined, para, [marker]); return false; }); @@ -175,7 +174,7 @@ describe('retrieveModelFormatState', () => { const marker = createSelectionMarker(segmentFormat); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [marker]); + callback([path], undefined, para, [marker]); return false; }); @@ -201,7 +200,7 @@ describe('retrieveModelFormatState', () => { spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { callback( - path, + [path], { table: table, colIndex: 0, @@ -238,7 +237,7 @@ describe('retrieveModelFormatState', () => { spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { callback( - path, + [path], { table: table, colIndex: 0, @@ -287,7 +286,12 @@ describe('retrieveModelFormatState', () => { model.blocks.push(table); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, { table: table, rowIndex: 0, colIndex: 0, isWholeTableSelected: false }); + callback([path], { + table: table, + rowIndex: 0, + colIndex: 0, + isWholeTableSelected: false, + }); return false; }); @@ -309,8 +313,8 @@ describe('retrieveModelFormatState', () => { const marker2 = createSelectionMarker(); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para1, [marker1]); - callback(path, undefined, para2, [marker2]); + callback([path], undefined, para1, [marker1]); + callback([path], undefined, para2, [marker2]); return false; }); @@ -344,7 +348,7 @@ describe('retrieveModelFormatState', () => { const result: ContentModelFormatState = {}; spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [text1, text2]); + callback([path], undefined, para, [text1, text2]); return false; }); @@ -370,7 +374,7 @@ describe('retrieveModelFormatState', () => { const result: ContentModelFormatState = {}; spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [text1, text2]); + callback([path], undefined, para, [text1, text2]); return false; }); @@ -395,7 +399,7 @@ describe('retrieveModelFormatState', () => { const result: ContentModelFormatState = {}; spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [text, marker]); + callback([path], undefined, para, [text, marker]); return false; }); @@ -421,8 +425,8 @@ describe('retrieveModelFormatState', () => { const marker1 = createSelectionMarker(segmentFormat); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para1, [marker1]); - callback(path, undefined, divider); + callback([path], undefined, para1, [marker1]); + callback([path], undefined, divider); return false; }); @@ -444,8 +448,8 @@ describe('retrieveModelFormatState', () => { const marker1 = createSelectionMarker(segmentFormat); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, divider); - callback(path, undefined, para1, [marker1]); + callback([path], undefined, divider); + callback([path], undefined, para1, [marker1]); return false; }); @@ -471,7 +475,7 @@ describe('retrieveModelFormatState', () => { }; spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para1, [marker1]); + callback([path], undefined, para1, [marker1]); return false; }); @@ -622,7 +626,7 @@ describe('retrieveModelFormatState', () => { para.segments.push(image); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [image]); + callback([path], undefined, para, [image]); return false; }); @@ -662,7 +666,7 @@ describe('retrieveModelFormatState', () => { para.segments.push(image2); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [image, image2]); + callback([path], undefined, para, [image, image2]); return false; }); @@ -691,7 +695,7 @@ describe('retrieveModelFormatState', () => { para.segments.push(marker); spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [marker]); + callback([path], undefined, para, [marker]); return false; }); @@ -726,7 +730,7 @@ describe('retrieveModelFormatState', () => { text1.isSelected = true; spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [text1]); + callback([path], undefined, para, [text1]); return false; }); @@ -760,7 +764,7 @@ describe('retrieveModelFormatState', () => { text2.isSelected = true; spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { - callback(path, undefined, para, [text1, text2]); + callback([path], undefined, para, [text1, text2]); return false; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/wrapBlockTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/common/wrapBlockTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/wrapBlockTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/common/wrapBlockTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/entity/insertEntityModelTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/entity/insertEntityModelTest.ts index 1c59a012554..49cf8bb1a6b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/entity/insertEntityModelTest.ts @@ -1,6 +1,5 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { ContentModelDocument, InsertEntityPosition } from 'roosterjs-content-model-types'; import { insertEntityModel } from '../../../lib/modelApi/entity/insertEntityModel'; -import { InsertEntityPosition } from '../../../lib/publicTypes/parameter/InsertEntityOptions'; import { createBr, createContentModelDocument, diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/image/applyImageBorderFormatTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/image/applyImageBorderFormatTest.ts similarity index 96% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/image/applyImageBorderFormatTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/image/applyImageBorderFormatTest.ts index 4290a0cf78a..acf4bd0038a 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/image/applyImageBorderFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/image/applyImageBorderFormatTest.ts @@ -1,6 +1,5 @@ import applyImageBorderFormat from '../../../lib/modelApi/image/applyImageBorderFormat'; -import { Border } from '../../../lib/publicTypes/interface/Border'; -import { ContentModelImage } from 'roosterjs-content-model-types'; +import { Border, ContentModelImage } from 'roosterjs-content-model-types'; describe('applyImageBorderFormat', () => { function createImage(border?: string): ContentModelImage { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/list/findListItemsInSameThreadTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/list/findListItemsInSameThreadTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/list/findListItemsInSameThreadTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/list/findListItemsInSameThreadTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/list/setListTypeTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/list/setListTypeTest.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/list/setListTypeTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/list/setListTypeTest.ts index 0774263e390..72013ea6e08 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/list/setListTypeTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/list/setListTypeTest.ts @@ -72,6 +72,8 @@ describe('indent', () => { direction: undefined, textAlign: undefined, marginTop: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, dataset: {}, }, @@ -299,6 +301,8 @@ describe('indent', () => { direction: undefined, textAlign: undefined, marginTop: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, dataset: {}, }, @@ -362,6 +366,8 @@ describe('indent', () => { direction: 'rtl', textAlign: 'start', marginTop: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], @@ -441,6 +447,8 @@ describe('indent', () => { textAlign: undefined, marginTop: undefined, marginBottom: '0', + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], @@ -469,6 +477,8 @@ describe('indent', () => { textAlign: undefined, startNumberOverride: undefined, marginTop: '0', + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], @@ -523,6 +533,8 @@ describe('indent', () => { direction: undefined, textAlign: undefined, marginTop: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], @@ -579,6 +591,8 @@ describe('indent', () => { direction: undefined, textAlign: undefined, marginTop: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], @@ -606,6 +620,8 @@ describe('indent', () => { direction: undefined, textAlign: undefined, marginTop: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], @@ -633,6 +649,8 @@ describe('indent', () => { direction: undefined, textAlign: undefined, startNumberOverride: undefined, + marginBlockEnd: '0px', + marginBlockStart: '0px', }, }, ], diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/adjustSegmentSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustSegmentSelectionTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/adjustSegmentSelectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustSegmentSelectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/adjustWordSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustWordSelectionTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/adjustWordSelectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/selection/adjustWordSelectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collapseTableSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/selection/collapseTableSelectionTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collapseTableSelectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/selection/collapseTableSelectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/alignTableCellTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/alignTableCellTest.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/alignTableCellTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/alignTableCellTest.ts index d0773936966..5b987c81476 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/alignTableCellTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/alignTableCellTest.ts @@ -1,13 +1,13 @@ -import { ContentModelTableCellFormat } from 'roosterjs-content-model-types'; import { createTable, createTableCell } from 'roosterjs-content-model-dom'; +import { + ContentModelTableCellFormat, + TableCellHorizontalAlignOperation, + TableCellVerticalAlignOperation, +} from 'roosterjs-content-model-types'; import { alignTableCellHorizontally, alignTableCellVertically, } from '../../../lib/modelApi/table/alignTableCell'; -import { - TableCellHorizontalAlignOperation, - TableCellVerticalAlignOperation, -} from '../../../lib/publicTypes/parameter/TableOperation'; describe('alignTableCellHorizontally', () => { function runTest( diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/alignTableTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/alignTableTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/alignTableTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/alignTableTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/canMergeCellsTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/canMergeCellsTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/canMergeCellsTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/canMergeCellsTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/createTableStructureTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/createTableStructureTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/createTableStructureTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/createTableStructureTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/deleteTableColumnTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/deleteTableColumnTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/deleteTableColumnTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/deleteTableColumnTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/deleteTableRowTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/deleteTableRowTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/deleteTableRowTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/deleteTableRowTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/deleteTableTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/deleteTableTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/deleteTableTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/deleteTableTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/ensureFocusableParagraphForTableTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/ensureFocusableParagraphForTableTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/ensureFocusableParagraphForTableTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/ensureFocusableParagraphForTableTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/getSelectedCellsTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/getSelectedCellsTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/getSelectedCellsTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/getSelectedCellsTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/insertTableColumnTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/insertTableColumnTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/insertTableColumnTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/insertTableColumnTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/insertTableRowTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/insertTableRowTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/insertTableRowTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/insertTableRowTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/mergeTableCellsTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/mergeTableCellsTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/mergeTableCellsTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/mergeTableCellsTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/mergeTableColumnTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/mergeTableColumnTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/mergeTableColumnTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/mergeTableColumnTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/mergeTableRowTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/mergeTableRowTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/mergeTableRowTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/mergeTableRowTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/splitTableCellHorizontallyTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/splitTableCellHorizontallyTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/splitTableCellHorizontallyTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/splitTableCellHorizontallyTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/splitTableCellVerticallyTest.ts b/packages-content-model/roosterjs-content-model-api/test/modelApi/table/splitTableCellVerticallyTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/splitTableCellVerticallyTest.ts rename to packages-content-model/roosterjs-content-model-api/test/modelApi/table/splitTableCellVerticallyTest.ts diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/block/paragraphTestCommon.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/paragraphTestCommon.ts new file mode 100644 index 00000000000..80604aed393 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/paragraphTestCommon.ts @@ -0,0 +1,38 @@ +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; + +export function paragraphTestCommon( + apiName: string, + executionCallback: (editor: IStandaloneEditor) => void, + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number +) { + let formatResult: boolean | undefined; + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + }); + const editor = ({ + focus: jasmine.createSpy(), + getCustomData: () => ({}), + getFocusedPosition: () => ({}), + formatContentModel, + } as any) as IStandaloneEditor; + + executionCallback(editor); + + expect(model).toEqual(result); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(calledTimes > 0); + expect(model).toEqual(result); +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts index 124af97e439..c6fc09cedb2 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setAlignmentTest.ts @@ -1,12 +1,14 @@ -import * as normalizeTable from '../../../lib/modelApi/table/normalizeTable'; +import * as normalizeTable from 'roosterjs-content-model-core/lib/publicApi/table/normalizeTable'; import setAlignment from '../../../lib/publicApi/block/setAlignment'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; import { paragraphTestCommon } from './paragraphTestCommon'; import { ContentModelDocument, ContentModelListItem, ContentModelTable, + ContentModelFormatter, + FormatWithContentModelOptions, } from 'roosterjs-content-model-types'; describe('setAlignment', () => { @@ -413,14 +415,12 @@ describe('setAlignment', () => { }); describe('setAlignment in table', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; + let editor: IStandaloneEditor; + let createContentModel: jasmine.Spy; let triggerPluginEvent: jasmine.Spy; let getVisibleViewport: jasmine.Spy; beforeEach(() => { - setContentModel = jasmine.createSpy('setContentModel'); createContentModel = jasmine.createSpy('createContentModel'); triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); getVisibleViewport = jasmine.createSpy('getVisibleViewport'); @@ -430,12 +430,11 @@ describe('setAlignment in table', () => { editor = ({ focus: () => {}, addUndoSnapshot: (callback: Function) => callback(), - setContentModel, createContentModel, isDarkMode: () => false, triggerPluginEvent, getVisibleViewport, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor; }); function runTest( @@ -448,18 +447,25 @@ describe('setAlignment in table', () => { createContentModel.and.returnValue(model); + editor.formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + setAlignment(editor, alignment); if (expectedTable) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [expectedTable], - }, - undefined, - undefined - ); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [expectedTable], + }); } } @@ -814,9 +820,9 @@ describe('setAlignment in table', () => { }); describe('setAlignment in list', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; + let editor: IStandaloneEditor; + let setContentModel: jasmine.Spy; + let createContentModel: jasmine.Spy; let triggerPluginEvent: jasmine.Spy; let getVisibleViewport: jasmine.Spy; @@ -834,7 +840,7 @@ describe('setAlignment in list', () => { isDarkMode: () => false, triggerPluginEvent, getVisibleViewport, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor; }); function runTest( @@ -846,19 +852,31 @@ describe('setAlignment in list', () => { model.blocks.push(list); createContentModel.and.returnValue(model); + let result: boolean | undefined; + + editor.formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + result = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: options.rawEvent, + }); + } + ); setAlignment(editor, alignment); if (expectedList) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [expectedList], - }, - undefined, - undefined - ); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [expectedList], + }); + expect(result).toBeTrue(); + } else { + expect(result).toBeFalse(); } } diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setDirectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setDirectionTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setDirectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/block/setDirectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setHeadingLevelTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setHeadingLevelTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setHeadingLevelTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/block/setHeadingLevelTest.ts diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setIndentationTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setIndentationTest.ts new file mode 100644 index 00000000000..adddd59431e --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setIndentationTest.ts @@ -0,0 +1,74 @@ +import * as setModelIndentation from '../../../lib/modelApi/block/setModelIndentation'; +import setIndentation from '../../../lib/publicApi/block/setIndentation'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelFormatter, + FormatWithContentModelContext, +} from 'roosterjs-content-model-types'; + +describe('setIndentation', () => { + const fakeModel: any = { a: 'b' }; + let editor: IStandaloneEditor; + let formatContentModelSpy: jasmine.Spy; + let context: FormatWithContentModelContext; + + beforeEach(() => { + context = undefined!; + formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter) => { + context = { + newEntities: [], + newImages: [], + deletedEntities: [], + }; + callback(fakeModel, context); + }); + + editor = ({ + formatContentModel: formatContentModelSpy, + focus: jasmine.createSpy('focus'), + getPendingFormat: () => null as any, + } as any) as IStandaloneEditor; + }); + + it('indent', () => { + spyOn(setModelIndentation, 'setModelIndentation'); + + setIndentation(editor, 'indent'); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(setModelIndentation.setModelIndentation).toHaveBeenCalledTimes(1); + expect(setModelIndentation.setModelIndentation).toHaveBeenCalledWith( + fakeModel, + 'indent', + undefined + ); + expect(context).toEqual({ + newEntities: [], + newImages: [], + deletedEntities: [], + newPendingFormat: 'preserve', + }); + }); + + it('outdent', () => { + spyOn(setModelIndentation, 'setModelIndentation'); + + setIndentation(editor, 'outdent'); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(setModelIndentation.setModelIndentation).toHaveBeenCalledTimes(1); + expect(setModelIndentation.setModelIndentation).toHaveBeenCalledWith( + fakeModel, + 'outdent', + undefined + ); + expect(context).toEqual({ + newEntities: [], + newImages: [], + deletedEntities: [], + newPendingFormat: 'preserve', + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setParagraphMarginTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setParagraphMarginTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setParagraphMarginTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/block/setParagraphMarginTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setSpacingTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/setSpacingTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setSpacingTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/block/setSpacingTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/toggleBlockQuoteTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/toggleBlockQuoteTest.ts similarity index 54% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/block/toggleBlockQuoteTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/block/toggleBlockQuoteTest.ts index e1516101856..6d897dd1b17 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/toggleBlockQuoteTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/block/toggleBlockQuoteTest.ts @@ -1,26 +1,43 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import * as toggleModelBlockQuote from '../../../lib/modelApi/block/toggleModelBlockQuote'; import toggleBlockQuote from '../../../lib/publicApi/block/toggleBlockQuote'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelFormatter, + FormatWithContentModelContext, +} from 'roosterjs-content-model-types'; describe('toggleBlockQuote', () => { const fakeModel: any = { a: 'b' }; - let editor: IContentModelEditor; + let editor: IStandaloneEditor; + let formatContentModelSpy: jasmine.Spy; + let context: FormatWithContentModelContext; beforeEach(() => { + context = undefined!; + + formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter) => { + context = { + newEntities: [], + newImages: [], + deletedEntities: [], + }; + callback(fakeModel, context); + }); + editor = ({ focus: jasmine.createSpy('focus'), - createContentModel: () => fakeModel, - } as any) as IContentModelEditor; + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor; }); it('toggleBlockQuote', () => { - spyOn(formatWithContentModel, 'formatWithContentModel').and.callThrough(); spyOn(toggleModelBlockQuote, 'toggleModelBlockQuote'); toggleBlockQuote(editor, { a: 'b', c: 'd' } as any); - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(toggleModelBlockQuote.toggleModelBlockQuote).toHaveBeenCalledTimes(1); expect(toggleModelBlockQuote.toggleModelBlockQuote).toHaveBeenCalledWith(fakeModel, { marginTop: '1em', @@ -31,15 +48,20 @@ describe('toggleBlockQuote', () => { a: 'b', c: 'd', } as any); + expect(context).toEqual({ + newEntities: [], + newImages: [], + deletedEntities: [], + newPendingFormat: 'preserve', + }); }); it('toggleBlockQuote with real format', () => { - spyOn(formatWithContentModel, 'formatWithContentModel').and.callThrough(); spyOn(toggleModelBlockQuote, 'toggleModelBlockQuote'); toggleBlockQuote(editor, { lineHeight: '2', textColor: 'red' }); - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(toggleModelBlockQuote.toggleModelBlockQuote).toHaveBeenCalledTimes(1); expect(toggleModelBlockQuote.toggleModelBlockQuote).toHaveBeenCalledWith(fakeModel, { marginTop: '1em', @@ -50,5 +72,11 @@ describe('toggleBlockQuote', () => { lineHeight: '2', textColor: 'red', } as any); + expect(context).toEqual({ + newEntities: [], + newImages: [], + deletedEntities: [], + newPendingFormat: 'preserve', + }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts index 79ff8e2b200..7706ab548ef 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts @@ -1,13 +1,15 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import * as insertEntityModel from '../../../lib/modelApi/entity/insertEntityModel'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import insertEntity from '../../../lib/publicApi/entity/insertEntity'; -import { ChangeSource } from '../../../lib/publicTypes/event/ContentModelContentChangedEvent'; -import { FormatWithContentModelContext } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { ChangeSource } from 'roosterjs-content-model-core'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + FormatWithContentModelContext, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; describe('insertEntity', () => { - let editor: IContentModelEditor; + let editor: IStandaloneEditor; let context: FormatWithContentModelContext; let wrapper: HTMLElement; const model = 'MockedModel' as any; @@ -46,13 +48,12 @@ describe('insertEntity', () => { appendChild: appendChildSpy, } as any; - formatWithContentModelSpy = spyOn( - formatWithContentModel, - 'formatWithContentModel' - ).and.callFake((editor, apiName, formatter, options) => { - formatter(model, context); - options?.getChangeData?.(); - }); + formatWithContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake((formatter: Function, options: FormatWithContentModelOptions) => { + formatter(model, context); + }); + triggerContentChangedEventSpy = jasmine.createSpy('triggerContentChangedEventSpy'); createElementSpy = jasmine.createSpy('createElementSpy').and.returnValue(wrapper); getDocumentSpy = jasmine.createSpy('getDocumentSpy').and.returnValue({ @@ -65,6 +66,7 @@ describe('insertEntity', () => { getDocument: getDocumentSpy, isDarkMode: isDarkModeSpy, transformToDarkColor: transformToDarkColorSpy, + formatContentModel: formatWithContentModelSpy, } as any; }); @@ -74,9 +76,8 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('span'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(appendChildSpy).not.toHaveBeenCalled(); - expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); - expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); - expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( + expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName); + expect(formatWithContentModelSpy.calls.argsFor(0)[1].changeSource).toEqual( ChangeSource.InsertEntity ); expect(insertEntityModelSpy).toHaveBeenCalledWith( @@ -120,9 +121,8 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('div'); expect(setPropertySpy).toHaveBeenCalledWith('display', null); expect(appendChildSpy).not.toHaveBeenCalled(); - expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); - expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); - expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( + expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName); + expect(formatWithContentModelSpy.calls.argsFor(0)[1].changeSource).toEqual( ChangeSource.InsertEntity ); expect(insertEntityModelSpy).toHaveBeenCalledWith( @@ -173,9 +173,8 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('div'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'none'); expect(appendChildSpy).toHaveBeenCalledWith(contentNode); - expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); - expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); - expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( + expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName); + expect(formatWithContentModelSpy.calls.argsFor(0)[1].changeSource).toEqual( ChangeSource.InsertEntity ); @@ -222,9 +221,8 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('span'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(appendChildSpy).not.toHaveBeenCalled(); - expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); - expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); - expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( + expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName); + expect(formatWithContentModelSpy.calls.argsFor(0)[1].changeSource).toBe( ChangeSource.InsertEntity ); expect(insertEntityModelSpy).toHaveBeenCalledWith( diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts similarity index 55% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts index 7eddbeba0bf..7a5893e74dd 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts @@ -1,28 +1,35 @@ import * as clearModelFormat from '../../../lib/modelApi/common/clearModelFormat'; -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import clearFormat from '../../../lib/publicApi/format/clearFormat'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; describe('clearFormat', () => { it('Clear format', () => { + const model = ('Model' as any) as ContentModelDocument; + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toEqual('clearFormat'); + callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); + } + ); + const editor = ({ focus: () => {}, - } as any) as IContentModelEditor; - const model = ('Model' as any) as ContentModelDocument; + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor; - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_, apiName, callback) => { - expect(apiName).toEqual('clearFormat'); - callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); - } - ); spyOn(clearModelFormat, 'clearModelFormat'); spyOn(normalizeContentModel, 'normalizeContentModel'); clearFormat(editor); - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(clearModelFormat.clearModelFormat).toHaveBeenCalledTimes(1); expect(clearModelFormat.clearModelFormat).toHaveBeenCalledWith(model, [], [], []); expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/format/getFormatStateTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/format/getFormatStateTest.ts index ecc1bbe8775..55393ebc998 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/format/getFormatStateTest.ts @@ -1,8 +1,7 @@ -import * as getPendingFormat from '../../../lib/modelApi/format/pendingFormat'; +import * as getSelectionRootNode from 'roosterjs-content-model-core/lib/publicApi/selection/getSelectionRootNode'; import * as retrieveModelFormatState from '../../../lib/modelApi/common/retrieveModelFormatState'; -import { ContentModelFormatState } from '../../../lib/publicTypes/format/formatState/ContentModelFormatState'; -import { DomToModelContext } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { ContentModelFormatState, DomToModelContext } from 'roosterjs-content-model-types'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; import getFormatState, { reducedModelChildProcessor, } from '../../../lib/publicApi/format/getFormatState'; @@ -37,6 +36,7 @@ describe('getFormatState', () => { }), isDarkMode: () => false, getZoomScale: () => 1, + getPendingFormat: () => pendingFormat, createContentModel: (options: DomToModelOption) => { const model = createContentModelDocument(); const editorDiv = document.createElement('div'); @@ -63,9 +63,7 @@ describe('getFormatState', () => { return model; }, - } as any) as IContentModelEditor; - - spyOn(getPendingFormat, 'getPendingFormat').and.returnValue(pendingFormat); + } as any) as IStandaloneEditor; const result = getFormatState(editor); @@ -180,8 +178,10 @@ describe('getFormatState', () => { ); }); }); + describe('reducedModelChildProcessor', () => { let context: DomToModelContext; + let getSelectionRootNodeSpy: jasmine.Spy; beforeEach(() => { context = createDomToModelContext(undefined, { @@ -189,6 +189,11 @@ describe('reducedModelChildProcessor', () => { child: reducedModelChildProcessor, }, }); + + getSelectionRootNodeSpy = spyOn( + getSelectionRootNode, + 'getSelectionRootNode' + ).and.callThrough(); }); it('Empty DOM', () => { @@ -201,6 +206,7 @@ describe('reducedModelChildProcessor', () => { blockGroupType: 'Document', blocks: [], }); + expect(getSelectionRootNodeSpy).toHaveBeenCalledTimes(1); }); it('Single child node, with selected Node in context', () => { @@ -236,6 +242,7 @@ describe('reducedModelChildProcessor', () => { }, ], }); + expect(getSelectionRootNodeSpy).toHaveBeenCalledTimes(1); }); it('Multiple child nodes, with selected Node in context', () => { @@ -277,6 +284,7 @@ describe('reducedModelChildProcessor', () => { }, ], }); + expect(getSelectionRootNodeSpy).toHaveBeenCalledTimes(1); }); it('Multiple child nodes, with selected Node in context, with more child nodes under selected node', () => { @@ -340,6 +348,7 @@ describe('reducedModelChildProcessor', () => { }, ], }); + expect(getSelectionRootNodeSpy).toHaveBeenCalledTimes(1); }); it('Multiple layer with multiple child nodes, with selected Node in context, with more child nodes under selected node', () => { @@ -399,6 +408,7 @@ describe('reducedModelChildProcessor', () => { { blockType: 'Paragraph', segments: [], format: {}, isImplicit: true }, ], }); + expect(getSelectionRootNodeSpy).toHaveBeenCalledTimes(1); }); it('With table, need to do format for all table cells', () => { @@ -478,5 +488,6 @@ describe('reducedModelChildProcessor', () => { }, ], }); + expect(getSelectionRootNodeSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/adjustImageSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/adjustImageSelectionTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/image/adjustImageSelectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/image/adjustImageSelectionTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts similarity index 71% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts index a2fec41de95..d018958bee3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/changeImageTest.ts @@ -1,9 +1,12 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import * as readFile from '../../../lib/domUtils/readFile'; +import * as readFile from '../../../lib/modelApi/domUtils/readFile'; import changeImage from '../../../lib/publicApi/image/changeImage'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; import { PluginEventType } from 'roosterjs-editor-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { addSegment, createContentModelDocument, @@ -19,49 +22,42 @@ describe('changeImage', () => { function runTest( model: ContentModelDocument, - executionCallback: (editor: IContentModelEditor) => void, + executionCallback: (editor: IStandaloneEditor) => void, result: ContentModelDocument, calledTimes: number ) { - const addUndoSnapshot = jasmine - .createSpy('addUndoSnapshot') - .and.callFake( - (callback: () => void, source: string, canUndoByBackspace, param: any) => { - expect(source).toBe(undefined!); - expect(param.formatApiName).toBe('changeImage'); - callback(); - } - ); - const setContentModel = jasmine.createSpy().and.callFake((model: ContentModelDocument) => { - expect(model).toEqual(result); - }); - - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); - const getDOMSelection = jasmine .createSpy() .and.returnValues({ type: 'image', image: imageNode }); triggerPluginEvent = jasmine.createSpy('triggerPluginEvent').and.callThrough(); - const getVisibleViewport = jasmine.createSpy().and.callThrough(); + + let formatResult: boolean | undefined; + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: options.rawEvent, + }); + } + ); const editor = ({ - createContentModel: () => model, - addUndoSnapshot, focus: jasmine.createSpy(), - setContentModel, isDisposed: () => false, - getDocument: () => document, + getPendingFormat: () => null as any, getDOMSelection, triggerPluginEvent, - getVisibleViewport, - isDarkMode: () => false, - } as any) as IContentModelEditor; + formatContentModel, + } as any) as IStandaloneEditor; executionCallback(editor); - expect(addUndoSnapshot).toHaveBeenCalledTimes(calledTimes); - expect(setContentModel).toHaveBeenCalledTimes(calledTimes); + expect(formatResult).toBe(calledTimes > 0); + expect(formatContentModel).toHaveBeenCalledTimes(1); expect(model).toEqual(result); } @@ -154,15 +150,6 @@ describe('changeImage', () => { }, 1 ); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { - contentModel: doc, - selection: undefined, - source: 'Format', - data: undefined, - additionalData: { formatApiName: 'changeImage' }, - }); }); it('Doc with selection and image', () => { @@ -207,19 +194,12 @@ describe('changeImage', () => { 1 ); - expect(triggerPluginEvent).toHaveBeenCalledTimes(2); + expect(triggerPluginEvent).toHaveBeenCalledTimes(1); expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EditImage, { image: imageNode, newSrc: testUrl, previousSrc: 'test', originalSrc: '', }); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { - contentModel: doc, - selection: undefined, - source: 'Format', - data: undefined, - additionalData: { formatApiName: 'changeImage' }, - }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/insertImageTest.ts similarity index 80% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/image/insertImageTest.ts index 2beb29181ab..60a3e63a7f3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/insertImageTest.ts @@ -1,7 +1,11 @@ -import * as readFile from '../../../lib/domUtils/readFile'; +import * as readFile from '../../../lib/modelApi/domUtils/readFile'; import insertImage from '../../../lib/publicApi/image/insertImage'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { addSegment, createContentModelDocument, @@ -13,42 +17,38 @@ describe('insertImage', () => { function runTest( apiName: string, - executionCallback: (editor: IContentModelEditor) => void, + executionCallback: (editor: IStandaloneEditor) => void, model: ContentModelDocument, result: ContentModelDocument, calledTimes: number ) { - const addUndoSnapshot = jasmine - .createSpy() + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') .and.callFake( - (callback: () => void, source: string, canUndoByBackspace, param: any) => { - expect(source).toBe(undefined!); - expect(param.formatApiName).toBe(apiName); - callback(); + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); } ); - const setContentModel = jasmine.createSpy().and.callFake((model: ContentModelDocument) => { - expect(model).toEqual(result); - }); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); const editor = ({ - createContentModel: () => model, - addUndoSnapshot, focus: jasmine.createSpy(), - setContentModel, isDisposed: () => false, - getDocument: () => document, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + formatContentModel, + } as any) as IStandaloneEditor; executionCallback(editor); - expect(addUndoSnapshot).toHaveBeenCalledTimes(calledTimes); - expect(setContentModel).toHaveBeenCalledTimes(calledTimes); expect(model).toEqual(result); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatContentModel.calls.argsFor(0)[1]).toEqual({ + apiName, + }); + expect(formatResult).toBe(calledTimes > 0); } beforeEach(() => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageAltTextTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageAltTextTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageAltTextTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageAltTextTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageBorderTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageBorderTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageBorderTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageBorderTest.ts index 32dac75e5ea..2110db87731 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageBorderTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageBorderTest.ts @@ -1,6 +1,5 @@ import setImageBorder from '../../../lib/publicApi/image/setImageBorder'; -import { Border } from '../../../lib/publicTypes/interface/Border'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { Border, ContentModelDocument } from 'roosterjs-content-model-types'; import { segmentTestCommon } from '../segment/segmentTestCommon'; import { addSegment, diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageBoxShadowTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageBoxShadowTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/image/setImageBoxShadowTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/image/setImageBoxShadowTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts similarity index 89% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts index 28ce8e237be..bd4fe07ee36 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/adjustLinkSelectionTest.ts @@ -1,6 +1,11 @@ import adjustLinkSelection from '../../../lib/publicApi/link/adjustLinkSelection'; -import { ContentModelDocument, ContentModelLink } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelLink, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { addLink, addSegment, @@ -12,27 +17,35 @@ import { } from 'roosterjs-content-model-dom'; describe('adjustLinkSelection', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let editor: IStandaloneEditor; + let createContentModel: jasmine.Spy; + let formatContentModel: jasmine.Spy; + let formatResult: boolean | undefined; + let model: ContentModelDocument | undefined; beforeEach(() => { - setContentModel = jasmine.createSpy('setContentModel'); createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); + + model = undefined; + formatResult = undefined; + + formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + model = createContentModel(); + + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); editor = ({ - focus: () => {}, - addUndoSnapshot: (callback: Function) => callback(), - setContentModel, - createContentModel, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + formatContentModel, + } as any) as IStandaloneEditor; }); function runTest( @@ -45,11 +58,11 @@ describe('adjustLinkSelection', () => { const [text, url] = adjustLinkSelection(editor); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(!!expectedModel); + if (expectedModel) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith(expectedModel, undefined, undefined); - } else { - expect(setContentModel).not.toHaveBeenCalled(); + expect(model).toEqual(expectedModel); } expect(text).toBe(expectedText); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts similarity index 86% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts index da7c7fd0422..b63fcf8da10 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/insertLinkTest.ts @@ -1,9 +1,14 @@ -import ContentModelEditor from '../../../lib/editor/ContentModelEditor'; import insertLink from '../../../lib/publicApi/link/insertLink'; -import { ChangeSource } from '../../../lib/publicTypes/event/ContentModelContentChangedEvent'; -import { ContentModelDocument, ContentModelLink } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { ChangeSource } from 'roosterjs-content-model-core'; +import { ContentModelEditor } from 'roosterjs-content-model-editor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; import { PluginEventType } from 'roosterjs-editor-types'; +import { + ContentModelDocument, + ContentModelLink, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { addSegment, createContentModelDocument, @@ -13,29 +18,13 @@ import { } from 'roosterjs-content-model-dom'; describe('insertLink', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let editor: IStandaloneEditor; beforeEach(() => { - setContentModel = jasmine.createSpy('setContentModel'); - createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - editor = ({ focus: () => {}, - addUndoSnapshot: (callback: Function) => callback(), - setContentModel, - createContentModel, - getCustomData: () => ({}), - getFocusedPosition: () => ({}), - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + getPendingFormat: () => null as any, + } as any) as IStandaloneEditor; }); function runTest( @@ -46,21 +35,40 @@ describe('insertLink', () => { displayText?: string, target?: string ) { - createContentModel.and.returnValue(model); + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + + editor.formatContentModel = formatContentModel; insertLink(editor, url, title, displayText, target); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(!!expectedModel); + if (expectedModel) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel.calls.argsFor(0)[0]).toEqual(expectedModel); - expect(typeof setContentModel.calls.argsFor(0)[2]).toEqual('function'); - } else { - expect(setContentModel).not.toHaveBeenCalled(); + expect(model).toEqual(expectedModel); } } it('Empty link string', () => { - runTest(createContentModelDocument(), '', null); + const formatContentModel = jasmine.createSpy('formatContentModel'); + + editor.formatContentModel = formatContentModel; + + insertLink(editor, ''); + + expect(formatContentModel).not.toHaveBeenCalled(); }); it('Valid url', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts similarity index 85% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts index f4f36884304..d50ab264da6 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/link/removeLinkTest.ts @@ -1,6 +1,11 @@ import removeLink from '../../../lib/publicApi/link/removeLink'; -import { ContentModelDocument, ContentModelLink } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelLink, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { addLink, addSegment, @@ -10,39 +15,39 @@ import { } from 'roosterjs-content-model-dom'; describe('removeLink', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let editor: IStandaloneEditor; beforeEach(() => { - setContentModel = jasmine.createSpy('setContentModel'); - createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - editor = ({ focus: () => {}, - addUndoSnapshot: (callback: Function) => callback(), - setContentModel, - createContentModel, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor; }); function runTest(model: ContentModelDocument, expectedModel: ContentModelDocument | null) { - createContentModel.and.returnValue(model); + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: options.rawEvent, + }); + } + ); + + editor.formatContentModel = formatContentModel; removeLink(editor); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(!!expectedModel); + if (expectedModel) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith(expectedModel, undefined, undefined); - } else { - expect(setContentModel).not.toHaveBeenCalled(); + expect(model).toEqual(expectedModel); } } diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/setListStartNumberTest.ts similarity index 90% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/list/setListStartNumberTest.ts index 1aec33f79ae..a049ab7ad31 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/setListStartNumberTest.ts @@ -1,6 +1,9 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import setListStartNumber from '../../../lib/publicApi/list/setListStartNumber'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; describe('setListStartNumber', () => { function runTest( @@ -8,27 +11,30 @@ describe('setListStartNumber', () => { expectedModel: ContentModelDocument, expectedResult: boolean ) { - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (editor, apiName, callback) => { - expect(apiName).toBe('setListStartNumber'); - const result = callback(input, { - newEntities: [], - deletedEntities: [], - newImages: [], - }); - - expect(result).toBe(expectedResult); - } - ); + let formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toBe('setListStartNumber'); + const result = callback(input, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + + expect(result).toBe(expectedResult); + } + ); setListStartNumber( { + formatContentModel: formatContentModelSpy, focus: () => {}, } as any, 2 ); - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(input).toEqual(expectedModel); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/setListStyleTest.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/list/setListStyleTest.ts index 14efd88584e..811b7ad8972 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/setListStyleTest.ts @@ -1,4 +1,3 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import setListStyle from '../../../lib/publicApi/list/setListStyle'; import { ContentModelDocument, ListMetadataFormat } from 'roosterjs-content-model-types'; @@ -9,9 +8,10 @@ describe('setListStyle', () => { expectedModel: ContentModelDocument, expectedResult: boolean ) { - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (editor, apiName, callback) => { - expect(apiName).toBe('setListStyle'); + const formatWithContentModelSpy = jasmine + .createSpy('formatWithContentModel') + .and.callFake((callback, options) => { + expect(options.apiName).toBe('setListStyle'); const result = callback(input, { newEntities: [], deletedEntities: [], @@ -19,17 +19,17 @@ describe('setListStyle', () => { }); expect(result).toBe(expectedResult); - } - ); + }); setListStyle( { focus: () => {}, + formatContentModel: formatWithContentModelSpy, } as any, style ); - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); + expect(formatWithContentModelSpy).toHaveBeenCalledTimes(1); expect(input).toEqual(expectedModel); } diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleBulletTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleBulletTest.ts new file mode 100644 index 00000000000..e603930384b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleBulletTest.ts @@ -0,0 +1,60 @@ +import * as setListType from '../../../lib/modelApi/list/setListType'; +import toggleBullet from '../../../lib/publicApi/list/toggleBullet'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelContext, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; + +describe('toggleBullet', () => { + let editor = ({} as any) as IStandaloneEditor; + let formatContentModel: jasmine.Spy; + let focus: jasmine.Spy; + let mockedModel: ContentModelDocument; + let context: FormatWithContentModelContext; + + beforeEach(() => { + mockedModel = ({} as any) as ContentModelDocument; + + context = undefined!; + formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + context = { + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: options.rawEvent, + }; + callback(mockedModel, context); + } + ); + focus = jasmine.createSpy('focus'); + + editor = ({ + focus, + formatContentModel, + getCustomData: () => ({}), + getFocusedPosition: () => ({}), + } as any) as IStandaloneEditor; + + spyOn(setListType, 'setListType').and.returnValue(true); + }); + + it('toggleBullet', () => { + toggleBullet(editor); + + expect(setListType.setListType).toHaveBeenCalledTimes(1); + expect(setListType.setListType).toHaveBeenCalledWith(mockedModel, 'UL'); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: undefined, + newPendingFormat: 'preserve', + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleNumberingTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleNumberingTest.ts new file mode 100644 index 00000000000..20f6f6f92da --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/list/toggleNumberingTest.ts @@ -0,0 +1,58 @@ +import * as setListType from '../../../lib/modelApi/list/setListType'; +import toggleNumbering from '../../../lib/publicApi/list/toggleNumbering'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelContext, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; + +describe('toggleNumbering', () => { + let editor = ({} as any) as IStandaloneEditor; + let focus: jasmine.Spy; + let mockedModel: ContentModelDocument; + let context: FormatWithContentModelContext; + + beforeEach(() => { + mockedModel = ({} as any) as ContentModelDocument; + + context = undefined!; + focus = jasmine.createSpy('focus'); + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + context = { + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: options.rawEvent, + }; + callback(mockedModel, context); + } + ); + + editor = ({ + focus, + formatContentModel, + } as any) as IStandaloneEditor; + + spyOn(setListType, 'setListType').and.returnValue(true); + }); + + it('toggleNumbering', () => { + toggleNumbering(editor); + + expect(setListType.setListType).toHaveBeenCalledTimes(1); + expect(setListType.setListType).toHaveBeenCalledWith(mockedModel, 'OL'); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: undefined, + newPendingFormat: 'preserve', + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/applySegmentFormatTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/applySegmentFormatTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/applySegmentFormatTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/applySegmentFormatTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeCapitalizationTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/changeCapitalizationTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeCapitalizationTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/changeCapitalizationTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/changeFontSizeTest.ts similarity index 86% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/changeFontSizeTest.ts index 0ea70c15776..69de2270a9b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/changeFontSizeTest.ts @@ -1,10 +1,14 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; import changeFontSize from '../../../lib/publicApi/segment/changeFontSize'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createDomToModelContext, domToContentModel } from 'roosterjs-content-model-dom'; import { createRange } from 'roosterjs-editor-dom'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; import { segmentTestCommon } from './segmentTestCommon'; +import { + ContentModelDocument, + ContentModelSegmentFormat, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; describe('changeFontSize', () => { function runTest( @@ -328,15 +332,6 @@ describe('changeFontSize', () => { }); it('Test format parser', () => { - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - - const addUndoSnapshot = jasmine.createSpy().and.callFake((callback: () => void) => { - callback(); - }); - const setContentModel = jasmine.createSpy(); const div = document.createElement('div'); const sub = document.createElement('sub'); @@ -344,44 +339,53 @@ describe('changeFontSize', () => { div.appendChild(sub); div.style.fontSize = '20pt'; + const model = domToContentModel(div, createDomToModelContext(undefined), { + type: 'range', + range: createRange(sub), + }); + + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + const editor = ({ - createContentModel: (option: any) => - domToContentModel(div, createDomToModelContext(undefined), { - type: 'range', - range: createRange(sub), - }), - addUndoSnapshot, + formatContentModel, focus: jasmine.createSpy(), - setContentModel, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + getPendingFormat: () => null as ContentModelSegmentFormat, + } as any) as IStandaloneEditor; changeFontSize(editor, 'increase'); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test', - format: { superOrSubScriptSequence: 'sub' }, - isSelected: true, - }, - ], - isImplicit: true, - }, - ], - }, - undefined, - undefined - ); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { superOrSubScriptSequence: 'sub' }, + isSelected: true, + }, + ], + isImplicit: true, + }, + ], + }); }); it('Paragraph has font size', () => { diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/segmentTestCommon.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/segmentTestCommon.ts new file mode 100644 index 00000000000..4ed22a5ea0c --- /dev/null +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/segmentTestCommon.ts @@ -0,0 +1,39 @@ +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { NodePosition } from 'roosterjs-editor-types'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; + +export function segmentTestCommon( + apiName: string, + executionCallback: (editor: IStandaloneEditor) => void, + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number +) { + let formatResult: boolean | undefined; + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toBe(apiName); + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + }); + const editor = ({ + focus: jasmine.createSpy(), + getFocusedPosition: () => null as NodePosition, + getPendingFormat: () => null as any, + formatContentModel, + } as any) as IStandaloneEditor; + + executionCallback(editor); + + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(calledTimes > 0); + expect(model).toEqual(result); +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setBackgroundColorTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setBackgroundColorTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setBackgroundColorTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setBackgroundColorTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setFontNameTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setFontNameTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setFontNameTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setFontNameTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setFontSizeTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setFontSizeTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setFontSizeTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setFontSizeTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setTextColorTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setTextColorTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/setTextColorTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/setTextColorTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleBoldTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleBoldTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleBoldTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleBoldTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleCodeTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleCodeTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleCodeTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleCodeTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleItalicTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleItalicTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleItalicTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleItalicTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleStrikethroughTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleStrikethroughTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleStrikethroughTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleStrikethroughTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleSubscriptTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleSubscriptTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleSubscriptTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleSubscriptTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleSuperscriptTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleSuperscriptTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleSuperscriptTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleSuperscriptTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleUnderlineTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleUnderlineTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/toggleUnderlineTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/segment/toggleUnderlineTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInBlockTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/selection/hasSelectionInBlockTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInBlockTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/selection/hasSelectionInBlockTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInSegmentTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/selection/hasSelectionInSegmentTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInSegmentTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/selection/hasSelectionInSegmentTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/applyTableBorderFormatTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/table/applyTableBorderFormatTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts index 387c3477a70..5644056ff92 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/applyTableBorderFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/table/applyTableBorderFormatTest.ts @@ -1,18 +1,19 @@ -import * as normalizeTable from '../../../lib/modelApi/table/normalizeTable'; +import * as normalizeTable from 'roosterjs-content-model-core/lib/publicApi/table/normalizeTable'; import applyTableBorderFormat from '../../../lib/publicApi/table/applyTableBorderFormat'; -import { Border } from '../../../lib/publicTypes/interface/Border'; -import { BorderOperations } from '../../../lib/publicTypes/enum/BorderOperations'; -import { ContentModelTable, ContentModelTableCell } from 'roosterjs-content-model-types'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; import { createTable, createTableCell } from 'roosterjs-content-model-dom'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + Border, + BorderOperations, + ContentModelTable, + ContentModelTableCell, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; describe('applyTableBorderFormat', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let editor: IStandaloneEditor; const width = '3px'; const style = 'double'; const color = '#AABBCC'; @@ -40,22 +41,9 @@ describe('applyTableBorderFormat', () => { } beforeEach(() => { - setContentModel = jasmine.createSpy('setContentModel'); - createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - spyOn(normalizeTable, 'normalizeTable'); - editor = ({ - focus: () => {}, - addUndoSnapshot: (callback: Function) => callback(), - setContentModel, - createContentModel, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + editor = ({} as any) as IStandaloneEditor; }); function runTest( @@ -67,23 +55,30 @@ describe('applyTableBorderFormat', () => { const model = createContentModelDocument(); model.blocks.push(table); - createContentModel.and.returnValue(model); + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + + editor.formatContentModel = formatContentModel; applyTableBorderFormat(editor, border, operation); - if (expectedTable) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [expectedTable], - }, - undefined, - undefined - ); - } else { - expect(setContentModel).not.toHaveBeenCalled(); - } + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [expectedTable], + }); } it('All Borders', () => { runTest( diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/table/setTableCellShadeTest.ts similarity index 91% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/table/setTableCellShadeTest.ts index d4df63630a1..e1b7c52b50c 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/table/setTableCellShadeTest.ts @@ -1,33 +1,22 @@ -import * as normalizeTable from '../../../lib/modelApi/table/normalizeTable'; +import * as normalizeTable from 'roosterjs-content-model-core/lib/publicApi/table/normalizeTable'; import setTableCellShade from '../../../lib/publicApi/table/setTableCellShade'; -import { ContentModelTable } from 'roosterjs-content-model-types'; import { createContentModelDocument } from 'roosterjs-content-model-dom'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelTable, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; describe('setTableCellShade', () => { - let editor: IContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let editor: IStandaloneEditor; beforeEach(() => { - setContentModel = jasmine.createSpy('setContentModel'); - createContentModel = jasmine.createSpy('createContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - spyOn(normalizeTable, 'normalizeTable'); editor = ({ focus: () => {}, - addUndoSnapshot: (callback: Function) => callback(), - setContentModel, - createContentModel, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor; }); function runTest( @@ -38,22 +27,32 @@ describe('setTableCellShade', () => { const model = createContentModelDocument(); model.blocks.push(table); - createContentModel.and.returnValue(model); + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + + editor.formatContentModel = formatContentModel; setTableCellShade(editor, colorValue); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(!!expectedTable); + if (expectedTable) { - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [expectedTable], - }, - undefined, - undefined - ); - } else { - expect(setContentModel).not.toHaveBeenCalled(); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [expectedTable], + }); } } it('Empty table', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatImageWithContentModelTest.ts similarity index 79% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatImageWithContentModelTest.ts index 0cd533b469e..37f40684079 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatImageWithContentModelTest.ts @@ -1,8 +1,11 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; import formatImageWithContentModel from '../../../lib/publicApi/utils/formatImageWithContentModel'; -import { ContentModelDocument, ContentModelImage } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { NodePosition } from 'roosterjs-editor-types'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelImage, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { addSegment, createContentModelDocument, @@ -193,40 +196,30 @@ describe('formatImageWithContentModel', () => { function segmentTestForPluginEvent( apiName: string, - executionCallback: (editor: IContentModelEditor) => void, + executionCallback: (editor: IStandaloneEditor) => void, model: ContentModelDocument, result: ContentModelDocument, calledTimes: number ) { - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); - - const addUndoSnapshot = jasmine - .createSpy() - .and.callFake((callback: () => void, source: string, canUndoByBackspace, param: any) => { - expect(source).toBe(undefined!); - expect(param.formatApiName).toBe(apiName); - callback(); + let formatResult: boolean | undefined; + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toBe(apiName); + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); }); - const triggerPluginEvent = jasmine.createSpy().and.callFake(() => {}); - const getVisibleViewport = jasmine.createSpy().and.callFake(() => {}); - const setContentModel = jasmine.createSpy().and.callFake((model: ContentModelDocument) => { - expect(model).toEqual(result); - }); const editor = ({ - createContentModel: () => model, - addUndoSnapshot, - focus: jasmine.createSpy(), - setContentModel, - isDisposed: () => false, - getFocusedPosition: () => null as NodePosition, - triggerPluginEvent, - isDarkMode: () => false, - getVisibleViewport, - } as any) as IContentModelEditor; + formatContentModel, + getPendingFormat: () => null as any, + } as any) as IStandaloneEditor; executionCallback(editor); - expect(addUndoSnapshot).toHaveBeenCalledTimes(calledTimes); - expect(setContentModel).toHaveBeenCalledTimes(calledTimes); + + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(calledTimes > 0); expect(model).toEqual(result); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatParagraphWithContentModelTest.ts similarity index 50% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatParagraphWithContentModelTest.ts index 38760f3ae5e..cb7d301229c 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatParagraphWithContentModelTest.ts @@ -1,7 +1,12 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; import { formatParagraphWithContentModel } from '../../../lib/publicApi/utils/formatParagraphWithContentModel'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelParagraph, + ContentModelFormatter, + FormatWithContentModelContext, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { createContentModelDocument, createParagraph, @@ -9,13 +14,9 @@ import { } from 'roosterjs-content-model-dom'; describe('formatParagraphWithContentModel', () => { - let editor: IContentModelEditor; - let addUndoSnapshot: jasmine.Spy; - let setContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let focus: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let editor: IStandaloneEditor; let model: ContentModelDocument; + let context: FormatWithContentModelContext; const mockedContainer = 'C' as any; const mockedOffset = 'O' as any; @@ -23,23 +24,28 @@ describe('formatParagraphWithContentModel', () => { const apiName = 'mockedApi'; beforeEach(() => { - addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); - setContentModel = jasmine.createSpy('setContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - focus = jasmine.createSpy('focus'); + context = undefined!; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + context = { + newEntities: [], + newImages: [], + deletedEntities: [], + rawEvent: options.rawEvent, + }; + + callback(model, context); + } + ); editor = ({ - focus, - addUndoSnapshot, - createContentModel: () => model, - setContentModel, - isDarkMode: () => false, getCustomData: () => ({}), getFocusedPosition: () => ({ node: mockedContainer, offset: mockedOffset }), - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + formatContentModel, + } as any) as IStandaloneEditor; }); it('empty doc', () => { @@ -55,7 +61,6 @@ describe('formatParagraphWithContentModel', () => { blockGroupType: 'Document', blocks: [], }); - expect(addUndoSnapshot).not.toHaveBeenCalled(); }); it('doc with selection', () => { @@ -90,7 +95,6 @@ describe('formatParagraphWithContentModel', () => { }, ], }); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); }); it('Preserve pending format', () => { @@ -103,30 +107,18 @@ describe('formatParagraphWithContentModel', () => { para.segments.push(text); model.blocks.push(para); - let cachedPendingFormat: any = 'PendingFormat'; - let cachedPendingContainer: any = 'PendingContainer'; - let cachedPendingOffset: any = 'PendingOffset'; + const callback = (paragraph: ContentModelParagraph) => { + paragraph.format.backgroundColor = 'red'; + }; - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(cachedPendingFormat); - spyOn(pendingFormat, 'setPendingFormat').and.callFake((_, format, container, offset) => { - cachedPendingFormat = format; - cachedPendingContainer = container; - cachedPendingOffset = offset; - }); - spyOn(pendingFormat, 'clearPendingFormat').and.callFake(() => { - cachedPendingFormat = null; - cachedPendingContainer = null; - cachedPendingOffset = null; - }); - - formatParagraphWithContentModel( - editor, - apiName, - paragraph => (paragraph.format.backgroundColor = 'red') - ); + formatParagraphWithContentModel(editor, apiName, callback); - expect(cachedPendingFormat).toEqual('PendingFormat'); - expect(cachedPendingContainer).toEqual(mockedContainer); - expect(cachedPendingOffset).toEqual(mockedOffset); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: undefined, + newPendingFormat: 'preserve', + }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts similarity index 77% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts rename to packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts index c61a6067f43..0512432c32a 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/utils/formatSegmentWithContentModelTest.ts @@ -1,8 +1,12 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import { ContentModelDocument, ContentModelSegmentFormat } from 'roosterjs-content-model-types'; import { formatSegmentWithContentModel } from '../../../lib/publicApi/utils/formatSegmentWithContentModel'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { NodePosition } from 'roosterjs-editor-types'; +import { IStandaloneEditor } from 'roosterjs-content-model-types'; +import { + ContentModelDocument, + ContentModelSegmentFormat, + ContentModelFormatter, + FormatWithContentModelContext, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; import { createContentModelDocument, createParagraph, @@ -11,38 +15,41 @@ import { } from 'roosterjs-content-model-dom'; describe('formatSegmentWithContentModel', () => { - let editor: IContentModelEditor; - let addUndoSnapshot: jasmine.Spy; - let setContentModel: jasmine.Spy; + let editor: IStandaloneEditor; let focus: jasmine.Spy; let model: ContentModelDocument; let getPendingFormat: jasmine.Spy; - let setPendingFormat: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; + let formatContentModel: jasmine.Spy; + let formatResult: boolean | undefined; + let context: FormatWithContentModelContext | undefined; const apiName = 'mockedApi'; beforeEach(() => { - addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); - setContentModel = jasmine.createSpy('setContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); + context = undefined; + formatResult = undefined; focus = jasmine.createSpy('focus'); - setPendingFormat = spyOn(pendingFormat, 'setPendingFormat'); - getPendingFormat = spyOn(pendingFormat, 'getPendingFormat'); + formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + context = { + newEntities: [], + deletedEntities: [], + newImages: [], + }; + formatResult = callback(model, context); + } + ); + + getPendingFormat = jasmine.createSpy('getPendingFormat'); editor = ({ focus, - addUndoSnapshot, - createContentModel: () => model, - setContentModel, - getFocusedPosition: () => null as NodePosition, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; + formatContentModel, + getPendingFormat, + } as any) as IStandaloneEditor; }); it('empty doc', () => { @@ -54,9 +61,9 @@ describe('formatSegmentWithContentModel', () => { blockGroupType: 'Document', blocks: [], }); - expect(addUndoSnapshot).not.toHaveBeenCalled(); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeFalse(); expect(getPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledTimes(0); }); it('doc with selection', () => { @@ -89,9 +96,14 @@ describe('formatSegmentWithContentModel', () => { }, ], }); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); expect(getPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledTimes(0); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + }); }); it('doc with selection, all segments are already in expected state', () => { @@ -135,13 +147,18 @@ describe('formatSegmentWithContentModel', () => { }, ], }); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); expect(segmentHasStyleCallback).toHaveBeenCalledTimes(1); expect(segmentHasStyleCallback).toHaveBeenCalledWith(text.format, text, para); expect(toggleStyleCallback).toHaveBeenCalledTimes(1); expect(toggleStyleCallback).toHaveBeenCalledWith(text.format, false, text, para); expect(getPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledTimes(0); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + }); }); it('doc with selection, some segments are in expected state', () => { @@ -205,7 +222,8 @@ describe('formatSegmentWithContentModel', () => { }, ], }); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); expect(segmentHasStyleCallback).toHaveBeenCalledTimes(2); expect(segmentHasStyleCallback).toHaveBeenCalledWith(text1.format, text1, para); expect(segmentHasStyleCallback).toHaveBeenCalledWith(text3.format, text3, para); @@ -213,7 +231,11 @@ describe('formatSegmentWithContentModel', () => { expect(toggleStyleCallback).toHaveBeenCalledWith(text1.format, true, text1, para); expect(toggleStyleCallback).toHaveBeenCalledWith(text3.format, true, text3, para); expect(getPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledTimes(0); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + }); }); it('Collapsed selection', () => { @@ -227,11 +249,6 @@ describe('formatSegmentWithContentModel', () => { para.segments.push(marker); model.blocks.push(para); - const mockedContainer = 'C' as any; - const mockedOffset = 'O' as any; - - editor.getFocusedPosition = () => ({ node: mockedContainer, offset: mockedOffset } as any); - formatSegmentWithContentModel(editor, apiName, format => (format.fontFamily = 'test')); expect(model).toEqual({ blockGroupType: 'Document', @@ -252,18 +269,18 @@ describe('formatSegmentWithContentModel', () => { }, ], }); - expect(addUndoSnapshot).toHaveBeenCalledTimes(0); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeFalse(); expect(getPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith( - editor, - { + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { fontSize: '10px', fontFamily: 'test', }, - mockedContainer, - mockedOffset - ); + }); }); it('With pending format', () => { @@ -297,12 +314,17 @@ describe('formatSegmentWithContentModel', () => { }, ], }); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); expect(pendingFormat).toEqual({ fontSize: '10px', fontFamily: 'test', }); expect(getPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledTimes(0); + expect(context).toEqual({ + newEntities: [], + deletedEntities: [], + newImages: [], + }); }); }); diff --git a/packages-content-model/roosterjs-content-model-core/lib/constants/ChangeSource.ts b/packages-content-model/roosterjs-content-model-core/lib/constants/ChangeSource.ts new file mode 100644 index 00000000000..7744e04c4f4 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/constants/ChangeSource.ts @@ -0,0 +1,59 @@ +/** + * Possible change sources. Here are the predefined sources. + * It can also be other string if the change source can't fall into these sources. + */ +export const ChangeSource = { + /** + * Content changed by auto link + */ + AutoLink: 'AutoLink', + /** + * Content changed by create link + */ + CreateLink: 'CreateLink', + /** + * Content changed by format + */ + Format: 'Format', + /** + * Content changed by image resize + */ + ImageResize: 'ImageResize', + /** + * Content changed by paste + */ + Paste: 'Paste', + /** + * Content changed by setContent API + */ + SetContent: 'SetContent', + /** + * Content changed by cut operation + */ + Cut: 'Cut', + /** + * Content changed by drag & drop operation + */ + Drop: 'Drop', + /** + * Insert a new entity into editor + */ + InsertEntity: 'InsertEntity', + /** + * Editor is switched to dark mode, content color is changed + */ + SwitchToDarkMode: 'SwitchToDarkMode', + /** + * Editor is switched to light mode, content color is changed + */ + SwitchToLightMode: 'SwitchToLightMode', + /** + * List chain reorganized numbers of lists + */ + ListChain: 'ListChain', + /** + * Keyboard event, used by Content Model. + * Data of this event will be the key code number + */ + Keyboard: 'Keyboard', +}; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts similarity index 86% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts index 071648a0a20..a4428a172ac 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/createContentModel.ts @@ -1,14 +1,16 @@ -import { cloneModel } from '../../modelApi/common/cloneModel'; -import type { DOMSelection, DomToModelOption } from 'roosterjs-content-model-types'; +import { cloneModel } from '../publicApi/model/cloneModel'; import { createDomToModelContext, createDomToModelContextWithConfig, domToContentModel, } from 'roosterjs-content-model-dom'; +import type { EditorCore } from 'roosterjs-editor-types'; import type { - ContentModelEditorCore, + DOMSelection, + DomToModelOption, CreateContentModel, -} from '../../publicTypes/ContentModelEditorCore'; + StandaloneEditorCore, +} from 'roosterjs-content-model-types'; /** * @internal @@ -41,7 +43,7 @@ export const createContentModel: CreateContentModel = (core, option, selectionOv }; function internalCreateContentModel( - core: ContentModelEditorCore, + core: StandaloneEditorCore & EditorCore, selection?: DOMSelection, option?: DomToModelOption ) { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/createEditorContext.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/createEditorContext.ts index 08aafeef4b0..8e68e5f3f1e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/createEditorContext.ts @@ -1,5 +1,4 @@ -import type { CreateEditorContext } from '../../publicTypes/ContentModelEditorCore'; -import type { EditorContext } from 'roosterjs-content-model-types'; +import type { EditorContext, CreateEditorContext } from 'roosterjs-content-model-types'; /** * @internal 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 new file mode 100644 index 00000000000..86e3365d913 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/formatContentModel.ts @@ -0,0 +1,174 @@ +import { ChangeSource } from '../constants/ChangeSource'; +import { ColorTransformDirection, EntityOperation, PluginEventType } from 'roosterjs-editor-types'; +import type { EditorCore, Entity } from 'roosterjs-editor-types'; +import type { + ContentModelContentChangedEvent, + DOMSelection, + EntityRemovalOperation, + FormatContentModel, + FormatWithContentModelContext, + StandaloneEditorCore, +} from 'roosterjs-content-model-types'; + +/** + * @internal + * 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 core The StandaloneEditorCore object + * @param formatter Formatter function, see ContentModelFormatter + * @param options More options, see FormatWithContentModelOptions + */ +export const formatContentModel: FormatContentModel = (core, formatter, options) => { + const { apiName, onNodeCreated, getChangeData, changeSource, rawEvent, selectionOverride } = + options || {}; + + const model = core.api.createContentModel(core, undefined /*option*/, selectionOverride); + const context: FormatWithContentModelContext = { + newEntities: [], + deletedEntities: [], + rawEvent, + newImages: [], + }; + let selection: DOMSelection | undefined; + + if (formatter(model, context)) { + const writeBack = () => { + handleNewEntities(core, context); + handleDeletedEntities(core, context); + handleImages(core, context); + + selection = + core.api.setContentModel(core, model, undefined /*options*/, onNodeCreated) || + undefined; + + handlePendingFormat(core, context, selection); + }; + + if (context.skipUndoSnapshot) { + writeBack(); + } else { + core.api.addUndoSnapshot( + core, + writeBack, + null /*changeSource, passing undefined here to avoid triggering ContentChangedEvent. We will trigger it using it with Content Model below */, + false /*canUndoByBackspace*/, + { + formatApiName: apiName, + } + ); + } + + const eventData: ContentModelContentChangedEvent = { + eventType: PluginEventType.ContentChanged, + contentModel: context.clearModelCache ? undefined : model, + selection: context.clearModelCache ? undefined : selection, + source: changeSource || ChangeSource.Format, + data: getChangeData?.(), + additionalData: { + formatApiName: apiName, + }, + }; + core.api.triggerEvent(core, eventData, true /*broadcast*/); + } else { + if (context.clearModelCache) { + core.cache.cachedModel = undefined; + core.cache.cachedSelection = undefined; + } + + handlePendingFormat(core, context, core.api.getDOMSelection(core)); + } +}; + +function handleNewEntities(core: EditorCore, context: FormatWithContentModelContext) { + // TODO: Ideally we can trigger NewEntity event here. But to be compatible with original editor code, we don't do it here for now. + // Once Content Model Editor can be standalone, we can change this behavior to move triggering NewEntity event code + // from EntityPlugin to here + + if (core.lifecycle.isDarkMode) { + context.newEntities.forEach(entity => { + core.api.transformColor( + core, + entity.wrapper, + true /*includeSelf*/, + null /*callback*/, + ColorTransformDirection.LightToDark + ); + }); + } +} + +// This is only used for compatibility with old editor +// TODO: Remove this map once we have standalone editor +const EntityOperationMap: Record = { + overwrite: EntityOperation.Overwrite, + removeFromEnd: EntityOperation.RemoveFromEnd, + removeFromStart: EntityOperation.RemoveFromStart, +}; + +function handleDeletedEntities(core: EditorCore, context: FormatWithContentModelContext) { + context.deletedEntities.forEach( + ({ + entity: { + wrapper, + entityFormat: { id, entityType, isReadonly }, + }, + operation, + }) => { + if (id && entityType) { + // TODO: Revisit this entity parameter for standalone editor, we may just directly pass ContentModelEntity object instead + const entity: Entity = { + id, + type: entityType, + isReadonly: !!isReadonly, + wrapper, + }; + core.api.triggerEvent( + core, + { + eventType: PluginEventType.EntityOperation, + entity, + operation: EntityOperationMap[operation], + rawEvent: context.rawEvent, + }, + false /*broadcast*/ + ); + } + } + ); +} + +function handleImages(core: EditorCore, context: FormatWithContentModelContext) { + if (context.newImages.length > 0) { + const viewport = core.getVisibleViewport(); + + if (viewport) { + const { left, right } = viewport; + const minMaxImageSize = 10; + const maxWidth = Math.max(right - left, minMaxImageSize); + context.newImages.forEach(image => { + image.format.maxWidth = `${maxWidth}px`; + }); + } + } +} + +function handlePendingFormat( + core: StandaloneEditorCore, + context: FormatWithContentModelContext, + selection?: DOMSelection | null +) { + const pendingFormat = + context.newPendingFormat == 'preserve' + ? core.format.pendingFormat?.format + : context.newPendingFormat; + + if (pendingFormat && selection?.type == 'range' && selection.range.collapsed) { + core.format.pendingFormat = { + format: { ...pendingFormat }, + posContainer: selection.range.startContainer, + posOffset: selection.range.startOffset, + }; + } +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/getDOMSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/getDOMSelection.ts similarity index 81% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/getDOMSelection.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/getDOMSelection.ts index 9b73b537fde..9297df45613 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/getDOMSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/getDOMSelection.ts @@ -1,9 +1,6 @@ import { SelectionRangeTypes } from 'roosterjs-editor-types'; -import type { - ContentModelEditorCore, - GetDOMSelection, -} from '../../publicTypes/ContentModelEditorCore'; -import type { DOMSelection } from 'roosterjs-content-model-types'; +import type { EditorCore } from 'roosterjs-editor-types'; +import type { DOMSelection, GetDOMSelection } from 'roosterjs-content-model-types'; /** * @internal @@ -12,7 +9,7 @@ export const getDOMSelection: GetDOMSelection = core => { return core.cache.cachedSelection ?? getNewSelection(core); }; -function getNewSelection(core: ContentModelEditorCore): DOMSelection | null { +function getNewSelection(core: EditorCore): DOMSelection | null { // TODO: Get rid of getSelectionRangeEx when we have standalone editor const rangeEx = core.api.getSelectionRangeEx(core); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/setContentModel.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/setContentModel.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts index 96a4290e5fa..f0cb94159b6 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/setContentModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setContentModel.ts @@ -1,9 +1,9 @@ -import type { SetContentModel } from '../../publicTypes/ContentModelEditorCore'; import { contentModelToDom, createModelToDomContext, createModelToDomContextWithConfig, } from 'roosterjs-content-model-dom'; +import type { SetContentModel } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/setDOMSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/setDOMSelection.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts index 44273137c3e..be46307b163 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/setDOMSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/setDOMSelection.ts @@ -1,6 +1,6 @@ import { SelectionRangeTypes } from 'roosterjs-editor-types'; import type { SelectionRangeEx } from 'roosterjs-editor-types'; -import type { SetDOMSelection } from '../../publicTypes/ContentModelEditorCore'; +import type { SetDOMSelection } from 'roosterjs-content-model-types'; /** * @internal diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/switchShadowEdit.ts b/packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts similarity index 79% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/switchShadowEdit.ts rename to packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts index 7ab5578d6b9..42329e710a2 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/switchShadowEdit.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/coreApi/switchShadowEdit.ts @@ -1,8 +1,7 @@ -import { getSelectionPath } from 'roosterjs-editor-dom'; -import { iterateSelections } from '../../modelApi/selection/iterateSelections'; +import { iterateSelections } from '../publicApi/selection/iterateSelections'; import { PluginEventType } from 'roosterjs-editor-types'; -import type { ContentModelEditorCore } from '../../publicTypes/ContentModelEditorCore'; -import type { SwitchShadowEdit } from 'roosterjs-editor-types'; +import type { StandaloneEditorCore } from 'roosterjs-content-model-types'; +import type { EditorCore, SelectionPath, SwitchShadowEdit } from 'roosterjs-editor-types'; /** * @internal @@ -12,17 +11,19 @@ import type { SwitchShadowEdit } from 'roosterjs-editor-types'; */ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => { // TODO: Use strong-typed editor core object - const core = editorCore as ContentModelEditorCore; + const core = editorCore as StandaloneEditorCore & EditorCore; if (isOn != !!core.lifecycle.shadowEditFragment) { if (isOn) { const model = !core.cache.cachedModel ? core.api.createContentModel(core) : null; - const range = core.api.getSelectionRange(core, true /*tryGetFromCache*/); // Fake object, not used in Content Model Editor, just to satisfy original editor code // TODO: we can remove them once we have standalone Content Model Editor const fragment = core.contentDiv.ownerDocument.createDocumentFragment(); - const selectionPath = range && getSelectionPath(core.contentDiv, range); + const selectionPath: SelectionPath = { + start: [], + end: [], + }; core.api.triggerEvent( core, @@ -56,7 +57,7 @@ export const switchShadowEdit: SwitchShadowEdit = (editorCore, isOn): void => { if (core.cache.cachedModel) { // Force clear cached element from selected block - iterateSelections([core.cache.cachedModel], () => {}); + iterateSelections(core.cache.cachedModel, () => {}); core.api.setContentModel(core, core.cache.cachedModel, { ignoreSelection: true, // Do not set focus and selection when quit shadow edit, focus may remain in UI control (picker, ...) diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts index b72c8bce0a2..107e36a6b15 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCachePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCachePlugin.ts @@ -1,9 +1,11 @@ -import { areSameRangeEx } from '../../modelApi/selection/areSameRangeEx'; -import { isCharacterValue } from '../../domUtils/eventUtils'; +import { areSameSelection } from './utils/areSameSelection'; +import { isCharacterValue } from '../publicApi/domUtils/eventUtils'; import { PluginEventType } from 'roosterjs-editor-types'; -import type ContentModelContentChangedEvent from '../../publicTypes/event/ContentModelContentChangedEvent'; -import type { ContentModelCachePluginState } from '../../publicTypes/pluginState/ContentModelCachePluginState'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { + ContentModelCachePluginState, + ContentModelContentChangedEvent, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; import type { IEditor, PluginEvent, @@ -14,9 +16,8 @@ import type { /** * ContentModel cache plugin manages cached Content Model, and refresh the cache when necessary */ -export default class ContentModelCachePlugin - implements PluginWithState { - private editor: IContentModelEditor | null = null; +export class ContentModelCachePlugin implements PluginWithState { + private editor: (IEditor & IStandaloneEditor) | null = null; /** * Construct a new instance of ContentModelEditPlugin class @@ -41,7 +42,7 @@ export default class ContentModelCachePlugin */ initialize(editor: IEditor) { // TODO: Later we may need a different interface for Content Model editor plugin - this.editor = editor as IContentModelEditor; + this.editor = editor as IEditor & IStandaloneEditor; this.editor.getDocument().addEventListener('selectionchange', this.onNativeSelectionChange); } @@ -123,7 +124,7 @@ export default class ContentModelCachePlugin } } - private updateCachedModel(editor: IContentModelEditor, forceUpdate?: boolean) { + private updateCachedModel(editor: IStandaloneEditor, forceUpdate?: boolean) { const cachedSelection = this.state.cachedSelection; this.state.cachedSelection = undefined; // Clear it to force getDOMSelection() retrieve the latest selection range @@ -133,7 +134,7 @@ export default class ContentModelCachePlugin forceUpdate || !cachedSelection || !newRangeEx || - !areSameRangeEx(newRangeEx, cachedSelection); + !areSameSelection(newRangeEx, cachedSelection); if (isSelectionChanged) { if ( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts index 87e79986880..c0ce1892653 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelCopyPastePlugin.ts @@ -1,13 +1,11 @@ -import paste from '../../publicApi/utils/paste'; -import { addRangeToSelection } from '../../domUtils/addRangeToSelection'; -import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent'; -import { cloneModel } from '../../modelApi/common/cloneModel'; +import { addRangeToSelection } from './utils/addRangeToSelection'; +import { ChangeSource } from '../constants/ChangeSource'; +import { cloneModel } from '../publicApi/model/cloneModel'; import { ColorTransformDirection, PluginEventType } from 'roosterjs-editor-types'; -import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep'; -import { deleteSelection } from '../../modelApi/edit/deleteSelection'; +import { deleteSelection } from '../publicApi/selection/deleteSelection'; import { extractClipboardItems } from 'roosterjs-editor-dom'; -import { formatWithContentModel } from '../../publicApi/utils/formatWithContentModel'; -import { iterateSelections } from '../../modelApi/selection/iterateSelections'; +import { iterateSelections } from '../publicApi/selection/iterateSelections'; +import { paste } from '../publicApi/model/paste'; import { contentModelToDom, createModelToDomContext, @@ -18,8 +16,7 @@ import { toArray, wrap, } from 'roosterjs-content-model-dom'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { DOMSelection, OnNodeCreated } from 'roosterjs-content-model-types'; +import type { DOMSelection, IStandaloneEditor, OnNodeCreated } from 'roosterjs-content-model-types'; import type { CopyPastePluginState, IEditor, @@ -30,8 +27,8 @@ import type { /** * Copy and paste plugin for handling onCopy and onPaste event */ -export default class ContentModelCopyPastePlugin implements PluginWithState { - private editor: IContentModelEditor | null = null; +export class ContentModelCopyPastePlugin implements PluginWithState { + private editor: (IStandaloneEditor & IEditor) | null = null; private disposer: (() => void) | null = null; /** @@ -52,7 +49,7 @@ export default class ContentModelCopyPastePlugin implements PluginWithState this.onPaste(e), copy: e => this.onCutCopy(e, false /*isCut*/), @@ -112,7 +109,7 @@ export default class ContentModelCopyPastePlugin implements PluginWithState { + iterateSelections(pasteModel, (_, tableContext) => { if (tableContext?.table) { const table = tableContext?.table; table.rows = table.rows @@ -152,26 +149,24 @@ export default class ContentModelCopyPastePlugin implements PluginWithState { + this.editor.runAsync(e => { + const editor = e as IStandaloneEditor & IEditor; + cleanUpAndRestoreSelection(tempDiv); editor.focus(); - (editor as IContentModelEditor).setDOMSelection(selection); + editor.setDOMSelection(selection); if (isCut) { - formatWithContentModel( - editor as IContentModelEditor, - 'cut', + editor.formatContentModel( (model, context) => { - if ( - deleteSelection(model, [], context).deleteResult == - DeleteResult.Range - ) { + if (deleteSelection(model, [], context).deleteResult == 'range') { normalizeContentModel(model); } return true; }, { + apiName: 'cut', changeSource: ChangeSource.Cut, } ); @@ -290,3 +285,12 @@ export const onNodeCreated: OnNodeCreated = (_, node): void => { node.removeAttribute('contenteditable'); } }; + +/** + * @internal + * Create a new instance of ContentModelCopyPastePlugin + * @param state The plugin state object + */ +export function createContentModelCopyPastePlugin(state: CopyPastePluginState) { + return new ContentModelCopyPastePlugin(state); +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts similarity index 65% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts index a33376c502e..c5f516e1d9a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelFormatPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelFormatPlugin.ts @@ -1,12 +1,13 @@ -import applyDefaultFormat from '../../publicApi/format/applyDefaultFormat'; -import applyPendingFormat from '../../publicApi/format/applyPendingFormat'; -import { canApplyPendingFormat, clearPendingFormat } from '../../modelApi/format/pendingFormat'; +import { applyDefaultFormat } from './utils/applyDefaultFormat'; +import { applyPendingFormat } from './utils/applyPendingFormat'; import { getObjectKeys } from 'roosterjs-content-model-dom'; -import { isCharacterValue } from '../../domUtils/eventUtils'; +import { isCharacterValue } from '../publicApi/domUtils/eventUtils'; import { PluginEventType } from 'roosterjs-editor-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import type { IEditor, PluginEvent, PluginWithState } from 'roosterjs-editor-types'; -import type { ContentModelFormatPluginState } from '../../publicTypes/pluginState/ContentModelFormatPluginState'; +import type { + ContentModelFormatPluginState, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; // During IME input, KeyDown event will have "Process" as key const ProcessKey = 'Process'; @@ -26,9 +27,8 @@ const CursorMovingKeys = new Set([ * This includes: * 1. Handle pending format changes when selection is collapsed */ -export default class ContentModelFormatPlugin - implements PluginWithState { - private editor: IContentModelEditor | null = null; +export class ContentModelFormatPlugin implements PluginWithState { + private editor: (IStandaloneEditor & IEditor) | null = null; private hasDefaultFormat = false; /** @@ -54,7 +54,7 @@ export default class ContentModelFormatPlugin */ initialize(editor: IEditor) { // TODO: Later we may need a different interface for Content Model editor plugin - this.editor = editor as IContentModelEditor; + this.editor = editor as IStandaloneEditor & IEditor; this.hasDefaultFormat = getObjectKeys(this.state.defaultFormat).filter( x => typeof this.state.defaultFormat[x] !== 'undefined' @@ -90,8 +90,12 @@ export default class ContentModelFormatPlugin switch (event.eventType) { case PluginEventType.Input: + const env = this.editor.getEnvironment(); + // In Safari, isComposing will be undefined but isInIME() works - if (!event.rawEvent.isComposing && !this.editor.isInIME()) { + // For Android, we can skip checking isComposing since this property is not always reliable in all IME, + // and we have tested without this check it can still work correctly + if (env.isAndroid || (!event.rawEvent.isComposing && !this.editor.isInIME())) { this.checkAndApplyPendingFormat(event.rawEvent.data); } @@ -103,7 +107,7 @@ export default class ContentModelFormatPlugin case PluginEventType.KeyDown: if (CursorMovingKeys.has(event.rawEvent.key)) { - clearPendingFormat(this.editor); + this.clearPendingFormat(); } else if ( this.hasDefaultFormat && (isCharacterValue(event.rawEvent) || event.rawEvent.key == ProcessKey) @@ -115,19 +119,45 @@ export default class ContentModelFormatPlugin case PluginEventType.MouseUp: case PluginEventType.ContentChanged: - if (!canApplyPendingFormat(this.editor)) { - clearPendingFormat(this.editor); + if (!this.canApplyPendingFormat()) { + this.clearPendingFormat(); } break; } } private checkAndApplyPendingFormat(data: string | null) { - if (this.editor && data) { - applyPendingFormat(this.editor, data); - clearPendingFormat(this.editor); + if (this.editor && data && this.state.pendingFormat) { + applyPendingFormat(this.editor, data, this.state.pendingFormat.format); + this.clearPendingFormat(); } } + + private clearPendingFormat() { + this.state.pendingFormat = null; + } + + /** + * @internal + * Check if this editor can apply pending format + * @param editor The editor to get format from + */ + private canApplyPendingFormat(): boolean { + let result = false; + + if (this.state.pendingFormat && this.editor) { + const selection = this.editor.getDOMSelection(); + const range = + selection?.type == 'range' && selection.range.collapsed ? selection.range : null; + const { posContainer, posOffset } = this.state.pendingFormat; + + if (range && range.startContainer == posContainer && range.startOffset == posOffset) { + result = true; + } + } + + return result; + } } /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelTypeInContainerPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelTypeInContainerPlugin.ts similarity index 91% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelTypeInContainerPlugin.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelTypeInContainerPlugin.ts index db7747187be..a128c862f50 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelTypeInContainerPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/ContentModelTypeInContainerPlugin.ts @@ -3,7 +3,7 @@ import type { EditorPlugin } from 'roosterjs-editor-types'; /** * Dummy plugin, just to skip original TypeInContainerPlugin's behavior */ -export default class ContentModelTypeInContainerPlugin implements EditorPlugin { +export class ContentModelTypeInContainerPlugin implements EditorPlugin { /** * Get name of this plugin */ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/addRangeToSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/addRangeToSelection.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/addRangeToSelection.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/addRangeToSelection.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/applyDefaultFormat.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts similarity index 57% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/applyDefaultFormat.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts index 5aae073592e..3ced34cc1fb 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/applyDefaultFormat.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyDefaultFormat.ts @@ -1,10 +1,7 @@ -import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep'; -import { deleteSelection } from '../../modelApi/edit/deleteSelection'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat'; +import { deleteSelection } from '../../publicApi/selection/deleteSelection'; import { isBlockElement, isNodeOfType, normalizeContentModel } from 'roosterjs-content-model-dom'; -import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IEditor } from 'roosterjs-editor-types'; +import type { ContentModelSegmentFormat, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * @internal @@ -12,38 +9,43 @@ import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor' * @param editor The Content Model Editor * @param defaultFormat The default segment format to apply */ -export default function applyDefaultFormat( - editor: IContentModelEditor, +export function applyDefaultFormat( + editor: IStandaloneEditor & IEditor, defaultFormat: ContentModelSegmentFormat ) { const selection = editor.getDOMSelection(); const range = selection?.type == 'range' ? selection.range : null; const posContainer = range?.startContainer ?? null; const posOffset = range?.startOffset ?? null; - let node = posContainer; - while (node && editor.contains(node)) { - if (isNodeOfType(node, 'ELEMENT_NODE')) { - if (node.getAttribute?.('style')) { - return; - } else if (isBlockElement(node)) { - break; + if (posContainer) { + let node: Node | null = posContainer; + + while (node && editor.contains(node)) { + if (isNodeOfType(node, 'ELEMENT_NODE')) { + if (node.getAttribute?.('style')) { + return; + } else if (isBlockElement(node)) { + break; + } } - } - node = node.parentNode; + node = node.parentNode; + } + } else { + return; } - formatWithContentModel(editor, 'input', (model, context) => { + editor.formatContentModel((model, context) => { const result = deleteSelection(model, [], context); - if (result.deleteResult == DeleteResult.Range) { + if (result.deleteResult == 'range') { normalizeContentModel(model); editor.addUndoSnapshot(); return true; } else if ( - result.deleteResult == DeleteResult.NotDeleted && + result.deleteResult == 'notDeleted' && result.insertPoint && posContainer && posOffset !== null @@ -69,45 +71,34 @@ export default function applyDefaultFormat( const previousBlock = blocks[blockIndex - 1]; if (previousBlock?.blockType != 'Paragraph') { - internalApplyDefaultFormat( + context.newPendingFormat = getNewPendingFormat( editor, defaultFormat, - marker.format, - posContainer, - posOffset + marker.format ); } } else if (paragraph.segments.every(x => x.segmentType != 'Text')) { - internalApplyDefaultFormat( + context.newPendingFormat = getNewPendingFormat( editor, defaultFormat, - marker.format, - posContainer, - posOffset + marker.format ); } - - // We didn't do any change but just apply default format to pending format, so no need to write back - return false; - } else { - return false; } + + // We didn't do any change but just apply default format to pending format, so no need to write back + return false; }); } -function internalApplyDefaultFormat( - editor: IContentModelEditor, +function getNewPendingFormat( + editor: IStandaloneEditor, defaultFormat: ContentModelSegmentFormat, - currentFormat: ContentModelSegmentFormat, - posContainer: Node, - posOffset: number -) { - const pendingFormat = getPendingFormat(editor) || {}; - const newFormat: ContentModelSegmentFormat = { + markerFormat: ContentModelSegmentFormat +): ContentModelSegmentFormat { + return { ...defaultFormat, - ...pendingFormat, - ...currentFormat, + ...editor.getPendingFormat(), + ...markerFormat, }; - - setPendingFormat(editor, newFormat, posContainer, posOffset); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/applyPendingFormat.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyPendingFormat.ts similarity index 76% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/applyPendingFormat.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyPendingFormat.ts index 7a892435298..d16e0c501f8 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/applyPendingFormat.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/applyPendingFormat.ts @@ -1,7 +1,5 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getPendingFormat } from '../../modelApi/format/pendingFormat'; -import { iterateSelections } from '../../modelApi/selection/iterateSelections'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { iterateSelections } from '../../publicApi/selection/iterateSelections'; +import type { ContentModelSegmentFormat, IStandaloneEditor } from 'roosterjs-content-model-types'; import { createText, normalizeContentModel, @@ -12,18 +10,21 @@ const ANSI_SPACE = '\u0020'; const NON_BREAK_SPACE = '\u00A0'; /** + * @internal * Apply pending format to the text user just input * @param editor The editor to get format from * @param data The text user just input */ -export default function applyPendingFormat(editor: IContentModelEditor, data: string) { - const format = getPendingFormat(editor); +export function applyPendingFormat( + editor: IStandaloneEditor, + data: string, + format: ContentModelSegmentFormat +) { + let isChanged = false; - if (format) { - let isChanged = false; - - formatWithContentModel(editor, 'applyPendingFormat', (model, context) => { - iterateSelections([model], (_, __, block, segments) => { + editor.formatContentModel( + (model, context) => { + iterateSelections(model, (_, __, block, segments) => { if ( block?.blockType == 'Paragraph' && segments?.length == 1 && @@ -65,6 +66,9 @@ export default function applyPendingFormat(editor: IContentModelEditor, data: st } return isChanged; - }); - } + }, + { + apiName: 'applyPendingFormat', + } + ); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/areSameRangeEx.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/areSameSelection.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/areSameRangeEx.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/areSameSelection.ts index 7cdd8dca1ef..e4ca1ad1965 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/areSameRangeEx.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/areSameSelection.ts @@ -4,7 +4,7 @@ import type { DOMSelection } from 'roosterjs-content-model-types'; * @internal * Check if the given selections are the same */ -export function areSameRangeEx(sel1: DOMSelection, sel2: DOMSelection): boolean { +export function areSameSelection(sel1: DOMSelection, sel2: DOMSelection): boolean { if (sel1 == sel2) { return true; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/contentModelDomIndexer.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/contentModelDomIndexer.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/utils/contentModelDomIndexer.ts rename to packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/contentModelDomIndexer.ts index cea02e14a44..4b15da73392 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/contentModelDomIndexer.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/utils/contentModelDomIndexer.ts @@ -1,5 +1,5 @@ import { createSelectionMarker, createText, isNodeOfType } from 'roosterjs-content-model-dom'; -import { setSelection } from '../../modelApi/selection/setSelection'; +import { setSelection } from '../../publicApi/selection/setSelection'; import type { ContentModelDocument, ContentModelDomIndexer, diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/createContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/createContentModelEditorCore.ts new file mode 100644 index 00000000000..e847a849fd9 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/createContentModelEditorCore.ts @@ -0,0 +1,67 @@ +import { contentModelDomIndexer } from '../corePlugin/utils/contentModelDomIndexer'; +import { ContentModelTypeInContainerPlugin } from '../corePlugin/ContentModelTypeInContainerPlugin'; +import { createContentModelCachePlugin } from '../corePlugin/ContentModelCachePlugin'; +import { createContentModelCopyPastePlugin } from '../corePlugin/ContentModelCopyPastePlugin'; +import { createContentModelFormatPlugin } from '../corePlugin/ContentModelFormatPlugin'; +import { createEditorCore } from 'roosterjs-editor-core'; +import { promoteToContentModelEditorCore } from './promoteToContentModelEditorCore'; +import type { CoreCreator, EditorCore, EditorOptions } from 'roosterjs-editor-types'; +import type { + ContentModelPluginState, + StandaloneEditorCore, + StandaloneEditorOptions, +} from 'roosterjs-content-model-types'; + +/** + * Editor Core creator for Content Model editor + */ +export const createContentModelEditorCore: CoreCreator< + EditorCore & StandaloneEditorCore, + EditorOptions & StandaloneEditorOptions +> = (contentDiv, options) => { + const pluginState = getPluginState(options); + const modifiedOptions: EditorOptions & StandaloneEditorOptions = { + ...options, + plugins: [ + createContentModelCachePlugin(pluginState.cache), + createContentModelFormatPlugin(pluginState.format), + ...(options.plugins || []), + ], + corePluginOverride: { + typeInContainer: new ContentModelTypeInContainerPlugin(), + copyPaste: createContentModelCopyPastePlugin(pluginState.copyPaste), + ...options.corePluginOverride, + }, + }; + + const core = createEditorCore(contentDiv, modifiedOptions) as EditorCore & StandaloneEditorCore; + + promoteToContentModelEditorCore(core, modifiedOptions, pluginState); + + return core; +}; + +function getPluginState(options: EditorOptions & StandaloneEditorOptions): ContentModelPluginState { + const format = options.defaultFormat || {}; + return { + cache: { + domIndexer: options.cacheModel ? contentModelDomIndexer : undefined, + }, + copyPaste: { + allowedCustomPasteType: options.allowedCustomPasteType || [], + }, + format: { + defaultFormat: { + 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, + }, + pendingFormat: null, + }, + }; +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/promoteToContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/promoteToContentModelEditorCore.ts new file mode 100644 index 00000000000..99902794182 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/promoteToContentModelEditorCore.ts @@ -0,0 +1,87 @@ +import { createContentModel } from '../coreApi/createContentModel'; +import { createDomToModelConfig, createModelToDomConfig } from 'roosterjs-content-model-dom'; +import { createEditorContext } from '../coreApi/createEditorContext'; +import { formatContentModel } from '../coreApi/formatContentModel'; +import { getDOMSelection } from '../coreApi/getDOMSelection'; +import { listItemMetadataApplier, listLevelMetadataApplier } from '../metadata/updateListMetadata'; +import { setContentModel } from '../coreApi/setContentModel'; +import { setDOMSelection } from '../coreApi/setDOMSelection'; +import { switchShadowEdit } from '../coreApi/switchShadowEdit'; +import { tablePreProcessor } from '../override/tablePreProcessor'; +import type { + ContentModelPluginState, + StandaloneEditorCore, + StandaloneEditorOptions, +} from 'roosterjs-content-model-types'; +import type { EditorCore, EditorOptions } from 'roosterjs-editor-types'; + +/** + * Creator Content Model Editor Core from Editor Core + * @param core The original EditorCore object + * @param options Options of this editor + */ +export function promoteToContentModelEditorCore( + core: EditorCore, + options: EditorOptions & StandaloneEditorOptions, + pluginState: ContentModelPluginState +) { + const cmCore = core as EditorCore & StandaloneEditorCore; + + promoteCorePluginState(cmCore, pluginState); + promoteContentModelInfo(cmCore, options); + promoteCoreApi(cmCore); + promoteEnvironment(cmCore); +} + +function promoteCorePluginState( + cmCore: StandaloneEditorCore, + pluginState: ContentModelPluginState +) { + Object.assign(cmCore, pluginState); +} + +function promoteContentModelInfo(cmCore: StandaloneEditorCore, options: StandaloneEditorOptions) { + cmCore.defaultDomToModelOptions = [ + { + processorOverride: { + table: tablePreProcessor, + }, + }, + options.defaultDomToModelOptions, + ]; + cmCore.defaultModelToDomOptions = [ + { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + options.defaultModelToDomOptions, + ]; + cmCore.defaultDomToModelConfig = createDomToModelConfig(cmCore.defaultDomToModelOptions); + cmCore.defaultModelToDomConfig = createModelToDomConfig(cmCore.defaultModelToDomOptions); +} + +function promoteCoreApi(cmCore: StandaloneEditorCore) { + cmCore.api.createEditorContext = createEditorContext; + cmCore.api.createContentModel = createContentModel; + cmCore.api.setContentModel = setContentModel; + cmCore.api.switchShadowEdit = switchShadowEdit; + cmCore.api.getDOMSelection = getDOMSelection; + cmCore.api.setDOMSelection = setDOMSelection; + cmCore.api.formatContentModel = formatContentModel; + cmCore.originalApi.createEditorContext = createEditorContext; + cmCore.originalApi.createContentModel = createContentModel; + cmCore.originalApi.setContentModel = setContentModel; + cmCore.originalApi.getDOMSelection = getDOMSelection; + cmCore.originalApi.setDOMSelection = setDOMSelection; + cmCore.originalApi.formatContentModel = formatContentModel; +} + +function promoteEnvironment(cmCore: StandaloneEditorCore) { + cmCore.environment = {}; + + // It is ok to use global window here since the environment should always be the same for all windows in one session + cmCore.environment.isMac = window.navigator.appVersion.indexOf('Mac') != -1; + cmCore.environment.isAndroid = /android/i.test(window.navigator.userAgent); +} diff --git a/packages-content-model/roosterjs-content-model-core/lib/index.ts b/packages-content-model/roosterjs-content-model-core/lib/index.ts new file mode 100644 index 00000000000..a22614c4e03 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/lib/index.ts @@ -0,0 +1,50 @@ +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 { + getClosestAncestorBlockGroupIndex, + TypeOfBlockGroup, +} from './publicApi/model/getClosestAncestorBlockGroupIndex'; + +export { + iterateSelections, + IterateSelectionsCallback, + IterateSelectionsOption, +} from './publicApi/selection/iterateSelections'; +export { getSelectionRootNode } from './publicApi/selection/getSelectionRootNode'; +export { deleteSelection } from './publicApi/selection/deleteSelection'; +export { deleteSegment } from './publicApi/selection/deleteSegment'; +export { deleteBlock } from './publicApi/selection/deleteBlock'; +export { + OperationalBlocks, + getFirstSelectedListItem, + getFirstSelectedTable, + getOperationalBlocks, + getSelectedParagraphs, + getSelectedSegments, + getSelectedSegmentsAndParagraphs, +} from './publicApi/selection/collectSelections'; +export { setSelection } from './publicApi/selection/setSelection'; + +export { applyTableFormat } from './publicApi/table/applyTableFormat'; +export { normalizeTable } from './publicApi/table/normalizeTable'; +export { setTableCellBackgroundColor } from './publicApi/table/setTableCellBackgroundColor'; + +export { isCharacterValue, isModifierKey } from './publicApi/domUtils/eventUtils'; +export { combineBorderValue, extractBorderValues } from './publicApi/domUtils/borderValues'; +export { isPunctuation, isSpace, normalizeText } from './publicApi/domUtils/stringUtil'; + +export { updateImageMetadata } from './metadata/updateImageMetadata'; +export { updateTableCellMetadata } from './metadata/updateTableCellMetadata'; +export { updateTableMetadata } from './metadata/updateTableMetadata'; +export { updateListMetadata } from './metadata/updateListMetadata'; + +export { promoteToContentModelEditorCore } from './editor/promoteToContentModelEditorCore'; +export { createContentModelEditorCore } from './editor/createContentModelEditorCore'; +export { ChangeSource } from './constants/ChangeSource'; + +export { ContentModelCachePlugin } from './corePlugin/ContentModelCachePlugin'; +export { ContentModelCopyPastePlugin } from './corePlugin/ContentModelCopyPastePlugin'; +export { ContentModelFormatPlugin } from './corePlugin/ContentModelFormatPlugin'; +export { ContentModelTypeInContainerPlugin } from './corePlugin/ContentModelTypeInContainerPlugin'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/definitionCreators.ts b/packages-content-model/roosterjs-content-model-core/lib/metadata/definitionCreators.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/definitionCreators.ts rename to packages-content-model/roosterjs-content-model-core/lib/metadata/definitionCreators.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateImageMetadata.ts b/packages-content-model/roosterjs-content-model-core/lib/metadata/updateImageMetadata.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateImageMetadata.ts rename to packages-content-model/roosterjs-content-model-core/lib/metadata/updateImageMetadata.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateListMetadata.ts b/packages-content-model/roosterjs-content-model-core/lib/metadata/updateListMetadata.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateListMetadata.ts rename to packages-content-model/roosterjs-content-model-core/lib/metadata/updateListMetadata.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateTableCellMetadata.ts b/packages-content-model/roosterjs-content-model-core/lib/metadata/updateTableCellMetadata.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateTableCellMetadata.ts rename to packages-content-model/roosterjs-content-model-core/lib/metadata/updateTableCellMetadata.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateTableMetadata.ts b/packages-content-model/roosterjs-content-model-core/lib/metadata/updateTableMetadata.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/metadata/updateTableMetadata.ts rename to packages-content-model/roosterjs-content-model-core/lib/metadata/updateTableMetadata.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteExpandedSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/modelApi/edit/deleteExpandedSelection.ts similarity index 80% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteExpandedSelection.ts rename to packages-content-model/roosterjs-content-model-core/lib/modelApi/edit/deleteExpandedSelection.ts index bf7bcf69abd..36191b7b502 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteExpandedSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/modelApi/edit/deleteExpandedSelection.ts @@ -1,12 +1,17 @@ -import { createInsertPoint } from '../utils/createInsertPoint'; -import { deleteBlock } from '../utils/deleteBlock'; -import { DeleteResult } from '../utils/DeleteSelectionStep'; -import { deleteSegment } from '../utils/deleteSegment'; -import { iterateSelections } from '../../selection/iterateSelections'; -import type { ContentModelDocument } from 'roosterjs-content-model-types'; -import type { DeleteSelectionContext } from '../utils/DeleteSelectionStep'; -import type { FormatWithContentModelContext } from '../../../publicTypes/parameter/FormatWithContentModelContext'; -import type { IterateSelectionsOption } from '../../selection/iterateSelections'; +import { deleteBlock } from '../../publicApi/selection/deleteBlock'; +import { deleteSegment } from '../../publicApi/selection/deleteSegment'; +import { iterateSelections } from '../../publicApi/selection/iterateSelections'; +import type { IterateSelectionsOption } from '../../publicApi/selection/iterateSelections'; +import type { + ContentModelBlockGroup, + ContentModelDocument, + ContentModelParagraph, + ContentModelSelectionMarker, + DeleteSelectionContext, + FormatWithContentModelContext, + InsertPoint, + TableSelectionContext, +} from 'roosterjs-content-model-types'; import { createBr, createParagraph, @@ -30,13 +35,13 @@ export function deleteExpandedSelection( formatContext?: FormatWithContentModelContext ): DeleteSelectionContext { const context: DeleteSelectionContext = { - deleteResult: DeleteResult.NotDeleted, + deleteResult: 'notDeleted', insertPoint: null, formatContext, }; iterateSelections( - [model], + model, (path, tableContext, block, segments) => { // Set paragraph, format and index for default position where we will put cursor to. // Later we can overwrite these info when process the selections @@ -75,14 +80,14 @@ export function deleteExpandedSelection( tableContext ); } else if (deleteSegment(block, segment, context.formatContext)) { - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } }); // Since we are operating on this paragraph and it possible we delete everything from this paragraph, // Need to make it "not implicit" so that it will always have a container element, so that when we do normalization // of this paragraph, a BR can be added if need - if (context.deleteResult == DeleteResult.Range) { + if (context.deleteResult == 'range') { setParagraphNotImplicit(block); } } @@ -91,7 +96,7 @@ export function deleteExpandedSelection( const blocks = path[0].blocks; if (deleteBlock(blocks, block, paragraph, context.formatContext)) { - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } } else if (tableContext) { // Delete a whole table cell @@ -105,7 +110,7 @@ export function deleteExpandedSelection( delete cell.cachedElement; delete row.cachedElement; - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } if (!context.insertPoint) { @@ -122,3 +127,17 @@ export function deleteExpandedSelection( return context; } + +function createInsertPoint( + marker: ContentModelSelectionMarker, + paragraph: ContentModelParagraph, + path: ContentModelBlockGroup[], + tableContext: TableSelectionContext | undefined +): InsertPoint { + return { + marker, + paragraph, + path, + tableContext, + }; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteSingleChar.ts b/packages-content-model/roosterjs-content-model-core/lib/modelApi/edit/deleteSingleChar.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteSingleChar.ts rename to packages-content-model/roosterjs-content-model-core/lib/modelApi/edit/deleteSingleChar.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts b/packages-content-model/roosterjs-content-model-core/lib/override/tablePreProcessor.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts rename to packages-content-model/roosterjs-content-model-core/lib/override/tablePreProcessor.ts index 08c09a384d4..3122c2d84fb 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/overrides/tablePreProcessor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/override/tablePreProcessor.ts @@ -1,5 +1,5 @@ import { entityProcessor, hasMetadata, tableProcessor } from 'roosterjs-content-model-dom'; -import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode'; +import { getSelectionRootNode } from '../publicApi/selection/getSelectionRootNode'; import type { DomToModelContext, ElementProcessor } from 'roosterjs-content-model-types'; /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/borderValues.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/borderValues.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/borderValues.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/borderValues.ts index 585742a4e70..11be7401d45 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/borderValues.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/borderValues.ts @@ -1,4 +1,4 @@ -import type { Border } from '../publicTypes/interface/Border'; +import type { Border } from 'roosterjs-content-model-types'; const BorderStyles = [ 'none', diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/eventUtils.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/eventUtils.ts index b652765096a..adf658532ca 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/eventUtils.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/eventUtils.ts @@ -3,7 +3,6 @@ const ALT_CHAR_CODE = 'Alt'; const META_CHAR_CODE = 'Meta'; /** - * @internal * Returns true when the event was fired from a modifier key, otherwise false * @param event The keyboard event object */ @@ -16,7 +15,6 @@ export function isModifierKey(event: KeyboardEvent): boolean { } /** - * @internal * Returns true when the event was fired from a key that produces a character value, otherwise false * This detection is not 100% accurate. event.key is not fully supported by all browsers, and in some browsers (e.g. IE), * event.key is longer than 1 for num pad input. But here we just want to improve performance as much as possible. diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/stringUtil.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/stringUtil.ts similarity index 96% rename from packages-content-model/roosterjs-content-model-editor/lib/domUtils/stringUtil.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/stringUtil.ts index 94ebff5742c..ac6d179a37e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/domUtils/stringUtil.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/domUtils/stringUtil.ts @@ -2,7 +2,6 @@ const SPACES_REGEX = /[\u2000\u2009\u200a​\u200b​\u202f\u205f​\u3000\s\t\r const PUNCTUATIONS = '.,?!:"()[]\\/'; /** - * @internal * Check if the given character is punctuation * @param char The character to check */ @@ -11,7 +10,6 @@ export function isPunctuation(char: string) { } /** - * @internal * Check if the give character is a space. A space can be normal ASCII pace (32) or non-break space (160) or other kinds of spaces * such as ZeroWidthSpace, ... * @param char The character to check @@ -22,7 +20,6 @@ export function isSpace(char: string) { } /** - * @internal * Normalize spaces of the given string. After normalization, all leading (for forward) or trailing (for backward) spaces * will be replaces with non-break space (160) * @param txt The string to normalize diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/cloneModel.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/model/cloneModel.ts index a92d5e1fe58..7488c0e6bb3 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/cloneModel.ts @@ -28,7 +28,12 @@ import type { } from 'roosterjs-content-model-types'; /** - * @internal + * Function type used for cloneModel API to specify how to handle cached element when clone a model + * @param node The cached node + * @param type Type of the node, it can be + * - general: DOM element of ContentModelGeneralSegment or ContentModelGeneralBlock + * - entity: Wrapper element in ContentModelEntity + * - cache: Cached node in other model element that supports cache */ export type CachedElementHandler = ( node: HTMLElement, @@ -36,7 +41,7 @@ export type CachedElementHandler = ( ) => HTMLElement | undefined; /** - * @internal + * * Options for cloneModel API */ export interface CloneModelOptions { @@ -51,7 +56,9 @@ export interface CloneModelOptions { } /** - * @internal + * Clone a content model + * @param model The content model to clone + * @param options @optional Options to specify customize the clone behavior */ export function cloneModel( model: ContentModelDocument, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/getClosestAncestorBlockGroupIndex.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/getClosestAncestorBlockGroupIndex.ts similarity index 76% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/getClosestAncestorBlockGroupIndex.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/model/getClosestAncestorBlockGroupIndex.ts index 2180768f52d..19823cdc894 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/getClosestAncestorBlockGroupIndex.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/getClosestAncestorBlockGroupIndex.ts @@ -5,14 +5,17 @@ import type { } from 'roosterjs-content-model-types'; /** - * @internal + * Retrieve block group type string from a given block group */ export type TypeOfBlockGroup< T extends ContentModelBlockGroup > = T extends ContentModelBlockGroupBase ? U : never; /** - * @internal + * Get index of closest ancestor block group of the expected block group type. If not found, return -1 + * @param path The block group path, from the closest one to root + * @param blockGroupTypes The expected block group types + * @param stopTypes @optional Block group types that will cause stop searching */ export function getClosestAncestorBlockGroupIndex( path: ContentModelBlockGroup[], diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/isBlockGroupOfType.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/isBlockGroupOfType.ts similarity index 74% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/isBlockGroupOfType.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/model/isBlockGroupOfType.ts index da24115d6dc..8c4c61003ee 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/isBlockGroupOfType.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/isBlockGroupOfType.ts @@ -2,7 +2,9 @@ import type { ContentModelBlock, ContentModelBlockGroup } from 'roosterjs-conten import type { TypeOfBlockGroup } from './getClosestAncestorBlockGroupIndex'; /** - * @internal + * Check if the given content model block or block group is of the expected block group type + * @param input The object to check + * @param type The expected type */ export function isBlockGroupOfType( input: ContentModelBlock | ContentModelBlockGroup | null | undefined, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/mergeModel.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/model/mergeModel.ts index bd43e655cd6..abb7648dd89 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/mergeModel.ts @@ -1,5 +1,5 @@ import { applyTableFormat } from '../table/applyTableFormat'; -import { deleteSelection } from '../edit/deleteSelection'; +import { deleteSelection } from '../../publicApi/selection/deleteSelection'; import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; import { normalizeTable } from '../table/normalizeTable'; import { @@ -11,8 +11,6 @@ import { getObjectKeys, normalizeContentModel, } from 'roosterjs-content-model-dom'; -import type { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext'; -import type { InsertPoint } from '../../publicTypes/selection/InsertPoint'; import type { ContentModelBlock, ContentModelBlockFormat, @@ -22,12 +20,13 @@ import type { ContentModelParagraph, ContentModelSegmentFormat, ContentModelTable, + FormatWithContentModelContext, + InsertPoint, } from 'roosterjs-content-model-types'; const HeadingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; /** - * @internal * Options to specify how to merge models */ export interface MergeModelOption { @@ -57,11 +56,10 @@ export interface MergeModelOption { } /** - * @internal * Merge source model into target mode * @param target Target Content Model that will merge content into * @param source Source Content Model will be merged to target model - * @param context Format context. When call this function inside formatWithContentModel, provide this context so that formatWithContentModel will do extra handling to the result + * @param context Format context. When call this function inside formatContentModel, provide this context so that formatContentModel will do extra handling to the result * @param options More options, see MergeModelOption * @returns Insert point after merge, or null if there is no insert point */ diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/paste.ts similarity index 83% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/model/paste.ts index 2fc03bfe8f0..3a9804013c4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/model/paste.ts @@ -1,25 +1,24 @@ -import getSelectedSegments from '../selection/getSelectedSegments'; -import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent'; -import { formatWithContentModel } from './formatWithContentModel'; +import { ChangeSource } from '../../constants/ChangeSource'; import { GetContentMode, PasteType as OldPasteType, PluginEventType } from 'roosterjs-editor-types'; -import { mergeModel } from '../../modelApi/common/mergeModel'; -import { setPendingFormat } from '../../modelApi/format/pendingFormat'; -import type { InsertPoint } from '../../publicTypes/selection/InsertPoint'; +import { getSelectedSegments } from '../selection/collectSelections'; +import { mergeModel } from './mergeModel'; import type { ContentModelDocument, ContentModelSegmentFormat, + FormatWithContentModelContext, + InsertPoint, + PasteType, + ContentModelBeforePasteEventData, + ContentModelBeforePasteEvent, + IStandaloneEditor, } from 'roosterjs-content-model-types'; -import type { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { ClipboardData } from 'roosterjs-editor-types'; +import type { ClipboardData, IEditor } from 'roosterjs-editor-types'; import { applySegmentFormatToElement, createDomToModelContext, domToContentModel, moveChildNodes, } from 'roosterjs-content-model-dom'; -import type { ContentModelBeforePasteEventData } from '../../publicTypes/event/ContentModelBeforePasteEvent'; -import type ContentModelBeforePasteEvent from '../../publicTypes/event/ContentModelBeforePasteEvent'; import { createDefaultHtmlSanitizerOptions, handleImagePaste, @@ -27,7 +26,6 @@ import { retrieveMetadataFromClipboard, sanitizePasteContent, } from 'roosterjs-editor-dom'; -import type { PasteType } from '../../publicTypes/parameter/PasteType'; // Map new PasteType to old PasteType // TODO: We can remove this once we have standalone editor @@ -57,8 +55,8 @@ const EmptySegmentFormat: Required = { * @param clipboardData Clipboard data retrieved from clipboard * @param pasteType Type of content to paste. @default normal */ -export default function paste( - editor: IContentModelEditor, +export function paste( + editor: IStandaloneEditor & IEditor, clipboardData: ClipboardData, pasteType: PasteType = 'normal' ) { @@ -72,9 +70,7 @@ export default function paste( editor.focus(); let originalFormat: ContentModelSegmentFormat | undefined; - formatWithContentModel( - editor, - 'Paste', + editor.formatContentModel( (model, context) => { const eventData = createBeforePasteEventData(editor, clipboardData, pasteType); const currentSegment = getSelectedSegments(model, true /*includingFormatHolder*/)[0]; @@ -109,25 +105,19 @@ export default function paste( originalFormat = insertPoint.marker.format; } + if (originalFormat) { + context.newPendingFormat = { ...EmptySegmentFormat, ...originalFormat }; // Use empty format as initial value to clear any other format inherits from pasted content + } + return true; }, { changeSource: ChangeSource.Paste, getChangeData: () => clipboardData, + apiName: 'paste', } ); - - const pos = editor.getFocusedPosition(); - - if (originalFormat && pos) { - setPendingFormat( - editor, - { ...EmptySegmentFormat, ...originalFormat }, // Use empty format as initial value to clear any other format inherits from pasted content - pos.node, - pos.offset - ); - } } /** @@ -167,7 +157,7 @@ function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined } function createBeforePasteEventData( - editor: IContentModelEditor, + editor: IEditor, clipboardData: ClipboardData, pasteType: PasteType ): ContentModelBeforePasteEventData { @@ -193,7 +183,7 @@ function createBeforePasteEventData( * This function will also create a DocumentFragment for paste. */ function triggerPluginEventAndCreatePasteFragment( - editor: IContentModelEditor, + editor: IEditor, clipboardData: ClipboardData, pasteType: PasteType, eventData: ContentModelBeforePasteEventData, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/collectSelections.ts similarity index 78% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/collectSelections.ts index 1089c46eeb4..bf9ce4cd9b8 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/collectSelections.ts @@ -1,8 +1,7 @@ -import { getClosestAncestorBlockGroupIndex } from '../common/getClosestAncestorBlockGroupIndex'; -import { isBlockGroupOfType } from '../common/isBlockGroupOfType'; +import { getClosestAncestorBlockGroupIndex } from '../model/getClosestAncestorBlockGroupIndex'; +import { isBlockGroupOfType } from '../model/isBlockGroupOfType'; import { iterateSelections } from './iterateSelections'; import type { IterateSelectionsOption } from './iterateSelections'; -import type { TableSelectionContext } from '../../publicTypes/selection/TableSelectionContext'; import type { ContentModelBlock, ContentModelBlockGroup, @@ -12,19 +11,29 @@ import type { ContentModelParagraph, ContentModelSegment, ContentModelTable, + TableSelectionContext, } from 'roosterjs-content-model-types'; -import type { TypeOfBlockGroup } from '../common/getClosestAncestorBlockGroupIndex'; +import type { TypeOfBlockGroup } from '../model/getClosestAncestorBlockGroupIndex'; /** - * @internal + * Represent a pair of parent block group and child block */ export type OperationalBlocks = { + /** + * The parent block group + */ parent: ContentModelBlockGroup; + + /** + * The child block + */ block: ContentModelBlock | T; }; /** - * @internal + * Get an array of selected parent paragraph and child segment pair + * @param model The Content Model to get selection from + * @param includingFormatHolder True means also include format holder as segment from list item, in that case paragraph will be null */ export function getSelectedSegmentsAndParagraphs( model: ContentModelDocument, @@ -49,7 +58,20 @@ export function getSelectedSegmentsAndParagraphs( } /** - * @internal + * Get an array of selected segments from a content model + * @param model The Content Model to get selection from + * @param includingFormatHolder True means also include format holder as segment from list item + */ +export function getSelectedSegments( + model: ContentModelDocument, + includingFormatHolder: boolean +): ContentModelSegment[] { + return getSelectedSegmentsAndParagraphs(model, includingFormatHolder).map(x => x[0]); +} + +/** + * Get any array of selected paragraphs from a content model + * @param model The Content Model to get selection from */ export function getSelectedParagraphs(model: ContentModelDocument): ContentModelParagraph[] { const selections = collectSelections(model, { includeListFormatHolder: 'never' }); @@ -67,7 +89,11 @@ export function getSelectedParagraphs(model: ContentModelDocument): ContentModel } /** - * @internal + * Get an array of block group - block pair that is of the expected block group type from selection + * @param model The Content Model to get selection from + * @param blockGroupTypes The expected block group types + * @param stopTypes Block group types that will stop searching when hit + * @param deepFirst True means search in deep first, otherwise wide first */ export function getOperationalBlocks( model: ContentModelDocument, @@ -110,7 +136,8 @@ export function getOperationalBlocks( } /** - * @internal + * Get the first selected table from content model + * @param model The Content Model to get selection from */ export function getFirstSelectedTable( model: ContentModelDocument @@ -142,7 +169,8 @@ export function getFirstSelectedTable( } /** - * @internal + * Get the first selected list item from content model + * @param model The Content Model to get selection from */ export function getFirstSelectedListItem( model: ContentModelDocument @@ -172,7 +200,7 @@ function collectSelections( const selections: SelectionInfo[] = []; iterateSelections( - [model], + model, (path, tableContext, block, segments) => { selections.push({ path, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteBlock.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteBlock.ts similarity index 76% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteBlock.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteBlock.ts index ce50a4ef827..f85b93cf66a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteBlock.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteBlock.ts @@ -1,11 +1,17 @@ -import type { ContentModelBlock } from 'roosterjs-content-model-types'; import type { + ContentModelBlock, EntityRemovalOperation, FormatWithContentModelContext, -} from '../../../publicTypes/parameter/FormatWithContentModelContext'; +} from 'roosterjs-content-model-types'; /** - * @internal + * Delete a content model block from current selection + * @param blocks Array of the block to delete + * @param blockToDelete The block to delete + * @param replacement @optional If specified, use this block to replace the deleted block + * @param context @optional Context object provided by formatContentModel API + * @param direction @optional Whether this is deleting forward or backward. This is only used for deleting entity. + * If not specified, only selected entity will be deleted */ export function deleteBlock( blocks: ContentModelBlock[], diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteSegment.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts similarity index 81% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteSegment.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts index 26630106e3a..95ce0c57bba 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/deleteSegment.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSegment.ts @@ -1,14 +1,20 @@ -import { deleteSingleChar } from './deleteSingleChar'; +import { deleteSingleChar } from '../../modelApi/edit/deleteSingleChar'; import { isWhiteSpacePreserved, normalizeSingleSegment } from 'roosterjs-content-model-dom'; -import { normalizeText } from '../../../domUtils/stringUtil'; -import type { ContentModelParagraph, ContentModelSegment } from 'roosterjs-content-model-types'; +import { normalizeText } from '../domUtils/stringUtil'; import type { + ContentModelParagraph, + ContentModelSegment, EntityRemovalOperation, FormatWithContentModelContext, -} from '../../../publicTypes/parameter/FormatWithContentModelContext'; +} from 'roosterjs-content-model-types'; /** - * @internal + * Delete a content model segment from current selection + * @param paragraph Parent paragraph of the segment to delete + * @param segmentToDelete The segment to delete + * @param context @optional Context object provided by formatContentModel API + * @param direction @optional Whether this is deleting forward or backward. This is only used for deleting entity. + * If not specified, only selected entity will be deleted */ export function deleteSegment( paragraph: ContentModelParagraph, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSelection.ts similarity index 67% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSelection.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSelection.ts index 7fa31e40c52..8a734613210 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/deleteSelection.ts @@ -1,16 +1,19 @@ -import { deleteExpandedSelection } from './utils/deleteExpandedSelection'; -import { DeleteResult } from './utils/DeleteSelectionStep'; -import type { ContentModelDocument } from 'roosterjs-content-model-types'; -import type { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext'; +import { deleteExpandedSelection } from '../../modelApi/edit/deleteExpandedSelection'; import type { + ContentModelDocument, DeleteSelectionContext, DeleteSelectionResult, DeleteSelectionStep, + FormatWithContentModelContext, ValidDeleteSelectionContext, -} from './utils/DeleteSelectionStep'; +} from 'roosterjs-content-model-types'; /** - * @internal + * Delete selected content from Content Model + * @param model The model to delete selected content from + * @param additionalSteps @optional Addition delete steps + * @param formatContext @optional A context object provided by formatContentModel API + * @returns A DeleteSelectionResult object to specify the deletion result */ export function deleteSelection( model: ContentModelDocument, @@ -23,7 +26,7 @@ export function deleteSelection( if ( step && isValidDeleteSelectionContext(context) && - context.deleteResult == DeleteResult.NotDeleted + context.deleteResult == 'notDeleted' ) { step(context); } @@ -46,8 +49,8 @@ function mergeParagraphAfterDelete(context: DeleteSelectionContext) { if ( insertPoint && - deleteResult != DeleteResult.NotDeleted && - deleteResult != DeleteResult.NothingToDelete && + deleteResult != 'notDeleted' && + deleteResult != 'nothingToDelete' && lastParagraph && lastParagraph != insertPoint.paragraph && lastTableContext == insertPoint.tableContext diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/getSelectionRootNode.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/getSelectionRootNode.ts similarity index 59% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/getSelectionRootNode.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/getSelectionRootNode.ts index bc7115d0006..185edb6164f 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/getSelectionRootNode.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/getSelectionRootNode.ts @@ -1,7 +1,11 @@ import type { DOMSelection } from 'roosterjs-content-model-types'; /** - * @internal + * Get root node of a given DOM selection + * For table selection, root node is the selected table + * For image selection, root node is the selected image + * For range selection, root node is the common ancestor container node of the selection range + * @param selection The selection to get root node from */ export function getSelectionRootNode(selection: DOMSelection | undefined): Node | undefined { return !selection diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/iterateSelections.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/iterateSelections.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/iterateSelections.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/iterateSelections.ts index 454832349e4..33f39d2a987 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/iterateSelections.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/iterateSelections.ts @@ -1,13 +1,13 @@ -import type { TableSelectionContext } from '../../publicTypes/selection/TableSelectionContext'; import type { ContentModelBlock, ContentModelBlockGroup, ContentModelBlockWithCache, ContentModelSegment, + TableSelectionContext, } from 'roosterjs-content-model-types'; /** - * @internal + * Options for iterateSelections API */ export interface IterateSelectionsOption { /** @@ -41,7 +41,11 @@ export interface IterateSelectionsOption { } /** - * @internal + * The callback function type for iterateSelections + * @param path The block group path of current selection + * @param tableContext Table context of current selection + * @param block Block of current selection + * @param segments Segments of current selection * @returns True to stop iterating, otherwise keep going */ export type IterateSelectionsCallback = ( @@ -52,16 +56,16 @@ export type IterateSelectionsCallback = ( ) => void | boolean; /** - * @internal - * @returns True to stop iterating, otherwise keep going + * Iterate all selected elements in a given model + * @param group The given Content Model to iterate selection from + * @param callback The callback function to access the selected element + * @param option Option to determine how to iterate */ export function iterateSelections( - path: ContentModelBlockGroup[], + group: ContentModelBlockGroup, callback: IterateSelectionsCallback, - option?: IterateSelectionsOption, - table?: TableSelectionContext, - treatAllAsSelect?: boolean -) { + option?: IterateSelectionsOption +): void { const internalCallback: IterateSelectionsCallback = (path, tableContext, block, segments) => { if (!!(block as ContentModelBlockWithCache)?.cachedElement) { // TODO: This is a temporary solution. A better solution would be making all results from iterationSelection() to be readonly, @@ -72,7 +76,7 @@ export function iterateSelections( return callback(path, tableContext, block, segments); }; - internalIterateSelections(path, internalCallback, option, table, treatAllAsSelect); + internalIterateSelections([group], internalCallback, option); } function internalIterateSelections( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/setSelection.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/setSelection.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/setSelection.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/setSelection.ts index 06a3371f7f8..e0e445fe92b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/setSelection.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/selection/setSelection.ts @@ -8,7 +8,10 @@ import type { } from 'roosterjs-content-model-types'; /** - * @internal + * Set selection into Content Model. If the Content Model already has selection, existing selection will be overwritten by the new one. + * @param group The root level group of Content Model + * @param start The start selected element. If not passed, existing selection of content model will be cleared + * @param end The end selected element. If not passed, only the start element will be selected. If passed, all elements between start and end elements will be selected */ export function setSelection(group: ContentModelBlockGroup, start?: Selectable, end?: Selectable) { setSelectionToBlockGroup(group, false /*isInSelection*/, start || null, end || null); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/applyTableFormat.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/table/applyTableFormat.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/applyTableFormat.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/table/applyTableFormat.ts index b41efeef019..ef4455ff0c0 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/applyTableFormat.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/table/applyTableFormat.ts @@ -1,9 +1,9 @@ import { BorderKeys } from 'roosterjs-content-model-dom'; -import { combineBorderValue, extractBorderValues } from '../../domUtils/borderValues'; +import { combineBorderValue, extractBorderValues } from '../domUtils/borderValues'; import { setTableCellBackgroundColor } from './setTableCellBackgroundColor'; import { TableBorderFormat } from 'roosterjs-content-model-types'; -import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; -import { updateTableMetadata } from '../../domUtils/metadata/updateTableMetadata'; +import { updateTableCellMetadata } from '../../metadata/updateTableCellMetadata'; +import { updateTableMetadata } from '../../metadata/updateTableMetadata'; import type { BorderFormat, ContentModelTable, @@ -33,7 +33,10 @@ type MetaOverrides = { }; /** - * @internal + * Apply table format from table metadata and the passed in new format + * @param table The table to apply format to + * @param newFormat @optional New format to apply. When passed, this value will be merged into existing metadata format and default format + * @param keepCellShade @optional When pass true, table cells with customized shade color will not be overwritten. @default false */ export function applyTableFormat( table: ContentModelTable, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/table/normalizeTable.ts similarity index 91% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/table/normalizeTable.ts index 7a278a3589f..3a43078f9cd 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/table/normalizeTable.ts @@ -9,7 +9,15 @@ import type { const MIN_HEIGHT = 22; /** - * @internal + * Normalize a Content Model table, make sure: + * 1. Fist cells are not spanned + * 2. Inner cells are not header + * 3. All cells have content and width + * 4. Table and table row have correct width/height + * 5. Spanned cell has no child blocks + * 6. default format is correctly applied + * @param table The table to normalize + * @param defaultSegmentFormat @optional Default segment format to apply to cell */ export function normalizeTable( table: ContentModelTable, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/setTableCellBackgroundColor.ts b/packages-content-model/roosterjs-content-model-core/lib/publicApi/table/setTableCellBackgroundColor.ts similarity index 89% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/setTableCellBackgroundColor.ts rename to packages-content-model/roosterjs-content-model-core/lib/publicApi/table/setTableCellBackgroundColor.ts index 9307566a3cd..816ac4e5505 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/setTableCellBackgroundColor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/publicApi/table/setTableCellBackgroundColor.ts @@ -1,4 +1,4 @@ -import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; +import { updateTableCellMetadata } from '../../metadata/updateTableCellMetadata'; import type { ContentModelTableCell } from 'roosterjs-content-model-types'; // Using the HSL (hue, saturation and lightness) representation for RGB color values. @@ -10,7 +10,11 @@ const White = '#ffffff'; const Black = '#000000'; /** - * @internal + * Set shade color of table cell + * @param cell The cell to set shade color to + * @param color The color to set + * @param isColorOverride @optional When pass true, it means this shade color is not part of table format, so it can be preserved when apply table format + * @param applyToSegments @optional When pass true, we will also apply text color from table cell to its child blocks and segments */ export function setTableCellBackgroundColor( cell: ContentModelTableCell, diff --git a/packages-content-model/roosterjs-content-model-core/package.json b/packages-content-model/roosterjs-content-model-core/package.json new file mode 100644 index 00000000000..c932d7dfbde --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/package.json @@ -0,0 +1,14 @@ +{ + "name": "roosterjs-content-model-core", + "description": "Content Model for roosterjs (Under development)", + "dependencies": { + "tslib": "^2.3.1", + "roosterjs-editor-types": "", + "roosterjs-editor-dom": "", + "roosterjs-editor-core": "", + "roosterjs-content-model-dom": "", + "roosterjs-content-model-types": "" + }, + "version": "0.0.0", + "main": "./lib/index.ts" +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts rename to packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts index ca87e4d8305..16e33c508a5 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/createContentModelTest.ts @@ -1,8 +1,9 @@ -import * as cloneModel from '../../../lib/modelApi/common/cloneModel'; +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 { ContentModelEditorCore } from '../../../lib/publicTypes/ContentModelEditorCore'; -import { createContentModel } from '../../../lib/editor/coreApi/createContentModel'; +import { createContentModel } from '../../lib/coreApi/createContentModel'; +import { EditorCore } from 'roosterjs-editor-types'; +import { StandaloneEditorCore } from 'roosterjs-content-model-types'; const mockedEditorContext = 'EDITORCONTEXT' as any; const mockedContext = 'CONTEXT' as any; @@ -12,7 +13,7 @@ const mockedCachedMode = 'CACHEDMODEL' as any; const mockedClonedModel = 'CLONEDMODEL' as any; describe('createContentModel', () => { - let core: ContentModelEditorCore; + let core: StandaloneEditorCore & EditorCore; let createEditorContext: jasmine.Spy; let getDOMSelection: jasmine.Spy; let domToContentModelSpy: jasmine.Spy; @@ -43,7 +44,7 @@ describe('createContentModel', () => { cachedModel: mockedCachedMode, }, lifecycle: {}, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; }); it('Reuse model, no cache, no shadow edit', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts rename to packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts index 6a4fbc1bfa7..f9fd8365aac 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/createEditorContextTest.ts @@ -1,5 +1,6 @@ -import { ContentModelEditorCore } from '../../../lib/publicTypes/ContentModelEditorCore'; -import { createEditorContext } from '../../../lib/editor/coreApi/createEditorContext'; +import { createEditorContext } from '../../lib/coreApi/createEditorContext'; +import { EditorCore } from 'roosterjs-editor-types'; +import { StandaloneEditorCore } from 'roosterjs-content-model-types'; describe('createEditorContext', () => { it('create a normal context', () => { @@ -28,7 +29,7 @@ describe('createEditorContext', () => { }, darkColorHandler, cache: {}, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; const context = createEditorContext(core); @@ -71,7 +72,7 @@ describe('createEditorContext', () => { cache: { domIndexer, }, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; const context = createEditorContext(core); @@ -87,7 +88,7 @@ describe('createEditorContext', () => { }); describe('createEditorContext - checkZoomScale', () => { - let core: ContentModelEditorCore; + let core: StandaloneEditorCore & EditorCore; let div: any; let getComputedStyleSpy: jasmine.Spy; let getBoundingClientRectSpy: jasmine.Spy; @@ -117,7 +118,7 @@ describe('createEditorContext - checkZoomScale', () => { }, darkColorHandler, cache: {}, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; }); it('Zoom scale = 1', () => { @@ -179,7 +180,7 @@ describe('createEditorContext - checkZoomScale', () => { }); describe('createEditorContext - checkRootDir', () => { - let core: ContentModelEditorCore; + let core: StandaloneEditorCore & EditorCore; let div: any; let getComputedStyleSpy: jasmine.Spy; let getBoundingClientRectSpy: jasmine.Spy; @@ -209,7 +210,7 @@ describe('createEditorContext - checkRootDir', () => { }, darkColorHandler, cache: {}, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; }); 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 new file mode 100644 index 00000000000..b518e96e7e2 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/formatContentModelTest.ts @@ -0,0 +1,700 @@ +import { ChangeSource } from '../../lib/constants/ChangeSource'; +import { createImage } from 'roosterjs-content-model-dom'; +import { formatContentModel } from '../../lib/coreApi/formatContentModel'; +import { + ColorTransformDirection, + EditorCore, + EntityOperation, + PluginEventType, +} from 'roosterjs-editor-types'; +import { + ContentModelDocument, + ContentModelSegmentFormat, + StandaloneEditorCore, +} from 'roosterjs-content-model-types'; + +describe('formatContentModel', () => { + let core: StandaloneEditorCore & EditorCore; + let addUndoSnapshot: jasmine.Spy; + let createContentModel: jasmine.Spy; + let setContentModel: jasmine.Spy; + let mockedModel: ContentModelDocument; + let cacheContentModel: jasmine.Spy; + let getFocusedPosition: jasmine.Spy; + let triggerEvent: jasmine.Spy; + let getDOMSelection: jasmine.Spy; + + const apiName = 'mockedApi'; + const mockedContainer = 'C' as any; + const mockedOffset = 'O' as any; + const mockedSelection = 'Selection' as any; + + beforeEach(() => { + mockedModel = ({} as any) as ContentModelDocument; + + addUndoSnapshot = jasmine + .createSpy('addUndoSnapshot') + .and.callFake((_, callback) => callback?.()); + createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); + setContentModel = jasmine.createSpy('setContentModel').and.returnValue(mockedSelection); + cacheContentModel = jasmine.createSpy('cacheContentModel'); + getFocusedPosition = jasmine + .createSpy('getFocusedPosition') + .and.returnValue({ node: mockedContainer, offset: mockedOffset }); + triggerEvent = jasmine.createSpy('triggerPluginEvent'); + getDOMSelection = jasmine.createSpy('getDOMSelection').and.returnValue(null); + + core = ({ + api: { + addUndoSnapshot, + createContentModel, + setContentModel, + cacheContentModel, + getFocusedPosition, + triggerEvent, + getDOMSelection, + }, + lifecycle: {}, + cache: {}, + } as any) as StandaloneEditorCore & EditorCore; + }); + + it('Callback return false', () => { + const callback = jasmine.createSpy('callback').and.returnValue(false); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).not.toHaveBeenCalled(); + expect(setContentModel).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); + }); + + it('Callback return true', () => { + const callback = jasmine.createSpy('callback').and.returnValue(true); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot.calls.argsFor(0)[2]).toBe(null); + expect(addUndoSnapshot.calls.argsFor(0)[3]).toBe(false); + expect(addUndoSnapshot.calls.argsFor(0)[4]).toEqual({ + formatApiName: apiName, + }); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Skip undo snapshot', () => { + const callback = jasmine.createSpy('callback').and.callFake((model, context) => { + context.skipUndoSnapshot = true; + return true; + }); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + skipUndoSnapshot: true, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).not.toHaveBeenCalled(); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Customize change source', () => { + const callback = jasmine.createSpy('callback').and.returnValue(true); + + formatContentModel(core, callback, { changeSource: 'TEST', apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(addUndoSnapshot.calls.argsFor(0)[2]).toBe(null!); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: 'TEST', + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Customize change source, getChangeData and skip undo snapshot', () => { + const callback = jasmine.createSpy('callback').and.callFake((model, context) => { + context.skipUndoSnapshot = true; + return true; + }); + const returnData = 'DATA'; + + formatContentModel(core, callback, { + apiName, + changeSource: 'TEST', + getChangeData: () => returnData, + }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + skipUndoSnapshot: true, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).not.toHaveBeenCalled(); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: 'TEST', + data: returnData, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Has onNodeCreated', () => { + const callback = jasmine.createSpy('callback').and.returnValue(true); + const onNodeCreated = jasmine.createSpy('onNodeCreated'); + + formatContentModel(core, callback, { onNodeCreated: onNodeCreated, apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, onNodeCreated); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Has entity got deleted', () => { + const entity1 = { + entityFormat: { id: 'E1', entityType: 'E', isReadonly: true }, + wrapper: {}, + } as any; + const entity2 = { + entityFormat: { id: 'E2', entityType: 'E', isReadonly: true }, + wrapper: {}, + } as any; + const rawEvent = 'RawEvent' as any; + + formatContentModel( + core, + (model, context) => { + context.deletedEntities.push( + { + entity: entity1, + operation: 'removeFromStart', + }, + { + entity: entity2, + operation: 'removeFromEnd', + } + ); + return true; + }, + { + apiName, + rawEvent: rawEvent, + } + ); + + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + + expect(triggerEvent).toHaveBeenCalledTimes(3); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.EntityOperation, + entity: { id: 'E1', type: 'E', isReadonly: true, wrapper: entity1.wrapper }, + operation: EntityOperation.RemoveFromStart, + rawEvent: rawEvent, + }, + false + ); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.EntityOperation, + entity: { id: 'E2', type: 'E', isReadonly: true, wrapper: entity2.wrapper }, + operation: EntityOperation.RemoveFromEnd, + rawEvent: rawEvent, + }, + false + ); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Has new entity in dark mode', () => { + const wrapper1 = 'W1' as any; + const wrapper2 = 'W2' as any; + const entity1 = { + entityFormat: { id: 'E1', entityType: 'E', isReadonly: true }, + wrapper: wrapper1, + } as any; + const entity2 = { + entityFormat: { id: 'E2', entityType: 'E', isReadonly: true }, + wrapper: wrapper2, + } as any; + const rawEvent = 'RawEvent' as any; + const transformToDarkColorSpy = jasmine.createSpy('transformToDarkColor'); + const mockedData = 'DATA'; + + core.lifecycle.isDarkMode = true; + core.api.transformColor = transformToDarkColorSpy; + + formatContentModel( + core, + (model, context) => { + context.newEntities.push(entity1, entity2); + return true; + }, + { + apiName, + rawEvent: rawEvent, + getChangeData: () => mockedData, + } + ); + + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: mockedData, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + expect(transformToDarkColorSpy).toHaveBeenCalledTimes(2); + expect(transformToDarkColorSpy).toHaveBeenCalledWith( + core, + wrapper1, + true, + null, + ColorTransformDirection.LightToDark + ); + expect(transformToDarkColorSpy).toHaveBeenCalledWith( + core, + wrapper2, + true, + null, + ColorTransformDirection.LightToDark + ); + }); + + it('With selectionOverride', () => { + const range = 'MockedRangeEx' as any; + + formatContentModel(core, () => true, { + apiName, + selectionOverride: range, + }); + + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(createContentModel).toHaveBeenCalledWith(core, undefined, range); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Has image', () => { + const image = createImage('test'); + const rawEvent = 'RawEvent' as any; + const getVisibleViewportSpy = jasmine + .createSpy('getVisibleViewport') + .and.returnValue({ top: 100, bottom: 200, left: 100, right: 200 }); + core.getVisibleViewport = getVisibleViewportSpy; + + formatContentModel( + core, + (model, context) => { + context.newImages.push(image); + return true; + }, + { + apiName, + rawEvent: rawEvent, + } + ); + + expect(getVisibleViewportSpy).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Has shouldClearCachedModel', () => { + formatContentModel( + core, + (model, context) => { + context.clearModelCache = true; + return true; + }, + { + apiName, + } + ); + + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: PluginEventType.ContentChanged, + contentModel: undefined, + selection: undefined, + source: ChangeSource.Format, + data: undefined, + additionalData: { + formatApiName: apiName, + }, + }, + true + ); + }); + + it('Has shouldClearCachedModel, and callback return false', () => { + core.cache.cachedModel = 'Model' as any; + core.cache.cachedSelection = 'Selection' as any; + + formatContentModel( + core, + (model, context) => { + context.clearModelCache = true; + return false; + }, + { + apiName, + } + ); + + expect(addUndoSnapshot).not.toHaveBeenCalled(); + expect(setContentModel).not.toHaveBeenCalled(); + expect(triggerEvent).not.toHaveBeenCalled(); + expect(core.cache).toEqual({ + cachedModel: undefined, + cachedSelection: undefined, + }); + }); + + describe('Pending format', () => { + const mockedStartContainer1 = 'CONTAINER1' as any; + const mockedStartOffset1 = 'OFFSET1' as any; + const mockedFormat1: ContentModelSegmentFormat = { fontSize: '10pt' }; + + const mockedStartContainer2 = 'CONTAINER2' as any; + const mockedStartOffset2 = 'OFFSET2' as any; + const mockedFormat2: ContentModelSegmentFormat = { fontFamily: 'Arial' }; + + beforeEach(() => { + core.format = { + defaultFormat: {}, + pendingFormat: null, + }; + + const mockedRange = { + type: 'range', + range: { + collapsed: true, + startContainer: mockedStartContainer2, + startOffset: mockedStartOffset2, + }, + } as any; + + core.api.setContentModel = () => mockedRange; + core.api.getDOMSelection = () => mockedRange; + }); + + it('No pending format, callback returns true, preserve pending format', () => { + formatContentModel(core, (model, context) => { + context.newPendingFormat = 'preserve'; + return true; + }); + + expect(core.format.pendingFormat).toBeNull(); + }); + + it('No pending format, callback returns false, preserve pending format', () => { + formatContentModel(core, (model, context) => { + context.newPendingFormat = 'preserve'; + return false; + }); + + expect(core.format.pendingFormat).toBeNull(); + }); + + it('Has pending format, callback returns true, preserve pending format', () => { + core.format.pendingFormat = { + format: mockedFormat1, + posContainer: mockedStartContainer1, + posOffset: mockedStartOffset1, + }; + + formatContentModel(core, (model, context) => { + context.newPendingFormat = 'preserve'; + return true; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat1, + posContainer: mockedStartContainer2, + posOffset: mockedStartOffset2, + } as any); + }); + + it('Has pending format, callback returns false, preserve pending format', () => { + core.format.pendingFormat = { + format: mockedFormat1, + posContainer: mockedStartContainer1, + posOffset: mockedStartOffset1, + }; + + formatContentModel(core, (model, context) => { + context.newPendingFormat = 'preserve'; + return false; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat1, + posContainer: mockedStartContainer2, + posOffset: mockedStartOffset2, + } as any); + }); + + it('No pending format, callback returns true, new format', () => { + formatContentModel(core, (model, context) => { + context.newPendingFormat = mockedFormat2; + return true; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat2, + posContainer: mockedStartContainer2, + posOffset: mockedStartOffset2, + }); + }); + + it('No pending format, callback returns false, new format', () => { + formatContentModel(core, (model, context) => { + context.newPendingFormat = mockedFormat2; + return false; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat2, + posContainer: mockedStartContainer2, + posOffset: mockedStartOffset2, + }); + }); + + it('Has pending format, callback returns true, new format', () => { + core.format.pendingFormat = { + format: mockedFormat1, + posContainer: mockedStartContainer1, + posOffset: mockedStartOffset1, + }; + + formatContentModel(core, (model, context) => { + context.newPendingFormat = mockedFormat2; + return true; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat2, + posContainer: mockedStartContainer2, + posOffset: mockedStartOffset2, + }); + }); + + it('Has pending format, callback returns false, new format', () => { + core.format.pendingFormat = { + format: mockedFormat1, + posContainer: mockedStartContainer1, + posOffset: mockedStartOffset1, + }; + + formatContentModel(core, (model, context) => { + context.newPendingFormat = mockedFormat2; + return false; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat2, + posContainer: mockedStartContainer2, + posOffset: mockedStartOffset2, + }); + }); + + it('Has pending format, callback returns false, preserve format, selection is not collapsed', () => { + core.format.pendingFormat = { + format: mockedFormat1, + posContainer: mockedStartContainer1, + posOffset: mockedStartOffset1, + }; + + core.api.getDOMSelection = () => + ({ + type: 'range', + range: { + collapsed: false, + startContainer: mockedStartContainer2, + startOffset: mockedStartOffset2, + }, + } as any); + + formatContentModel(core, (model, context) => { + context.newPendingFormat = 'preserve'; + return false; + }); + + expect(core.format.pendingFormat).toEqual({ + format: mockedFormat1, + posContainer: mockedStartContainer1, + posOffset: mockedStartOffset1, + }); + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/setContentModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts similarity index 93% rename from packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/setContentModelTest.ts rename to packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts index c7d5198daef..f65e179a403 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/setContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/setContentModelTest.ts @@ -1,7 +1,8 @@ import * as contentModelToDom from 'roosterjs-content-model-dom/lib/modelToDom/contentModelToDom'; import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext'; -import { ContentModelEditorCore } from '../../../lib/publicTypes/ContentModelEditorCore'; -import { setContentModel } from '../../../lib/editor/coreApi/setContentModel'; +import { EditorCore } from 'roosterjs-editor-types'; +import { setContentModel } from '../../lib/coreApi/setContentModel'; +import { StandaloneEditorCore } from 'roosterjs-content-model-types'; const mockedRange = 'RANGE' as any; const mockedDoc = 'DOCUMENT' as any; @@ -12,7 +13,7 @@ const mockedDiv = { ownerDocument: mockedDoc } as any; const mockedConfig = 'CONFIG' as any; describe('setContentModel', () => { - let core: ContentModelEditorCore; + let core: StandaloneEditorCore & EditorCore; let contentModelToDomSpy: jasmine.Spy; let createEditorContext: jasmine.Spy; let createModelToDomContextSpy: jasmine.Spy; @@ -48,7 +49,7 @@ describe('setContentModel', () => { lifecycle: {}, defaultModelToDomConfig: mockedConfig, cache: {}, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; }); it('no default option, no shadow edit', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/switchShadowEditTest.ts b/packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts similarity index 90% rename from packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/switchShadowEditTest.ts rename to packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts index 4d679d7b833..effd9f25284 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/switchShadowEditTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/coreApi/switchShadowEditTest.ts @@ -1,13 +1,13 @@ -import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelections'; -import { ContentModelEditorCore } from '../../../lib/publicTypes/ContentModelEditorCore'; -import { PluginEventType } from 'roosterjs-editor-types'; -import { switchShadowEdit } from '../../../lib/editor/coreApi/switchShadowEdit'; +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'; const mockedModel = 'MODEL' as any; const mockedCachedModel = 'CACHEMODEL' as any; describe('switchShadowEdit', () => { - let core: ContentModelEditorCore; + let core: StandaloneEditorCore & EditorCore; let createContentModel: jasmine.Spy; let setContentModel: jasmine.Spy; let getSelectionRange: jasmine.Spy; @@ -29,7 +29,7 @@ describe('switchShadowEdit', () => { lifecycle: {}, contentDiv: document.createElement('div'), cache: {}, - } as any) as ContentModelEditorCore; + } as any) as StandaloneEditorCore & EditorCore; }); describe('was off', () => { @@ -46,7 +46,7 @@ describe('switchShadowEdit', () => { { eventType: PluginEventType.EnteredShadowEdit, fragment: document.createDocumentFragment(), - selectionPath: undefined, + selectionPath: { start: [], end: [] }, }, false ); @@ -67,7 +67,7 @@ describe('switchShadowEdit', () => { { eventType: PluginEventType.EnteredShadowEdit, fragment: document.createDocumentFragment(), - selectionPath: undefined, + selectionPath: { start: [], end: [] }, }, false ); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCachePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts similarity index 96% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCachePluginTest.ts rename to packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts index 1379c66c658..ca503a4feaf 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCachePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCachePluginTest.ts @@ -1,13 +1,15 @@ -import { ContentModelCachePluginState } from '../../../lib/publicTypes/pluginState/ContentModelCachePluginState'; -import { ContentModelDomIndexer } from 'roosterjs-content-model-types'; -import { default as ContentModelCachePlugin } from '../../../lib/editor/corePlugins/ContentModelCachePlugin'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { PluginEventType } from 'roosterjs-editor-types'; +import { ContentModelCachePlugin } from '../../lib/corePlugin/ContentModelCachePlugin'; +import { IEditor, PluginEventType } from 'roosterjs-editor-types'; +import { + ContentModelCachePluginState, + ContentModelDomIndexer, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; describe('ContentModelCachePlugin', () => { let plugin: ContentModelCachePlugin; let state: ContentModelCachePluginState; - let editor: IContentModelEditor; + let editor: IStandaloneEditor & IEditor; let addEventListenerSpy: jasmine.Spy; let removeEventListenerSpy: jasmine.Spy; @@ -37,7 +39,7 @@ describe('ContentModelCachePlugin', () => { removeEventListener: removeEventListenerSpy, }; }, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor & IEditor; plugin = new ContentModelCachePlugin(state); plugin.initialize(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts similarity index 86% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts rename to packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts index 88f180cf63b..40ae5ce1799 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelCopyPastePluginTest.ts @@ -1,20 +1,25 @@ -import * as addRangeToSelection from '../../../lib/domUtils/addRangeToSelection'; -import * as cloneModelFile from '../../../lib/modelApi/common/cloneModel'; +import * as addRangeToSelection from '../../lib/corePlugin/utils/addRangeToSelection'; +import * as cloneModelFile from '../../lib/publicApi/model/cloneModel'; import * as contentModelToDomFile from 'roosterjs-content-model-dom/lib/modelToDom/contentModelToDom'; -import * as deleteSelectionsFile from '../../../lib/modelApi/edit/deleteSelection'; +import * as deleteSelectionsFile from '../../lib/publicApi/selection/deleteSelection'; import * as extractClipboardItemsFile from 'roosterjs-editor-dom/lib/clipboard/extractClipboardItems'; -import * as iterateSelectionsFile from '../../../lib/modelApi/selection/iterateSelections'; +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/utils/paste'; +import * as PasteFile from '../../lib/publicApi/model/paste'; import { createModelToDomContext } from 'roosterjs-content-model-dom'; import { createRange } from 'roosterjs-editor-dom'; -import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; -import { DOMSelection } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; import { setEntityElementClasses } from 'roosterjs-content-model-dom/test/domUtils/entityUtilTest'; -import ContentModelCopyPastePlugin, { +import { + ContentModelDocument, + DOMSelection, + ContentModelFormatter, + FormatWithContentModelOptions, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; +import { + ContentModelCopyPastePlugin, onNodeCreated, -} from '../../../lib/editor/corePlugins/ContentModelCopyPastePlugin'; +} from '../../lib/corePlugin/ContentModelCopyPastePlugin'; import { ClipboardData, ColorTransformDirection, @@ -23,7 +28,6 @@ import { } from 'roosterjs-editor-types'; const modelValue = 'model' as any; -const darkColorHandler = 'darkColorHandler' as any; const pasteModelValue = 'pasteModelValue' as any; const insertPointValue = 'insertPoint' as any; const deleteResultValue = 'deleteResult' as any; @@ -41,17 +45,21 @@ describe('ContentModelCopyPastePlugin |', () => { let createContentModelSpy: jasmine.Spy; let triggerPluginEventSpy: jasmine.Spy; let focusSpy: jasmine.Spy; - let undoSnapShotSpy: jasmine.Spy; + let formatContentModelSpy: jasmine.Spy; let setDOMSelectionSpy: jasmine.Spy; - let setContentModelSpy: jasmine.Spy; let isDisposed: jasmine.Spy; let pasteSpy: jasmine.Spy; let cloneModelSpy: jasmine.Spy; let transformToDarkColorSpy: jasmine.Spy; let getVisibleViewportSpy: jasmine.Spy; + let formatResult: boolean | undefined; + let modelResult: ContentModelDocument | undefined; beforeEach(() => { + modelResult = undefined; + formatResult = undefined; + div = document.createElement('div'); getDOMSelectionSpy = jasmine .createSpy('getDOMSelection') @@ -61,9 +69,7 @@ describe('ContentModelCopyPastePlugin |', () => { .and.returnValue(modelValue); triggerPluginEventSpy = jasmine.createSpy('triggerPluginEventSpy'); focusSpy = jasmine.createSpy('focusSpy'); - undoSnapShotSpy = jasmine.createSpy('undoSnapShotSpy'); setDOMSelectionSpy = jasmine.createSpy('setDOMSelection'); - setContentModelSpy = jasmine.createSpy('setContentModel'); pasteSpy = jasmine.createSpy('paste_'); isDisposed = jasmine.createSpy('isDisposed'); getVisibleViewportSpy = jasmine.createSpy('getVisibleViewport'); @@ -72,12 +78,25 @@ describe('ContentModelCopyPastePlugin |', () => { (model: any) => pasteModelValue ); transformToDarkColorSpy = jasmine.createSpy('transformToDarkColor'); + formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + modelResult = createContentModelSpy(); + formatResult = callback(modelResult!, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + spyOn(addRangeToSelection, 'addRangeToSelection'); plugin = new ContentModelCopyPastePlugin({ allowedCustomPasteType, }); - editor = ({ + editor = ({ addDomEventHandler: ( nameOrMap: string | Record, handler?: DOMEventHandlerFunction @@ -97,15 +116,8 @@ describe('ContentModelCopyPastePlugin |', () => { focus() { focusSpy(); }, - addUndoSnapshot(callback: any, changeSource: any, canUndoByBackspace: any) { - callback?.(); - undoSnapShotSpy(callback, changeSource, canUndoByBackspace); - }, getDOMSelection: getDOMSelectionSpy, setDOMSelection: setDOMSelectionSpy, - setContentModel(model: any, option: any) { - setContentModelSpy(model, option); - }, getDocument() { return document; }, @@ -116,9 +128,6 @@ describe('ContentModelCopyPastePlugin |', () => { ) { return div; }, - getDarkColorHandler: () => { - return darkColorHandler; - }, isDarkMode: () => { return false; }, @@ -128,6 +137,7 @@ describe('ContentModelCopyPastePlugin |', () => { transformToDarkColor: transformToDarkColorSpy, isDisposed, getVisibleViewport: getVisibleViewportSpy, + formatContentModel: formatContentModelSpy, }); plugin.initialize(editor); @@ -143,9 +153,7 @@ describe('ContentModelCopyPastePlugin |', () => { createContentModelSpy.and.callThrough(); triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); - undoSnapShotSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); domEvents.copy?.({}); @@ -153,9 +161,8 @@ describe('ContentModelCopyPastePlugin |', () => { expect(createContentModelSpy).not.toHaveBeenCalled(); expect(triggerPluginEventSpy).not.toHaveBeenCalled(); expect(focusSpy).not.toHaveBeenCalled(); - expect(undoSnapShotSpy).not.toHaveBeenCalled(); expect(setDOMSelectionSpy).not.toHaveBeenCalled(); - expect(setContentModelSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalsy(); }); it('Selection not Collapsed and normal selection', () => { @@ -172,7 +179,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.copy?.({}); @@ -194,8 +200,8 @@ describe('ContentModelCopyPastePlugin |', () => { expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); // On Cut Spy - expect(undoSnapShotSpy).not.toHaveBeenCalled(); - expect(setContentModelSpy).not.toHaveBeenCalledWith(); + expect(formatContentModelSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalsy(); }); it('Selection not Collapsed and table selection', () => { @@ -221,7 +227,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.copy?.({}); @@ -243,8 +248,8 @@ describe('ContentModelCopyPastePlugin |', () => { expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); // On Cut Spy - expect(undoSnapShotSpy).not.toHaveBeenCalled(); - expect(setContentModelSpy).not.toHaveBeenCalledWith(); + expect(formatContentModelSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalsy(); }); it('Selection not Collapsed and image selection', () => { @@ -266,7 +271,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.copy?.({}); @@ -287,8 +291,8 @@ describe('ContentModelCopyPastePlugin |', () => { expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); // On Cut Spy - expect(undoSnapShotSpy).not.toHaveBeenCalled(); - expect(setContentModelSpy).not.toHaveBeenCalledWith(); + expect(formatContentModelSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalsy(); expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalledTimes(0); }); @@ -314,7 +318,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); editor.isDarkMode = () => true; @@ -359,8 +362,8 @@ describe('ContentModelCopyPastePlugin |', () => { expect(cloneModelSpy).toHaveBeenCalledTimes(1); // On Cut Spy - expect(undoSnapShotSpy).not.toHaveBeenCalled(); - expect(setContentModelSpy).not.toHaveBeenCalledWith(); + expect(formatContentModelSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalsy(); expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalledTimes(0); }); }); @@ -376,9 +379,7 @@ describe('ContentModelCopyPastePlugin |', () => { createContentModelSpy.and.callThrough(); triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); - undoSnapShotSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.cut?.({}); @@ -388,9 +389,9 @@ describe('ContentModelCopyPastePlugin |', () => { expect(createContentModelSpy).not.toHaveBeenCalled(); expect(triggerPluginEventSpy).not.toHaveBeenCalled(); expect(focusSpy).not.toHaveBeenCalled(); - expect(undoSnapShotSpy).not.toHaveBeenCalled(); + expect(formatContentModelSpy).not.toHaveBeenCalled(); expect(setDOMSelectionSpy).not.toHaveBeenCalled(); - expect(setContentModelSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalsy(); }); it('Selection not Collapsed', () => { @@ -414,7 +415,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.cut?.({}); @@ -430,13 +430,14 @@ describe('ContentModelCopyPastePlugin |', () => { onNodeCreated ); expect(createContentModelSpy).toHaveBeenCalled(); - expect(triggerPluginEventSpy).toHaveBeenCalledTimes(2); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(focusSpy).toHaveBeenCalled(); expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); // On Cut Spy - expect(undoSnapShotSpy).toHaveBeenCalled(); - expect(setContentModelSpy).toHaveBeenCalledWith(modelValue, undefined); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); + expect(modelResult).toEqual(modelValue); }); it('Selection not Collapsed and table selection', () => { @@ -449,7 +450,7 @@ describe('ContentModelCopyPastePlugin |', () => { }; spyOn(deleteSelectionsFile, 'deleteSelection').and.returnValue({ - deleteResult: DeleteResult.Range, + deleteResult: 'range', insertPoint: null!, }); spyOn(contentModelToDomFile, 'contentModelToDom').and.callFake(() => { @@ -465,7 +466,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.cut?.({}); @@ -480,15 +480,15 @@ describe('ContentModelCopyPastePlugin |', () => { onNodeCreated ); expect(createContentModelSpy).toHaveBeenCalled(); - expect(triggerPluginEventSpy).toHaveBeenCalledTimes(2); expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalled(); expect(focusSpy).toHaveBeenCalled(); expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); // On Cut Spy - expect(undoSnapShotSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(deleteSelectionsFile.deleteSelection).toHaveBeenCalled(); - expect(setContentModelSpy).toHaveBeenCalledWith(modelValue, undefined); + expect(formatResult).toBeTrue(); + expect(modelResult).toEqual(modelValue); expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(modelValue); }); @@ -502,7 +502,7 @@ describe('ContentModelCopyPastePlugin |', () => { }; spyOn(deleteSelectionsFile, 'deleteSelection').and.returnValue({ - deleteResult: DeleteResult.Range, + deleteResult: 'range', insertPoint: null!, }); spyOn(contentModelToDomFile, 'contentModelToDom').and.callFake(() => { @@ -515,7 +515,6 @@ describe('ContentModelCopyPastePlugin |', () => { triggerPluginEventSpy.and.callThrough(); focusSpy.and.callThrough(); setDOMSelectionSpy.and.callThrough(); - setContentModelSpy.and.callThrough(); // Act domEvents.cut?.({}); @@ -530,14 +529,15 @@ describe('ContentModelCopyPastePlugin |', () => { onNodeCreated ); expect(createContentModelSpy).toHaveBeenCalled(); - expect(triggerPluginEventSpy).toHaveBeenCalledTimes(2); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(focusSpy).toHaveBeenCalled(); expect(setDOMSelectionSpy).toHaveBeenCalledWith(selectionValue); // On Cut Spy - expect(undoSnapShotSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(deleteSelectionsFile.deleteSelection).toHaveBeenCalled(); - expect(setContentModelSpy).toHaveBeenCalledWith(modelValue, undefined); + expect(formatResult).toBeTrue(); + expect(modelResult).toEqual(modelValue); expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(modelValue); }); }); @@ -546,7 +546,7 @@ describe('ContentModelCopyPastePlugin |', () => { let clipboardData = {}; it('Handle', () => { - spyOn(PasteFile, 'default').and.callFake(() => {}); + spyOn(PasteFile, 'paste').and.callFake(() => {}); const preventDefaultSpy = jasmine.createSpy('preventDefaultPaste'); let clipboardEvent = { clipboardData: ({ @@ -566,7 +566,7 @@ describe('ContentModelCopyPastePlugin |', () => { domEvents.paste?.(clipboardEvent); expect(pasteSpy).not.toHaveBeenCalledWith(clipboardData); - expect(PasteFile.default).toHaveBeenCalled(); + expect(PasteFile.paste).toHaveBeenCalled(); expect(extractClipboardItemsFile.default).toHaveBeenCalledWith( Array.from(clipboardEvent.clipboardData!.items), { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/corePlugins/ContentModelFormatPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts similarity index 52% rename from packages-content-model/roosterjs-content-model-editor/test/editor/corePlugins/ContentModelFormatPluginTest.ts rename to packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts index 9e42c3e47be..ba2bfeb59fb 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/corePlugins/ContentModelFormatPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/ContentModelFormatPluginTest.ts @@ -1,28 +1,34 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import ContentModelFormatPlugin from '../../../lib/editor/corePlugins/ContentModelFormatPlugin'; -import { ChangeSource } from '../../../lib/publicTypes/event/ContentModelContentChangedEvent'; -import { ContentModelFormatPluginState } from '../../../lib/publicTypes/pluginState/ContentModelFormatPluginState'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { PluginEventType } from 'roosterjs-editor-types'; +import * as applyPendingFormat from '../../lib/corePlugin/utils/applyPendingFormat'; +import { ContentModelFormatPlugin } from '../../lib/corePlugin/ContentModelFormatPlugin'; +import { IEditor, PluginEventType } from 'roosterjs-editor-types'; +import { + ContentModelFormatPluginState, + IStandaloneEditor, + PendingFormat, +} from 'roosterjs-content-model-types'; import { addSegment, createContentModelDocument, createSelectionMarker, - createText, } from 'roosterjs-content-model-dom'; describe('ContentModelFormatPlugin', () => { - it('no pending format, trigger key down event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); + const mockedFormat = { + fontSize: '10px', + }; + + beforeEach(() => { + spyOn(applyPendingFormat, 'applyPendingFormat'); + }); + it('no pending format, trigger key down event', () => { const editor = ({ cacheContentModel: () => {}, isDarkMode: () => false, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: ({} as any) as PendingFormat, }; const plugin = new ContentModelFormatPlugin(state); plugin.initialize(editor); @@ -34,26 +40,23 @@ describe('ContentModelFormatPlugin', () => { plugin.dispose(); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); + expect(applyPendingFormat.applyPendingFormat).not.toHaveBeenCalled(); + expect(state.pendingFormat).toBeNull(); }); it('no selection, trigger input event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); const editor = ({ focus: jasmine.createSpy('focus'), createContentModel: () => model, - setContentModel, isInIME: () => false, cacheContentModel: () => {}, - } as any) as IContentModelEditor; + getEnvironment: () => ({}), + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); const model = createContentModelDocument(); @@ -67,17 +70,16 @@ describe('ContentModelFormatPlugin', () => { plugin.dispose(); - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); + expect(applyPendingFormat.applyPendingFormat).toHaveBeenCalledTimes(1); + expect(applyPendingFormat.applyPendingFormat).toHaveBeenCalledWith( + editor, + 'a', + mockedFormat + ); + expect(state.pendingFormat).toBeNull(); }); - it('with pending format and selection, has correct text before, trigger input event with isComposing = true', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - const setContentModel = jasmine.createSpy('setContentModel'); + it('with pending format and selection, trigger input event with isComposing = true', () => { const model = createContentModelDocument(); const marker = createSelectionMarker(); @@ -85,11 +87,14 @@ describe('ContentModelFormatPlugin', () => { const editor = ({ createContentModel: () => model, - setContentModel, cacheContentModel: () => {}, - } as any) as IContentModelEditor; + getEnvironment: () => ({}), + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); plugin.initialize(editor); @@ -99,134 +104,17 @@ describe('ContentModelFormatPlugin', () => { }); plugin.dispose(); - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(0); - }); - - it('with pending format and selection, no correct text before, trigger input event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', + expect(applyPendingFormat.applyPendingFormat).not.toHaveBeenCalled(); + expect(state.pendingFormat).toEqual({ + format: mockedFormat, }); - - const setContentModel = jasmine.createSpy('setContentModel'); - const model = createContentModelDocument(); - const marker = createSelectionMarker(); - - addSegment(model, marker); - - const editor = ({ - focus: jasmine.createSpy('focus'), - createContentModel: () => model, - setContentModel, - isInIME: () => false, - cacheContentModel: () => {}, - } as any) as IContentModelEditor; - const state = { - defaultFormat: {}, - }; - const plugin = new ContentModelFormatPlugin(state); - plugin.initialize(editor); - plugin.onPluginEvent({ - eventType: PluginEventType.Input, - rawEvent: ({ data: 'a' } as any) as InputEvent, - }); - plugin.dispose(); - - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); }); - it('with pending format and selection, has correct text before, trigger input event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); - const model = createContentModelDocument(); - const text = createText('a'); - const marker = createSelectionMarker(); - - addSegment(model, text); - addSegment(model, marker); - - const editor = ({ - createContentModel: () => model, - setContentModel, - isInIME: () => false, - focus: () => {}, - addUndoSnapshot: (callback: () => void) => { - callback(); - }, - cacheContentModel: () => {}, - isDarkMode: () => false, - triggerPluginEvent: jasmine.createSpy('triggerPluginEvent'), - getVisibleViewport: jasmine.createSpy('getVisibleViewport'), - } as any) as IContentModelEditor; - const state = { - defaultFormat: {}, - }; - const plugin = new ContentModelFormatPlugin(state); - plugin.initialize(editor); - plugin.onPluginEvent({ - eventType: PluginEventType.Input, - rawEvent: ({ data: 'a' } as any) as InputEvent, - }); - plugin.dispose(); - - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - isImplicit: false, - segments: [ - { - segmentType: 'Text', - format: { fontSize: '10px' }, - text: 'a', - }, - { - segmentType: 'SelectionMarker', - format: { fontSize: '10px' }, - isSelected: true, - }, - ], - }, - ], - }, - undefined, - undefined - ); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); - }); - - it('with pending format and selection, has correct text before, trigger CompositionEnd event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); + it('with pending format and selection, trigger CompositionEnd event', () => { const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - const model = createContentModelDocument(); - const text = createText('test a test', { fontFamily: 'Arial' }); - const marker = createSelectionMarker(); - - addSegment(model, text); - addSegment(model, marker); const editor = ({ - createContentModel: () => model, - setContentModel, focus: () => {}, addUndoSnapshot: (callback: () => void) => { callback(); @@ -235,9 +123,12 @@ describe('ContentModelFormatPlugin', () => { isDarkMode: () => false, triggerPluginEvent, getVisibleViewport, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); @@ -248,67 +139,26 @@ describe('ContentModelFormatPlugin', () => { }); plugin.dispose(); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { - contentModel: model, - selection: undefined, - data: undefined, - source: ChangeSource.Format, - additionalData: { - formatApiName: 'applyPendingFormat', - }, - }); - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith( - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - isImplicit: false, - segments: [ - { - segmentType: 'Text', - format: { fontFamily: 'Arial' }, - text: 'test a ', - }, - { - segmentType: 'Text', - format: { fontSize: '10px', fontFamily: 'Arial' }, - text: 'test', - }, - { - segmentType: 'SelectionMarker', - format: { fontSize: '10px' }, - isSelected: true, - }, - ], - }, - ], - }, - undefined, - undefined + expect(applyPendingFormat.applyPendingFormat).toHaveBeenCalledWith( + editor, + 'test', + mockedFormat ); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); + expect(state.pendingFormat).toBeNull(); }); it('Non-input and cursor moving key down should not trigger pending format change', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const editor = ({ createContentModel: () => model, - setContentModel, cacheContentModel: () => {}, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); plugin.initialize(editor); @@ -318,33 +168,32 @@ describe('ContentModelFormatPlugin', () => { }); plugin.dispose(); - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(0); + expect(applyPendingFormat.applyPendingFormat).not.toHaveBeenCalled(); + expect(state.pendingFormat).toEqual({ + format: mockedFormat, + }); }); it('Content changed event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'canApplyPendingFormat').and.returnValue(false); - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const editor = ({ createContentModel: () => model, - setContentModel, addUndoSnapshot: (callback: () => void) => { callback(); }, cacheContentModel: () => {}, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); + + spyOn(plugin as any, 'canApplyPendingFormat').and.returnValue(false); + plugin.initialize(editor); plugin.onPluginEvent({ eventType: PluginEventType.ContentChanged, @@ -352,32 +201,28 @@ describe('ContentModelFormatPlugin', () => { }); plugin.dispose(); - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); - expect(pendingFormat.canApplyPendingFormat).toHaveBeenCalledTimes(1); + expect(applyPendingFormat.applyPendingFormat).not.toHaveBeenCalled(); + expect(state.pendingFormat).toBeNull(); + expect((plugin as any).canApplyPendingFormat).toHaveBeenCalledTimes(1); }); it('Mouse up event', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'canApplyPendingFormat').and.returnValue(false); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const editor = ({ createContentModel: () => model, - setContentModel, cacheContentModel: () => {}, - } as any) as IContentModelEditor; + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); + spyOn(plugin as any, 'canApplyPendingFormat').and.returnValue(false); + plugin.initialize(editor); plugin.onPluginEvent({ eventType: PluginEventType.MouseUp, @@ -385,32 +230,29 @@ describe('ContentModelFormatPlugin', () => { }); plugin.dispose(); - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); - expect(pendingFormat.canApplyPendingFormat).toHaveBeenCalledTimes(1); + expect(applyPendingFormat.applyPendingFormat).not.toHaveBeenCalled(); + expect(state.pendingFormat).toBeNull(); + expect((plugin as any).canApplyPendingFormat).toHaveBeenCalledTimes(1); }); it('Mouse up event and pending format can still be applied', () => { - spyOn(pendingFormat, 'clearPendingFormat'); - spyOn(pendingFormat, 'canApplyPendingFormat').and.returnValue(true); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - - const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const editor = ({ createContentModel: () => model, - setContentModel, cacheContentModel: () => {}, - } as any) as IContentModelEditor; + getEnvironment: () => ({}), + } as any) as IStandaloneEditor & IEditor; const state = { defaultFormat: {}, + pendingFormat: { + format: mockedFormat, + } as any, }; const plugin = new ContentModelFormatPlugin(state); + spyOn(plugin as any, 'canApplyPendingFormat').and.returnValue(true); + plugin.initialize(editor); plugin.onPluginEvent({ eventType: PluginEventType.MouseUp, @@ -418,41 +260,46 @@ describe('ContentModelFormatPlugin', () => { }); plugin.dispose(); - expect(setContentModel).toHaveBeenCalledTimes(0); - expect(pendingFormat.clearPendingFormat).not.toHaveBeenCalled(); - expect(pendingFormat.canApplyPendingFormat).toHaveBeenCalledTimes(1); + expect(applyPendingFormat.applyPendingFormat).not.toHaveBeenCalled(); + expect(state.pendingFormat).toEqual({ + format: mockedFormat, + }); + expect((plugin as any).canApplyPendingFormat).toHaveBeenCalledTimes(1); }); }); describe('ContentModelFormatPlugin for default format', () => { - let editor: IContentModelEditor; + let editor: IStandaloneEditor & IEditor; let contentDiv: HTMLDivElement; let getDOMSelection: jasmine.Spy; let getPendingFormatSpy: jasmine.Spy; - let setPendingFormatSpy: jasmine.Spy; let cacheContentModelSpy: jasmine.Spy; let addUndoSnapshotSpy: jasmine.Spy; + let formatContentModelSpy: jasmine.Spy; beforeEach(() => { - setPendingFormatSpy = spyOn(pendingFormat, 'setPendingFormat'); - getPendingFormatSpy = spyOn(pendingFormat, 'getPendingFormat'); + getPendingFormatSpy = jasmine.createSpy('getPendingFormat'); getDOMSelection = jasmine.createSpy('getDOMSelection'); cacheContentModelSpy = jasmine.createSpy('cacheContentModel'); addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); + formatContentModelSpy = jasmine.createSpy('formatContentModelSpy'); contentDiv = document.createElement('div'); editor = ({ contains: (e: Node) => contentDiv != e && contentDiv.contains(e), getDOMSelection, + getPendingFormat: getPendingFormatSpy, cacheContentModel: cacheContentModelSpy, addUndoSnapshot: addUndoSnapshotSpy, - } as any) as IContentModelEditor; + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor & IEditor; }); it('Collapsed range, text input, under editor directly', () => { const state: ContentModelFormatPluginState = { defaultFormat: { fontFamily: 'Arial' }, + pendingFormat: null, }; const plugin = new ContentModelFormatPlugin(state); const rawEvent = { key: 'a' } as any; @@ -466,9 +313,11 @@ describe('ContentModelFormatPlugin for default format', () => { }, }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_1: any, _2: any, callback: Function) => { - callback({ + let context = {} as any; + + formatContentModelSpy.and.callFake((callback: Function) => { + callback( + { blockGroupType: 'Document', blocks: [ { @@ -484,9 +333,10 @@ describe('ContentModelFormatPlugin for default format', () => { ], }, ], - }); - } - ); + }, + context + ); + }); plugin.initialize(editor); @@ -495,20 +345,19 @@ describe('ContentModelFormatPlugin for default format', () => { rawEvent, }); - expect(setPendingFormatSpy).toHaveBeenCalledWith( - editor, - { fontFamily: 'Arial' }, - contentDiv, - 0 - ); + expect(context).toEqual({ + newPendingFormat: { fontFamily: 'Arial' }, + }); }); it('Expanded range, text input, under editor directly', () => { const state = { defaultFormat: { fontFamily: 'Arial' }, + pendingFormat: null as any, }; const plugin = new ContentModelFormatPlugin(state); const rawEvent = { key: 'a' } as any; + const context = {} as any; getDOMSelection.and.returnValue({ type: 'range', @@ -519,9 +368,9 @@ describe('ContentModelFormatPlugin for default format', () => { }, }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_1: any, _2: any, callback: Function) => { - callback({ + formatContentModelSpy.and.callFake((callback: Function) => { + callback( + { blockGroupType: 'Document', blocks: [ { @@ -538,9 +387,10 @@ describe('ContentModelFormatPlugin for default format', () => { ], }, ], - }); - } - ); + }, + context + ); + }); plugin.initialize(editor); @@ -549,16 +399,18 @@ describe('ContentModelFormatPlugin for default format', () => { rawEvent, }); - expect(setPendingFormatSpy).not.toHaveBeenCalled(); + expect(context).toEqual({}); expect(addUndoSnapshotSpy).toHaveBeenCalledTimes(1); }); it('Collapsed range, IME input, under editor directly', () => { const state = { defaultFormat: { fontFamily: 'Arial' }, + pendingFormat: null as any, }; const plugin = new ContentModelFormatPlugin(state); const rawEvent = { key: 'Process' } as any; + const context = {} as any; getDOMSelection.and.returnValue({ type: 'range', @@ -569,9 +421,9 @@ describe('ContentModelFormatPlugin for default format', () => { }, }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_1: any, _2: any, callback: Function) => { - callback({ + formatContentModelSpy.and.callFake((callback: Function) => { + callback( + { blockGroupType: 'Document', blocks: [ { @@ -587,9 +439,10 @@ describe('ContentModelFormatPlugin for default format', () => { ], }, ], - }); - } - ); + }, + context + ); + }); plugin.initialize(editor); @@ -598,20 +451,19 @@ describe('ContentModelFormatPlugin for default format', () => { rawEvent, }); - expect(setPendingFormatSpy).toHaveBeenCalledWith( - editor, - { fontFamily: 'Arial' }, - contentDiv, - 0 - ); + expect(context).toEqual({ + newPendingFormat: { fontFamily: 'Arial' }, + }); }); it('Collapsed range, other input, under editor directly', () => { - const state = { + const state: ContentModelFormatPluginState = { defaultFormat: { fontFamily: 'Arial' }, + pendingFormat: null as any, }; const plugin = new ContentModelFormatPlugin(state); const rawEvent = { key: 'Up' } as any; + const context = {} as any; getDOMSelection.and.returnValue({ type: 'range', @@ -622,9 +474,9 @@ describe('ContentModelFormatPlugin for default format', () => { }, }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_1: any, _2: any, callback: Function) => { - callback({ + formatContentModelSpy.and.callFake((callback: Function) => { + callback( + { blockGroupType: 'Document', blocks: [ { @@ -640,9 +492,10 @@ describe('ContentModelFormatPlugin for default format', () => { ], }, ], - }); - } - ); + }, + context + ); + }); plugin.initialize(editor); @@ -651,16 +504,18 @@ describe('ContentModelFormatPlugin for default format', () => { rawEvent, }); - expect(setPendingFormatSpy).not.toHaveBeenCalled(); + expect(context).toEqual({}); }); it('Collapsed range, normal input, not under editor directly, no style', () => { const state = { defaultFormat: { fontFamily: 'Arial' }, + pendingFormat: null as any, }; const plugin = new ContentModelFormatPlugin(state); const rawEvent = { key: 'a' } as any; const div = document.createElement('div'); + const context = {} as any; contentDiv.appendChild(div); @@ -673,9 +528,9 @@ describe('ContentModelFormatPlugin for default format', () => { }, }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_1: any, _2: any, callback: Function) => { - callback({ + formatContentModelSpy.and.callFake((callback: Function) => { + callback( + { blockGroupType: 'Document', blocks: [ { @@ -690,9 +545,10 @@ describe('ContentModelFormatPlugin for default format', () => { ], }, ], - }); - } - ); + }, + context + ); + }); plugin.initialize(editor); @@ -701,15 +557,21 @@ describe('ContentModelFormatPlugin for default format', () => { rawEvent, }); - expect(setPendingFormatSpy).toHaveBeenCalledWith(editor, { fontFamily: 'Arial' }, div, 0); + expect(context).toEqual({ + newPendingFormat: { fontFamily: 'Arial' }, + }); }); it('Collapsed range, text input, under editor directly, has pending format', () => { const state = { defaultFormat: { fontFamily: 'Arial' }, + pendingFormat: null as any, }; const plugin = new ContentModelFormatPlugin(state); const rawEvent = { key: 'a' } as any; + const context = {} as any; + + getPendingFormatSpy.and.returnValue(null); getDOMSelection.and.returnValue({ type: 'range', @@ -720,9 +582,9 @@ describe('ContentModelFormatPlugin for default format', () => { }, }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_1: any, _2: any, callback: Function) => { - callback({ + formatContentModelSpy.and.callFake((callback: Function) => { + callback( + { blockGroupType: 'Document', blocks: [ { @@ -738,9 +600,10 @@ describe('ContentModelFormatPlugin for default format', () => { ], }, ], - }); - } - ); + }, + context + ); + }); getPendingFormatSpy.and.returnValue({ fontSize: '10pt', @@ -753,11 +616,8 @@ describe('ContentModelFormatPlugin for default format', () => { rawEvent, }); - expect(setPendingFormatSpy).toHaveBeenCalledWith( - editor, - { fontFamily: 'Arial', fontSize: '10pt' }, - contentDiv, - 0 - ); + expect(context).toEqual({ + newPendingFormat: { fontFamily: 'Arial', fontSize: '10pt' }, + }); }); }); 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 new file mode 100644 index 00000000000..a48c4ad531f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyDefaultFormatTest.ts @@ -0,0 +1,408 @@ +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, + ContentModelSegmentFormat, + FormatWithContentModelContext, + FormatWithContentModelOptions, + IStandaloneEditor, + InsertPoint, +} from 'roosterjs-content-model-types'; +import { + createContentModelDocument, + createDivider, + createImage, + createParagraph, + createSelectionMarker, + createText, +} from 'roosterjs-content-model-dom'; + +describe('applyDefaultFormat', () => { + let editor: IStandaloneEditor & IEditor; + let getDOMSelectionSpy: jasmine.Spy; + let formatContentModelSpy: jasmine.Spy; + let deleteSelectionSpy: jasmine.Spy; + let normalizeContentModelSpy: jasmine.Spy; + let addUndoSnapshotSpy: jasmine.Spy; + let getPendingFormatSpy: jasmine.Spy; + + let context: FormatWithContentModelContext | undefined; + let model: ContentModelDocument; + + let formatResult: boolean | undefined; + + const defaultFormat: ContentModelSegmentFormat = { + fontFamily: 'Arial', + fontSize: '10pt', + }; + + beforeEach(() => { + context = undefined; + formatResult = undefined; + model = createContentModelDocument(); + + getDOMSelectionSpy = jasmine.createSpy('getDOMSelectionSpy'); + deleteSelectionSpy = spyOn(deleteSelection, 'deleteSelection'); + normalizeContentModelSpy = spyOn(normalizeContentModel, 'normalizeContentModel'); + addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); + getPendingFormatSpy = jasmine.createSpy('getPendingFormat'); + + formatContentModelSpy = jasmine + .createSpy('formatContentModelSpy') + .and.callFake( + (formatter: ContentModelFormatter, options: FormatWithContentModelOptions) => { + context = { + deletedEntities: [], + newEntities: [], + newImages: [], + }; + + formatResult = formatter(model, context); + } + ); + + editor = { + contains: () => true, + getDOMSelection: getDOMSelectionSpy, + formatContentModel: formatContentModelSpy, + addUndoSnapshot: addUndoSnapshotSpy, + getPendingFormat: getPendingFormatSpy, + } as any; + }); + + it('No selection', () => { + getDOMSelectionSpy.and.returnValue(null); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).not.toHaveBeenCalled(); + }); + + it('Selection already has style', () => { + const node = document.createElement('div'); + node.style.fontFamily = 'Tahoma'; + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).not.toHaveBeenCalled(); + }); + + it('Good selection, delete range ', () => { + const node = document.createElement('div'); + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'range', + insertPoint: null!, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).toHaveBeenCalledWith(model); + expect(addUndoSnapshotSpy).toHaveBeenCalledTimes(1); + expect(formatResult).toBeTrue(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + }); + + it('Good selection, NothingToDelete ', () => { + const node = document.createElement('div'); + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'nothingToDelete', + insertPoint: null!, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalledWith(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + }); + + it('Good selection, SingleChar ', () => { + const node = document.createElement('div'); + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'singleChar', + insertPoint: null!, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalledWith(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + }); + + it('Good selection, NotDeleted, has text segment ', () => { + const node = document.createElement('div'); + const marker = createSelectionMarker(); + const text = createText('test'); + const para = createParagraph(); + + para.segments.push(text, marker); + model.blocks.push(para); + + const insertPoint: InsertPoint = { + marker, + path: [model], + paragraph: para, + }; + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'notDeleted', + insertPoint, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalled(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + }); + + it('Good selection, NotDeleted, no text segment ', () => { + const node = document.createElement('div'); + const marker = createSelectionMarker(); + const img = createImage('test'); + const para = createParagraph(); + + para.segments.push(img, marker); + model.blocks.push(para); + + const insertPoint: InsertPoint = { + marker, + path: [model], + paragraph: para, + }; + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'notDeleted', + insertPoint, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalled(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + newPendingFormat: { fontFamily: 'Arial', fontSize: '10pt' }, + }); + }); + + it('Good selection, NotDeleted, implicit and marker is the first segment, previous block is paragraph', () => { + const node = document.createElement('div'); + const marker = createSelectionMarker(); + const paraPrev = createParagraph(); + const para = createParagraph(true /*isImplicit*/); + + para.segments.push(marker); + model.blocks.push(paraPrev, para); + + const insertPoint: InsertPoint = { + marker, + path: [model], + paragraph: para, + }; + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'notDeleted', + insertPoint, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalled(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + }); + }); + + it('Good selection, NotDeleted, implicit and marker is the first segment, previous block is not paragraph', () => { + const node = document.createElement('div'); + const marker = createSelectionMarker(); + const divider = createDivider('hr'); + const para = createParagraph(true /*isImplicit*/); + + para.segments.push(marker); + model.blocks.push(divider, para); + + const insertPoint: InsertPoint = { + marker, + path: [model], + paragraph: para, + }; + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'notDeleted', + insertPoint, + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalled(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + newPendingFormat: { fontFamily: 'Arial', fontSize: '10pt' }, + }); + }); + + it('Good selection, NotDeleted, no text segment, has pending format and marker format', () => { + const node = document.createElement('div'); + const marker = createSelectionMarker({ + textColor: 'green', + backgroundColor: 'yellow', + }); + const img = createImage('test'); + const para = createParagraph(); + + para.segments.push(img, marker); + model.blocks.push(para); + + const insertPoint: InsertPoint = { + marker, + path: [model], + paragraph: para, + }; + + getDOMSelectionSpy.and.returnValue({ + type: 'range', + range: { + startContainer: node, + startOffset: 0, + }, + }); + + deleteSelectionSpy.and.returnValue({ + deleteResult: 'notDeleted', + insertPoint, + }); + + getPendingFormatSpy.and.returnValue({ + fontSize: '20pt', + textColor: 'red', + }); + + applyDefaultFormat(editor, defaultFormat); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(normalizeContentModelSpy).not.toHaveBeenCalled(); + expect(addUndoSnapshotSpy).not.toHaveBeenCalled(); + expect(formatResult).toBeFalse(); + expect(context).toEqual({ + deletedEntities: [], + newEntities: [], + newImages: [], + newPendingFormat: { + fontFamily: 'Arial', + fontSize: '20pt', + textColor: 'green', + backgroundColor: 'yellow', + }, + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts similarity index 71% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts rename to packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts index fe370b86b14..d05da7ffa3e 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/applyPendingFormatTest.ts @@ -1,14 +1,15 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; -import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelections'; +import * as iterateSelections from '../../../lib/publicApi/selection/iterateSelections'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import applyPendingFormat from '../../../lib/publicApi/format/applyPendingFormat'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { applyPendingFormat } from '../../../lib/corePlugin/utils/applyPendingFormat'; +import { IEditor } from 'roosterjs-editor-types'; import { ContentModelDocument, ContentModelParagraph, ContentModelSelectionMarker, ContentModelText, + ContentModelFormatter, + FormatWithContentModelOptions, + IStandaloneEditor, } from 'roosterjs-content-model-types'; import { createContentModelDocument, @@ -19,7 +20,6 @@ import { describe('applyPendingFormat', () => { it('Has pending format', () => { - const editor = ({} as any) as IContentModelEditor; const text: ContentModelText = { segmentType: 'Text', text: 'abc', @@ -40,27 +40,33 @@ describe('applyPendingFormat', () => { blocks: [paragraph], }; - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toEqual('applyPendingFormat'); + callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + + const editor = ({ + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor & IEditor; - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_, apiName, callback) => { - expect(apiName).toEqual('applyPendingFormat'); - callback(model, { - newEntities: [], - deletedEntities: [], - newImages: [], - }); - } - ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); return false; }); - applyPendingFormat(editor, 'c'); + applyPendingFormat(editor, 'c', { + fontSize: '10px', + }); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(model).toEqual({ blockGroupType: 'Document', blocks: [ @@ -92,7 +98,6 @@ describe('applyPendingFormat', () => { }); it('Has pending format but wrong text', () => { - const editor = ({} as any) as IContentModelEditor; const text: ContentModelText = { segmentType: 'Text', text: 'abc', @@ -113,23 +118,29 @@ describe('applyPendingFormat', () => { blocks: [paragraph], }; - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toEqual('applyPendingFormat'); + callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); + } + ); + + const editor = ({ + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor & IEditor; - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_, apiName, callback) => { - expect(apiName).toEqual('applyPendingFormat'); - callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); - } - ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); return false; }); - applyPendingFormat(editor, 'd'); + applyPendingFormat(editor, 'd', { + fontSize: '10px', + }); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(model).toEqual({ blockGroupType: 'Document', blocks: [ @@ -154,7 +165,6 @@ describe('applyPendingFormat', () => { }); it('No pending format', () => { - const editor = ({} as any) as IContentModelEditor; const text: ContentModelText = { segmentType: 'Text', text: 'abc', @@ -175,20 +185,17 @@ describe('applyPendingFormat', () => { blocks: [paragraph], }; - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); + const formatContentModelSpy = jasmine.createSpy('formatContentModel'); + const editor = ({ + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor & IEditor; - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_, apiName, callback) => { - expect(apiName).toEqual('applyPendingFormat'); - callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); - } - ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); return false; }); - applyPendingFormat(editor, 'd'); + applyPendingFormat(editor, 'd', {}); expect(model).toEqual({ blockGroupType: 'Document', @@ -214,7 +221,6 @@ describe('applyPendingFormat', () => { }); it('Selection is not collapsed', () => { - const editor = ({} as any) as IContentModelEditor; const text: ContentModelText = { segmentType: 'Text', text: 'abc', @@ -231,23 +237,29 @@ describe('applyPendingFormat', () => { blocks: [paragraph], }; - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toEqual('applyPendingFormat'); + callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); + } + ); + + const editor = ({ + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor & IEditor; - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_, apiName, callback) => { - expect(apiName).toEqual('applyPendingFormat'); - callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); - } - ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [text]); return false; }); - applyPendingFormat(editor, 'd'); + applyPendingFormat(editor, 'd', { + fontSize: '10px', + }); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(model).toEqual({ blockGroupType: 'Document', blocks: [ @@ -268,7 +280,6 @@ describe('applyPendingFormat', () => { }); it('Implicit paragraph', () => { - const editor = ({} as any) as IContentModelEditor; const text = createText('test'); const marker = createSelectionMarker(); const paragraph = createParagraph(true /*isImplicit*/); @@ -277,23 +288,30 @@ describe('applyPendingFormat', () => { paragraph.segments.push(text, marker); model.blocks.push(paragraph); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ - fontSize: '10px', - }); - spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( - (_, apiName, callback) => { - expect(apiName).toEqual('applyPendingFormat'); - callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); - } - ); + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toEqual('applyPendingFormat'); + callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); + } + ); + + const editor = ({ + formatContentModel: formatContentModelSpy, + } as any) as IStandaloneEditor & IEditor; + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { callback([model], undefined, paragraph, [marker]); return false; }); spyOn(normalizeContentModel, 'normalizeContentModel').and.callThrough(); - applyPendingFormat(editor, 't'); + applyPendingFormat(editor, 't', { + fontSize: '10px', + }); + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(model).toEqual({ blockGroupType: 'Document', blocks: [ diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/areSameRangeExTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/areSameRangeExTest.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/areSameRangeExTest.ts rename to packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/areSameRangeExTest.ts index 63f5cdbcac7..a97c0de20db 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/areSameRangeExTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/areSameRangeExTest.ts @@ -1,7 +1,7 @@ -import { areSameRangeEx } from '../../../lib/modelApi/selection/areSameRangeEx'; +import { areSameSelection } from '../../../lib/corePlugin/utils/areSameSelection'; import { DOMSelection } from 'roosterjs-content-model-types'; -describe('areSameRangeEx', () => { +describe('areSameSelection', () => { const startContainer = 'MockedStartContainer' as any; const endContainer = 'MockedEndContainer' as any; const startOffset = 1; @@ -10,7 +10,7 @@ describe('areSameRangeEx', () => { const image = 'MockedImage' as any; function runTest(r1: DOMSelection, r2: DOMSelection, result: boolean) { - expect(areSameRangeEx(r1, r2)).toBe(result); + expect(areSameSelection(r1, r2)).toBe(result); } it('Same object', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/contentModelDomIndexerTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/contentModelDomIndexerTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/utils/contentModelDomIndexerTest.ts rename to packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/contentModelDomIndexerTest.ts index 469a80ffce4..a6d1c33c4a2 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/contentModelDomIndexerTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/utils/contentModelDomIndexerTest.ts @@ -1,5 +1,5 @@ -import * as setSelection from '../../../lib/modelApi/selection/setSelection'; -import { contentModelDomIndexer } from '../../../lib/editor/utils/contentModelDomIndexer'; +import * as setSelection from '../../../lib/publicApi/selection/setSelection'; +import { contentModelDomIndexer } from '../../../lib/corePlugin/utils/contentModelDomIndexer'; import { createRange } from 'roosterjs-editor-dom'; import { ContentModelDocument, diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/createContentModelEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/createContentModelEditorCoreTest.ts new file mode 100644 index 00000000000..fb1cb935557 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/createContentModelEditorCoreTest.ts @@ -0,0 +1,237 @@ +import * as ContentModelCachePlugin from '../../lib/corePlugin/ContentModelCachePlugin'; +import * as ContentModelCopyPastePlugin from '../../lib/corePlugin/ContentModelCopyPastePlugin'; +import * as ContentModelFormatPlugin from '../../lib/corePlugin/ContentModelFormatPlugin'; +import * as createEditorCore from 'roosterjs-editor-core/lib/editor/createEditorCore'; +import * as promoteToContentModelEditorCore from '../../lib/editor/promoteToContentModelEditorCore'; +import { contentModelDomIndexer } from '../../lib/corePlugin/utils/contentModelDomIndexer'; +import { ContentModelTypeInContainerPlugin } from '../../lib/corePlugin/ContentModelTypeInContainerPlugin'; +import { createContentModelEditorCore } from '../../lib/editor/createContentModelEditorCore'; +import { EditorOptions } from 'roosterjs-editor-types'; +import { StandaloneEditorOptions } from 'roosterjs-content-model-types'; + +const mockedSwitchShadowEdit = 'SHADOWEDIT' as any; +const mockedFormatPlugin = 'FORMATPLUGIN' as any; +const mockedCachePlugin = 'CACHPLUGIN' as any; +const mockedCopyPastePlugin = 'COPYPASTE' as any; +const mockedCopyPastePlugin2 = 'COPYPASTE2' as any; + +describe('createContentModelEditorCore', () => { + let createEditorCoreSpy: jasmine.Spy; + let promoteToContentModelEditorCoreSpy: jasmine.Spy; + let mockedCore: any; + let contentDiv: any; + + beforeEach(() => { + contentDiv = { + style: {}, + } as any; + + mockedCore = { + lifecycle: { + experimentalFeatures: [], + }, + api: { + switchShadowEdit: mockedSwitchShadowEdit, + }, + originalApi: { + a: 'b', + }, + contentDiv, + } as any; + + createEditorCoreSpy = spyOn(createEditorCore, 'createEditorCore').and.returnValue( + mockedCore + ); + promoteToContentModelEditorCoreSpy = spyOn( + promoteToContentModelEditorCore, + 'promoteToContentModelEditorCore' + ); + spyOn(ContentModelFormatPlugin, 'createContentModelFormatPlugin').and.returnValue( + mockedFormatPlugin + ); + spyOn(ContentModelCachePlugin, 'createContentModelCachePlugin').and.returnValue( + mockedCachePlugin + ); + spyOn(ContentModelCopyPastePlugin, 'createContentModelCopyPastePlugin').and.returnValue( + mockedCopyPastePlugin + ); + }); + + it('No additional option', () => { + const core = createContentModelEditorCore(contentDiv, {}); + + const expectedOptions = { + plugins: [mockedCachePlugin, mockedFormatPlugin], + corePluginOverride: { + typeInContainer: new ContentModelTypeInContainerPlugin(), + copyPaste: mockedCopyPastePlugin, + }, + }; + const expectedPluginState: any = { + cache: { domIndexer: undefined }, + copyPaste: { allowedCustomPasteType: [] }, + format: { + defaultFormat: { + fontWeight: undefined, + italic: undefined, + underline: undefined, + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + backgroundColor: undefined, + }, + pendingFormat: null, + }, + }; + + expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, expectedOptions); + expect(promoteToContentModelEditorCoreSpy).toHaveBeenCalledWith( + core, + expectedOptions, + expectedPluginState + ); + }); + + it('With additional option', () => { + const defaultDomToModelOptions = { a: '1' } as any; + const defaultModelToDomOptions = { b: '2' } as any; + + const options = { + defaultDomToModelOptions, + defaultModelToDomOptions, + corePluginOverride: { + copyPaste: mockedCopyPastePlugin2, + }, + }; + const core = createContentModelEditorCore(contentDiv, options); + + const expectedOptions = { + defaultDomToModelOptions, + defaultModelToDomOptions, + plugins: [mockedCachePlugin, mockedFormatPlugin], + corePluginOverride: { + typeInContainer: new ContentModelTypeInContainerPlugin(), + copyPaste: mockedCopyPastePlugin2, + }, + }; + const expectedPluginState: any = { + cache: { domIndexer: undefined }, + copyPaste: { allowedCustomPasteType: [] }, + format: { + defaultFormat: { + fontWeight: undefined, + italic: undefined, + underline: undefined, + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + backgroundColor: undefined, + }, + pendingFormat: null, + }, + }; + + expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, expectedOptions); + expect(promoteToContentModelEditorCoreSpy).toHaveBeenCalledWith( + core, + expectedOptions, + expectedPluginState + ); + }); + + it('With default format', () => { + const options = { + defaultFormat: { + bold: true, + italic: true, + underline: true, + fontFamily: 'Arial', + fontSize: '10pt', + textColor: 'red', + backgroundColor: 'blue', + }, + }; + + const core = createContentModelEditorCore(contentDiv, options); + + const expectedOptions = { + plugins: [mockedCachePlugin, mockedFormatPlugin], + corePluginOverride: { + typeInContainer: new ContentModelTypeInContainerPlugin(), + copyPaste: mockedCopyPastePlugin, + }, + defaultFormat: { + bold: true, + italic: true, + underline: true, + fontFamily: 'Arial', + fontSize: '10pt', + textColor: 'red', + backgroundColor: 'blue', + }, + }; + const expectedPluginState: any = { + cache: { domIndexer: undefined }, + copyPaste: { allowedCustomPasteType: [] }, + format: { + defaultFormat: { + fontWeight: 'bold', + italic: true, + underline: true, + fontFamily: 'Arial', + fontSize: '10pt', + textColor: 'red', + backgroundColor: 'blue', + }, + pendingFormat: null, + }, + }; + + expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, expectedOptions); + expect(promoteToContentModelEditorCoreSpy).toHaveBeenCalledWith( + core, + expectedOptions, + expectedPluginState + ); + }); + + it('Allow dom indexer', () => { + const options: StandaloneEditorOptions & EditorOptions = { + cacheModel: true, + }; + + const core = createContentModelEditorCore(contentDiv, options); + + const expectedOptions = { + plugins: [mockedCachePlugin, mockedFormatPlugin], + corePluginOverride: { + typeInContainer: new ContentModelTypeInContainerPlugin(), + copyPaste: mockedCopyPastePlugin, + }, + cacheModel: true, + }; + const expectedPluginState: any = { + cache: { domIndexer: contentModelDomIndexer }, + copyPaste: { allowedCustomPasteType: [] }, + format: { + defaultFormat: { + fontWeight: undefined, + italic: undefined, + underline: undefined, + fontFamily: undefined, + fontSize: undefined, + textColor: undefined, + backgroundColor: undefined, + }, + pendingFormat: null, + }, + }; + + expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, expectedOptions); + expect(promoteToContentModelEditorCoreSpy).toHaveBeenCalledWith( + core, + expectedOptions, + expectedPluginState + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/promoteToContentModelEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/promoteToContentModelEditorCoreTest.ts new file mode 100644 index 00000000000..3b9d35f8840 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/editor/promoteToContentModelEditorCoreTest.ts @@ -0,0 +1,180 @@ +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 { ContentModelPluginState } from 'roosterjs-content-model-types'; +import { createContentModel } from '../../lib/coreApi/createContentModel'; +import { createEditorContext } from '../../lib/coreApi/createEditorContext'; +import { EditorCore } from 'roosterjs-editor-types'; +import { formatContentModel } from '../../lib/coreApi/formatContentModel'; +import { getDOMSelection } from '../../lib/coreApi/getDOMSelection'; +import { promoteToContentModelEditorCore } from '../../lib/editor/promoteToContentModelEditorCore'; +import { setContentModel } from '../../lib/coreApi/setContentModel'; +import { setDOMSelection } from '../../lib/coreApi/setDOMSelection'; +import { switchShadowEdit } from '../../lib/coreApi/switchShadowEdit'; +import { tablePreProcessor } from '../../lib/override/tablePreProcessor'; +import { + listItemMetadataApplier, + listLevelMetadataApplier, +} from '../../lib/metadata/updateListMetadata'; + +describe('promoteToContentModelEditorCore', () => { + let pluginState: ContentModelPluginState; + let core: EditorCore; + const mockedSwitchShadowEdit = 'SHADOWEDIT' as any; + const mockedDomToModelConfig = { + config: 'mockedDomToModelConfig', + } as any; + const mockedModelToDomConfig = { + config: 'mockedModelToDomConfig', + } as any; + + const baseResult: any = { + contentDiv: null!, + darkColorHandler: null!, + domEvent: null!, + edit: null!, + entity: null!, + getVisibleViewport: null!, + lifecycle: null!, + pendingFormatState: null!, + trustedHTMLHandler: null!, + undo: null!, + sizeTransformer: null!, + zoomScale: 1, + plugins: [], + }; + + beforeEach(() => { + pluginState = { + cache: {}, + copyPaste: { allowedCustomPasteType: [] }, + format: { + defaultFormat: {}, + pendingFormat: null, + }, + }; + core = { + ...baseResult, + api: { + switchShadowEdit: mockedSwitchShadowEdit, + } as any, + originalApi: { + switchShadowEdit: mockedSwitchShadowEdit, + } as any, + copyPaste: { allowedCustomPasteType: [] }, + }; + + spyOn(createDomToModelContext, 'createDomToModelConfig').and.returnValue( + mockedDomToModelConfig + ); + spyOn(createModelToDomContext, 'createModelToDomConfig').and.returnValue( + mockedModelToDomConfig + ); + }); + + it('No additional option', () => { + promoteToContentModelEditorCore(core, {}, pluginState); + + expect(core).toEqual({ + ...baseResult, + api: { + switchShadowEdit, + createEditorContext, + createContentModel, + setContentModel, + getDOMSelection, + setDOMSelection, + formatContentModel, + }, + originalApi: { + switchShadowEdit: mockedSwitchShadowEdit, + createEditorContext, + createContentModel, + setContentModel, + getDOMSelection, + setDOMSelection, + formatContentModel, + }, + defaultDomToModelOptions: [ + { processorOverride: { table: tablePreProcessor } }, + undefined, + ], + defaultModelToDomOptions: [ + { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + undefined, + ], + defaultDomToModelConfig: mockedDomToModelConfig, + defaultModelToDomConfig: mockedModelToDomConfig, + format: { + defaultFormat: {}, + pendingFormat: null, + }, + cache: {}, + copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false, isAndroid: false }, + } as any); + }); + + it('With additional option', () => { + const defaultDomToModelOptions = { a: '1' } as any; + const defaultModelToDomOptions = { b: '2' } as any; + const mockedPlugin = 'PLUGIN' as any; + const options = { + defaultDomToModelOptions, + defaultModelToDomOptions, + corePluginOverride: { + copyPaste: mockedPlugin, + }, + }; + + promoteToContentModelEditorCore(core, options, pluginState); + + expect(core).toEqual({ + ...baseResult, + api: { + switchShadowEdit, + createEditorContext, + createContentModel, + setContentModel, + getDOMSelection, + setDOMSelection, + formatContentModel, + }, + originalApi: { + switchShadowEdit: mockedSwitchShadowEdit, + createEditorContext, + createContentModel, + setContentModel, + getDOMSelection, + setDOMSelection, + formatContentModel, + }, + defaultDomToModelOptions: [ + { processorOverride: { table: tablePreProcessor } }, + defaultDomToModelOptions, + ], + defaultModelToDomOptions: [ + { + metadataAppliers: { + listItem: listItemMetadataApplier, + listLevel: listLevelMetadataApplier, + }, + }, + defaultModelToDomOptions, + ], + defaultDomToModelConfig: mockedDomToModelConfig, + defaultModelToDomConfig: mockedModelToDomConfig, + format: { + defaultFormat: {}, + pendingFormat: null, + }, + cache: {}, + copyPaste: { allowedCustomPasteType: [] }, + environment: { isMac: false, isAndroid: false }, + } as any); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/handleListItemWithMetadataTest.ts b/packages-content-model/roosterjs-content-model-core/test/metadata/handleListItemWithMetadataTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/handleListItemWithMetadataTest.ts rename to packages-content-model/roosterjs-content-model-core/test/metadata/handleListItemWithMetadataTest.ts index d86b1dc0ee0..3069b2e9100 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/handleListItemWithMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/metadata/handleListItemWithMetadataTest.ts @@ -18,7 +18,7 @@ import { import { listItemMetadataApplier, listLevelMetadataApplier, -} from '../../../lib/domUtils/metadata/updateListMetadata'; +} from '../../lib/metadata/updateListMetadata'; describe('handleListItem with metadata', () => { let context: ModelToDomContext; diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/handleListWithMetadataTest.ts b/packages-content-model/roosterjs-content-model-core/test/metadata/handleListWithMetadataTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/handleListWithMetadataTest.ts rename to packages-content-model/roosterjs-content-model-core/test/metadata/handleListWithMetadataTest.ts index 4936cfec9a6..cce69e71cfb 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/handleListWithMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/metadata/handleListWithMetadataTest.ts @@ -9,7 +9,7 @@ import { import { listItemMetadataApplier, listLevelMetadataApplier, -} from '../../../lib/domUtils/metadata/updateListMetadata'; +} from '../../lib/metadata/updateListMetadata'; describe('handleList with metadata', () => { let context: ModelToDomContext; diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateImageMetadataTest.ts b/packages-content-model/roosterjs-content-model-core/test/metadata/updateImageMetadataTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateImageMetadataTest.ts rename to packages-content-model/roosterjs-content-model-core/test/metadata/updateImageMetadataTest.ts index 7350d97afc7..d3f003d0991 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateImageMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/metadata/updateImageMetadataTest.ts @@ -1,5 +1,5 @@ import { ContentModelImage, ImageMetadataFormat } from 'roosterjs-content-model-types'; -import { updateImageMetadata } from '../../../lib/domUtils/metadata/updateImageMetadata'; +import { updateImageMetadata } from '../../lib/metadata/updateImageMetadata'; describe('updateImageMetadataTest', () => { it('No value', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateListMetadataTest.ts b/packages-content-model/roosterjs-content-model-core/test/metadata/updateListMetadataTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateListMetadataTest.ts rename to packages-content-model/roosterjs-content-model-core/test/metadata/updateListMetadataTest.ts index d5279e200a8..939fec274ed 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateListMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/metadata/updateListMetadataTest.ts @@ -11,7 +11,7 @@ import { listItemMetadataApplier, listLevelMetadataApplier, updateListMetadata, -} from '../../../lib/domUtils/metadata/updateListMetadata'; +} from '../../lib/metadata/updateListMetadata'; describe('updateListMetadata', () => { it('No value', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateTableCellMetadataTest.ts b/packages-content-model/roosterjs-content-model-core/test/metadata/updateTableCellMetadataTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateTableCellMetadataTest.ts rename to packages-content-model/roosterjs-content-model-core/test/metadata/updateTableCellMetadataTest.ts index c3a177e62c9..d5ecf9822ea 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateTableCellMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/metadata/updateTableCellMetadataTest.ts @@ -1,5 +1,5 @@ import { ContentModelTableCell, TableCellMetadataFormat } from 'roosterjs-content-model-types'; -import { updateTableCellMetadata } from '../../../lib/domUtils/metadata/updateTableCellMetadata'; +import { updateTableCellMetadata } from '../../lib/metadata/updateTableCellMetadata'; describe('updateTableCellMetadata', () => { it('No value', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateTableMetadataTest.ts b/packages-content-model/roosterjs-content-model-core/test/metadata/updateTableMetadataTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateTableMetadataTest.ts rename to packages-content-model/roosterjs-content-model-core/test/metadata/updateTableMetadataTest.ts index 9afb2b357a3..a4ecc891d51 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/metadata/updateTableMetadataTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/metadata/updateTableMetadataTest.ts @@ -1,4 +1,4 @@ -import { updateTableMetadata } from '../../../lib/domUtils/metadata/updateTableMetadata'; +import { updateTableMetadata } from '../../lib/metadata/updateTableMetadata'; import { ContentModelTable, TableBorderFormat, diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/utils/deleteSingleCharTest.ts b/packages-content-model/roosterjs-content-model-core/test/modelApi/edit/deleteSingleCharTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/utils/deleteSingleCharTest.ts rename to packages-content-model/roosterjs-content-model-core/test/modelApi/edit/deleteSingleCharTest.ts index 873f4d24147..d39aac74970 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/utils/deleteSingleCharTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/modelApi/edit/deleteSingleCharTest.ts @@ -1,4 +1,4 @@ -import { deleteSingleChar } from '../../../../lib/modelApi/edit/utils/deleteSingleChar'; +import { deleteSingleChar } from '../../../lib/modelApi/edit/deleteSingleChar'; describe('deleteSingleChar', () => { const tests = ['', 'a', '\u200b', 'ć„œ', 'đŸ‘©â€đŸ’»', '🎉', '🛏', '🎉', 'đŸ‘©â€â€ïžâ€đŸ’‹â€đŸ‘©']; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts b/packages-content-model/roosterjs-content-model-core/test/overrides/tablePreProcessorTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts rename to packages-content-model/roosterjs-content-model-core/test/overrides/tablePreProcessorTest.ts index 4dd8d30ef19..e759c7b4a19 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/overrides/tablePreProcessorTest.ts @@ -1,6 +1,6 @@ import * as tableProcessor from 'roosterjs-content-model-dom/lib/domToModel/processors/tableProcessor'; import { createContentModelDocument, createDomToModelContext } from 'roosterjs-content-model-dom'; -import { tablePreProcessor } from '../../../lib/editor/overrides/tablePreProcessor'; +import { tablePreProcessor } from '../../lib/override/tablePreProcessor'; describe('tablePreProcessor', () => { it('Table without metadata, use Entity', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/domUtils/borderValuesTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/domUtils/borderValuesTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/domUtils/borderValuesTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/domUtils/borderValuesTest.ts index 2dd23043397..16d895fb2f8 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domUtils/borderValuesTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/domUtils/borderValuesTest.ts @@ -1,4 +1,7 @@ -import { combineBorderValue, extractBorderValues } from '../../lib/domUtils/borderValues'; +import { + combineBorderValue, + extractBorderValues, +} from '../../../lib/publicApi/domUtils/borderValues'; describe('extractBorderValues', () => { it('undefined string', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/cloneModelTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/model/cloneModelTest.ts index 017f0e37797..6ba07467196 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/cloneModelTest.ts @@ -1,4 +1,4 @@ -import { cloneModel } from '../../../lib/modelApi/common/cloneModel'; +import { cloneModel } from '../../../lib/publicApi/model/cloneModel'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createEntity } from 'roosterjs-content-model-dom'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/getClosestAncestorBlockGroupIndexTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/getClosestAncestorBlockGroupIndexTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/getClosestAncestorBlockGroupIndexTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/model/getClosestAncestorBlockGroupIndexTest.ts index a2434b552ce..c8a7199c1cf 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/getClosestAncestorBlockGroupIndexTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/getClosestAncestorBlockGroupIndexTest.ts @@ -1,4 +1,4 @@ -import { getClosestAncestorBlockGroupIndex } from '../../../lib/modelApi/common/getClosestAncestorBlockGroupIndex'; +import { getClosestAncestorBlockGroupIndex } from '../../../lib/publicApi/model/getClosestAncestorBlockGroupIndex'; describe('getClosestAncestorBlockGroupIndex', () => { it('Empty path', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/isBlockGroupOfTypeTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/isBlockGroupOfTypeTest.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/isBlockGroupOfTypeTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/model/isBlockGroupOfTypeTest.ts index 5eb54d00f70..23c524a6860 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/isBlockGroupOfTypeTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/isBlockGroupOfTypeTest.ts @@ -1,4 +1,4 @@ -import { isBlockGroupOfType } from '../../../lib/modelApi/common/isBlockGroupOfType'; +import { isBlockGroupOfType } from '../../../lib/publicApi/model/isBlockGroupOfType'; describe('isBlockGroupOfType', () => { it('null input', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/mergeModelTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/model/mergeModelTest.ts index b83ad88b109..90401cf5b54 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/mergeModelTest.ts @@ -1,7 +1,6 @@ -import * as applyTableFormat from '../../../lib/modelApi/table/applyTableFormat'; -import * as normalizeTable from '../../../lib/modelApi/table/normalizeTable'; -import { FormatWithContentModelContext } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; -import { mergeModel } from '../../../lib/modelApi/common/mergeModel'; +import * as applyTableFormat from '../../../lib/publicApi/table/applyTableFormat'; +import * as normalizeTable from '../../../lib/publicApi/table/normalizeTable'; +import { mergeModel } from '../../../lib/publicApi/model/mergeModel'; import { ContentModelDocument, ContentModelImage, @@ -10,6 +9,7 @@ import { ContentModelSelectionMarker, ContentModelTable, ContentModelTableCell, + FormatWithContentModelContext, } from 'roosterjs-content-model-types'; import { createBr, diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/pasteTest.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/model/pasteTest.ts index 8277f055433..b0e61a6a330 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/model/pasteTest.ts @@ -1,25 +1,33 @@ -import * as addParserF from '../../../lib/editor/plugins/PastePlugin/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 '../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; -import * as getPasteSourceF from '../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import * as getSelectedSegmentsF from '../../../lib/publicApi/selection/getSelectedSegments'; -import * as mergeModelFile from '../../../lib/modelApi/common/mergeModel'; -import * as pendingFormatF from '../../../lib/modelApi/format/pendingFormat'; -import * as PPT from '../../../lib/editor/plugins/PastePlugin/PowerPoint/processPastedContentFromPowerPoint'; -import * as setProcessorF from '../../../lib/editor/plugins/PastePlugin/utils/setProcessor'; -import * as WacComponents from '../../../lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents'; -import * as WordDesktopFile from '../../../lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop'; -import ContentModelEditor from '../../../lib/editor/ContentModelEditor'; -import ContentModelPastePlugin from '../../../lib/editor/plugins/PastePlugin/ContentModelPastePlugin'; -import { ChangeSource } from '../../../lib/publicTypes/event/ContentModelContentChangedEvent'; -import { ContentModelDocument, DomToModelOption } from 'roosterjs-content-model-types'; +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 { 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 { expectEqual, initEditor } from '../../editor/plugins/paste/e2e/testUtils'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import paste, * as pasteF from '../../../lib/publicApi/utils/paste'; +import { + ContentModelDocument, + DomToModelOption, + ContentModelFormatter, + FormatWithContentModelContext, + FormatWithContentModelOptions, + IStandaloneEditor, +} from 'roosterjs-content-model-types'; +import { + expectEqual, + initEditor, +} from '../../../../roosterjs-content-model-plugins/test/paste/e2e/testUtils'; import { BeforePasteEvent, ClipboardData, + IEditor, PasteType, PluginEvent, PluginEventType, @@ -30,10 +38,8 @@ let clipboardData: ClipboardData; const DEFAULT_TIMES_ADD_PARSER_CALLED = 4; describe('Paste ', () => { - let editor: IContentModelEditor; - let addUndoSnapshot: jasmine.Spy; + let editor: IStandaloneEditor & IEditor; let createContentModel: jasmine.Spy; - let setContentModel: jasmine.Spy; let focus: jasmine.Spy; let mockedModel: ContentModelDocument; let mockedMergeModel: ContentModelDocument; @@ -45,7 +51,8 @@ describe('Paste ', () => { let triggerPluginEvent: jasmine.Spy; let getVisibleViewport: jasmine.Spy; let mergeModelSpy: jasmine.Spy; - let setPendingFormatSpy: jasmine.Spy; + let formatResult: boolean | undefined; + let context: FormatWithContentModelContext | undefined; const mockedPos = 'POS' as any; @@ -66,14 +73,11 @@ describe('Paste ', () => { mockedModel = ({} as any) as ContentModelDocument; mockedMergeModel = ({} as any) as ContentModelDocument; - addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); - setContentModel = jasmine.createSpy('setContentModel'); focus = jasmine.createSpy('focus'); getFocusedPosition = jasmine.createSpy('getFocusedPosition').and.returnValue(mockedPos); getContent = jasmine.createSpy('getContent'); getDocument = jasmine.createSpy('getDocument').and.returnValue(document); - setPendingFormatSpy = spyOn(pendingFormatF, 'setPendingFormat'); triggerPluginEvent = jasmine.createSpy('triggerPluginEvent').and.returnValue({ clipboardData, fragment: document.createDocumentFragment(), @@ -105,7 +109,7 @@ describe('Paste ', () => { mockedModel = mockedMergeModel; return null; }); - spyOn(getSelectedSegmentsF, 'default').and.returnValue([ + spyOn(getSelectedSegmentsF, 'getSelectedSegments').and.returnValue([ { format: { fontSize: '1pt', @@ -113,12 +117,25 @@ describe('Paste ', () => { }, } as any, ]); + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + context = { + newEntities: [], + deletedEntities: [], + newImages: [], + }; + formatResult = callback(mockedModel, context); + } + ); + + formatResult = undefined; + context = undefined; editor = ({ focus, - addUndoSnapshot, createContentModel, - setContentModel, getFocusedPosition, getContent, getSelectionRange, @@ -127,7 +144,8 @@ describe('Paste ', () => { triggerPluginEvent, getVisibleViewport, isDarkMode: () => false, - } as any) as IContentModelEditor; + formatContentModel, + } as any) as IStandaloneEditor & IEditor; }); afterEach(() => { @@ -136,11 +154,10 @@ describe('Paste ', () => { }); it('Execute', () => { - pasteF.default(editor, clipboardData); + pasteF.paste(editor, clipboardData); - expect(setContentModel).toHaveBeenCalled(); + expect(formatResult).toBeTrue(); expect(focus).toHaveBeenCalled(); - expect(addUndoSnapshot).toHaveBeenCalled(); expect(getContent).toHaveBeenCalled(); expect(triggerPluginEvent).toHaveBeenCalled(); expect(getDocument).toHaveBeenCalled(); @@ -149,22 +166,11 @@ describe('Paste ', () => { }); it('Execute | As plain text', () => { - pasteF.default(editor, clipboardData, 'asPlainText'); + pasteF.paste(editor, clipboardData, 'asPlainText'); - expect(setContentModel).toHaveBeenCalled(); + expect(formatResult).toBeTrue(); expect(focus).toHaveBeenCalled(); - expect(addUndoSnapshot).toHaveBeenCalled(); expect(getContent).toHaveBeenCalled(); - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { - contentModel: mockedModel, - selection: undefined, - data: clipboardData, - source: ChangeSource.Paste, - additionalData: { - formatApiName: 'Paste', - }, - }); expect(getDocument).toHaveBeenCalled(); expect(getTrustedHTMLHandler).toHaveBeenCalled(); expect(mockedModel).toEqual(mockedMergeModel); @@ -188,7 +194,7 @@ describe('Paste ', () => { }, }); - paste(editor, clipboardData); + pasteF.paste(editor, clipboardData); editor.createContentModel({ processorOverride: { @@ -196,9 +202,11 @@ describe('Paste ', () => { }, }); - expect(setPendingFormatSpy).toHaveBeenCalledWith( - editor, - { + expect(context).toEqual({ + newEntities: [], + newImages: [], + deletedEntities: [], + newPendingFormat: { backgroundColor: '', fontFamily: 'Arial', fontSize: '', @@ -211,9 +219,7 @@ describe('Paste ', () => { textColor: '', underline: false, }, - mockedNode, - mockedOffset - ); + }); }); }); @@ -250,7 +256,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); - pasteF.default(editor!, clipboardData); + pasteF.paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 3); @@ -261,7 +267,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); - pasteF.default(editor!, clipboardData); + pasteF.paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(4); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 4); @@ -272,7 +278,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.default(editor!, clipboardData); + pasteF.paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); @@ -283,7 +289,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.default(editor!, clipboardData); + pasteF.paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 1); @@ -294,7 +300,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); - pasteF.default(editor!, clipboardData); + pasteF.paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED); @@ -306,7 +312,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wordDesktop'); spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); - pasteF.default(editor!, clipboardData, 'asPlainText'); + pasteF.paste(editor!, clipboardData, 'asPlainText'); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -317,7 +323,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('wacComponents'); spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); - pasteF.default(editor!, clipboardData, 'asPlainText'); + pasteF.paste(editor!, clipboardData, 'asPlainText'); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -328,7 +334,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelOnline'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.default(editor!, clipboardData, 'asPlainText'); + pasteF.paste(editor!, clipboardData, 'asPlainText'); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -339,7 +345,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('excelDesktop'); spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); - pasteF.default(editor!, clipboardData, 'asPlainText'); + pasteF.paste(editor!, clipboardData, 'asPlainText'); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -350,7 +356,7 @@ describe('paste with content model & paste plugin', () => { spyOn(getPasteSourceF, 'getPasteSource').and.returnValue('powerPointDesktop'); spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); - pasteF.default(editor!, clipboardData, 'asPlainText'); + pasteF.paste(editor!, clipboardData, 'asPlainText'); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); expect(addParserF.default).toHaveBeenCalledTimes(0); @@ -386,7 +392,7 @@ describe('paste with content model & paste plugin', () => { ], }); - pasteF.default(editor!, clipboardData); + pasteF.paste(editor!, clipboardData); expect(eventChecker?.clipboardData).toEqual(clipboardData); expect(eventChecker?.htmlBefore).toBeTruthy(); @@ -623,7 +629,7 @@ describe('mergePasteContent', () => { }); describe('Paste with clipboardData', () => { - let editor: IContentModelEditor = undefined!; + let editor: IEditor & IStandaloneEditor = undefined!; const ID = 'EDITOR_ID'; beforeEach(() => { @@ -649,7 +655,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = '

Test

'; - paste(editor, clipboardData); + pasteF.paste(editor, clipboardData); const model = editor.createContentModel({ processorOverride: { @@ -692,7 +698,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = 'Link'; - paste(editor, clipboardData); + pasteF.paste(editor, clipboardData); const model = editor.createContentModel({ processorOverride: { @@ -724,7 +730,7 @@ describe('Paste with clipboardData', () => { clipboardData.rawHtml = 'Link'; - paste(editor, clipboardData); + pasteF.paste(editor, clipboardData); const model = editor.createContentModel({ processorOverride: { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/collectSelectionsTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/selection/collectSelectionsTest.ts index a9080dc1aa4..e1d56510259 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/collectSelectionsTest.ts @@ -1,5 +1,13 @@ -import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelections'; -import { TableSelectionContext } from '../../../lib/publicTypes/selection/TableSelectionContext'; +import * as iterateSelections from '../../../lib/publicApi/selection/iterateSelections'; +import { + ContentModelBlock, + ContentModelBlockGroup, + ContentModelBlockGroupType, + ContentModelParagraph, + ContentModelSegment, + ContentModelTable, + TableSelectionContext, +} from 'roosterjs-content-model-types'; import { createContentModelDocument, createDivider, @@ -13,14 +21,6 @@ import { createTableCell, createText, } from 'roosterjs-content-model-dom'; -import { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelBlockGroupType, - ContentModelParagraph, - ContentModelSegment, - ContentModelTable, -} from 'roosterjs-content-model-types'; import { getSelectedParagraphs, getFirstSelectedListItem, @@ -28,7 +28,7 @@ import { getOperationalBlocks, OperationalBlocks, getSelectedSegmentsAndParagraphs, -} from '../../../lib/modelApi/selection/collectSelections'; +} from '../../../lib/publicApi/selection/collectSelections'; interface SelectionInfo { path: ContentModelBlockGroup[]; diff --git a/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/deleteSelectionTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/deleteSelectionTest.ts new file mode 100644 index 00000000000..e8c0cef5414 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/deleteSelectionTest.ts @@ -0,0 +1,965 @@ +import { ContentModelSelectionMarker, DeletedEntity } from 'roosterjs-content-model-types'; +import { deleteSelection } from '../../../lib/publicApi/selection/deleteSelection'; +import { + createContentModelDocument, + createDivider, + createEntity, + createGeneralBlock, + createGeneralSegment, + createImage, + createParagraph, + createSelectionMarker, + createTable, + createTableCell, + createText, +} from 'roosterjs-content-model-dom'; + +describe('deleteSelection - selectionOnly', () => { + it('empty selection', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + + model.blocks.push(para); + + const result = deleteSelection(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [], + }, + ], + }); + + expect(result.deleteResult).toBe('notDeleted'); + expect(result.insertPoint).toBeNull(); + }); + + it('Single selection marker', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker({ fontSize: '10px' }); + + para.segments.push(marker); + model.blocks.push(para); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('notDeleted'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: { fontSize: '10px' }, + isSelected: true, + }, + paragraph: para, + path: [model], + tableContext: undefined, + }); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: { fontSize: '10px' }, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Single text selection', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const text = createText('test1', { fontSize: '10px' }); + + text.isSelected = true; + para.segments.push(text); + model.blocks.push(para); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: { fontSize: '10px' }, + isSelected: true, + }, + paragraph: para, + path: [model], + tableContext: undefined, + }); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: { fontSize: '10px' }, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Multiple text selection in multiple paragraphs', () => { + const model = createContentModelDocument(); + const para1 = createParagraph(); + const para2 = createParagraph(); + const text0 = createText('test0', { fontSize: '10px' }); + const text1 = createText('test1', { fontSize: '11px' }); + const text2 = createText('test2', { fontSize: '12px' }); + + text1.isSelected = true; + text2.isSelected = true; + + para1.segments.push(text0); + para1.segments.push(text1); + para2.segments.push(text2); + + model.blocks.push(para1); + model.blocks.push(para2); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: { fontSize: '11px' }, + isSelected: true, + }, + paragraph: para1, + path: [model], + tableContext: undefined, + }); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test0', + format: { fontSize: '10px' }, + }, + { + segmentType: 'SelectionMarker', + format: { fontSize: '11px' }, + isSelected: true, + }, + ], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [], + }, + ], + }); + }); + + it('Divider selection', () => { + const model = createContentModelDocument(); + const divider = createDivider('div'); + + divider.isSelected = true; + model.blocks.push(divider); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + isImplicit: false, + }, + ], + }); + }); + + it('2 Divider selection and paragraph after it', () => { + const model = createContentModelDocument(); + const divider1 = createDivider('div'); + const divider2 = createDivider('hr'); + const para1 = createParagraph(); + const para2 = createParagraph(); + + divider1.isSelected = true; + divider2.isSelected = true; + model.blocks.push(para1, divider1, divider2, para2); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + isImplicit: false, + }, + { + blockType: 'Paragraph', + format: {}, + segments: [], + isImplicit: true, + }, + { + blockType: 'Paragraph', + format: {}, + segments: [], + }, + ], + }); + }); + + it('Some table cell selection', () => { + const model = createContentModelDocument(); + const table = createTable(1); + const cell1 = createTableCell(); + const cell2 = createTableCell(); + + cell2.isSelected = true; + + table.rows[0].cells.push(cell1, cell2); + model.blocks.push(table); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + isImplicit: false, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + }, + path: [cell2, model], + tableContext: { + table: table, + colIndex: 1, + rowIndex: 0, + isWholeTableSelected: false, + }, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + format: {}, + dataset: {}, + widths: [], + rows: [ + { + format: {}, + height: 0, + cells: [ + { + blockGroupType: 'TableCell', + format: {}, + dataset: {}, + spanAbove: false, + spanLeft: false, + isHeader: false, + blocks: [], + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: false, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + isSelected: true, + }, + ], + }, + ], + }, + ], + }); + }); + + it('All table cell selection', () => { + const model = createContentModelDocument(); + const table = createTable(1); + const cell = createTableCell(); + + cell.isSelected = true; + + table.rows[0].cells.push(cell); + model.blocks.push(table); + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + isImplicit: false, + }, + ], + }); + }); + + it('Entity selection, no callback', () => { + const model = createContentModelDocument(); + const wrapper = 'WRAPPER' as any; + const entity = createEntity(wrapper); + model.blocks.push(entity); + + entity.isSelected = true; + + const result = deleteSelection(model); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + isImplicit: false, + }, + ], + }); + }); + + it('Entity selection, callback returns false', () => { + const model = createContentModelDocument(); + const wrapper = 'WRAPPER' as any; + const entity = createEntity(wrapper); + const deletedEntities: DeletedEntity[] = []; + model.blocks.push(entity); + + entity.isSelected = true; + + const result = deleteSelection(model, [], { + newEntities: [], + deletedEntities, + newImages: [], + }); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + isImplicit: false, + }, + ], + }); + + expect(deletedEntities).toEqual([{ entity, operation: 'overwrite' }]); + }); + + it('Entity selection, callback returns true', () => { + const model = createContentModelDocument(); + const wrapper = 'WRAPPER' as any; + const entity = createEntity(wrapper); + model.blocks.push(entity); + + entity.isSelected = true; + + const deletedEntities: DeletedEntity[] = []; + const result = deleteSelection(model, [], { + newEntities: [], + deletedEntities, + newImages: [], + }); + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker: { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + isImplicit: false, + }, + ], + }); + + expect(deletedEntities).toEqual([{ entity, operation: 'overwrite' }]); + }); + + it('delete with default format', () => { + const model = createContentModelDocument({ + fontSize: '10pt', + }); + const divider = createDivider('div'); + + divider.isSelected = true; + model.blocks.push(divider); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: { fontSize: '10pt' }, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: false, + segmentFormat: { fontSize: '10pt' }, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + isImplicit: false, + segmentFormat: { fontSize: '10pt' }, + }, + ], + format: { fontSize: '10pt' }, + }); + }); + + it('delete with general block', () => { + const model = createContentModelDocument(); + const general = createGeneralBlock(null!); + + general.isSelected = true; + model.blocks.push(general); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + isImplicit: false, + }, + ], + }); + }); + + it('delete with general block and others', () => { + const model = createContentModelDocument(); + const divider = createDivider('div'); + const general = createGeneralBlock(null!); + + divider.isSelected = true; + general.isSelected = true; + model.blocks.push(divider, general); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + isImplicit: false, + }, + { + blockType: 'Paragraph', + format: {}, + segments: [], + isImplicit: true, + }, + ], + }); + }); + + it('delete with general segment', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const general = createGeneralSegment(null!); + + general.isSelected = true; + para.segments.push(general); + model.blocks.push(para); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + }, + ], + }); + }); + + it('delete with general segment and others', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const general = createGeneralSegment(null!); + const text = createText('test'); + + general.isSelected = true; + text.isSelected = true; + para.segments.push(general, text); + model.blocks.push(para); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + }, + ], + }); + }); + + it('Normalize spaces before deleted segment', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const text = createText('test '); + const image = createImage('test'); + + image.isSelected = true; + para.segments.push(text, image); + model.blocks.push(para); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test\u00A0', format: {} }, marker], + format: {}, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Text', text: 'test\u00A0', format: {} }, marker], + }, + ], + }); + }); + + it('Make paragraph not implicit when delete', () => { + const model = createContentModelDocument(); + const para = createParagraph(true /*isImplicit*/); + const text = createText('test '); + + text.isSelected = true; + para.segments.push(text); + model.blocks.push(para); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: false, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + isImplicit: false, + }, + ], + }); + }); + + it('Delete divider with default format', () => { + const model = createContentModelDocument({ fontFamily: 'Arial' }); + const divider = createDivider('hr'); + + divider.isSelected = true; + model.blocks.push(divider); + + const result = deleteSelection(model); + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + format: { fontFamily: 'Arial' }, + isSelected: true, + }; + + expect(result.deleteResult).toBe('range'); + expect(result.insertPoint).toEqual({ + marker, + paragraph: { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: false, + segmentFormat: { fontFamily: 'Arial' }, + }, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [marker], + isImplicit: false, + segmentFormat: { fontFamily: 'Arial' }, + }, + ], + format: { fontFamily: 'Arial' }, + }); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectedSegmentsTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectedSegmentsTest.ts index e091fbf6812..288cf8fa146 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectedSegmentsTest.ts @@ -1,6 +1,11 @@ -import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelections'; -import getSelectedSegments from '../../../lib/publicApi/selection/getSelectedSegments'; -import { TableSelectionContext } from '../../../lib/publicTypes/selection/TableSelectionContext'; +import * as iterateSelections from '../../../lib/publicApi/selection/iterateSelections'; +import { getSelectedSegments } from '../../../lib/publicApi/selection/collectSelections'; +import { + ContentModelBlock, + ContentModelBlockGroup, + ContentModelSegment, + TableSelectionContext, +} from 'roosterjs-content-model-types'; import { createDivider, createEntity, @@ -8,11 +13,6 @@ import { createSelectionMarker, createText, } from 'roosterjs-content-model-dom'; -import { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelSegment, -} from 'roosterjs-content-model-types'; interface SelectionInfo { path: ContentModelBlockGroup[]; diff --git a/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectionRootNodeTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectionRootNodeTest.ts new file mode 100644 index 00000000000..cbb1d37affa --- /dev/null +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/getSelectionRootNodeTest.ts @@ -0,0 +1,41 @@ +import { getSelectionRootNode } from '../../../lib/publicApi/selection/getSelectionRootNode'; + +describe('getSelectionRootNode', () => { + it('undefined input', () => { + const root = getSelectionRootNode(undefined); + + expect(root).toBeUndefined(); + }); + + it('range input', () => { + const mockedRoot = 'ROOT' as any; + const root = getSelectionRootNode({ + type: 'range', + range: { + commonAncestorContainer: mockedRoot, + } as any, + }); + + expect(root).toBe(mockedRoot); + }); + + it('table input', () => { + const mockedTable = 'TABLE' as any; + const root = getSelectionRootNode({ + type: 'table', + table: mockedTable, + } as any); + + expect(root).toBe(mockedTable); + }); + + it('image input', () => { + const mockedImage = 'IMAGE' as any; + const root = getSelectionRootNode({ + type: 'image', + image: mockedImage, + } as any); + + expect(root).toBe(mockedImage); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/iterateSelectionsTest.ts similarity index 83% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/selection/iterateSelectionsTest.ts index fba405cde0f..9bb7a27769c 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/iterateSelectionsTest.ts @@ -17,7 +17,7 @@ import { import { iterateSelections, IterateSelectionsCallback, -} from '../../../lib/modelApi/selection/iterateSelections'; +} from '../../../lib/publicApi/selection/iterateSelections'; describe('iterateSelections', () => { let callback: jasmine.Spy; @@ -28,7 +28,7 @@ describe('iterateSelections', () => { it('empty group', () => { const group = createContentModelDocument(); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).not.toHaveBeenCalled(); }); @@ -45,7 +45,7 @@ describe('iterateSelections', () => { group.blocks.push(para1); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).not.toHaveBeenCalled(); }); @@ -64,7 +64,7 @@ describe('iterateSelections', () => { group.blocks.push(para1); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [text1]); @@ -85,7 +85,7 @@ describe('iterateSelections', () => { group.blocks.push(para1); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [text1]); @@ -110,7 +110,7 @@ describe('iterateSelections', () => { group.blocks.push(listItem); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([listItem, group], undefined, para1, [text1]); @@ -137,7 +137,7 @@ describe('iterateSelections', () => { group.blocks.push(quote); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([quote, group], undefined, para1, [text1]); @@ -163,7 +163,7 @@ describe('iterateSelections', () => { group.blocks.push(table); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith( @@ -202,7 +202,7 @@ describe('iterateSelections', () => { group.blocks.push(table); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith( @@ -250,7 +250,7 @@ describe('iterateSelections', () => { group.blocks.push(table); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(3); expect(callback).toHaveBeenCalledWith( @@ -309,7 +309,7 @@ describe('iterateSelections', () => { group.blocks.push(table); - iterateSelections([group], callback, { + iterateSelections(group, callback, { contentUnderSelectedTableCell: 'ignoreForTableOrCell', }); @@ -348,7 +348,7 @@ describe('iterateSelections', () => { group.blocks.push(table); - iterateSelections([group], callback, { + iterateSelections(group, callback, { contentUnderSelectedTableCell: 'ignoreForTable', }); @@ -410,7 +410,7 @@ describe('iterateSelections', () => { group.blocks.push(table); - iterateSelections([group], callback, { contentUnderSelectedTableCell: 'ignoreForTable' }); + iterateSelections(group, callback, { contentUnderSelectedTableCell: 'ignoreForTable' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], undefined, table, undefined); @@ -429,7 +429,7 @@ describe('iterateSelections', () => { group.blocks.push(para1); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [marker]); @@ -449,7 +449,7 @@ describe('iterateSelections', () => { group.blocks.push(para1); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [text]); @@ -469,7 +469,7 @@ describe('iterateSelections', () => { group.blocks.push(para1); group.blocks.push(para2); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [marker]); @@ -493,7 +493,7 @@ describe('iterateSelections', () => { group.blocks.push(para2); group.blocks.push(para3); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(3); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [marker1]); @@ -524,7 +524,7 @@ describe('iterateSelections', () => { group.blocks.push(para2); group.blocks.push(para3); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(3); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [marker1, text1]); @@ -543,7 +543,7 @@ describe('iterateSelections', () => { listItem.blocks.push(para); group.blocks.push(listItem); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([listItem, group], undefined, para, [text]); @@ -565,7 +565,7 @@ describe('iterateSelections', () => { listItem.blocks.push(para); group.blocks.push(listItem); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([listItem, group], undefined, para, [text1]); @@ -584,7 +584,7 @@ describe('iterateSelections', () => { generalSpan.blocks.push(para); group.blocks.push(generalSpan); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalSpan, group], undefined, para, [text1]); @@ -604,7 +604,7 @@ describe('iterateSelections', () => { para2.segments.push(text1, text2); group.blocks.push(para1); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalSpan, group], undefined, para2, [text1]); @@ -622,14 +622,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(0); expect(callback1).toHaveBeenCalledTimes(0); @@ -656,14 +656,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalSpan, group], undefined, para2, [text2]); @@ -688,14 +688,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], undefined, para1, [generalSpan]); @@ -728,81 +728,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); - - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([generalSpan, group], undefined, para2, [ - text1, - text2, - ]); - - expect(callback1).toHaveBeenCalledTimes(1); - expect(callback1).toHaveBeenCalledWith([generalSpan, group], undefined, para2, [ - text1, - text2, - ]); - - expect(callback2).toHaveBeenCalledTimes(1); - expect(callback2).toHaveBeenCalledWith([group], undefined, para1, [generalSpan]); - - expect(callback3).toHaveBeenCalledTimes(2); - expect(callback3).toHaveBeenCalledWith([generalSpan, group], undefined, para2, [ - text1, - text2, - ]); - expect(callback3).toHaveBeenCalledWith([group], undefined, para1, [generalSpan]); - }); - - it('Get Selection from model that contains general segment, treat all as selected', () => { - const group = createContentModelDocument(); - const generalSpan = createGeneralSegment(document.createElement('span')); - const para1 = createParagraph(true /*implicit*/); - const para2 = createParagraph(true /*implicit*/); - const text1 = createText('test1'); - const text2 = createText('test1'); - - para1.segments.push(generalSpan); - generalSpan.blocks.push(para2); - para2.segments.push(text1, text2); - group.blocks.push(para1); - - const callback1 = jasmine.createSpy('callback1'); - const callback2 = jasmine.createSpy('callback2'); - const callback3 = jasmine.createSpy('callback3'); - - iterateSelections([group], callback, undefined, undefined, true); - iterateSelections( - [group], - callback1, - { - contentUnderSelectedGeneralElement: 'contentOnly', - }, - undefined, - true - ); - iterateSelections( - [group], - callback2, - { - contentUnderSelectedGeneralElement: 'generalElementOnly', - }, - undefined, - true - ); - iterateSelections( - [group], - callback3, - { contentUnderSelectedGeneralElement: 'both' }, - undefined, - true - ); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalSpan, group], undefined, para2, [ @@ -839,7 +772,7 @@ describe('iterateSelections', () => { para2.segments.push(text1, text2); group.blocks.push(generalDiv); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [text1]); @@ -855,14 +788,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(0); expect(callback1).toHaveBeenCalledTimes(0); @@ -887,14 +820,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [text2]); @@ -917,14 +850,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); @@ -955,79 +888,14 @@ describe('iterateSelections', () => { const callback2 = jasmine.createSpy('callback2'); const callback3 = jasmine.createSpy('callback3'); - iterateSelections([group], callback); - iterateSelections([group], callback1, { + iterateSelections(group, callback); + iterateSelections(group, callback1, { contentUnderSelectedGeneralElement: 'contentOnly', }); - iterateSelections([group], callback2, { + iterateSelections(group, callback2, { contentUnderSelectedGeneralElement: 'generalElementOnly', }); - iterateSelections([group], callback3, { contentUnderSelectedGeneralElement: 'both' }); - - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [ - text1, - text2, - ]); - - expect(callback1).toHaveBeenCalledTimes(1); - expect(callback1).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [ - text1, - text2, - ]); - - expect(callback2).toHaveBeenCalledTimes(1); - expect(callback2).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); - - expect(callback3).toHaveBeenCalledTimes(2); - expect(callback3).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [ - text1, - text2, - ]); - expect(callback3).toHaveBeenCalledWith([group], undefined, generalDiv, undefined); - }); - - it('Get Selection from model that contains general block, treat all as selected', () => { - const group = createContentModelDocument(); - const generalDiv = createGeneralBlock(document.createElement('div')); - const para2 = createParagraph(true /*implicit*/); - const text1 = createText('test1'); - const text2 = createText('test1'); - - generalDiv.blocks.push(para2); - para2.segments.push(text1, text2); - group.blocks.push(generalDiv); - - const callback1 = jasmine.createSpy('callback1'); - const callback2 = jasmine.createSpy('callback2'); - const callback3 = jasmine.createSpy('callback3'); - - iterateSelections([group], callback, undefined, undefined, true); - iterateSelections( - [group], - callback1, - { - contentUnderSelectedGeneralElement: 'contentOnly', - }, - undefined, - true - ); - iterateSelections( - [group], - callback2, - { - contentUnderSelectedGeneralElement: 'generalElementOnly', - }, - undefined, - true - ); - iterateSelections( - [group], - callback3, - { contentUnderSelectedGeneralElement: 'both' }, - undefined, - true - ); + iterateSelections(group, callback3, { contentUnderSelectedGeneralElement: 'both' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([generalDiv, group], undefined, para2, [ @@ -1059,7 +927,7 @@ describe('iterateSelections', () => { divider.isSelected = true; group.blocks.push(divider); - iterateSelections([group], callback); + iterateSelections(group, callback); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], undefined, divider, undefined); @@ -1086,7 +954,7 @@ describe('iterateSelections', () => { return block == para1; }); - iterateSelections([group], newCallback); + iterateSelections(group, newCallback); expect(newCallback).toHaveBeenCalledTimes(1); expect(newCallback).toHaveBeenCalledWith([group], undefined, para1, [text1]); @@ -1114,7 +982,7 @@ describe('iterateSelections', () => { return block == divider; }); - iterateSelections([group], newCallback); + iterateSelections(group, newCallback); expect(newCallback).toHaveBeenCalledTimes(2); expect(newCallback).toHaveBeenCalledWith([group], undefined, para1, [text1]); @@ -1146,7 +1014,7 @@ describe('iterateSelections', () => { return block == para1; }); - iterateSelections([group], newCallback); + iterateSelections(group, newCallback); expect(newCallback).toHaveBeenCalledTimes(1); expect(newCallback).toHaveBeenCalledWith([quote1, group], undefined, para1, [text1]); @@ -1173,7 +1041,7 @@ describe('iterateSelections', () => { return block == table; }); - iterateSelections([group], newCallback, { + iterateSelections(group, newCallback, { contentUnderSelectedTableCell: 'ignoreForTable', }); @@ -1207,7 +1075,7 @@ describe('iterateSelections', () => { } }); - iterateSelections([group], newCallback); + iterateSelections(group, newCallback); expect(newCallback).toHaveBeenCalledTimes(2); expect(newCallback).toHaveBeenCalledWith( @@ -1247,7 +1115,7 @@ describe('iterateSelections', () => { list.blocks.push(para); doc.blocks.push(list); - iterateSelections([doc], callback, { includeListFormatHolder: 'anySegment' }); + iterateSelections(doc, callback, { includeListFormatHolder: 'anySegment' }); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([list, doc], undefined, para, [text2]); @@ -1273,7 +1141,7 @@ describe('iterateSelections', () => { list.blocks.push(para); doc.blocks.push(list); - iterateSelections([doc], callback, { includeListFormatHolder: 'allSegments' }); + iterateSelections(doc, callback, { includeListFormatHolder: 'allSegments' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([list, doc], undefined, para, [text2]); @@ -1293,7 +1161,7 @@ describe('iterateSelections', () => { list.blocks.push(para); doc.blocks.push(list); - iterateSelections([doc], callback, { includeListFormatHolder: 'allSegments' }); + iterateSelections(doc, callback, { includeListFormatHolder: 'allSegments' }); expect(callback).toHaveBeenCalledTimes(2); expect(callback).toHaveBeenCalledWith([list, doc], undefined, para, [text1, text2]); @@ -1320,7 +1188,7 @@ describe('iterateSelections', () => { list.blocks.push(para); doc.blocks.push(list); - iterateSelections([doc], callback, { includeListFormatHolder: 'never' }); + iterateSelections(doc, callback, { includeListFormatHolder: 'never' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([list, doc], undefined, para, [text1, text2]); @@ -1336,7 +1204,7 @@ describe('iterateSelections', () => { para.segments.push(entity); doc.blocks.push(para); - iterateSelections([doc], callback, { includeListFormatHolder: 'never' }); + iterateSelections(doc, callback, { includeListFormatHolder: 'never' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([doc], undefined, para, [entity]); @@ -1368,7 +1236,7 @@ describe('iterateSelections', () => { doc.blocks.push(quote1, quote2, para1, para2, divider1, divider2); - iterateSelections([doc], callback); + iterateSelections(doc, callback); expect(doc).toEqual({ blockGroupType: 'Document', diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/setSelectionTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/setSelectionTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/setSelectionTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/selection/setSelectionTest.ts index c7c5d7fcefe..b1d555bf331 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/setSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/selection/setSelectionTest.ts @@ -1,4 +1,4 @@ -import { setSelection } from '../../../lib/modelApi/selection/setSelection'; +import { setSelection } from '../../../lib/publicApi/selection/setSelection'; import { createBr, createContentModelDocument, diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/applyTableFormatTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/table/applyTableFormatTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/applyTableFormatTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/table/applyTableFormatTest.ts index 549a6e9bb85..419354eb2fd 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/applyTableFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/table/applyTableFormatTest.ts @@ -1,4 +1,4 @@ -import { applyTableFormat } from '../../../lib/modelApi/table/applyTableFormat'; +import { applyTableFormat } from '../../../lib/publicApi/table/applyTableFormat'; import { ContentModelTable, ContentModelTableCell, diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/table/normalizeTableTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/table/normalizeTableTest.ts index f2379aa2f28..3d8b0282527 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/table/normalizeTableTest.ts @@ -1,4 +1,4 @@ -import { normalizeTable } from '../../../lib/modelApi/table/normalizeTable'; +import { normalizeTable } from '../../../lib/publicApi/table/normalizeTable'; import { ContentModelParagraph, ContentModelSegmentFormat, diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/setTableCellBackgroundColorTest.ts b/packages-content-model/roosterjs-content-model-core/test/publicApi/table/setTableCellBackgroundColorTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/table/setTableCellBackgroundColorTest.ts rename to packages-content-model/roosterjs-content-model-core/test/publicApi/table/setTableCellBackgroundColorTest.ts index 886ebca7659..efb0d3bcb4d 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/setTableCellBackgroundColorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/publicApi/table/setTableCellBackgroundColorTest.ts @@ -3,7 +3,7 @@ import { createTableCell as originalCreateTableCell } from 'roosterjs-content-mo import { parseColor, setTableCellBackgroundColor, -} from '../../../lib/modelApi/table/setTableCellBackgroundColor'; +} from '../../../lib/publicApi/table/setTableCellBackgroundColor'; function createTableCell( spanLeftOrColSpan?: boolean | number, diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/marginFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/marginFormatHandler.ts index 823735279b9..28d2b576ea7 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/marginFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/block/marginFormatHandler.ts @@ -35,6 +35,18 @@ export const marginFormatHandler: FormatHandler = { } } }); + + const marginBlockStart = element.style.marginBlockStart || defaultStyle.marginBlockStart; + const marginTop = element.style.marginTop || defaultStyle.marginTop; + if (marginBlockStart && !marginTop) { + format.marginBlockStart = parseValueWithUnit(marginBlockStart) + 'px'; + } + + const marginBlockEnd = element.style.marginBlockEnd || defaultStyle.marginBlockEnd; + const marginBottom = element.style.marginBottom || defaultStyle.marginBottom; + if (marginBlockEnd && !marginBottom) { + format.marginBlockEnd = parseValueWithUnit(marginBlockEnd) + 'px'; + } }, apply: (format, element, context) => { MarginKeys.forEach(key => { @@ -44,5 +56,13 @@ export const marginFormatHandler: FormatHandler = { element.style[key] = value || '0'; } }); + + if (format.marginBlockStart && !format.marginTop) { + element.style.marginBlockStart = format.marginBlockStart; + } + + if (format.marginBlockEnd && !format.marginBottom) { + element.style.marginBlockEnd = format.marginBlockEnd; + } }, }; diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/marginFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/marginFormatHandlerTest.ts index 6ad5cad7a04..da4aa15fd5a 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/marginFormatHandlerTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/block/marginFormatHandlerTest.ts @@ -49,6 +49,49 @@ describe('marginFormatHandler.parse', () => { marginLeft: '50px', }); }); + + it('Has margin block in CSS', () => { + div.style.marginBlockEnd = '1px'; + div.style.marginBlockStart = '1px'; + marginFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + marginBlockEnd: '1px', + marginBlockStart: '1px', + }); + }); + + it('Has margin block in default style', () => { + marginFormatHandler.parse(format, div, context, { + marginBlockEnd: '1em', + marginBlockStart: '1em', + }); + expect(format).toEqual({ + marginBlockEnd: '0px', + marginBlockStart: '0px', + }); + }); + + it('Merge margin values', () => { + div.style.marginBlockStart = '15pt'; + format.marginBlockStart = '30px'; + marginFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + marginBlockStart: '20px', + }); + }); + + it('Do not overlay margin values with margin block values', () => { + div.style.margin = '1px 2px 3px 4px'; + div.style.marginBlockEnd = '5px'; + div.style.marginBlockStart = '6px'; + marginFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + marginTop: '1px', + marginRight: '2px', + marginBottom: '3px', + marginLeft: '4px', + }); + }); }); describe('marginFormatHandler.apply', () => { @@ -110,4 +153,31 @@ describe('marginFormatHandler.apply', () => { marginFormatHandler.apply(format, div, context); expect(div.outerHTML).toBe('
'); }); + + it('No margin block', () => { + marginFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Has margin block', () => { + format.marginBlockEnd = '1px'; + format.marginBlockStart = '2px'; + + marginFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toBe('
'); + }); + + it('Do not overlay margin values with margin block values', () => { + format.marginTop = '1px'; + format.marginRight = '2px'; + format.marginBottom = '3px'; + format.marginLeft = '4px'; + format.marginBlockEnd = '5px'; + format.marginBlockStart = '6px'; + + marginFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toBe('
'); + }); }); 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 ea0a3a626e6..7ea629c336e 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,17 +1,20 @@ -import { createContentModelEditorCore } from './createContentModelEditorCore'; +import { createContentModelEditorCore } from 'roosterjs-content-model-core'; import { EditorBase } from 'roosterjs-editor-core'; import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore'; import type { ContentModelEditorOptions, - EditorEnvironment, IContentModelEditor, } from '../publicTypes/IContentModelEditor'; import type { ContentModelDocument, + ContentModelSegmentFormat, DOMSelection, DomToModelOption, ModelToDomOption, OnNodeCreated, + ContentModelFormatter, + FormatWithContentModelOptions, + EditorEnvironment, } from 'roosterjs-content-model-types'; /** @@ -92,4 +95,28 @@ export default class ContentModelEditor 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; + } } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts deleted file mode 100644 index b3eba12c47f..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/createContentModelEditorCore.ts +++ /dev/null @@ -1,147 +0,0 @@ -import ContentModelCopyPastePlugin from './corePlugins/ContentModelCopyPastePlugin'; -import ContentModelTypeInContainerPlugin from './corePlugins/ContentModelTypeInContainerPlugin'; -import { contentModelDomIndexer } from './utils/contentModelDomIndexer'; -import { createContentModel } from './coreApi/createContentModel'; -import { createContentModelCachePlugin } from './corePlugins/ContentModelCachePlugin'; -import { createContentModelEditPlugin } from './corePlugins/ContentModelEditPlugin'; -import { createContentModelFormatPlugin } from './corePlugins/ContentModelFormatPlugin'; -import { createDomToModelConfig, createModelToDomConfig } from 'roosterjs-content-model-dom'; -import { createEditorContext } from './coreApi/createEditorContext'; -import { createEditorCore } from 'roosterjs-editor-core'; -import { getDOMSelection } from './coreApi/getDOMSelection'; -import { setContentModel } from './coreApi/setContentModel'; -import { setDOMSelection } from './coreApi/setDOMSelection'; -import { switchShadowEdit } from './coreApi/switchShadowEdit'; -import { tablePreProcessor } from './overrides/tablePreProcessor'; -import { - listItemMetadataApplier, - listLevelMetadataApplier, -} from '../domUtils/metadata/updateListMetadata'; -import type { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore'; -import type { ContentModelEditorOptions } from '../publicTypes/IContentModelEditor'; -import type { ContentModelPluginState } from '../publicTypes/pluginState/ContentModelPluginState'; -import type { CoreCreator, EditorCore } from 'roosterjs-editor-types'; - -/** - * Editor Core creator for Content Model editor - */ -export const createContentModelEditorCore: CoreCreator< - ContentModelEditorCore, - ContentModelEditorOptions -> = (contentDiv, options) => { - const pluginState = getPluginState(options); - const modifiedOptions: ContentModelEditorOptions = { - ...options, - plugins: [ - createContentModelCachePlugin(pluginState.cache), - ...(options.plugins || []), - createContentModelFormatPlugin(pluginState.format), - createContentModelEditPlugin(), - ], - corePluginOverride: { - typeInContainer: new ContentModelTypeInContainerPlugin(), - copyPaste: new ContentModelCopyPastePlugin(pluginState.copyPaste), - ...options.corePluginOverride, - }, - }; - - const core = createEditorCore(contentDiv, modifiedOptions) as ContentModelEditorCore; - - core.environment = {}; - - promoteToContentModelEditorCore(core, modifiedOptions, pluginState); - - return core; -}; - -/** - * Creator Content Model Editor Core from Editor Core - * @param core The original EditorCore object - * @param options Options of this editor - */ -export function promoteToContentModelEditorCore( - core: EditorCore, - options: ContentModelEditorOptions, - pluginState: ContentModelPluginState -) { - const cmCore = core as ContentModelEditorCore; - - promoteCorePluginState(cmCore, pluginState); - promoteContentModelInfo(cmCore, options); - promoteCoreApi(cmCore); - promoteEnvironment(cmCore); -} - -function promoteCorePluginState( - cmCore: ContentModelEditorCore, - pluginState: ContentModelPluginState -) { - Object.assign(cmCore, pluginState); -} - -function promoteContentModelInfo( - cmCore: ContentModelEditorCore, - options: ContentModelEditorOptions -) { - cmCore.defaultDomToModelOptions = [ - { - processorOverride: { - table: tablePreProcessor, - }, - }, - options.defaultDomToModelOptions, - ]; - cmCore.defaultModelToDomOptions = [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - }, - options.defaultModelToDomOptions, - ]; - cmCore.defaultDomToModelConfig = createDomToModelConfig(cmCore.defaultDomToModelOptions); - cmCore.defaultModelToDomConfig = createModelToDomConfig(cmCore.defaultModelToDomOptions); -} - -function promoteCoreApi(cmCore: ContentModelEditorCore) { - cmCore.api.createEditorContext = createEditorContext; - cmCore.api.createContentModel = createContentModel; - cmCore.api.setContentModel = setContentModel; - cmCore.api.switchShadowEdit = switchShadowEdit; - cmCore.api.getDOMSelection = getDOMSelection; - cmCore.api.setDOMSelection = setDOMSelection; - cmCore.originalApi.createEditorContext = createEditorContext; - cmCore.originalApi.createContentModel = createContentModel; - cmCore.originalApi.setContentModel = setContentModel; - cmCore.originalApi.getDOMSelection = getDOMSelection; - cmCore.originalApi.setDOMSelection = setDOMSelection; -} - -function promoteEnvironment(cmCore: ContentModelEditorCore) { - cmCore.environment.isMac = window.navigator.appVersion.indexOf('Mac') != -1; -} - -function getPluginState(options: ContentModelEditorOptions): ContentModelPluginState { - const format = options.defaultFormat || {}; - return { - cache: { - domIndexer: options.cacheModel ? contentModelDomIndexer : undefined, - }, - copyPaste: { - allowedCustomPasteType: options.allowedCustomPasteType || [], - }, - format: { - defaultFormat: { - 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, - }, - }, - }; -} 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 d09f96b825c..d4b10a5c5de 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/index.ts @@ -1,133 +1,8 @@ -export { ContentModelFormatState } from './publicTypes/format/formatState/ContentModelFormatState'; -export { ImageFormatState } from './publicTypes/format/formatState/ImageFormatState'; -export { Border } from './publicTypes/interface/Border'; -export { BorderOperations } from './publicTypes/enum/BorderOperations'; export { - CreateEditorContext, ContentModelCoreApiMap, ContentModelEditorCore, - CreateContentModel, - SetContentModel, - GetDOMSelection, - SetDOMSelection, } from './publicTypes/ContentModelEditorCore'; -export { - default as ContentModelBeforePasteEvent, - ContentModelBeforePasteEventData, - CompatibleContentModelBeforePasteEvent, -} from './publicTypes/event/ContentModelBeforePasteEvent'; -export { - default as ContentModelContentChangedEvent, - CompatibleContentModelContentChangedEvent, - ContentModelContentChangedEventData, - ChangeSource, -} from './publicTypes/event/ContentModelContentChangedEvent'; - -export { - IContentModelEditor, - ContentModelEditorOptions, - EditorEnvironment, -} from './publicTypes/IContentModelEditor'; -export { InsertPoint } from './publicTypes/selection/InsertPoint'; -export { TableSelectionContext } from './publicTypes/selection/TableSelectionContext'; -export { - DeletedEntity, - FormatWithContentModelContext, - FormatWithContentModelOptions, - ContentModelFormatter, - EntityLifecycleOperation, - EntityOperation, - EntityRemovalOperation, -} from './publicTypes/parameter/FormatWithContentModelContext'; -export { - InsertEntityOptions, - InsertEntityPosition, -} from './publicTypes/parameter/InsertEntityOptions'; -export { - TableOperation, - TableVerticalInsertOperation, - TableHorizontalInsertOperation, - TableDeleteOperation, - TableVerticalMergeOperation, - TableHorizontalMergeOperation, - TableCellMergeOperation, - TableSplitOperation, - TableAlignOperation, - TableCellHorizontalAlignOperation, - TableCellVerticalAlignOperation, -} from './publicTypes/parameter/TableOperation'; -export { PasteType } from './publicTypes/parameter/PasteType'; - -export { default as insertTable } from './publicApi/table/insertTable'; -export { default as formatTable } from './publicApi/table/formatTable'; -export { default as setTableCellShade } from './publicApi/table/setTableCellShade'; -export { default as editTable } from './publicApi/table/editTable'; -export { default as applyTableBorderFormat } from './publicApi/table/applyTableBorderFormat'; -export { default as toggleBullet } from './publicApi/list/toggleBullet'; -export { default as toggleNumbering } from './publicApi/list/toggleNumbering'; -export { default as toggleBold } from './publicApi/segment/toggleBold'; -export { default as toggleItalic } from './publicApi/segment/toggleItalic'; -export { default as toggleUnderline } from './publicApi/segment/toggleUnderline'; -export { default as toggleStrikethrough } from './publicApi/segment/toggleStrikethrough'; -export { default as toggleSubscript } from './publicApi/segment/toggleSubscript'; -export { default as toggleSuperscript } from './publicApi/segment/toggleSuperscript'; -export { default as setBackgroundColor } from './publicApi/segment/setBackgroundColor'; -export { default as setFontName } from './publicApi/segment/setFontName'; -export { default as setFontSize } from './publicApi/segment/setFontSize'; -export { default as setTextColor } from './publicApi/segment/setTextColor'; -export { default as changeFontSize } from './publicApi/segment/changeFontSize'; -export { default as applySegmentFormat } from './publicApi/segment/applySegmentFormat'; -export { default as changeCapitalization } from './publicApi/segment/changeCapitalization'; -export { default as insertImage } from './publicApi/image/insertImage'; -export { default as setListStyle } from './publicApi/list/setListStyle'; -export { default as setListStartNumber } from './publicApi/list/setListStartNumber'; -export { default as hasSelectionInBlock } from './publicApi/selection/hasSelectionInBlock'; -export { default as hasSelectionInSegment } from './publicApi/selection/hasSelectionInSegment'; -export { default as hasSelectionInBlockGroup } from './publicApi/selection/hasSelectionInBlockGroup'; -export { default as getSelectedSegments } from './publicApi/selection/getSelectedSegments'; -export { default as setIndentation } from './publicApi/block/setIndentation'; -export { default as setAlignment } from './publicApi/block/setAlignment'; -export { default as setDirection } from './publicApi/block/setDirection'; -export { default as setHeadingLevel } from './publicApi/block/setHeadingLevel'; -export { default as toggleBlockQuote } from './publicApi/block/toggleBlockQuote'; -export { default as setSpacing } from './publicApi/block/setSpacing'; -export { default as setImageBorder } from './publicApi/image/setImageBorder'; -export { default as setImageBoxShadow } from './publicApi/image/setImageBoxShadow'; -export { default as changeImage } from './publicApi/image/changeImage'; -export { default as getFormatState } from './publicApi/format/getFormatState'; -export { default as applyPendingFormat } from './publicApi/format/applyPendingFormat'; -export { default as clearFormat } from './publicApi/format/clearFormat'; -export { default as insertLink } from './publicApi/link/insertLink'; -export { default as removeLink } from './publicApi/link/removeLink'; -export { default as adjustLinkSelection } from './publicApi/link/adjustLinkSelection'; -export { default as setImageAltText } from './publicApi/image/setImageAltText'; -export { default as adjustImageSelection } from './publicApi/image/adjustImageSelection'; -export { default as setParagraphMargin } from './publicApi/block/setParagraphMargin'; -export { default as toggleCode } from './publicApi/segment/toggleCode'; -export { default as paste } from './publicApi/utils/paste'; -export { default as insertEntity } from './publicApi/entity/insertEntity'; -export { formatWithContentModel } from './publicApi/utils/formatWithContentModel'; +export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor'; export { default as ContentModelEditor } from './editor/ContentModelEditor'; export { default as isContentModelEditor } from './editor/isContentModelEditor'; -export { default as ContentModelPastePlugin } from './editor/plugins/PastePlugin/ContentModelPastePlugin'; - -export { default as ContentModelFormatPlugin } from './editor/corePlugins/ContentModelFormatPlugin'; -export { default as ContentModelEditPlugin } from './editor/corePlugins/ContentModelEditPlugin'; -export { default as ContentModelTypeInContainerPlugin } from './editor/corePlugins/ContentModelTypeInContainerPlugin'; -export { default as ContentModelCopyPastePlugin } from './editor/corePlugins/ContentModelCopyPastePlugin'; -export { default as ContentModelCachePlugin } from './editor/corePlugins/ContentModelCachePlugin'; - -export { - createContentModelEditorCore, - promoteToContentModelEditorCore, -} from './editor/createContentModelEditorCore'; -export { combineBorderValue, extractBorderValues } from './domUtils/borderValues'; -export { updateImageMetadata } from './domUtils/metadata/updateImageMetadata'; -export { updateTableCellMetadata } from './domUtils/metadata/updateTableCellMetadata'; -export { updateTableMetadata } from './domUtils/metadata/updateTableMetadata'; -export { updateListMetadata } from './domUtils/metadata/updateListMetadata'; - -export { ContentModelCachePluginState } from './publicTypes/pluginState/ContentModelCachePluginState'; -export { ContentModelPluginState } from './publicTypes/pluginState/ContentModelPluginState'; -export { ContentModelFormatPluginState } from './publicTypes/pluginState/ContentModelFormatPluginState'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/DeleteSelectionStep.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/DeleteSelectionStep.ts deleted file mode 100644 index 6422aca74c0..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/DeleteSelectionStep.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { ContentModelParagraph } from 'roosterjs-content-model-types'; -import type { FormatWithContentModelContext } from '../../../publicTypes/parameter/FormatWithContentModelContext'; -import type { InsertPoint } from '../../../publicTypes/selection/InsertPoint'; -import type { TableSelectionContext } from '../../../publicTypes/selection/TableSelectionContext'; - -/** - * @internal - */ -export const enum DeleteResult { - NotDeleted, - SingleChar, - Range, - NothingToDelete, -} - -/** - * @internal - */ -export interface DeleteSelectionResult { - insertPoint: InsertPoint | null; - deleteResult: DeleteResult; -} - -/** - * @internal - */ -export interface DeleteSelectionContext extends DeleteSelectionResult { - lastParagraph?: ContentModelParagraph; - lastTableContext?: TableSelectionContext; - formatContext?: FormatWithContentModelContext; -} - -/** - * @internal - */ -export interface ValidDeleteSelectionContext extends DeleteSelectionContext { - insertPoint: InsertPoint; -} - -/** - * @internal - */ -export type DeleteSelectionStep = (context: ValidDeleteSelectionContext) => void; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/createInsertPoint.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/createInsertPoint.ts deleted file mode 100644 index c89e236d120..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/utils/createInsertPoint.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { InsertPoint } from '../../../publicTypes/selection/InsertPoint'; -import type { TableSelectionContext } from '../../../publicTypes/selection/TableSelectionContext'; -import type { - ContentModelBlockGroup, - ContentModelParagraph, - ContentModelSelectionMarker, -} from 'roosterjs-content-model-types'; - -/** - * @internal - */ -export function createInsertPoint( - marker: ContentModelSelectionMarker, - paragraph: ContentModelParagraph, - path: ContentModelBlockGroup[], - tableContext: TableSelectionContext | undefined -): InsertPoint { - return { - marker, - paragraph, - path, - tableContext, - }; -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/format/pendingFormat.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/format/pendingFormat.ts deleted file mode 100644 index 38f9a75dc09..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/format/pendingFormat.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * @internal - * Get pending segment format from editor if any, otherwise null - * @param editor The editor to get format from - */ -export function getPendingFormat(editor: IContentModelEditor): ContentModelSegmentFormat | null { - return getPendingFormatHolder(editor).format; -} - -/** - * @internal - * Set pending segment format to editor - * @param editor The editor to set pending format to - * @param format The format to set. - * @param posContainer Container node of current focus position - * @param posOffset Offset number of current focus position - */ -export function setPendingFormat( - editor: IContentModelEditor, - format: ContentModelSegmentFormat, - posContainer: Node, - posOffset: number -) { - const holder = getPendingFormatHolder(editor); - - holder.format = format; - holder.posContainer = posContainer; - holder.posOffset = posOffset; -} - -/** - * @internal Clear pending format if any - * @param editor The editor to set pending format to - */ -export function clearPendingFormat(editor: IContentModelEditor) { - const holder = getPendingFormatHolder(editor); - - holder.format = null; - holder.posContainer = null; - holder.posOffset = null; -} - -/** - * @internal - * Check if this editor can apply pending format - * @param editor The editor to get format from - */ -export function canApplyPendingFormat(editor: IContentModelEditor): boolean { - const holder = getPendingFormatHolder(editor); - let result = false; - - if (holder.format && holder.posContainer && holder.posOffset !== null) { - const position = editor.getFocusedPosition(); - - if (position?.node == holder.posContainer && position?.offset == holder.posOffset) { - result = true; - } - } - - return result; -} -interface PendingFormatHolder { - format: ContentModelSegmentFormat | null; - posContainer: Node | null; - posOffset: number | null; -} - -const PendingFormatHolderKey = '__ContentModelPendingFormat'; - -function getPendingFormatHolder(editor: IContentModelEditor): PendingFormatHolder { - return editor.getCustomData(PendingFormatHolderKey, () => ({ - format: null, - posContainer: null, - posOffset: null, - })); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setDirection.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setDirection.ts deleted file mode 100644 index 83804c4dabe..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/block/setDirection.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { setModelDirection } from '../../modelApi/block/setModelDirection'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Set text direction of selected paragraphs (Left to right or Right to left) - * @param editor The editor to set alignment - * @param direction Direction value: ltr (Left to right) or rtl (Right to left) - */ -export default function setDirection(editor: IContentModelEditor, direction: 'ltr' | 'rtl') { - editor.focus(); - - formatWithContentModel(editor, 'setDirection', model => setModelDirection(model, direction)); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/clearFormat.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/clearFormat.ts deleted file mode 100644 index 285ba03b6b7..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/format/clearFormat.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { clearModelFormat } from '../../modelApi/common/clearModelFormat'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { normalizeContentModel } from 'roosterjs-content-model-dom'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { - ContentModelBlock, - ContentModelBlockGroup, - ContentModelSegment, - ContentModelTable, -} from 'roosterjs-content-model-types'; - -/** - * Clear format of selection - * @param editor The editor to clear format from - */ -export default function clearFormat(editor: IContentModelEditor) { - editor.focus(); - - formatWithContentModel(editor, 'clearFormat', model => { - const blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][] = []; - const segmentsToClear: ContentModelSegment[] = []; - const tablesToClear: [ContentModelTable, boolean][] = []; - - clearModelFormat(model, blocksToClear, segmentsToClear, tablesToClear); - - normalizeContentModel(model); - - return blocksToClear.length > 0 || segmentsToClear.length > 0 || tablesToClear.length > 0; - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/adjustImageSelection.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/adjustImageSelection.ts deleted file mode 100644 index 52bf444be05..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/adjustImageSelection.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import type { ContentModelImage } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Adjust selection to make sure select an image if any - * @return Content Model Image object if an image is select, or null - */ -export default function adjustImageSelection( - editor: IContentModelEditor -): ContentModelImage | null { - let image: ContentModelImage | null = null; - - formatWithContentModel(editor, 'adjustImageSelection', model => - adjustSegmentSelection( - model, - target => { - if (target.isSelected && target.segmentType == 'Image') { - image = target; - return true; - } else { - return false; - } - }, - (target, ref) => target == ref - ) - ); - - return image; -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts deleted file mode 100644 index b815bd51fa7..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/image/insertImage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { addSegment, createContentModelDocument, createImage } from 'roosterjs-content-model-dom'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { mergeModel } from '../../modelApi/common/mergeModel'; -import { readFile } from '../../domUtils/readFile'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Insert an image into current selected position - * @param editor The editor to operate on - * @param file Image Blob file or source string - */ -export default function insertImage(editor: IContentModelEditor, imageFileOrSrc: File | string) { - editor.focus(); - - if (typeof imageFileOrSrc == 'string') { - insertImageWithSrc(editor, imageFileOrSrc); - } else { - readFile(imageFileOrSrc, dataUrl => { - if (dataUrl && !editor.isDisposed()) { - insertImageWithSrc(editor, dataUrl); - } - }); - } -} - -function insertImageWithSrc(editor: IContentModelEditor, src: string) { - formatWithContentModel(editor, 'insertImage', (model, context) => { - const image = createImage(src, { backgroundColor: '' }); - const doc = createContentModelDocument(); - - addSegment(doc, image); - mergeModel(model, doc, context, { - mergeFormat: 'mergeAll', - }); - - return true; - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/adjustLinkSelection.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/adjustLinkSelection.ts deleted file mode 100644 index 8c7101d18bf..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/adjustLinkSelection.ts +++ /dev/null @@ -1,41 +0,0 @@ -import getSelectedSegments from '../selection/getSelectedSegments'; -import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; -import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { setSelection } from '../../modelApi/selection/setSelection'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Adjust selection to make sure select a hyperlink if any, or a word if original selection is collapsed - * @return A combination of existing link display text and url if any. If there is no existing link, return selected text and null - */ -export default function adjustLinkSelection(editor: IContentModelEditor): [string, string | null] { - let text = ''; - let url: string | null = null; - - formatWithContentModel(editor, 'adjustLinkSelection', model => { - let changed = adjustSegmentSelection( - model, - target => !!target.isSelected && !!target.link, - (target, ref) => !!target.link && target.link.format.href == ref.link!.format.href - ); - let segments = getSelectedSegments(model, false /*includingFormatHolder*/); - const firstSegment = segments[0]; - - if (segments.length == 1 && firstSegment.segmentType == 'SelectionMarker') { - segments = adjustWordSelection(model, firstSegment); - - if (segments.length > 1) { - changed = true; - setSelection(model, segments[0], segments[segments.length - 1]); - } - } - - text = segments.map(x => (x.segmentType == 'Text' ? x.text : '')).join(''); - url = segments[0]?.link?.format.href || null; - - return changed; - }); - - return [text, url]; -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/removeLink.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/removeLink.ts deleted file mode 100644 index 45684396f2f..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/link/removeLink.ts +++ /dev/null @@ -1,37 +0,0 @@ -import getSelectedSegments from '../selection/getSelectedSegments'; -import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Remove link at selection. If no links at selection, do nothing. - * If selection contains multiple links, all of the link styles will be removed. - * If only part of a link is selected, the whole link style will be removed. - * @param editor The editor instance - */ -export default function removeLink(editor: IContentModelEditor) { - editor.focus(); - - formatWithContentModel(editor, 'removeLink', model => { - adjustSegmentSelection( - model, - target => !!target.isSelected && !!target.link, - (target, ref) => - target.isSelected || // Expand the selection to any link that is involved. So we can remove multiple links together - (!!target.link && target.link.format.href == ref.link!.format.href) - ); - - const segments = getSelectedSegments(model, false /*includingFormatHolder*/); - let isChanged = false; - - segments.forEach(segment => { - if (segment.link) { - isChanged = true; - - delete segment.link; - } - }); - - return isChanged; - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStartNumber.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStartNumber.ts deleted file mode 100644 index a1ebe108070..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStartNumber.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getFirstSelectedListItem } from '../../modelApi/selection/collectSelections'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Set start number of a list item - * @param editor The editor to operate on - * @param value The number to set to, must be equal or greater than 1 - */ -export default function setListStartNumber(editor: IContentModelEditor, value: number) { - editor.focus(); - - formatWithContentModel(editor, 'setListStartNumber', model => { - const listItem = getFirstSelectedListItem(model); - const level = listItem?.levels[listItem?.levels.length - 1]; - - if (level) { - level.format.startNumberOverride = value; - - return true; - } else { - return false; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStyle.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStyle.ts deleted file mode 100644 index aa2761d0257..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/setListStyle.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { findListItemsInSameThread } from '../../modelApi/list/findListItemsInSameThread'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getFirstSelectedListItem } from '../../modelApi/selection/collectSelections'; -import { updateListMetadata } from '../../domUtils/metadata/updateListMetadata'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { ListMetadataFormat } from 'roosterjs-content-model-types'; - -/** - * Set style of list items with in same thread of current item - * @param editor The editor to operate on - * @param style The target list item style to set - */ -export default function setListStyle(editor: IContentModelEditor, style: ListMetadataFormat) { - editor.focus(); - - formatWithContentModel(editor, 'setListStyle', model => { - const listItem = getFirstSelectedListItem(model); - - if (listItem) { - const listItems = findListItemsInSameThread(model, listItem); - const levelIndex = listItem.levels.length - 1; - - listItems.forEach(listItem => { - const level = listItem.levels[levelIndex]; - - if (level) { - updateListMetadata(level, metadata => Object.assign({}, metadata, style)); - } - }); - - return true; - } else { - return false; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleBullet.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleBullet.ts deleted file mode 100644 index 379ede04394..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleBullet.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { setListType } from '../../modelApi/list/setListType'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Toggle bullet list type - * - When there are some blocks not in bullet list, set all blocks to the given type - * - When all blocks are already in bullet list, turn off / outdent there list type - * @param editor The editor to operate on - */ -export default function toggleBullet(editor: IContentModelEditor) { - editor.focus(); - - formatWithContentModel(editor, 'toggleBullet', model => setListType(model, 'UL'), { - preservePendingFormat: true, - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleNumbering.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleNumbering.ts deleted file mode 100644 index 7400d3d0c0b..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/list/toggleNumbering.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { setListType } from '../../modelApi/list/setListType'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Toggle numbering list type - * - When there are some blocks not in numbering list, set all blocks to the given type - * - When all blocks are already in numbering list, turn off / outdent there list type - * @param editor The editor to operate on - */ -export default function toggleNumbering(editor: IContentModelEditor) { - editor.focus(); - - formatWithContentModel(editor, 'toggleNumbering', model => setListType(model, 'OL'), { - preservePendingFormat: true, - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/getSelectedSegments.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/getSelectedSegments.ts deleted file mode 100644 index fceae7b5a91..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/selection/getSelectedSegments.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { getSelectedSegmentsAndParagraphs } from '../../modelApi/selection/collectSelections'; -import type { ContentModelDocument, ContentModelSegment } from 'roosterjs-content-model-types'; - -/** - * Get selected segments from a content model - */ -export default function getSelectedSegments( - model: ContentModelDocument, - includingFormatHolder: boolean -): ContentModelSegment[] { - return getSelectedSegmentsAndParagraphs(model, includingFormatHolder).map(x => x[0]); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/applyTableBorderFormat.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/applyTableBorderFormat.ts deleted file mode 100644 index babe9e00e77..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/applyTableBorderFormat.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { extractBorderValues } from '../../domUtils/borderValues'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { getSelectedCells } from '../../modelApi/table/getSelectedCells'; -import { parseValueWithUnit } from 'roosterjs-content-model-dom'; -import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; -import type { BorderOperations } from '../../publicTypes/enum/BorderOperations'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { Border } from '../../publicTypes/interface/Border'; -import type { ContentModelTable, ContentModelTableCell } from 'roosterjs-content-model-types'; -import type { TableSelectionCoordinates } from '../../modelApi/table/getSelectedCells'; - -/** - * @internal - * Border positions - */ -type BorderPositions = 'borderTop' | 'borderBottom' | 'borderLeft' | 'borderRight'; - -/** - * @internal - * Perimeter of the table selection - * Used to determine where to apply border to the cells adjacent to the selection. - */ -type Perimeter = { - Top: boolean; - Bottom: boolean; - Left: boolean; - Right: boolean; -}; - -/** - * Operations to apply border - * @param editor The editor instance - * @param border The border to apply - * @param operation The operation to apply - */ -export default function applyTableBorderFormat( - editor: IContentModelEditor, - border: Border, - operation: BorderOperations -) { - formatWithContentModel(editor, 'tableBorder', model => { - const [tableModel] = getFirstSelectedTable(model); - - if (tableModel) { - const sel = getSelectedCells(tableModel); - const perimeter: Perimeter = { - Top: false, - Bottom: false, - Left: false, - Right: false, - }; - - // Create border format with table format as backup - let borderFormat = ''; - const format = tableModel.format; - const { width, style, color } = border; - const extractedBorder = extractBorderValues(format.borderTop); - const borderColor = extractedBorder.color; - const borderWidth = extractedBorder.width; - const borderStyle = extractedBorder.style; - - if (width) { - borderFormat = parseValueWithUnit(width) + 'px'; - } else if (borderWidth) { - borderFormat = borderWidth; - } else { - borderFormat = '1px'; - } - - if (style) { - borderFormat = `${borderFormat} ${style}`; - } else if (borderStyle) { - borderFormat = `${borderFormat} ${borderStyle}`; - } else { - borderFormat = `${borderFormat} solid`; - } - - if (color) { - borderFormat = `${borderFormat} ${color}`; - } else if (borderColor) { - borderFormat = `${borderFormat} ${borderColor}`; - } - - if (sel) { - const operations: BorderOperations[] = [operation]; - while (operations.length) { - switch (operations.pop()) { - case 'noBorders': - // Do All borders but with empty border format - borderFormat = ''; - operations.push('allBorders'); - break; - case 'allBorders': - const allBorders: BorderPositions[] = [ - 'borderTop', - 'borderBottom', - 'borderLeft', - 'borderRight', - ]; - for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { - for ( - let colIndex = sel.firstCol; - colIndex <= sel.lastCol; - colIndex++ - ) { - const cell = tableModel.rows[rowIndex].cells[colIndex]; - // Format cells - All borders - applyBorderFormat(cell, borderFormat, allBorders); - } - } - - // Format perimeter - perimeter.Top = true; - perimeter.Bottom = true; - perimeter.Left = true; - perimeter.Right = true; - break; - case 'leftBorders': - const leftBorder: BorderPositions[] = ['borderLeft']; - for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { - const cell = tableModel.rows[rowIndex].cells[sel.firstCol]; - // Format cells - Left border - applyBorderFormat(cell, borderFormat, leftBorder); - } - - // Format perimeter - perimeter.Left = true; - break; - case 'rightBorders': - const rightBorder: BorderPositions[] = ['borderRight']; - for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { - const cell = tableModel.rows[rowIndex].cells[sel.lastCol]; - // Format cells - Right border - applyBorderFormat(cell, borderFormat, rightBorder); - } - - // Format perimeter - perimeter.Right = true; - break; - case 'topBorders': - const topBorder: BorderPositions[] = ['borderTop']; - for (let colIndex = sel.firstCol; colIndex <= sel.lastCol; colIndex++) { - const cell = tableModel.rows[sel.firstRow].cells[colIndex]; - // Format cells - Top border - applyBorderFormat(cell, borderFormat, topBorder); - } - - // Format perimeter - perimeter.Top = true; - break; - case 'bottomBorders': - const bottomBorder: BorderPositions[] = ['borderBottom']; - for (let colIndex = sel.firstCol; colIndex <= sel.lastCol; colIndex++) { - const cell = tableModel.rows[sel.lastRow].cells[colIndex]; - // Format cells - Bottom border - applyBorderFormat(cell, borderFormat, bottomBorder); - } - - // Format perimeter - perimeter.Bottom = true; - break; - case 'insideBorders': - // Format cells - Inside borders - // Top left cell - applyBorderFormat( - tableModel.rows[sel.firstRow].cells[sel.firstCol], - borderFormat, - ['borderBottom', 'borderRight'] - ); - // Top right cell - applyBorderFormat( - tableModel.rows[sel.firstRow].cells[sel.lastCol], - borderFormat, - ['borderBottom', 'borderLeft'] - ); - // Bottom left cell - applyBorderFormat( - tableModel.rows[sel.lastRow].cells[sel.firstCol], - borderFormat, - ['borderTop', 'borderRight'] - ); - // Bottom right cell - applyBorderFormat( - tableModel.rows[sel.lastRow].cells[sel.lastCol], - borderFormat, - ['borderTop', 'borderLeft'] - ); - // First row - for ( - let colIndex = sel.firstCol + 1; - colIndex < sel.lastCol; - colIndex++ - ) { - const cell = tableModel.rows[sel.firstRow].cells[colIndex]; - applyBorderFormat(cell, borderFormat, [ - 'borderBottom', - 'borderLeft', - 'borderRight', - ]); - } - // Last row - for ( - let colIndex = sel.firstCol + 1; - colIndex < sel.lastCol; - colIndex++ - ) { - const cell = tableModel.rows[sel.lastRow].cells[colIndex]; - applyBorderFormat(cell, borderFormat, [ - 'borderTop', - 'borderLeft', - 'borderRight', - ]); - } - // First column - for ( - let rowIndex = sel.firstRow + 1; - rowIndex < sel.lastRow; - rowIndex++ - ) { - const cell = tableModel.rows[rowIndex].cells[sel.firstCol]; - applyBorderFormat(cell, borderFormat, [ - 'borderTop', - 'borderBottom', - 'borderRight', - ]); - } - // Last column - for ( - let rowIndex = sel.firstRow + 1; - rowIndex < sel.lastRow; - rowIndex++ - ) { - const cell = tableModel.rows[rowIndex].cells[sel.lastCol]; - applyBorderFormat(cell, borderFormat, [ - 'borderTop', - 'borderBottom', - 'borderLeft', - ]); - } - // Inner cells - sel.firstCol++; - sel.firstRow++; - sel.lastCol--; - sel.lastRow--; - operations.push('allBorders'); - break; - case 'outsideBorders': - // Format cells - Outside borders - operations.push('topBorders'); - operations.push('bottomBorders'); - operations.push('leftBorders'); - operations.push('rightBorders'); - break; - default: - break; - } - } - - //Format perimeter if necessary or possible - modifyPerimeter(tableModel, sel, borderFormat, perimeter); - } - - return true; - } else { - return false; - } - }); -} - -/** - * @internal - * Apply border format to a cell - * @param cell The cell to apply border format - * @param borderFormat The border format to apply - * @param positions The positions to apply - */ -function applyBorderFormat( - cell: ContentModelTableCell, - borderFormat: string, - positions: BorderPositions[] -) { - positions.forEach(pos => { - cell.format[pos] = borderFormat; - }); - - updateTableCellMetadata(cell, metadata => { - metadata = metadata || {}; - metadata.borderOverride = true; - return metadata; - }); - - // Cell was modified, so delete cached element - delete cell.cachedElement; -} - -/** - * @internal - * Modify the perimeter of the table selection - * @param tableModel The table model - * @param sel The table selection - * @param borderFormat The border format to apply - * If borderFormat is empty, the border will be removed - * @param perimeter Where in the perimeter to apply - */ -function modifyPerimeter( - tableModel: ContentModelTable, - sel: TableSelectionCoordinates, - borderFormat: string, - perimeter: Perimeter -) { - // Top of selection - if (perimeter.Top && sel.firstRow - 1 >= 0) { - for (let colIndex = sel.firstCol; colIndex <= sel.lastCol; colIndex++) { - const cell = tableModel.rows[sel.firstRow - 1].cells[colIndex]; - applyBorderFormat(cell, borderFormat, ['borderBottom']); - } - } - // Bottom of selection - if (perimeter.Bottom && sel.lastRow + 1 < tableModel.rows.length) { - for (let colIndex = sel.firstCol; colIndex <= sel.lastCol; colIndex++) { - const cell = tableModel.rows[sel.lastRow + 1].cells[colIndex]; - applyBorderFormat(cell, borderFormat, ['borderTop']); - } - } - // Left of selection - if (perimeter.Left && sel.firstCol - 1 >= 0) { - for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { - const cell = tableModel.rows[rowIndex].cells[sel.firstCol - 1]; - applyBorderFormat(cell, borderFormat, ['borderRight']); - } - } - // Right of selection - if (perimeter.Right && sel.lastCol + 1 < tableModel.rows[0].cells.length) { - for (let rowIndex = sel.firstRow; rowIndex <= sel.lastRow; rowIndex++) { - const cell = tableModel.rows[rowIndex].cells[sel.lastCol + 1]; - applyBorderFormat(cell, borderFormat, ['borderLeft']); - } - } -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/editTable.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/editTable.ts deleted file mode 100644 index 443b2cf6cb0..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/editTable.ts +++ /dev/null @@ -1,128 +0,0 @@ -import hasSelectionInBlock from '../selection/hasSelectionInBlock'; -import { alignTable } from '../../modelApi/table/alignTable'; -import { applyTableFormat } from '../../modelApi/table/applyTableFormat'; -import { deleteTable } from '../../modelApi/table/deleteTable'; -import { deleteTableColumn } from '../../modelApi/table/deleteTableColumn'; -import { deleteTableRow } from '../../modelApi/table/deleteTableRow'; -import { ensureFocusableParagraphForTable } from '../../modelApi/table/ensureFocusableParagraphForTable'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { insertTableColumn } from '../../modelApi/table/insertTableColumn'; -import { insertTableRow } from '../../modelApi/table/insertTableRow'; -import { mergeTableCells } from '../../modelApi/table/mergeTableCells'; -import { mergeTableColumn } from '../../modelApi/table/mergeTableColumn'; -import { mergeTableRow } from '../../modelApi/table/mergeTableRow'; -import { normalizeTable } from '../../modelApi/table/normalizeTable'; -import { setSelection } from '../../modelApi/selection/setSelection'; -import { splitTableCellHorizontally } from '../../modelApi/table/splitTableCellHorizontally'; -import { splitTableCellVertically } from '../../modelApi/table/splitTableCellVertically'; -import type { TableOperation } from '../../publicTypes/parameter/TableOperation'; -import { - alignTableCellHorizontally, - alignTableCellVertically, -} from '../../modelApi/table/alignTableCell'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import { - createSelectionMarker, - hasMetadata, - setParagraphNotImplicit, -} from 'roosterjs-content-model-dom'; - -/** - * Format current focused table with the given format - * @param editor The editor instance - * @param operation The table operation to apply - */ -export default function editTable(editor: IContentModelEditor, operation: TableOperation) { - editor.focus(); - - formatWithContentModel(editor, 'editTable', model => { - const [tableModel, path] = getFirstSelectedTable(model); - - if (tableModel) { - switch (operation) { - case 'alignCellLeft': - case 'alignCellCenter': - case 'alignCellRight': - alignTableCellHorizontally(tableModel, operation); - break; - case 'alignCellTop': - case 'alignCellMiddle': - case 'alignCellBottom': - alignTableCellVertically(tableModel, operation); - break; - case 'alignCenter': - case 'alignLeft': - case 'alignRight': - alignTable(tableModel, operation); - break; - - case 'deleteColumn': - deleteTableColumn(tableModel); - break; - - case 'deleteRow': - deleteTableRow(tableModel); - break; - - case 'deleteTable': - deleteTable(tableModel); - break; - - case 'insertAbove': - case 'insertBelow': - insertTableRow(tableModel, operation); - break; - - case 'insertLeft': - case 'insertRight': - insertTableColumn(tableModel, operation); - break; - - case 'mergeAbove': - case 'mergeBelow': - mergeTableRow(tableModel, operation); - break; - - case 'mergeCells': - mergeTableCells(tableModel); - break; - - case 'mergeLeft': - case 'mergeRight': - mergeTableColumn(tableModel, operation); - break; - - case 'splitHorizontally': - splitTableCellHorizontally(tableModel); - break; - - case 'splitVertically': - splitTableCellVertically(tableModel); - break; - } - - if (!hasSelectionInBlock(tableModel)) { - const paragraph = ensureFocusableParagraphForTable(model, path, tableModel); - - if (paragraph) { - const marker = createSelectionMarker(model.format); - - paragraph.segments.unshift(marker); - setParagraphNotImplicit(paragraph); - setSelection(model, marker); - } - } - - normalizeTable(tableModel, model.format); - - if (hasMetadata(tableModel)) { - applyTableFormat(tableModel, undefined /*newFormat*/, true /*keepCellShade*/); - } - - return true; - } else { - return false; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/formatTable.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/formatTable.ts deleted file mode 100644 index 949028cdf47..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/formatTable.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { applyTableFormat } from '../../modelApi/table/applyTableFormat'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { TableMetadataFormat } from 'roosterjs-content-model-types'; - -/** - * Format current focused table with the given format - * @param editor The editor instance - * @param format The table format to apply - * @param keepCellShade Whether keep existing shade color when apply format if there is a manually set shade color - */ -export default function formatTable( - editor: IContentModelEditor, - format: TableMetadataFormat, - keepCellShade?: boolean -) { - editor.focus(); - - formatWithContentModel(editor, 'formatTable', model => { - const [tableModel] = getFirstSelectedTable(model); - - if (tableModel) { - // Wipe border metadata - tableModel.rows.forEach(row => { - row.cells.forEach(cell => { - updateTableCellMetadata(cell, metadata => { - if (metadata) { - delete metadata.borderOverride; - } - return metadata; - }); - }); - }); - applyTableFormat(tableModel, format, keepCellShade); - return true; - } else { - return false; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/insertTable.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/insertTable.ts deleted file mode 100644 index c46dc0b5e85..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/insertTable.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { applyTableFormat } from '../../modelApi/table/applyTableFormat'; -import { createContentModelDocument, createSelectionMarker } from 'roosterjs-content-model-dom'; -import { createTableStructure } from '../../modelApi/table/createTableStructure'; -import { deleteSelection } from '../../modelApi/edit/deleteSelection'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getPendingFormat } from '../../modelApi/format/pendingFormat'; -import { mergeModel } from '../../modelApi/common/mergeModel'; -import { normalizeTable } from '../../modelApi/table/normalizeTable'; -import { setSelection } from '../../modelApi/selection/setSelection'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { TableMetadataFormat } from 'roosterjs-content-model-types'; - -/** - * Insert table into editor at current selection - * @param editor The editor instance - * @param columns Number of columns in table, it also controls the default table cell width: - * if columns <= 4, width = 120px; if columns <= 6, width = 100px; else width = 70px - * @param rows Number of rows in table - * @param format (Optional) The table format. If not passed, the default format will be applied: - * background color: #FFF; border color: #ABABAB - */ -export default function insertTable( - editor: IContentModelEditor, - columns: number, - rows: number, - format?: Partial -) { - editor.focus(); - - formatWithContentModel(editor, 'insertTable', (model, context) => { - const insertPosition = deleteSelection(model, [], context).insertPoint; - - if (insertPosition) { - const doc = createContentModelDocument(); - const table = createTableStructure(doc, columns, rows); - - normalizeTable(table, getPendingFormat(editor) || insertPosition.marker.format); - // Assign default vertical align - format = format || { verticalAlign: 'top' }; - applyTableFormat(table, format); - mergeModel(model, doc, context, { - insertPosition, - mergeFormat: 'mergeAll', - }); - - const firstBlock = table.rows[0]?.cells[0]?.blocks[0]; - - if (firstBlock?.blockType == 'Paragraph') { - const marker = createSelectionMarker(firstBlock.segments[0]?.format); - firstBlock.segments.unshift(marker); - setSelection(model, marker); - } - - return true; - } else { - return false; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/setTableCellShade.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/setTableCellShade.ts deleted file mode 100644 index f9483e86267..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/table/setTableCellShade.ts +++ /dev/null @@ -1,35 +0,0 @@ -import hasSelectionInBlockGroup from '../selection/hasSelectionInBlockGroup'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { normalizeTable } from '../../modelApi/table/normalizeTable'; -import { setTableCellBackgroundColor } from '../../modelApi/table/setTableCellBackgroundColor'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * Set table cell shade color - * @param editor The editor instance - * @param color The color to set. Pass null to remove existing shade color - */ -export default function setTableCellShade(editor: IContentModelEditor, color: string | null) { - editor.focus(); - - formatWithContentModel(editor, 'setTableCellShade', model => { - const [table] = getFirstSelectedTable(model); - - if (table) { - normalizeTable(table); - - table.rows.forEach(row => - row.cells.forEach(cell => { - if (hasSelectionInBlockGroup(cell)) { - setTableCellBackgroundColor(cell, color, true /*isColorOverride*/); - } - }) - ); - - return true; - } else { - return false; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatParagraphWithContentModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatParagraphWithContentModel.ts deleted file mode 100644 index 21321732213..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatParagraphWithContentModel.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { formatWithContentModel } from './formatWithContentModel'; -import { getSelectedParagraphs } from '../../modelApi/selection/collectSelections'; -import type { ContentModelParagraph } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; - -/** - * @internal - */ -export function formatParagraphWithContentModel( - editor: IContentModelEditor, - apiName: string, - setStyleCallback: (paragraph: ContentModelParagraph) => void -) { - formatWithContentModel( - editor, - apiName, - model => { - const paragraphs = getSelectedParagraphs(model); - - paragraphs.forEach(setStyleCallback); - - return paragraphs.length > 0; - }, - { - preservePendingFormat: true, - } - ); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatSegmentWithContentModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatSegmentWithContentModel.ts deleted file mode 100644 index db26c1dd112..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatSegmentWithContentModel.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; -import { formatWithContentModel } from './formatWithContentModel'; -import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat'; -import { getSelectedSegmentsAndParagraphs } from '../../modelApi/selection/collectSelections'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { - ContentModelDocument, - ContentModelParagraph, - ContentModelSegment, - ContentModelSegmentFormat, -} from 'roosterjs-content-model-types'; -/** - * @internal - */ -export function formatSegmentWithContentModel( - editor: IContentModelEditor, - apiName: string, - toggleStyleCallback: ( - format: ContentModelSegmentFormat, - isTuringOn: boolean, - segment: ContentModelSegment | null, - paragraph: ContentModelParagraph | null - ) => void, - segmentHasStyleCallback?: ( - format: ContentModelSegmentFormat, - segment: ContentModelSegment | null, - paragraph: ContentModelParagraph | null - ) => boolean, - includingFormatHolder?: boolean, - afterFormatCallback?: (model: ContentModelDocument) => void -) { - formatWithContentModel(editor, apiName, model => { - let segmentAndParagraphs = getSelectedSegmentsAndParagraphs(model, !!includingFormatHolder); - const pendingFormat = getPendingFormat(editor); - let isCollapsedSelection = - segmentAndParagraphs.length == 1 && - segmentAndParagraphs[0][0].segmentType == 'SelectionMarker'; - - if (isCollapsedSelection) { - const para = segmentAndParagraphs[0][1]; - - segmentAndParagraphs = adjustWordSelection(model, segmentAndParagraphs[0][0]).map(x => [ - x, - para, - ]); - - if (segmentAndParagraphs.length > 1) { - isCollapsedSelection = false; - } - } - - const formatsAndSegments: [ - ContentModelSegmentFormat, - ContentModelSegment | null, - ContentModelParagraph | null - ][] = pendingFormat - ? [[pendingFormat, null, null]] - : segmentAndParagraphs.map(item => [item[0].format, item[0], item[1]]); - - const isTurningOff = segmentHasStyleCallback - ? formatsAndSegments.every(([format, segment, paragraph]) => - segmentHasStyleCallback(format, segment, paragraph) - ) - : false; - - formatsAndSegments.forEach(([format, segment, paragraph]) => - toggleStyleCallback(format, !isTurningOff, segment, paragraph) - ); - - afterFormatCallback?.(model); - - if (!pendingFormat && isCollapsedSelection) { - const pos = editor.getFocusedPosition(); - - if (pos) { - setPendingFormat(editor, segmentAndParagraphs[0][0].format, pos.node, pos.offset); - } - } - - if (isCollapsedSelection) { - editor.focus(); - return false; - } else { - return formatsAndSegments.length > 0; - } - }); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts deleted file mode 100644 index 135725ceb86..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent'; -import { EntityOperation, PluginEventType } from 'roosterjs-editor-types'; -import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat'; -import type { Entity } from 'roosterjs-editor-types'; -import type { ContentModelContentChangedEventData } from '../../publicTypes/event/ContentModelContentChangedEvent'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; -import type { - ContentModelFormatter, - EntityRemovalOperation, - FormatWithContentModelContext, - FormatWithContentModelOptions, -} from '../../publicTypes/parameter/FormatWithContentModelContext'; -import type { DOMSelection } from 'roosterjs-content-model-types'; - -/** - * 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 editor Content Model editor - * @param apiName Name of the format API - * @param formatter Formatter function, see ContentModelFormatter - * @param options More options, see FormatWithContentModelOptions - */ -export function formatWithContentModel( - editor: IContentModelEditor, - apiName: string, - formatter: ContentModelFormatter, - options?: FormatWithContentModelOptions -) { - const { - onNodeCreated, - preservePendingFormat, - getChangeData, - changeSource, - rawEvent, - selectionOverride, - } = options || {}; - - const model = editor.createContentModel(undefined /*option*/, selectionOverride); - const context: FormatWithContentModelContext = { - newEntities: [], - deletedEntities: [], - rawEvent, - newImages: [], - }; - let selection: DOMSelection | undefined; - - if (formatter(model, context)) { - const writeBack = () => { - handleNewEntities(editor, context); - handleDeletedEntities(editor, context); - handleImages(editor, context); - - selection = - editor.setContentModel(model, undefined /*options*/, onNodeCreated) || undefined; - - if (preservePendingFormat) { - const pendingFormat = getPendingFormat(editor); - const pos = editor.getFocusedPosition(); - - if (pendingFormat && pos) { - setPendingFormat(editor, pendingFormat, pos.node, pos.offset); - } - } - }; - - if (context.skipUndoSnapshot) { - writeBack(); - } else { - editor.addUndoSnapshot( - writeBack, - undefined /*changeSource, passing undefined here to avoid triggering ContentChangedEvent. We will trigger it using it with Content Model below */, - false /*canUndoByBackspace*/, - { - formatApiName: apiName, - } - ); - } - - const eventData: ContentModelContentChangedEventData = { - contentModel: model, - selection: selection, - source: changeSource || ChangeSource.Format, - data: getChangeData?.(), - additionalData: { - formatApiName: apiName, - }, - }; - editor.triggerPluginEvent(PluginEventType.ContentChanged, eventData); - } -} - -function handleNewEntities(editor: IContentModelEditor, context: FormatWithContentModelContext) { - // TODO: Ideally we can trigger NewEntity event here. But to be compatible with original editor code, we don't do it here for now. - // Once Content Model Editor can be standalone, we can change this behavior to move triggering NewEntity event code - // from EntityPlugin to here - - if (editor.isDarkMode()) { - context.newEntities.forEach(entity => { - editor.transformToDarkColor(entity.wrapper); - }); - } -} - -// This is only used for compatibility with old editor -// TODO: Remove this map once we have standalone editor -const EntityOperationMap: Record = { - overwrite: EntityOperation.Overwrite, - removeFromEnd: EntityOperation.RemoveFromEnd, - removeFromStart: EntityOperation.RemoveFromStart, -}; - -function handleDeletedEntities( - editor: IContentModelEditor, - context: FormatWithContentModelContext -) { - context.deletedEntities.forEach( - ({ - entity: { - wrapper, - entityFormat: { id, entityType, isReadonly }, - }, - operation, - }) => { - if (id && entityType) { - // TODO: Revisit this entity parameter for standalone editor, we may just directly pass ContentModelEntity object instead - const entity: Entity = { - id, - type: entityType, - isReadonly: !!isReadonly, - wrapper, - }; - editor.triggerPluginEvent(PluginEventType.EntityOperation, { - entity, - operation: EntityOperationMap[operation], - rawEvent: context.rawEvent, - }); - } - } - ); -} - -function handleImages(editor: IContentModelEditor, context: FormatWithContentModelContext) { - if (context.newImages.length > 0) { - const viewport = editor.getVisibleViewport(); - if (viewport) { - const { left, right } = viewport; - const minMaxImageSize = 10; - const maxWidth = Math.max(right - left, minMaxImageSize); - context.newImages.forEach(image => { - image.format.maxWidth = `${maxWidth}px`; - }); - } - } -} 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 77c718a272a..7a16d2ee5af 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,106 +1,16 @@ -import type { EditorEnvironment } from './IContentModelEditor'; -import type { ContentModelPluginState } from './pluginState/ContentModelPluginState'; import type { CoreApiMap, EditorCore } from 'roosterjs-editor-types'; -import type { - ContentModelDocument, - DOMSelection, - DomToModelOption, - DomToModelSettings, - EditorContext, - ModelToDomOption, - ModelToDomSettings, - OnNodeCreated, -} from 'roosterjs-content-model-types'; - -/** - * Create a EditorContext object used by ContentModel API - * @param core The ContentModelEditorCore object - */ -export type CreateEditorContext = (core: ContentModelEditorCore) => EditorContext; - -/** - * Create Content Model from DOM tree in this editor - * @param core The ContentModelEditorCore object - * @param option The option to customize the behavior of DOM to Content Model conversion - * @param selectionOverride When passed, use this selection range instead of current selection in editor - */ -export type CreateContentModel = ( - core: ContentModelEditorCore, - option?: DomToModelOption, - selectionOverride?: DOMSelection -) => ContentModelDocument; - -/** - * Get current DOM selection from editor - * @param core The ContentModelEditorCore object - */ -export type GetDOMSelection = (core: ContentModelEditorCore) => DOMSelection | null; - -/** - * Set content with content model. This is the replacement of core API getSelectionRangeEx - * @param core The ContentModelEditorCore object - * @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 - */ -export type SetContentModel = ( - core: ContentModelEditorCore, - model: ContentModelDocument, - option?: ModelToDomOption, - onNodeCreated?: OnNodeCreated -) => DOMSelection | null; - -/** - * Set current DOM selection from editor. This is the replacement of core API select - * @param core The ContentModelEditorCore object - * @param selection The selection to set - */ -export type SetDOMSelection = (core: ContentModelEditorCore, selection: DOMSelection) => void; +import type { StandaloneCoreApiMap, StandaloneEditorCore } from 'roosterjs-content-model-types'; /** * The interface for the map of core API for Content Model editor. * Editor can call call API from this map under ContentModelEditorCore object */ -export interface ContentModelCoreApiMap extends CoreApiMap { - /** - * Create a EditorContext object used by ContentModel API - * @param core The ContentModelEditorCore object - */ - createEditorContext: CreateEditorContext; - - /** - * Create Content Model from DOM tree in this editor - * @param core The ContentModelEditorCore object - * @param option The option to customize the behavior of DOM to Content Model conversion - */ - createContentModel: CreateContentModel; - - /** - * Get current DOM selection from editor - * @param core The ContentModelEditorCore object - */ - getDOMSelection: GetDOMSelection; - - /** - * Set content with content model - * @param core The ContentModelEditorCore object - * @param model The content model to set - * @param option Additional options to customize the behavior of Content Model to DOM conversion - */ - setContentModel: SetContentModel; - - /** - * Set current DOM selection from editor. This is the replacement of core API select - * @param core The ContentModelEditorCore object - * @param selection The selection to set - */ - setDOMSelection: SetDOMSelection; -} +export interface ContentModelCoreApiMap extends CoreApiMap, StandaloneCoreApiMap {} /** * Represents the core data structure of a Content Model editor */ -export interface ContentModelEditorCore extends EditorCore, ContentModelPluginState { +export interface ContentModelEditorCore extends EditorCore, StandaloneEditorCore { /** * Core API map of this editor */ @@ -110,31 +20,4 @@ export interface ContentModelEditorCore extends EditorCore, ContentModelPluginSt * 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; - - /** - * Default DOM to Content Model options - */ - defaultDomToModelOptions: (DomToModelOption | undefined)[]; - - /** - * Default Content Model to DOM options - */ - defaultModelToDomOptions: (ModelToDomOption | undefined)[]; - - /** - * Default DOM to Content Model config, calculated from defaultDomToModelOptions, - * will be used for creating content model if there is no other customized options - */ - defaultDomToModelConfig: DomToModelSettings; - - /** - * Default Content Model to DOM config, calculated from defaultModelToDomOptions, - * will be used for setting content model if there is no other customized options - */ - defaultModelToDomConfig: ModelToDomSettings; - - /** - * Editor running environment - */ - environment: EditorEnvironment; } 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 95777221d44..0eabedea19b 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,86 +1,13 @@ import type { EditorOptions, IEditor } from 'roosterjs-editor-types'; -import type { - ContentModelDocument, - DOMSelection, - DomToModelOption, - ModelToDomOption, - OnNodeCreated, -} from 'roosterjs-content-model-types'; - -/** - * Current running environment - */ -export interface EditorEnvironment { - /** - * Whether editor is running on Mac - */ - isMac?: boolean; -} +import type { StandaloneEditorOptions, IStandaloneEditor } from 'roosterjs-content-model-types'; /** * An interface of editor with Content Model support. * (This interface is still under development, and may still be changed in the future with some breaking changes) */ -export interface IContentModelEditor extends IEditor { - /** - * Create Content Model from DOM tree in this editor - * @param rootNode Optional start node. If provided, Content Model will be created from this node (including itself), - * otherwise it will create Content Model for the whole content in editor. - * @param option The options to customize the behavior of DOM to Content Model conversion - * @param selectionOverride When specified, use this selection to override existing selection inside editor - */ - createContentModel( - option?: DomToModelOption, - selectionOverride?: DOMSelection - ): ContentModelDocument; - - /** - * 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; - - /** - * Get current running environment, such as if editor is running on Mac - */ - getEnvironment(): EditorEnvironment; - - /** - * Get current DOM selection. - * This is the replacement of IEditor.getSelectionRangeEx. - */ - getDOMSelection(): DOMSelection | null; - - /** - * Set DOMSelection into editor content. - * This is the replacement of IEditor.select. - * @param selection The selection to set - */ - setDOMSelection(selection: DOMSelection): void; -} +export interface IContentModelEditor extends IEditor, IStandaloneEditor {} /** * Options for Content Model editor */ -export interface ContentModelEditorOptions extends EditorOptions { - /** - * Default options used for DOM to Content Model conversion - */ - defaultDomToModelOptions?: DomToModelOption; - - /** - * Default options used for Content Model to DOM conversion - */ - defaultModelToDomOptions?: ModelToDomOption; - - /** - * Reuse existing DOM structure if possible, and update the model when content or selection is changed - */ - cacheModel?: boolean; -} +export interface ContentModelEditorOptions extends EditorOptions, StandaloneEditorOptions {} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelContentChangedEvent.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelContentChangedEvent.ts deleted file mode 100644 index f9e00e08a4b..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelContentChangedEvent.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { ContentModelDocument, DOMSelection } from 'roosterjs-content-model-types'; -import type { - CompatibleContentChangedEvent, - ContentChangedEvent, - ContentChangedEventData, -} from 'roosterjs-editor-types'; - -/** - * Possible change sources. Here are the predefined sources. - * It can also be other string if the change source can't fall into these sources. - */ -export enum ChangeSource { - /** - * Content changed by auto link - */ - AutoLink = 'AutoLink', - /** - * Content changed by create link - */ - CreateLink = 'CreateLink', - /** - * Content changed by format - */ - Format = 'Format', - /** - * Content changed by image resize - */ - ImageResize = 'ImageResize', - /** - * Content changed by paste - */ - Paste = 'Paste', - /** - * Content changed by setContent API - */ - SetContent = 'SetContent', - /** - * Content changed by cut operation - */ - Cut = 'Cut', - /** - * Content changed by drag & drop operation - */ - Drop = 'Drop', - /** - * Insert a new entity into editor - */ - InsertEntity = 'InsertEntity', - /** - * Editor is switched to dark mode, content color is changed - */ - SwitchToDarkMode = 'SwitchToDarkMode', - /** - * Editor is switched to light mode, content color is changed - */ - SwitchToLightMode = 'SwitchToLightMode', - /** - * List chain reorganized numbers of lists - */ - ListChain = 'ListChain', - /** - * Keyboard event, used by Content Model. - * Data of this event will be the key code number - */ - Keyboard = 'Keyboard', -} - -/** - * Data of ContentModelContentChangedEvent - */ -export interface ContentModelContentChangedEventData extends ContentChangedEventData { - /** - * The content model that is applied which causes this content changed event - */ - contentModel: ContentModelDocument; - - /** - * Selection range applied to the document - */ - selection?: DOMSelection; -} - -/** - * Represents a change to the editor made by another plugin with content model inside - */ -export default 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-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts deleted file mode 100644 index ce7a0ff5b9b..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { - ContentModelDocument, - ContentModelEntity, - ContentModelImage, - DOMSelection, - OnNodeCreated, -} from 'roosterjs-content-model-types'; - -/** - * Define entity lifecycle related operations - */ -export type EntityLifecycleOperation = - /** - * Notify plugins that there is a new plugin was added into editor. - * Plugin can handle this event to entity hydration. - * This event will be only fired once for each entity DOM node. - * After undo, or copy/paste, since new DOM nodes were added, this event will be fired - * for those entities represented by newly added nodes. - */ - | 'newEntity' - - /** - * Notify plugins that editor is generating HTML content for save. - * Plugin should use this event to remove any temporary content, and only leave DOM nodes that - * should be saved as HTML string. - * This event will provide a cloned DOM tree for each entity, do NOT compare the DOM nodes with cached nodes - * because it will always return false. - */ - | 'replaceTemporaryContent' - /** - * Notify plugins that a new entity state need to be updated to an entity. - * This is normally happened when user undo/redo the content with an entity snapshot added by a plugin that handles entity - */ - | 'UpdateEntityState'; - -/** - * Define entity removal related operations - */ -export type EntityRemovalOperation = - /** - * Notify plugins that user is removing an entity from its start position using DELETE key - */ - | 'removeFromStart' - - /** - * Notify plugins that user is remove an entity from its end position using BACKSPACE key - */ - | 'removeFromEnd' - - /** - * Notify plugins that an entity is being overwritten. - * This can be caused by key in, cut, paste, delete, backspace ... on a selection - * which contains some entities. - */ - | 'overwrite'; - -/** - * Define possible operations to an entity - */ -export type EntityOperation = EntityLifecycleOperation | EntityRemovalOperation; - -/** - * Represents an entity that is deleted by a specified entity operation - */ -export interface DeletedEntity { - entity: ContentModelEntity; - operation: EntityRemovalOperation; -} - -/** - * Context object for API formatWithContentModel - */ -export interface FormatWithContentModelContext { - /** - * New entities added during the format process - */ - readonly newEntities: ContentModelEntity[]; - - /** - * Entities got deleted during formatting. Need to be set by the formatter function - */ - readonly deletedEntities: DeletedEntity[]; - - /** - * Images inserted in the editor that needs to have their size adjusted - */ - readonly newImages: ContentModelImage[]; - - /** - * Raw Event that triggers this format call - */ - readonly rawEvent?: Event; - - /** - * @optional - * When pass true, skip adding undo snapshot when write Content Model back to DOM. - * Need to be set by the formatter function - */ - skipUndoSnapshot?: boolean; -} - -/** - * Options for API formatWithContentModel - */ -export interface FormatWithContentModelOptions { - /** - * When set to true, if there is pending format, it will be preserved after this format operation is done - */ - preservePendingFormat?: boolean; - - /** - * Raw event object that triggers this call - */ - rawEvent?: Event; - - /** - * Change source used for triggering a ContentChanged event. @default ChangeSource.Format. - */ - changeSource?: string; - - /** - * An optional callback that will be called when a DOM node is created - * @param modelElement The related Content Model element - * @param node The node created for this model element - */ - onNodeCreated?: OnNodeCreated; - - /** - * Optional callback to get an object used for change data in ContentChangedEvent - */ - getChangeData?: () => any; - - /** - * When specified, use this selection range to override current selection inside editor - */ - selectionOverride?: DOMSelection; -} - -/** - * Type of formatter used for format Content Model. - * @param model The source Content Model to format - * @param context A context object used for pass in and out more parameters - * @returns True means the model is changed and need to write back to editor, otherwise false - */ -export type ContentModelFormatter = ( - model: ContentModelDocument, - context: FormatWithContentModelContext -) => boolean; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelFormatPluginState.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelFormatPluginState.ts deleted file mode 100644 index 81444445060..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelFormatPluginState.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; - -/** - * Plugin state for ContentModelFormatPlugin - */ -export interface ContentModelFormatPluginState { - /** - * Default format of this editor - */ - defaultFormat: ContentModelSegmentFormat; -} diff --git a/packages-content-model/roosterjs-content-model-editor/package.json b/packages-content-model/roosterjs-content-model-editor/package.json index 00f04bf6314..6debbfeb1cb 100644 --- a/packages-content-model/roosterjs-content-model-editor/package.json +++ b/packages-content-model/roosterjs-content-model-editor/package.json @@ -6,6 +6,7 @@ "roosterjs-editor-types": "", "roosterjs-editor-dom": "", "roosterjs-editor-core": "", + "roosterjs-content-model-core": "", "roosterjs-content-model-dom": "", "roosterjs-content-model-types": "" }, 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 5b082f10a81..47a14d34e74 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 @@ -4,6 +4,7 @@ import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelT import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import ContentModelEditor from '../../lib/editor/ContentModelEditor'; import { ContentModelDocument, EditorContext } from 'roosterjs-content-model-types'; +import { ContentModelEditorCore } from '../../lib/publicTypes/ContentModelEditorCore'; import { EditorPlugin, PluginEventType } from 'roosterjs-editor-types'; const editorContext: EditorContext = { @@ -202,6 +203,19 @@ describe('ContentModelEditor', () => { expect(domToContentModel.domToContentModel).not.toHaveBeenCalled(); }); + it('formatContentModel', () => { + const div = document.createElement('div'); + const editor = new ContentModelEditor(div); + const core = (editor as any).core; + const formatContentModelSpy = spyOn(core.api, 'formatContentModel'); + const callback = jasmine.createSpy('callback'); + const options = 'Options' as any; + + editor.formatContentModel(callback, options); + + expect(formatContentModelSpy).toHaveBeenCalledWith(core, callback, options); + }); + it('default format', () => { const div = document.createElement('div'); const editor = new ContentModelEditor(div, { @@ -235,6 +249,21 @@ describe('ContentModelEditor', () => { }); }); + it('getPendingFormat', () => { + const div = document.createElement('div'); + const editor = new ContentModelEditor(div); + const core: ContentModelEditorCore = (editor as any).core; + const mockedFormat = 'FORMAT' as any; + + expect(editor.getPendingFormat()).toBeNull(); + + core.format.pendingFormat = { + format: mockedFormat, + } as any; + + expect(editor.getPendingFormat()).toEqual(mockedFormat); + }); + it('dispose', () => { const div = document.createElement('div'); div.style.fontFamily = 'Arial'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts deleted file mode 100644 index b76d619f47a..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/createContentModelEditorCoreTest.ts +++ /dev/null @@ -1,463 +0,0 @@ -import * as ContentModelCachePlugin from '../../lib/editor/corePlugins/ContentModelCachePlugin'; -import * as ContentModelEditPlugin from '../../lib/editor/corePlugins/ContentModelEditPlugin'; -import * as ContentModelFormatPlugin from '../../lib/editor/corePlugins/ContentModelFormatPlugin'; -import * as createDomToModelContext from 'roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext'; -import * as createEditorCore from 'roosterjs-editor-core/lib/editor/createEditorCore'; -import * as createModelToDomContext from 'roosterjs-content-model-dom/lib/modelToDom/context/createModelToDomContext'; -import ContentModelTypeInContainerPlugin from '../../lib/editor/corePlugins/ContentModelTypeInContainerPlugin'; -import { contentModelDomIndexer } from '../../lib/editor/utils/contentModelDomIndexer'; -import { ContentModelEditorOptions } from '../../lib/publicTypes/IContentModelEditor'; -import { createContentModel } from '../../lib/editor/coreApi/createContentModel'; -import { createContentModelEditorCore } from '../../lib/editor/createContentModelEditorCore'; -import { createEditorContext } from '../../lib/editor/coreApi/createEditorContext'; -import { getDOMSelection } from '../../lib/editor/coreApi/getDOMSelection'; -import { setContentModel } from '../../lib/editor/coreApi/setContentModel'; -import { setDOMSelection } from '../../lib/editor/coreApi/setDOMSelection'; -import { switchShadowEdit } from '../../lib/editor/coreApi/switchShadowEdit'; -import { tablePreProcessor } from '../../lib/editor/overrides/tablePreProcessor'; -import { - listItemMetadataApplier, - listLevelMetadataApplier, -} from '../../lib/domUtils/metadata/updateListMetadata'; - -const mockedSwitchShadowEdit = 'SHADOWEDIT' as any; -const mockedDomToModelConfig = { - config: 'mockedDomToModelConfig', -} as any; -const mockedModelToDomConfig = { - config: 'mockedModelToDomConfig', -} as any; -const mockedFormatPlugin = 'FORMATPLUGIN' as any; -const mockedEditPlugin = 'EDITPLUGIN' as any; -const mockedCachePlugin = 'CACHPLUGIN' as any; - -describe('createContentModelEditorCore', () => { - let createEditorCoreSpy: jasmine.Spy; - let mockedCore: any; - let contentDiv: any; - - let copyPastePlugin = 'copyPastePlugin' as any; - - beforeEach(() => { - contentDiv = { - style: {}, - } as any; - - mockedCore = { - lifecycle: { - experimentalFeatures: [], - }, - api: { - switchShadowEdit: mockedSwitchShadowEdit, - }, - originalApi: { - a: 'b', - }, - contentDiv, - } as any; - - createEditorCoreSpy = spyOn(createEditorCore, 'createEditorCore').and.returnValue( - mockedCore - ); - spyOn(ContentModelFormatPlugin, 'createContentModelFormatPlugin').and.returnValue( - mockedFormatPlugin - ); - spyOn(ContentModelEditPlugin, 'createContentModelEditPlugin').and.returnValue( - mockedEditPlugin - ); - spyOn(ContentModelCachePlugin, 'createContentModelCachePlugin').and.returnValue( - mockedCachePlugin - ); - - spyOn(createDomToModelContext, 'createDomToModelConfig').and.returnValue( - mockedDomToModelConfig - ); - spyOn(createModelToDomContext, 'createModelToDomConfig').and.returnValue( - mockedModelToDomConfig - ); - }); - - it('No additional option', () => { - const options = { - corePluginOverride: { - copyPaste: copyPastePlugin, - }, - }; - const core = createContentModelEditorCore(contentDiv, options); - - expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, { - plugins: [mockedCachePlugin, mockedFormatPlugin, mockedEditPlugin], - corePluginOverride: { - typeInContainer: new ContentModelTypeInContainerPlugin(), - copyPaste: copyPastePlugin, - }, - }); - expect(core).toEqual({ - lifecycle: { - experimentalFeatures: [], - }, - api: { - switchShadowEdit, - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - originalApi: { - a: 'b', - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - defaultDomToModelOptions: [ - { processorOverride: { table: tablePreProcessor } }, - undefined, - ], - defaultModelToDomOptions: [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - }, - undefined, - ], - defaultDomToModelConfig: mockedDomToModelConfig, - defaultModelToDomConfig: mockedModelToDomConfig, - format: { - defaultFormat: { - fontWeight: undefined, - italic: undefined, - underline: undefined, - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - backgroundColor: undefined, - }, - }, - contentDiv: { - style: {}, - }, - cache: { domIndexer: undefined }, - copyPaste: { allowedCustomPasteType: [] }, - environment: { isMac: false }, - } as any); - }); - - it('With additional option', () => { - const defaultDomToModelOptions = { a: '1' } as any; - const defaultModelToDomOptions = { b: '2' } as any; - - const options = { - defaultDomToModelOptions, - defaultModelToDomOptions, - corePluginOverride: { - copyPaste: copyPastePlugin, - }, - }; - const core = createContentModelEditorCore(contentDiv, options); - - expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, { - defaultDomToModelOptions, - defaultModelToDomOptions, - plugins: [mockedCachePlugin, mockedFormatPlugin, mockedEditPlugin], - corePluginOverride: { - typeInContainer: new ContentModelTypeInContainerPlugin(), - copyPaste: copyPastePlugin, - }, - }); - - expect(core).toEqual({ - lifecycle: { - experimentalFeatures: [], - }, - api: { - switchShadowEdit, - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - originalApi: { - a: 'b', - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - defaultDomToModelOptions: [ - { processorOverride: { table: tablePreProcessor } }, - defaultDomToModelOptions, - ], - defaultModelToDomOptions: [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - }, - defaultModelToDomOptions, - ], - defaultDomToModelConfig: mockedDomToModelConfig, - defaultModelToDomConfig: mockedModelToDomConfig, - format: { - defaultFormat: { - fontWeight: undefined, - italic: undefined, - underline: undefined, - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - backgroundColor: undefined, - }, - }, - contentDiv: { - style: {}, - }, - cache: { - domIndexer: undefined, - }, - copyPaste: { allowedCustomPasteType: [] }, - environment: { isMac: false }, - } as any); - }); - - it('With default format', () => { - const options = { - corePluginOverride: { - copyPaste: copyPastePlugin, - }, - defaultFormat: { - bold: true, - italic: true, - underline: true, - fontFamily: 'Arial', - fontSize: '10pt', - textColor: 'red', - backgroundColor: 'blue', - }, - }; - - const core = createContentModelEditorCore(contentDiv, options); - - expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, { - plugins: [mockedCachePlugin, mockedFormatPlugin, mockedEditPlugin], - corePluginOverride: { - typeInContainer: new ContentModelTypeInContainerPlugin(), - copyPaste: copyPastePlugin, - }, - defaultFormat: { - bold: true, - italic: true, - underline: true, - fontFamily: 'Arial', - fontSize: '10pt', - textColor: 'red', - backgroundColor: 'blue', - }, - }); - expect(core).toEqual({ - lifecycle: { - experimentalFeatures: [], - }, - api: { - switchShadowEdit, - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - originalApi: { - a: 'b', - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - defaultDomToModelOptions: [ - { processorOverride: { table: tablePreProcessor } }, - undefined, - ], - defaultModelToDomOptions: [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - }, - undefined, - ], - defaultDomToModelConfig: mockedDomToModelConfig, - defaultModelToDomConfig: mockedModelToDomConfig, - format: { - defaultFormat: { - fontWeight: 'bold', - italic: true, - underline: true, - fontFamily: 'Arial', - fontSize: '10pt', - textColor: 'red', - backgroundColor: 'blue', - }, - }, - contentDiv: { - style: {}, - }, - cache: { domIndexer: undefined }, - copyPaste: { allowedCustomPasteType: [] }, - environment: { isMac: false }, - } as any); - }); - - it('Reuse model', () => { - const options = { - corePluginOverride: { - copyPaste: copyPastePlugin, - }, - }; - - const core = createContentModelEditorCore(contentDiv, options); - - expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, { - plugins: [mockedCachePlugin, mockedFormatPlugin, mockedEditPlugin], - corePluginOverride: { - typeInContainer: new ContentModelTypeInContainerPlugin(), - copyPaste: copyPastePlugin, - }, - }); - expect(core).toEqual({ - lifecycle: { - experimentalFeatures: [], - }, - api: { - switchShadowEdit: switchShadowEdit, - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - originalApi: { - a: 'b', - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - defaultDomToModelOptions: [ - { processorOverride: { table: tablePreProcessor } }, - undefined, - ], - defaultModelToDomOptions: [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - }, - undefined, - ], - format: { - defaultFormat: { - fontWeight: undefined, - italic: undefined, - underline: undefined, - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - backgroundColor: undefined, - }, - }, - defaultDomToModelConfig: mockedDomToModelConfig, - defaultModelToDomConfig: mockedModelToDomConfig, - - contentDiv: { - style: {}, - }, - cache: { domIndexer: undefined }, - copyPaste: { allowedCustomPasteType: [] }, - environment: { isMac: false }, - } as any); - }); - - it('Allow dom indexer', () => { - const options: ContentModelEditorOptions = { - corePluginOverride: { - copyPaste: copyPastePlugin, - }, - cacheModel: true, - }; - - const core = createContentModelEditorCore(contentDiv, options); - - expect(createEditorCoreSpy).toHaveBeenCalledWith(contentDiv, { - plugins: [mockedCachePlugin, mockedFormatPlugin, mockedEditPlugin], - corePluginOverride: { - typeInContainer: new ContentModelTypeInContainerPlugin(), - copyPaste: copyPastePlugin, - }, - cacheModel: true, - }); - expect(core).toEqual({ - lifecycle: { - experimentalFeatures: [], - }, - api: { - switchShadowEdit, - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - originalApi: { - a: 'b', - createEditorContext, - createContentModel, - setContentModel, - getDOMSelection, - setDOMSelection, - }, - defaultDomToModelOptions: [ - { processorOverride: { table: tablePreProcessor } }, - undefined, - ], - defaultModelToDomOptions: [ - { - metadataAppliers: { - listItem: listItemMetadataApplier, - listLevel: listLevelMetadataApplier, - }, - }, - undefined, - ], - defaultDomToModelConfig: mockedDomToModelConfig, - defaultModelToDomConfig: mockedModelToDomConfig, - format: { - defaultFormat: { - fontWeight: undefined, - italic: undefined, - underline: undefined, - fontFamily: undefined, - fontSize: undefined, - textColor: undefined, - backgroundColor: undefined, - }, - }, - contentDiv: { - style: {}, - }, - cache: { domIndexer: contentModelDomIndexer }, - copyPaste: { allowedCustomPasteType: [] }, - environment: { isMac: false }, - } as any); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/format/pendingFormatTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/format/pendingFormatTest.ts deleted file mode 100644 index 61f88ab0d05..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/format/pendingFormatTest.ts +++ /dev/null @@ -1,173 +0,0 @@ -import ContentModelEditor from '../../../lib/editor/ContentModelEditor'; -import { - canApplyPendingFormat, - clearPendingFormat, - getPendingFormat, - setPendingFormat, -} from '../../../lib/modelApi/format/pendingFormat'; - -describe('pendingFormat.getPendingFormat', () => { - it('no format', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const format = getPendingFormat(editor); - - expect(format).toBeNull(); - }); - - it('has format', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const mockedFormat = 'FORMAT' as any; - - (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { - value: { - format: mockedFormat, - }, - }; - - const format = getPendingFormat(editor); - - expect(format).toBe(mockedFormat); - }); -}); - -describe('pendingFormat.setPendingFormat', () => { - it('set format', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const mockedFormat = 'FORMAT' as any; - const mockedContainer = 'C' as any; - const mockedOffset = 'O' as any; - - setPendingFormat(editor, mockedFormat, mockedContainer, mockedOffset); - - expect((editor as any).core.lifecycle.customData.__ContentModelPendingFormat.value).toEqual( - { - format: mockedFormat, - posContainer: mockedContainer, - posOffset: mockedOffset, - } - ); - }); -}); - -describe('pendingFormat.clearPendingFormat', () => { - it('clear format', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const mockedFormat = 'FORMAT' as any; - const mockedContainer = 'C' as any; - const mockedOffset = 'O' as any; - - (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { - value: { - format: mockedFormat, - posContainer: mockedContainer, - posOffset: mockedOffset, - }, - }; - - clearPendingFormat(editor); - - expect((editor as any).core.lifecycle.customData.__ContentModelPendingFormat.value).toEqual( - { - format: null, - posContainer: null, - posOffset: null, - } - ); - }); -}); - -describe('pendingFormat.canApplyPendingFormat', () => { - it('can apply format', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const mockedFormat = 'FORMAT' as any; - - const mockedContainer = 'C' as any; - const mockedOffset = 'O' as any; - - editor.getFocusedPosition = () => ({ node: mockedContainer, offset: mockedOffset } as any); - - (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { - value: { - format: mockedFormat, - posContainer: mockedContainer, - posOffset: mockedOffset, - }, - }; - - const result = canApplyPendingFormat(editor); - - expect(result).toBeTrue(); - }); - - it('no pending format', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - - const equalTo = jasmine.createSpy('equalto').and.returnValue(true); - const mockedPosition2 = { - equalTo, - }; - - editor.getFocusedPosition = () => mockedPosition2 as any; - - const result = canApplyPendingFormat(editor); - - expect(result).toBeFalse(); - expect(equalTo).not.toHaveBeenCalled(); - }); - - it('no current position', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const mockedFormat = 'FORMAT' as any; - const mockedPosition = 'POSITION' as any; - - const equalTo = jasmine.createSpy('equalto').and.returnValue(true); - - editor.getFocusedPosition = () => null as any; - - (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { - value: { - format: mockedFormat, - position: mockedPosition, - }, - }; - - const result = canApplyPendingFormat(editor); - - expect(result).toBeFalse(); - expect(equalTo).not.toHaveBeenCalledWith(); - }); - - it('position is not the same', () => { - const div = document.createElement('div'); - const editor = new ContentModelEditor(div); - const mockedFormat = 'FORMAT' as any; - const mockedContainer1 = 'C1'; - const mockedContainer2 = 'C2'; - - const mockedPosition2 = { - node: mockedContainer2, - offset: 1, - }; - - editor.getFocusedPosition = () => mockedPosition2 as any; - - (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { - value: { - format: mockedFormat, - posContainer: mockedContainer1, - posOffset: 0, - }, - }; - - const result = canApplyPendingFormat(editor); - - expect(result).toBeFalse(); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts deleted file mode 100644 index e5f4dd98010..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; - -export function paragraphTestCommon( - apiName: string, - executionCallback: (editor: IContentModelEditor) => void, - model: ContentModelDocument, - result: ContentModelDocument, - calledTimes: number -) { - const addUndoSnapshot = jasmine - .createSpy() - .and.callFake((callback: () => void, source: string, canUndoByBackspace, param: any) => { - expect(source).toBe(undefined!); - expect(param.formatApiName).toBe(apiName); - callback(); - }); - const setContentModel = jasmine.createSpy().and.callFake((model: ContentModelDocument) => { - expect(model).toEqual(result); - }); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - const editor = ({ - createContentModel: () => model, - addUndoSnapshot, - focus: jasmine.createSpy(), - setContentModel, - getCustomData: () => ({}), - getFocusedPosition: () => ({}), - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; - - executionCallback(editor); - - expect(addUndoSnapshot).toHaveBeenCalledTimes(calledTimes); - expect(setContentModel).toHaveBeenCalledTimes(calledTimes); - expect(model).toEqual(result); -} diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setIndentationTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setIndentationTest.ts deleted file mode 100644 index d8dda555f8e..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setIndentationTest.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; -import * as setModelIndentation from '../../../lib/modelApi/block/setModelIndentation'; -import setIndentation from '../../../lib/publicApi/block/setIndentation'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; - -describe('setIndentation', () => { - const fakeModel: any = { a: 'b' }; - let editor: IContentModelEditor; - - beforeEach(() => { - editor = ({ - createContentModel: () => fakeModel, - focus: jasmine.createSpy('focus'), - } as any) as IContentModelEditor; - }); - - it('indent', () => { - spyOn(formatWithContentModel, 'formatWithContentModel').and.callThrough(); - spyOn(setModelIndentation, 'setModelIndentation'); - - setIndentation(editor, 'indent'); - - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); - expect(setModelIndentation.setModelIndentation).toHaveBeenCalledTimes(1); - expect(setModelIndentation.setModelIndentation).toHaveBeenCalledWith( - fakeModel, - 'indent', - undefined - ); - }); - - it('outdent', () => { - spyOn(formatWithContentModel, 'formatWithContentModel').and.callThrough(); - spyOn(setModelIndentation, 'setModelIndentation'); - - setIndentation(editor, 'outdent'); - - expect(formatWithContentModel.formatWithContentModel).toHaveBeenCalledTimes(1); - expect(setModelIndentation.setModelIndentation).toHaveBeenCalledTimes(1); - expect(setModelIndentation.setModelIndentation).toHaveBeenCalledWith( - fakeModel, - 'outdent', - undefined - ); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts deleted file mode 100644 index 0a1a3834a15..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { NodePosition } from 'roosterjs-editor-types'; - -export function editingTestCommon( - apiName: string, - executionCallback: (editor: IContentModelEditor) => void, - model: ContentModelDocument, - result: ContentModelDocument, - calledTimes: number -) { - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); - - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - const triggerContentChangedEvent = jasmine.createSpy('triggerContentChangedEvent'); - - const addUndoSnapshot = jasmine - .createSpy('addUndoSnapshot') - .and.callFake((callback: () => void, source: string, _, param: any) => { - expect(source).toBe('Format'); - expect(param.formatApiName).toBe(apiName); - callback(); - }); - const setContentModel = jasmine - .createSpy('setContentModel') - .and.callFake((model: ContentModelDocument) => { - expect(model).toEqual(result); - }); - const editor = ({ - createContentModel: () => model, - cacheContentModel: jasmine.createSpy('cacheContentModel'), - addUndoSnapshot, - focus: jasmine.createSpy(), - setContentModel, - triggerPluginEvent, - isDisposed: () => false, - getFocusedPosition: () => null! as NodePosition, - triggerContentChangedEvent, - getVisibleViewport, - isDarkMode: () => false, - getEnvironment: () => ({}), - } as any) as IContentModelEditor; - - executionCallback(editor); - - expect(addUndoSnapshot).toHaveBeenCalledTimes(0); // Should not add undo snapshot since this will be handled by UndoPlugin instead - expect(setContentModel).toHaveBeenCalledTimes(calledTimes); - expect(model).toEqual(result); -} diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts deleted file mode 100644 index 7538416b763..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as setListType from '../../../lib/modelApi/list/setListType'; -import toggleBullet from '../../../lib/publicApi/list/toggleBullet'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; - -describe('toggleBullet', () => { - let editor = ({} as any) as IContentModelEditor; - let addUndoSnapshot: jasmine.Spy; - let createContentModel: jasmine.Spy; - let setContentModel: jasmine.Spy; - let focus: jasmine.Spy; - let mockedModel: ContentModelDocument; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; - - beforeEach(() => { - mockedModel = ({} as any) as ContentModelDocument; - - addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); - createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); - setContentModel = jasmine.createSpy('setContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - focus = jasmine.createSpy('focus'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - - editor = ({ - focus, - addUndoSnapshot, - createContentModel, - setContentModel, - getCustomData: () => ({}), - getFocusedPosition: () => ({}), - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; - - spyOn(setListType, 'setListType').and.returnValue(true); - }); - - it('toggleBullet', () => { - toggleBullet(editor); - - expect(setListType.setListType).toHaveBeenCalledTimes(1); - expect(setListType.setListType).toHaveBeenCalledWith(mockedModel, 'UL'); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts deleted file mode 100644 index 927b1385bbf..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as setListType from '../../../lib/modelApi/list/setListType'; -import toggleNumbering from '../../../lib/publicApi/list/toggleNumbering'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; - -describe('toggleNumbering', () => { - let editor = ({} as any) as IContentModelEditor; - let addUndoSnapshot: jasmine.Spy; - let createContentModel: jasmine.Spy; - let setContentModel: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let focus: jasmine.Spy; - let mockedModel: ContentModelDocument; - let getVisibleViewport: jasmine.Spy; - - beforeEach(() => { - mockedModel = ({} as any) as ContentModelDocument; - - addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); - createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); - setContentModel = jasmine.createSpy('setContentModel'); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - focus = jasmine.createSpy('focus'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - - editor = ({ - focus, - addUndoSnapshot, - createContentModel, - setContentModel, - getCustomData: () => ({}), - getFocusedPosition: () => ({}), - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; - - spyOn(setListType, 'setListType').and.returnValue(true); - }); - - it('toggleNumbering', () => { - toggleNumbering(editor); - - expect(setListType.setListType).toHaveBeenCalledTimes(1); - expect(setListType.setListType).toHaveBeenCalledWith(mockedModel, 'OL'); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts deleted file mode 100644 index 56c0dbc8b29..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { NodePosition } from 'roosterjs-editor-types'; - -export function segmentTestCommon( - apiName: string, - executionCallback: (editor: IContentModelEditor) => void, - model: ContentModelDocument, - result: ContentModelDocument, - calledTimes: number -) { - spyOn(pendingFormat, 'setPendingFormat'); - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); - - const addUndoSnapshot = jasmine - .createSpy() - .and.callFake((callback: () => void, source: string, canUndoByBackspace, param: any) => { - expect(source).toBe(undefined!); - expect(param.formatApiName).toBe(apiName); - callback(); - }); - const setContentModel = jasmine.createSpy().and.callFake((model: ContentModelDocument) => { - expect(model).toEqual(result); - }); - const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - const getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - const editor = ({ - createContentModel: () => model, - addUndoSnapshot, - focus: jasmine.createSpy(), - setContentModel, - isDisposed: () => false, - getFocusedPosition: () => null as NodePosition, - isDarkMode: () => false, - triggerPluginEvent, - getVisibleViewport, - } as any) as IContentModelEditor; - - executionCallback(editor); - - expect(addUndoSnapshot).toHaveBeenCalledTimes(calledTimes); - expect(setContentModel).toHaveBeenCalledTimes(calledTimes); - expect(model).toEqual(result); -} diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts deleted file mode 100644 index 1c0c7ea1824..00000000000 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts +++ /dev/null @@ -1,342 +0,0 @@ -import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; -import { ChangeSource } from '../../../lib/publicTypes/event/ContentModelContentChangedEvent'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; -import { createImage } from 'roosterjs-content-model-dom'; -import { EntityOperation, PluginEventType } from 'roosterjs-editor-types'; -import { formatWithContentModel } from '../../../lib/publicApi/utils/formatWithContentModel'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; - -describe('formatWithContentModel', () => { - let editor: IContentModelEditor; - let addUndoSnapshot: jasmine.Spy; - let createContentModel: jasmine.Spy; - let setContentModel: jasmine.Spy; - let mockedModel: ContentModelDocument; - let cacheContentModel: jasmine.Spy; - let getFocusedPosition: jasmine.Spy; - let triggerPluginEvent: jasmine.Spy; - let getVisibleViewport: jasmine.Spy; - - const apiName = 'mockedApi'; - const mockedContainer = 'C' as any; - const mockedOffset = 'O' as any; - - beforeEach(() => { - mockedModel = ({} as any) as ContentModelDocument; - - addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); - createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); - setContentModel = jasmine.createSpy('setContentModel'); - cacheContentModel = jasmine.createSpy('cacheContentModel'); - getFocusedPosition = jasmine - .createSpy('getFocusedPosition') - .and.returnValue({ node: mockedContainer, offset: mockedOffset }); - triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); - getVisibleViewport = jasmine.createSpy('getVisibleViewport'); - - editor = ({ - addUndoSnapshot, - createContentModel, - setContentModel, - cacheContentModel, - getFocusedPosition, - triggerPluginEvent, - getVisibleViewport, - isDarkMode: () => false, - } as any) as IContentModelEditor; - }); - - it('Callback return false', () => { - const callback = jasmine.createSpy('callback').and.returnValue(false); - - formatWithContentModel(editor, apiName, callback); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).not.toHaveBeenCalled(); - expect(setContentModel).not.toHaveBeenCalled(); - }); - - it('Callback return true', () => { - const callback = jasmine.createSpy('callback').and.returnValue(true); - - formatWithContentModel(editor, apiName, callback); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot.calls.argsFor(0)[1]).toBe(undefined); - expect(addUndoSnapshot.calls.argsFor(0)[2]).toBe(false); - expect(addUndoSnapshot.calls.argsFor(0)[3]).toEqual({ - formatApiName: apiName, - }); - expect(setContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith(mockedModel, undefined, undefined); - }); - - it('Preserve pending format', () => { - const callback = jasmine.createSpy('callback').and.returnValue(true); - const mockedFormat = 'FORMAT' as any; - - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(mockedFormat); - spyOn(pendingFormat, 'setPendingFormat'); - - formatWithContentModel(editor, apiName, callback, { - preservePendingFormat: true, - }); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot.calls.argsFor(0)[1]).toBe(undefined!); - expect(addUndoSnapshot.calls.argsFor(0)[2]).toBe(false); - expect(addUndoSnapshot.calls.argsFor(0)[3]).toEqual({ - formatApiName: apiName, - }); - expect(pendingFormat.setPendingFormat).toHaveBeenCalledTimes(1); - expect(pendingFormat.setPendingFormat).toHaveBeenCalledWith( - editor, - mockedFormat, - mockedContainer, - mockedOffset - ); - }); - - it('Skip undo snapshot', () => { - const callback = jasmine.createSpy('callback').and.callFake((model, context) => { - context.skipUndoSnapshot = true; - return true; - }); - const mockedFormat = 'FORMAT' as any; - - spyOn(pendingFormat, 'getPendingFormat').and.returnValue(mockedFormat); - spyOn(pendingFormat, 'setPendingFormat'); - - formatWithContentModel(editor, apiName, callback); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - skipUndoSnapshot: true, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).not.toHaveBeenCalled(); - }); - - it('Customize change source', () => { - const callback = jasmine.createSpy('callback').and.returnValue(true); - - formatWithContentModel(editor, apiName, callback, { changeSource: 'TEST' }); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).toHaveBeenCalled(); - expect(addUndoSnapshot.calls.argsFor(0)[1]).toBe(undefined!); - expect(triggerPluginEvent).toHaveBeenCalled(); - }); - - it('Customize change source and skip undo snapshot', () => { - const callback = jasmine.createSpy('callback').and.callFake((model, context) => { - context.skipUndoSnapshot = true; - return true; - }); - formatWithContentModel(editor, apiName, callback, { - changeSource: 'TEST', - getChangeData: () => 'DATA', - }); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - skipUndoSnapshot: true, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).not.toHaveBeenCalled(); - }); - - it('Has onNodeCreated', () => { - const callback = jasmine.createSpy('callback').and.returnValue(true); - const onNodeCreated = jasmine.createSpy('onNodeCreated'); - - formatWithContentModel(editor, apiName, callback, { onNodeCreated: onNodeCreated }); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(addUndoSnapshot).toHaveBeenCalled(); - expect(setContentModel).toHaveBeenCalledWith(mockedModel, undefined, onNodeCreated); - }); - - it('Has getChangeData', () => { - const callback = jasmine.createSpy('callback').and.returnValue(true); - const mockedData = 'DATA' as any; - const getChangeData = jasmine.createSpy('getChangeData').and.returnValue(mockedData); - - formatWithContentModel(editor, apiName, callback, { getChangeData }); - - expect(callback).toHaveBeenCalledWith(mockedModel, { - newEntities: [], - deletedEntities: [], - rawEvent: undefined, - newImages: [], - }); - expect(createContentModel).toHaveBeenCalledTimes(1); - expect(setContentModel).toHaveBeenCalledWith(mockedModel, undefined, undefined); - expect(addUndoSnapshot).toHaveBeenCalled(); - expect(getChangeData).toHaveBeenCalled(); - }); - - it('Has entity got deleted', () => { - const entity1 = { - entityFormat: { id: 'E1', entityType: 'E', isReadonly: true }, - wrapper: {}, - } as any; - const entity2 = { - entityFormat: { id: 'E2', entityType: 'E', isReadonly: true }, - wrapper: {}, - } as any; - const rawEvent = 'RawEvent' as any; - - formatWithContentModel( - editor, - apiName, - (model, context) => { - context.deletedEntities.push( - { - entity: entity1, - operation: 'removeFromStart', - }, - { - entity: entity2, - operation: 'removeFromEnd', - } - ); - return true; - }, - { - rawEvent: rawEvent, - } - ); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(3); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - entity: { id: 'E1', type: 'E', isReadonly: true, wrapper: entity1.wrapper }, - operation: EntityOperation.RemoveFromStart, - rawEvent: rawEvent, - }); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - entity: { id: 'E2', type: 'E', isReadonly: true, wrapper: entity2.wrapper }, - operation: EntityOperation.RemoveFromEnd, - rawEvent: rawEvent, - }); - }); - - it('Has new entity in dark mode', () => { - const wrapper1 = 'W1' as any; - const wrapper2 = 'W2' as any; - const entity1 = { - entityFormat: { id: 'E1', entityType: 'E', isReadonly: true }, - wrapper: wrapper1, - } as any; - const entity2 = { - entityFormat: { id: 'E2', entityType: 'E', isReadonly: true }, - wrapper: wrapper2, - } as any; - const rawEvent = 'RawEvent' as any; - const transformToDarkColorSpy = jasmine.createSpy('transformToDarkColor'); - const mockedData = 'DATA'; - - editor.isDarkMode = () => true; - editor.transformToDarkColor = transformToDarkColorSpy; - - formatWithContentModel( - editor, - apiName, - (model, context) => { - context.newEntities.push(entity1, entity2); - return true; - }, - { - rawEvent: rawEvent, - getChangeData: () => mockedData, - } - ); - - expect(triggerPluginEvent).toHaveBeenCalledTimes(1); - expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.ContentChanged, { - contentModel: mockedModel, - selection: undefined, - source: ChangeSource.Format, - data: mockedData, - additionalData: { - formatApiName: apiName, - }, - }); - expect(transformToDarkColorSpy).toHaveBeenCalledTimes(2); - expect(transformToDarkColorSpy).toHaveBeenCalledWith(wrapper1); - expect(transformToDarkColorSpy).toHaveBeenCalledWith(wrapper2); - }); - - it('With selectionOverride', () => { - const range = 'MockedRangeEx' as any; - - formatWithContentModel(editor, apiName, () => true, { - selectionOverride: range, - }); - - expect(createContentModel).toHaveBeenCalledWith(undefined, range); - }); - - it('Has image', () => { - const image = createImage('test'); - const rawEvent = 'RawEvent' as any; - const getVisibleViewportSpy = jasmine - .createSpy('getVisibleViewport') - .and.returnValue({ top: 100, bottom: 200, left: 100, right: 200 }); - const mockedData = 'DATA'; - editor.getVisibleViewport = getVisibleViewportSpy; - - formatWithContentModel( - editor, - apiName, - (model, context) => { - context.newImages.push(image); - return true; - }, - { - rawEvent: rawEvent, - getChangeData: () => mockedData, - } - ); - - expect(getVisibleViewportSpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelEditPlugin.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/ContentModelEditPlugin.ts similarity index 83% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelEditPlugin.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/ContentModelEditPlugin.ts index 4cec1d433f6..02d347a71b0 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelEditPlugin.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/ContentModelEditPlugin.ts @@ -1,6 +1,6 @@ -import keyboardDelete from '../../publicApi/editing/keyboardDelete'; +import { keyboardDelete } from './keyboardDelete'; import { PluginEventType } from 'roosterjs-editor-types'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IContentModelEditor } from 'roosterjs-content-model-editor'; import type { EditorPlugin, IEditor, @@ -9,12 +9,12 @@ import type { } from 'roosterjs-editor-types'; /** - * ContentModel plugins helps editor to do editing operation on top of content model. + * ContentModel edit plugins helps editor to do editing operation on top of content model. * This includes: * 1. Delete Key * 2. Backspace Key */ -export default class ContentModelEditPlugin implements EditorPlugin { +export class ContentModelEditPlugin implements EditorPlugin { private editor: IContentModelEditor | null = null; /** @@ -76,12 +76,3 @@ export default class ContentModelEditPlugin implements EditorPlugin { } } } - -/** - * @internal - * Create a new instance of ContentModelEditPlugin class. - * This is mostly for unit test - */ -export function createContentModelEditPlugin() { - return new ContentModelEditPlugin(); -} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts similarity index 63% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts index 3a2d1624a07..9039d67b664 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteAllSegmentBefore.ts @@ -1,6 +1,5 @@ -import { DeleteResult } from '../utils/DeleteSelectionStep'; -import { deleteSegment } from '../utils/deleteSegment'; -import type { DeleteSelectionStep } from '../utils/DeleteSelectionStep'; +import { deleteSegment } from 'roosterjs-content-model-core'; +import type { DeleteSelectionStep } from 'roosterjs-content-model-types'; /** * @internal @@ -15,7 +14,7 @@ export const deleteAllSegmentBefore: DeleteSelectionStep = context => { segment.isSelected = true; if (deleteSegment(paragraph, segment, context.formatContext)) { - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } } }; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteCollapsedSelection.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts similarity index 79% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteCollapsedSelection.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts index 48fa94cf146..e1c4af0340b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteCollapsedSelection.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteCollapsedSelection.ts @@ -1,12 +1,8 @@ -import { createInsertPoint } from '../utils/createInsertPoint'; -import { deleteBlock } from '../utils/deleteBlock'; -import { DeleteResult } from '../utils/DeleteSelectionStep'; -import { deleteSegment } from '../utils/deleteSegment'; -import { getLeafSiblingBlock } from '../../block/getLeafSiblingBlock'; +import { deleteBlock, deleteSegment } from 'roosterjs-content-model-core'; +import { getLeafSiblingBlock } from '../utils/getLeafSiblingBlock'; import { setParagraphNotImplicit } from 'roosterjs-content-model-dom'; -import type { BlockAndPath } from '../../block/getLeafSiblingBlock'; -import type { ContentModelSegment } from 'roosterjs-content-model-types'; -import type { DeleteSelectionStep } from '../utils/DeleteSelectionStep'; +import type { BlockAndPath } from '../utils/getLeafSiblingBlock'; +import type { ContentModelSegment, DeleteSelectionStep } from 'roosterjs-content-model-types'; function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteSelectionStep { return context => { @@ -22,7 +18,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS if (segmentToDelete) { if (deleteSegment(paragraph, segmentToDelete, context.formatContext, direction)) { - context.deleteResult = DeleteResult.SingleChar; + context.deleteResult = 'singleChar'; // It is possible that we have deleted everything from this paragraph, so we need to mark it as not implicit // to avoid losing its format. See https://github.com/microsoft/roosterjs/issues/1953 @@ -35,7 +31,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS if (siblingSegment) { // When selection is under general segment, need to check if it has a sibling sibling, and delete from it if (deleteSegment(block, siblingSegment, context.formatContext, direction)) { - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } } else { if (isForward) { @@ -45,12 +41,17 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS block.segments.pop(); } - context.insertPoint = createInsertPoint(marker, block, path, tableContext); + context.insertPoint = { + marker, + paragraph: block, + path, + tableContext, + }; context.lastParagraph = paragraph; delete block.cachedElement; } - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } // When go across table, getLeafSiblingBlock will return null, when we are here, we must be in the same table context @@ -65,14 +66,14 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS direction ) ) { - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } } } else { // We have nothing to delete, in this case we don't want browser handle it as well. // Because when Backspace on an empty document, it will also delete the only DIV and SPAN element, causes // editor is really empty. We don't want that happen. So the handling should stop here. - context.deleteResult = DeleteResult.NothingToDelete; + context.deleteResult = 'nothingToDelete'; } }; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts index c00bfecd59e..6435a2e1c1b 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/edit/deleteSteps/deleteWordSelection.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/deleteSteps/deleteWordSelection.ts @@ -1,8 +1,10 @@ -import { DeleteResult } from '../utils/DeleteSelectionStep'; -import { isPunctuation, isSpace, normalizeText } from '../../../domUtils/stringUtil'; +import { isPunctuation, isSpace, normalizeText } from 'roosterjs-content-model-core'; import { isWhiteSpacePreserved } from 'roosterjs-content-model-dom'; -import type { ContentModelParagraph } from 'roosterjs-content-model-types'; -import type { DeleteSelectionContext, DeleteSelectionStep } from '../utils/DeleteSelectionStep'; +import type { + ContentModelParagraph, + DeleteSelectionContext, + DeleteSelectionStep, +} from 'roosterjs-content-model-types'; const enum DeleteWordState { Start, @@ -124,7 +126,7 @@ function* iterateSegments( newText = normalizeText(newText, forward); } - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; if (newText) { segment.text = newText; @@ -155,7 +157,7 @@ function* iterateSegments( i -= step; } - context.deleteResult = DeleteResult.Range; + context.deleteResult = 'range'; } break; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/handleKeyboardEventCommon.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts similarity index 75% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/utils/handleKeyboardEventCommon.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts index af82943e0af..da3f811049e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/utils/handleKeyboardEventCommon.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/handleKeyboardEventCommon.ts @@ -1,9 +1,11 @@ -import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep'; import { normalizeContentModel } from 'roosterjs-content-model-dom'; import { PluginEventType } from 'roosterjs-editor-types'; -import type { ContentModelDocument } from 'roosterjs-content-model-types'; -import type { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import type { IContentModelEditor } from 'roosterjs-content-model-editor'; +import type { + ContentModelDocument, + DeleteResult, + FormatWithContentModelContext, +} from 'roosterjs-content-model-types'; /** * @internal @@ -17,24 +19,28 @@ export function handleKeyboardEventResult( context: FormatWithContentModelContext ): boolean { context.skipUndoSnapshot = true; + context.clearModelCache = false; switch (result) { - case DeleteResult.NotDeleted: - // We have not delete anything, we will let browser handle this event + case 'notDeleted': + // We have not delete anything, we will let browser handle this event, so that current cached model may be invalid + context.clearModelCache = true; + + // Return false here since we didn't do any change to Content Model, so no need to rewrite with Content Model return false; - case DeleteResult.NothingToDelete: + case 'nothingToDelete': // We known there is nothing to delete, no need to let browser keep handling the event rawEvent.preventDefault(); return false; - case DeleteResult.Range: - case DeleteResult.SingleChar: + case 'range': + case 'singleChar': // We have deleted what we need from content model, no need to let browser keep handling the event rawEvent.preventDefault(); normalizeContentModel(model); - if (result == DeleteResult.Range) { + if (result == 'range') { // A range is about to be deleted, so add an undo snapshot immediately context.skipUndoSnapshot = false; } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts similarity index 70% rename from packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts index d22a9b8ee50..8c8a279edd3 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/editing/keyboardDelete.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/edit/keyboardDelete.ts @@ -1,25 +1,21 @@ -import { ChangeSource } from '../../publicTypes/event/ContentModelContentChangedEvent'; -import { deleteAllSegmentBefore } from '../../modelApi/edit/deleteSteps/deleteAllSegmentBefore'; -import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep'; -import { deleteSelection } from '../../modelApi/edit/deleteSelection'; -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { isModifierKey } from '../../domUtils/eventUtils'; +import { ChangeSource, deleteSelection, isModifierKey } from 'roosterjs-content-model-core'; +import { deleteAllSegmentBefore } from './deleteSteps/deleteAllSegmentBefore'; import { isNodeOfType } from 'roosterjs-content-model-dom'; -import type { DeleteSelectionStep } from '../../modelApi/edit/utils/DeleteSelectionStep'; -import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { handleKeyboardEventResult, shouldDeleteAllSegmentsBefore, shouldDeleteWord, -} from '../../editor/utils/handleKeyboardEventCommon'; +} from './handleKeyboardEventCommon'; import { backwardDeleteWordSelection, forwardDeleteWordSelection, -} from '../../modelApi/edit/deleteSteps/deleteWordSelection'; +} from './deleteSteps/deleteWordSelection'; import { backwardDeleteCollapsedSelection, forwardDeleteCollapsedSelection, -} from '../../modelApi/edit/deleteSteps/deleteCollapsedSelection'; +} from './deleteSteps/deleteCollapsedSelection'; +import type { IContentModelEditor } from 'roosterjs-content-model-editor'; +import type { DeleteSelectionStep } from 'roosterjs-content-model-types'; /** * @internal @@ -28,18 +24,13 @@ import { * @param rawEvent DOM keyboard event * @returns True if the event is handled with this function, otherwise false */ -export default function keyboardDelete( - editor: IContentModelEditor, - rawEvent: KeyboardEvent -): boolean { +export function keyboardDelete(editor: IContentModelEditor, rawEvent: KeyboardEvent): boolean { const selection = editor.getDOMSelection(); const range = selection?.type == 'range' ? selection.range : null; let isDeleted = false; if (shouldDeleteWithContentModel(range, rawEvent)) { - formatWithContentModel( - editor, - rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey', + editor.formatContentModel( (model, context) => { const result = deleteSelection( model, @@ -47,7 +38,7 @@ export default function keyboardDelete( context ).deleteResult; - isDeleted = result != DeleteResult.NotDeleted; + isDeleted = result != 'notDeleted'; return handleKeyboardEventResult(editor, model, rawEvent, result, context); }, @@ -55,6 +46,7 @@ export default function keyboardDelete( rawEvent, changeSource: ChangeSource.Keyboard, getChangeData: () => rawEvent.which, + apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey', } ); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/getLeafSiblingBlock.ts b/packages-content-model/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/modelApi/block/getLeafSiblingBlock.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/edit/utils/getLeafSiblingBlock.ts diff --git a/packages-content-model/roosterjs-content-model-plugins/lib/index.ts b/packages-content-model/roosterjs-content-model-plugins/lib/index.ts new file mode 100644 index 00000000000..373fd4da9eb --- /dev/null +++ b/packages-content-model/roosterjs-content-model-plugins/lib/index.ts @@ -0,0 +1,2 @@ +export { ContentModelPastePlugin } from './paste/ContentModelPastePlugin'; +export { ContentModelEditPlugin } from './edit/ContentModelEditPlugin'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts similarity index 95% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts index 95b51dfa5f2..a4ff2e111e7 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/ContentModelPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/ContentModelPastePlugin.ts @@ -10,15 +10,15 @@ import { processPastedContentFromExcel } from './Excel/processPastedContentFromE import { processPastedContentFromPowerPoint } from './PowerPoint/processPastedContentFromPowerPoint'; import { processPastedContentFromWordDesktop } from './WordDesktop/processPastedContentFromWordDesktop'; import { processPastedContentWacComponents } from './WacComponents/processPastedContentWacComponents'; -import type { PasteType } from '../../../publicTypes/parameter/PasteType'; -import type ContentModelBeforePasteEvent from '../../../publicTypes/event/ContentModelBeforePasteEvent'; +import type { IContentModelEditor } from 'roosterjs-content-model-editor'; import type { BorderFormat, + ContentModelBeforePasteEvent, ContentModelBlockFormat, ContentModelTableCellFormat, FormatParser, + PasteType, } from 'roosterjs-content-model-types'; -import type { IContentModelEditor } from '../../../publicTypes/IContentModelEditor'; import type { EditorPlugin, HtmlSanitizerOptions, @@ -43,7 +43,7 @@ const PasteTypeMap: Record = { * 4. Content copied from Power Point * (This class is still under development, and may still be changed in the future with some breaking changes) */ -export default class ContentModelPastePlugin implements EditorPlugin { +export class ContentModelPastePlugin implements EditorPlugin { private editor: IContentModelEditor | null = null; /** diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts similarity index 96% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts index 2bd97574f30..08fd346901c 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/Excel/processPastedContentFromExcel.ts @@ -1,8 +1,8 @@ import addParser from '../utils/addParser'; import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom'; import { setProcessor } from '../utils/setProcessor'; -import type ContentModelBeforePasteEvent from '../../../../publicTypes/event/ContentModelBeforePasteEvent'; import type { TrustedHTMLHandler } from 'roosterjs-editor-types'; +import type { ContentModelBeforePasteEvent } 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-editor/lib/editor/plugins/PastePlugin/PowerPoint/processPastedContentFromPowerPoint.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/PowerPoint/processPastedContentFromPowerPoint.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts index 24e50482002..4d382fa7172 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts @@ -1,7 +1,7 @@ import addParser from '../utils/addParser'; import { setProcessor } from '../utils/setProcessor'; -import type ContentModelBeforePasteEvent from '../../../../publicTypes/event/ContentModelBeforePasteEvent'; import type { + ContentModelBeforePasteEvent, ContentModelBlockFormat, ContentModelBlockGroup, ContentModelListItemLevelFormat, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts index 965e55daf60..ef69d8b74d1 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop.ts +++ b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processPastedContentFromWordDesktop.ts @@ -5,8 +5,8 @@ import { moveChildNodes } from 'roosterjs-content-model-dom'; import { processWordComments } from './processWordComments'; import { processWordList } from './processWordLists'; import { setProcessor } from '../utils/setProcessor'; -import type ContentModelBeforePasteEvent from '../../../../publicTypes/event/ContentModelBeforePasteEvent'; import type { + ContentModelBeforePasteEvent, ContentModelBlockFormat, ContentModelListItemFormat, ContentModelListItemLevelFormat, diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processWordComments.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordComments.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processWordComments.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordComments.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processWordLists.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/WordDesktop/processWordLists.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/WordDesktop/processWordLists.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/constants.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/constants.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/constants.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/constants.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/documentContainWacElements.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/documentContainWacElements.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/documentContainWacElements.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/documentContainWacElements.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isExcelDesktopDocument.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isExcelDesktopDocument.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isExcelDesktopDocument.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isExcelDesktopDocument.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isExcelOnlineDocument.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isExcelOnlineDocument.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isExcelOnlineDocument.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isExcelOnlineDocument.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isGoogleSheetDocument.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isGoogleSheetDocument.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isGoogleSheetDocument.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isGoogleSheetDocument.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isPowerPointDesktopDocument.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isPowerPointDesktopDocument.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isPowerPointDesktopDocument.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isPowerPointDesktopDocument.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isWordDesktopDocument.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isWordDesktopDocument.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/isWordDesktopDocument.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/isWordDesktopDocument.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/shouldConvertToSingleImage.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/shouldConvertToSingleImage.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/pasteSourceValidations/shouldConvertToSingleImage.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/shouldConvertToSingleImage.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/addParser.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/addParser.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/addParser.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/addParser.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/deprecatedColorParser.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/deprecatedColorParser.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/deprecatedColorParser.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/deprecatedColorParser.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/getStyles.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/getStyles.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/getStyles.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/getStyles.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/linkParser.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/linkParser.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/linkParser.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/linkParser.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/setProcessor.ts b/packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/setProcessor.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/editor/plugins/PastePlugin/utils/setProcessor.ts rename to packages-content-model/roosterjs-content-model-plugins/lib/paste/utils/setProcessor.ts diff --git a/packages-content-model/roosterjs-content-model-plugins/package.json b/packages-content-model/roosterjs-content-model-plugins/package.json new file mode 100644 index 00000000000..e33c18cf24d --- /dev/null +++ b/packages-content-model/roosterjs-content-model-plugins/package.json @@ -0,0 +1,15 @@ +{ + "name": "roosterjs-content-model-plugins", + "description": "Content Model for roosterjs (Under development)", + "dependencies": { + "tslib": "^2.3.1", + "roosterjs-editor-types": "", + "roosterjs-editor-dom": "", + "roosterjs-content-model-core": "", + "roosterjs-content-model-editor": "", + "roosterjs-content-model-dom": "", + "roosterjs-content-model-types": "" + }, + "version": "0.0.0", + "main": "./lib/index.ts" +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/corePlugins/ContentModelEditPluginTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/ContentModelEditPluginTest.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/test/editor/corePlugins/ContentModelEditPluginTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/edit/ContentModelEditPluginTest.ts index 73b4c9bbbb2..e793fbd99e4 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/corePlugins/ContentModelEditPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/ContentModelEditPluginTest.ts @@ -1,7 +1,7 @@ -import * as keyboardDelete from '../../../lib/publicApi/editing/keyboardDelete'; -import ContentModelEditPlugin from '../../../lib/editor/corePlugins/ContentModelEditPlugin'; +import * as keyboardDelete from '../../lib/edit/keyboardDelete'; +import { ContentModelEditPlugin } from '../../lib/edit/ContentModelEditPlugin'; import { EntityOperation, PluginEventType } from 'roosterjs-editor-types'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; describe('ContentModelEditPlugin', () => { let editor: IContentModelEditor; @@ -19,7 +19,7 @@ describe('ContentModelEditPlugin', () => { let keyboardDeleteSpy: jasmine.Spy; beforeEach(() => { - keyboardDeleteSpy = spyOn(keyboardDelete, 'default').and.returnValue(true); + keyboardDeleteSpy = spyOn(keyboardDelete, 'keyboardDelete').and.returnValue(true); }); it('Backspace', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteCollapsedSelectionTest.ts similarity index 69% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteCollapsedSelectionTest.ts index 961c1ba5622..9a8dcd7d7fd 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteCollapsedSelectionTest.ts @@ -1,981 +1,28 @@ -import { ContentModelEntity, ContentModelSelectionMarker } from 'roosterjs-content-model-types'; -import { DeletedEntity } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; -import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; -import { deleteSelection } from '../../../lib/modelApi/edit/deleteSelection'; +import { deleteSelection } from 'roosterjs-content-model-core'; import { - createBr, - createContentModelDocument, - createDivider, - createEntity, - createFormatContainer, - createGeneralBlock, - createGeneralSegment, - createImage, - createListItem, - createParagraph, - createSelectionMarker, - createTable, - createTableCell, - createText, -} from 'roosterjs-content-model-dom'; -import { - backwardDeleteWordSelection, - forwardDeleteWordSelection, -} from '../../../lib/modelApi/edit/deleteSteps/deleteWordSelection'; -import { - backwardDeleteCollapsedSelection, - forwardDeleteCollapsedSelection, -} from '../../../lib/modelApi/edit/deleteSteps/deleteCollapsedSelection'; - -describe('deleteSelection - selectionOnly', () => { - it('empty selection', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - - model.blocks.push(para); - - const result = deleteSelection(model); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [], - }, - ], - }); - - expect(result.deleteResult).toBe(DeleteResult.NotDeleted); - expect(result.insertPoint).toBeNull(); - }); - - it('Single selection marker', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker({ fontSize: '10px' }); - - para.segments.push(marker); - model.blocks.push(para); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.NotDeleted); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: { fontSize: '10px' }, - isSelected: true, - }, - paragraph: para, - path: [model], - tableContext: undefined, - }); - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: { fontSize: '10px' }, - isSelected: true, - }, - ], - }, - ], - }); - }); - - it('Single text selection', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const text = createText('test1', { fontSize: '10px' }); - - text.isSelected = true; - para.segments.push(text); - model.blocks.push(para); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: { fontSize: '10px' }, - isSelected: true, - }, - paragraph: para, - path: [model], - tableContext: undefined, - }); - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: { fontSize: '10px' }, - isSelected: true, - }, - ], - }, - ], - }); - }); - - it('Multiple text selection in multiple paragraphs', () => { - const model = createContentModelDocument(); - const para1 = createParagraph(); - const para2 = createParagraph(); - const text0 = createText('test0', { fontSize: '10px' }); - const text1 = createText('test1', { fontSize: '11px' }); - const text2 = createText('test2', { fontSize: '12px' }); - - text1.isSelected = true; - text2.isSelected = true; - - para1.segments.push(text0); - para1.segments.push(text1); - para2.segments.push(text2); - - model.blocks.push(para1); - model.blocks.push(para2); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: { fontSize: '11px' }, - isSelected: true, - }, - paragraph: para1, - path: [model], - tableContext: undefined, - }); - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test0', - format: { fontSize: '10px' }, - }, - { - segmentType: 'SelectionMarker', - format: { fontSize: '11px' }, - isSelected: true, - }, - ], - }, - { - blockType: 'Paragraph', - format: {}, - segments: [], - }, - ], - }); - }); - - it('Divider selection', () => { - const model = createContentModelDocument(); - const divider = createDivider('div'); - - divider.isSelected = true; - model.blocks.push(divider); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - isImplicit: false, - }, - ], - }); - }); - - it('2 Divider selection and paragraph after it', () => { - const model = createContentModelDocument(); - const divider1 = createDivider('div'); - const divider2 = createDivider('hr'); - const para1 = createParagraph(); - const para2 = createParagraph(); - - divider1.isSelected = true; - divider2.isSelected = true; - model.blocks.push(para1, divider1, divider2, para2); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [], - }, - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - isImplicit: false, - }, - { - blockType: 'Paragraph', - format: {}, - segments: [], - isImplicit: true, - }, - { - blockType: 'Paragraph', - format: {}, - segments: [], - }, - ], - }); - }); - - it('Some table cell selection', () => { - const model = createContentModelDocument(); - const table = createTable(1); - const cell1 = createTableCell(); - const cell2 = createTableCell(); - - cell2.isSelected = true; - - table.rows[0].cells.push(cell1, cell2); - model.blocks.push(table); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - isImplicit: false, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - format: {}, - }, - path: [cell2, model], - tableContext: { - table: table, - colIndex: 1, - rowIndex: 0, - isWholeTableSelected: false, - }, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Table', - format: {}, - dataset: {}, - widths: [], - rows: [ - { - format: {}, - height: 0, - cells: [ - { - blockGroupType: 'TableCell', - format: {}, - dataset: {}, - spanAbove: false, - spanLeft: false, - isHeader: false, - blocks: [], - }, - { - blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - isImplicit: false, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Br', - format: {}, - }, - ], - }, - ], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - isSelected: true, - }, - ], - }, - ], - }, - ], - }); - }); - - it('All table cell selection', () => { - const model = createContentModelDocument(); - const table = createTable(1); - const cell = createTableCell(); - - cell.isSelected = true; - - table.rows[0].cells.push(cell); - model.blocks.push(table); - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - isImplicit: false, - }, - ], - }); - }); - - it('Entity selection, no callback', () => { - const model = createContentModelDocument(); - const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper); - model.blocks.push(entity); - - entity.isSelected = true; - - const result = deleteSelection(model); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - isImplicit: false, - }, - ], - }); - }); - - it('Entity selection, callback returns false', () => { - const model = createContentModelDocument(); - const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper); - const deletedEntities: DeletedEntity[] = []; - model.blocks.push(entity); - - entity.isSelected = true; - - const result = deleteSelection(model, [], { - newEntities: [], - deletedEntities, - newImages: [], - }); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - isImplicit: false, - }, - ], - }); - - expect(deletedEntities).toEqual([{ entity, operation: 'overwrite' }]); - }); - - it('Entity selection, callback returns true', () => { - const model = createContentModelDocument(); - const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper); - model.blocks.push(entity); - - entity.isSelected = true; - - const deletedEntities: DeletedEntity[] = []; - const result = deleteSelection(model, [], { - newEntities: [], - deletedEntities, - newImages: [], - }); - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker: { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - paragraph: { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }, - ], - format: {}, - isImplicit: false, - }, - ], - }); - - expect(deletedEntities).toEqual([{ entity, operation: 'overwrite' }]); - }); - - it('delete with default format', () => { - const model = createContentModelDocument({ - fontSize: '10pt', - }); - const divider = createDivider('div'); - - divider.isSelected = true; - model.blocks.push(divider); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: { fontSize: '10pt' }, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: false, - segmentFormat: { fontSize: '10pt' }, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - isImplicit: false, - segmentFormat: { fontSize: '10pt' }, - }, - ], - format: { fontSize: '10pt' }, - }); - }); - - it('delete with general block', () => { - const model = createContentModelDocument(); - const general = createGeneralBlock(null!); - - general.isSelected = true; - model.blocks.push(general); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - isImplicit: false, - }, - ], - }); - }); - - it('delete with general block and others', () => { - const model = createContentModelDocument(); - const divider = createDivider('div'); - const general = createGeneralBlock(null!); - - divider.isSelected = true; - general.isSelected = true; - model.blocks.push(divider, general); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - isImplicit: false, - }, - { - blockType: 'Paragraph', - format: {}, - segments: [], - isImplicit: true, - }, - ], - }); - }); - - it('delete with general segment', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const general = createGeneralSegment(null!); - - general.isSelected = true; - para.segments.push(general); - model.blocks.push(para); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - }, - ], - }); - }); - - it('delete with general segment and others', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const general = createGeneralSegment(null!); - const text = createText('test'); - - general.isSelected = true; - text.isSelected = true; - para.segments.push(general, text); - model.blocks.push(para); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - }, - ], - }); - }); - - it('Normalize spaces before deleted segment', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const text = createText('test '); - const image = createImage('test'); - - image.isSelected = true; - para.segments.push(text, image); - model.blocks.push(para); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [{ segmentType: 'Text', text: 'test\u00A0', format: {} }, marker], - format: {}, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [{ segmentType: 'Text', text: 'test\u00A0', format: {} }, marker], - }, - ], - }); - }); - - it('Make paragraph not implicit when delete', () => { - const model = createContentModelDocument(); - const para = createParagraph(true /*isImplicit*/); - const text = createText('test '); - - text.isSelected = true; - para.segments.push(text); - model.blocks.push(para); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: false, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - isImplicit: false, - }, - ], - }); - }); - - it('Delete divider with default format', () => { - const model = createContentModelDocument({ fontFamily: 'Arial' }); - const divider = createDivider('hr'); - - divider.isSelected = true; - model.blocks.push(divider); - - const result = deleteSelection(model); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - format: { fontFamily: 'Arial' }, - isSelected: true, - }; - - expect(result.deleteResult).toBe(DeleteResult.Range); - expect(result.insertPoint).toEqual({ - marker, - paragraph: { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: false, - segmentFormat: { fontFamily: 'Arial' }, - }, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [marker], - isImplicit: false, - segmentFormat: { fontFamily: 'Arial' }, - }, - ], - format: { fontFamily: 'Arial' }, - }); - }); -}); + ContentModelEntity, + ContentModelSelectionMarker, + DeletedEntity, +} from 'roosterjs-content-model-types'; +import { + createBr, + createContentModelDocument, + createDivider, + createEntity, + createFormatContainer, + createGeneralSegment, + createImage, + createListItem, + createParagraph, + createSelectionMarker, + createTable, + createTableCell, + createText, +} from 'roosterjs-content-model-dom'; +import { + backwardDeleteCollapsedSelection, + forwardDeleteCollapsedSelection, +} from '../../../lib/edit/deleteSteps/deleteCollapsedSelection'; describe('deleteSelection - forward', () => { it('empty selection', () => { @@ -997,7 +44,7 @@ describe('deleteSelection - forward', () => { ], }); - expect(result.deleteResult).toBe(DeleteResult.NotDeleted); + expect(result.deleteResult).toBe('notDeleted'); expect(result.insertPoint).toBeNull(); }); @@ -1011,7 +58,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.NothingToDelete); + expect(result.deleteResult).toBe('nothingToDelete'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1051,7 +98,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1099,7 +146,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1157,7 +204,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1211,7 +258,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1270,7 +317,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1325,7 +372,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1367,7 +414,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1408,7 +455,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1450,7 +497,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1497,7 +544,7 @@ describe('deleteSelection - forward', () => { newImages: [], }); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1545,7 +592,7 @@ describe('deleteSelection - forward', () => { newImages: [], }); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1591,7 +638,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1659,7 +706,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1722,7 +769,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1787,7 +834,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1859,7 +906,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1913,7 +960,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -1961,7 +1008,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2015,7 +1062,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2085,7 +1132,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2186,7 +1233,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2244,7 +1291,7 @@ describe('deleteSelection - forward', () => { isSelected: true, }; - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker, paragraph: { @@ -2287,7 +1334,7 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.NothingToDelete); + expect(result.deleteResult).toBe('nothingToDelete'); expect(result.insertPoint).toEqual({ marker: marker, @@ -2338,203 +1385,12 @@ describe('deleteSelection - forward', () => { const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [general, model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - blockType: 'BlockGroup', - blockGroupType: 'General', - segmentType: 'General', - format: {}, - blocks: [ - { - blockType: 'Paragraph', - segments: [marker], - format: {}, - }, - ], - element: null!, - }, - { - segmentType: 'Text', - text: 'est', - format: {}, - }, - ], - }, - ], - }); - }); - - it('Delete text and need to convert space to  ', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text = createText(' test'); - - para.segments.push(marker, text); - model.blocks.push(para); - - const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - - expect(result.deleteResult).toBe(DeleteResult.SingleChar); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Text', - text: '\u00A0test', - format: {}, - }, - ], - }, - ], - }); - }); - - it('Delete text and no need to convert space to   when preserve white space', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text = createText(' test'); - - para.format.whiteSpace = 'pre'; - para.segments.push(marker, text); - model.blocks.push(para); - - const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - - expect(result.deleteResult).toBe(DeleteResult.SingleChar); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: { - whiteSpace: 'pre', - }, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Text', - text: ' test', - format: {}, - }, - ], - }, - ], - }); - }); - - it('Normalize text and space before deleted content', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('test1 '); - const text2 = createText('test2'); - - para.segments.push(text1, marker, text2); - model.blocks.push(para); - - const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - - expect(result.deleteResult).toBe(DeleteResult.SingleChar); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test1\u00A0', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Text', - text: 'est2', - format: {}, - }, - ], - }, - ], - }); - }); - - it('Normalize text and space before deleted content, delete empty text', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('test1 '); - const text2 = createText('a'); - - para.segments.push(text1, marker, text2); - model.blocks.push(para); - - const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: marker, paragraph: para, - path: [model], + path: [general, model], tableContext: undefined, }); @@ -2546,14 +1402,23 @@ describe('deleteSelection - forward', () => { format: {}, segments: [ { - segmentType: 'Text', - text: 'test1\u00A0', + blockType: 'BlockGroup', + blockGroupType: 'General', + segmentType: 'General', format: {}, + blocks: [ + { + blockType: 'Paragraph', + segments: [marker], + format: {}, + }, + ], + element: null!, }, { - segmentType: 'SelectionMarker', + segmentType: 'Text', + text: 'est', format: {}, - isSelected: true, }, ], }, @@ -2561,20 +1426,18 @@ describe('deleteSelection - forward', () => { }); }); - it('Delete word: text+space+text', () => { + it('Delete text and need to convert space to  ', () => { const model = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); - const text1 = createText('test1'); - const text2 = createText(' '); - const text3 = createText('test2'); + const text = createText(' test'); - para.segments.push(marker, text1, text2, text3); + para.segments.push(marker, text); model.blocks.push(para); - const result = deleteSelection(model, [forwardDeleteWordSelection]); + const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -2597,7 +1460,7 @@ describe('deleteSelection - forward', () => { }, { segmentType: 'Text', - text: 'test2', + text: '\u00A0test', format: {}, }, ], @@ -2606,18 +1469,19 @@ describe('deleteSelection - forward', () => { }); }); - it('Delete word: space+text+space+text', () => { + it('Delete text and no need to convert space to   when preserve white space', () => { const model = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); - const text1 = createText(' test1 test2'); + const text = createText(' test'); - para.segments.push(marker, text1); + para.format.whiteSpace = 'pre'; + para.segments.push(marker, text); model.blocks.push(para); - const result = deleteSelection(model, [forwardDeleteWordSelection]); + const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -2631,7 +1495,9 @@ describe('deleteSelection - forward', () => { blocks: [ { blockType: 'Paragraph', - format: {}, + format: { + whiteSpace: 'pre', + }, segments: [ { segmentType: 'SelectionMarker', @@ -2640,7 +1506,7 @@ describe('deleteSelection - forward', () => { }, { segmentType: 'Text', - text: 'test1 test2', + text: ' test', format: {}, }, ], @@ -2649,18 +1515,19 @@ describe('deleteSelection - forward', () => { }); }); - it('Delete word: text+punc+space+text', () => { + it('Normalize text and space before deleted content', () => { const model = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); - const text1 = createText('test1. test2'); + const text1 = createText('test1 '); + const text2 = createText('test2'); - para.segments.push(marker, text1); + para.segments.push(text1, marker, text2); model.blocks.push(para); - const result = deleteSelection(model, [forwardDeleteWordSelection]); + const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -2676,6 +1543,11 @@ describe('deleteSelection - forward', () => { blockType: 'Paragraph', format: {}, segments: [ + { + segmentType: 'Text', + text: 'test1\u00A0', + format: {}, + }, { segmentType: 'SelectionMarker', format: {}, @@ -2683,7 +1555,7 @@ describe('deleteSelection - forward', () => { }, { segmentType: 'Text', - text: '. test2', + text: 'est2', format: {}, }, ], @@ -2692,18 +1564,19 @@ describe('deleteSelection - forward', () => { }); }); - it('Delete word: punc+space+text', () => { + it('Normalize text and space before deleted content, delete empty text', () => { const model = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); - const text1 = createText('. test2'); + const text1 = createText('test1 '); + const text2 = createText('a'); - para.segments.push(marker, text1); + para.segments.push(text1, marker, text2); model.blocks.push(para); - const result = deleteSelection(model, [forwardDeleteWordSelection]); + const result = deleteSelection(model, [forwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -2720,14 +1593,14 @@ describe('deleteSelection - forward', () => { format: {}, segments: [ { - segmentType: 'SelectionMarker', + segmentType: 'Text', + text: 'test1\u00A0', format: {}, - isSelected: true, }, { - segmentType: 'Text', - text: 'test2', + segmentType: 'SelectionMarker', format: {}, + isSelected: true, }, ], }, @@ -2756,7 +1629,7 @@ describe('deleteSelection - backward', () => { ], }); - expect(result.deleteResult).toBe(DeleteResult.NotDeleted); + expect(result.deleteResult).toBe('notDeleted'); expect(result.insertPoint).toBeNull(); }); @@ -2770,7 +1643,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.NothingToDelete); + expect(result.deleteResult).toBe('nothingToDelete'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2810,7 +1683,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2858,7 +1731,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2916,7 +1789,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -2970,7 +1843,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3028,7 +1901,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3083,7 +1956,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3125,7 +1998,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3166,7 +2039,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3208,7 +2081,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3255,7 +2128,7 @@ describe('deleteSelection - backward', () => { newImages: [], }); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3304,7 +2177,7 @@ describe('deleteSelection - backward', () => { newImages: [], }); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3350,7 +2223,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3418,7 +2291,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3481,7 +2354,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3546,7 +2419,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3618,7 +2491,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3672,7 +2545,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3720,7 +2593,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3774,7 +2647,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3844,7 +2717,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -3945,7 +2818,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: { segmentType: 'SelectionMarker', @@ -4003,7 +2876,7 @@ describe('deleteSelection - backward', () => { isSelected: true, }; - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker, paragraph: { @@ -4046,7 +2919,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.NothingToDelete); + expect(result.deleteResult).toBe('nothingToDelete'); expect(result.insertPoint).toEqual({ marker: marker, @@ -4097,7 +2970,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.Range); + expect(result.deleteResult).toBe('range'); expect(result.insertPoint).toEqual({ marker: marker, @@ -4149,7 +3022,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -4193,7 +3066,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -4239,7 +3112,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -4288,7 +3161,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, @@ -4320,230 +3193,6 @@ describe('deleteSelection - backward', () => { }); }); - it('Delete word: text+space+text', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('test1'); - const text2 = createText(' '); - const text3 = createText('test2'); - - para.segments.push(text1, text2, text3, marker); - model.blocks.push(para); - - const result = deleteSelection(model, [backwardDeleteWordSelection]); - - expect(result.deleteResult).toBe(DeleteResult.Range); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test1', - format: {}, - }, - { - segmentType: 'Text', - text: ' ', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - }, - ], - }); - }); - - it('Delete word: space+text+space+text', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('\u00A0 \u00A0test1 \u00A0 test2'); - - para.segments.push(text1, marker); - model.blocks.push(para); - - const result = deleteSelection(model, [backwardDeleteWordSelection]); - - expect(result.deleteResult).toBe(DeleteResult.Range); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: '\u00A0 \u00A0test1 \u00A0\u00A0', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - }, - ], - }); - }); - - it('Delete word: text+punc+space+text', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('test1. test2'); - - para.segments.push(text1, marker); - model.blocks.push(para); - - const result = deleteSelection(model, [backwardDeleteWordSelection]); - - expect(result.deleteResult).toBe(DeleteResult.Range); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: 'test1.\u00A0', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - }, - ], - }); - }); - - it('Delete word: punc+space+text', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('. test2'); - - para.segments.push(text1, marker); - model.blocks.push(para); - - const result = deleteSelection(model, [backwardDeleteWordSelection]); - - expect(result.deleteResult).toBe(DeleteResult.Range); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'Text', - text: '.\u00A0', - format: {}, - }, - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - ], - }, - ], - }); - }); - - it('Delete all before', () => { - const model = createContentModelDocument(); - const para = createParagraph(); - const marker = createSelectionMarker(); - const text1 = createText('test1'); - const text2 = createText('test2'); - const text3 = createText('test3'); - - para.segments.push(text1, text2, marker, text3); - model.blocks.push(para); - - const result = deleteSelection(model, [backwardDeleteWordSelection]); - - expect(result.deleteResult).toBe(DeleteResult.Range); - - expect(result.insertPoint).toEqual({ - marker: marker, - paragraph: para, - path: [model], - tableContext: undefined, - }); - - expect(model).toEqual({ - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - format: {}, - segments: [ - { - segmentType: 'SelectionMarker', - format: {}, - isSelected: true, - }, - { - segmentType: 'Text', - text: 'test3', - format: {}, - }, - ], - }, - ], - }); - }); - it('Delete under an implicit paragraph', () => { const model = createContentModelDocument(); const para = createParagraph(); @@ -4556,7 +3205,7 @@ describe('deleteSelection - backward', () => { const result = deleteSelection(model, [backwardDeleteCollapsedSelection]); - expect(result.deleteResult).toBe(DeleteResult.SingleChar); + expect(result.deleteResult).toBe('singleChar'); expect(result.insertPoint).toEqual({ marker: marker, diff --git a/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteWordSelectionTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteWordSelectionTest.ts new file mode 100644 index 00000000000..88531728bdf --- /dev/null +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/deleteSteps/deleteWordSelectionTest.ts @@ -0,0 +1,413 @@ +import { deleteSelection } from 'roosterjs-content-model-core'; +import { + createContentModelDocument, + createParagraph, + createSelectionMarker, + createText, +} from 'roosterjs-content-model-dom'; +import { + backwardDeleteWordSelection, + forwardDeleteWordSelection, +} from '../../../lib/edit/deleteSteps/deleteWordSelection'; + +describe('deleteSelection - forward', () => { + it('Delete word: text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText(' '); + const text3 = createText('test2'); + + para.segments.push(marker, text1, text2, text3); + model.blocks.push(para); + + const result = deleteSelection(model, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test2', + format: {}, + }, + ], + }, + ], + }); + }); + + it('Delete word: space+text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText(' test1 test2'); + + para.segments.push(marker, text1); + model.blocks.push(para); + + const result = deleteSelection(model, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test1 test2', + format: {}, + }, + ], + }, + ], + }); + }); + + it('Delete word: text+punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1. test2'); + + para.segments.push(marker, text1); + model.blocks.push(para); + + const result = deleteSelection(model, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: '. test2', + format: {}, + }, + ], + }, + ], + }); + }); + + it('Delete word: punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('. test2'); + + para.segments.push(marker, text1); + model.blocks.push(para); + + const result = deleteSelection(model, [forwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test2', + format: {}, + }, + ], + }, + ], + }); + }); +}); + +describe('deleteSelection - backward', () => { + it('Delete word: text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText(' '); + const text3 = createText('test2'); + + para.segments.push(text1, text2, text3, marker); + model.blocks.push(para); + + const result = deleteSelection(model, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test1', + format: {}, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete word: space+text+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('\u00A0 \u00A0test1 \u00A0 test2'); + + para.segments.push(text1, marker); + model.blocks.push(para); + + const result = deleteSelection(model, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: '\u00A0 \u00A0test1 \u00A0\u00A0', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete word: text+punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1. test2'); + + para.segments.push(text1, marker); + model.blocks.push(para); + + const result = deleteSelection(model, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test1.\u00A0', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete word: punc+space+text', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('. test2'); + + para.segments.push(text1, marker); + model.blocks.push(para); + + const result = deleteSelection(model, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: '.\u00A0', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Delete all before', () => { + const model = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + + para.segments.push(text1, text2, marker, text3); + model.blocks.push(para); + + const result = deleteSelection(model, [backwardDeleteWordSelection]); + + expect(result.deleteResult).toBe('range'); + + expect(result.insertPoint).toEqual({ + marker: marker, + paragraph: para, + path: [model], + tableContext: undefined, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + { + segmentType: 'Text', + text: 'test3', + format: {}, + }, + ], + }, + ], + }); + }); +}); 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 new file mode 100644 index 00000000000..6d637e5f62f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/editingTestCommon.ts @@ -0,0 +1,42 @@ +import { IContentModelEditor } from 'roosterjs-content-model-editor'; +import { + ContentModelDocument, + ContentModelFormatter, + FormatWithContentModelOptions, +} from 'roosterjs-content-model-types'; + +export function editingTestCommon( + apiName: string, + executionCallback: (editor: IContentModelEditor) => void, + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number +) { + const triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); + + let formatResult: boolean | undefined; + + const formatContentModel = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter, options: FormatWithContentModelOptions) => { + expect(options.apiName).toBe(apiName); + formatResult = callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + rawEvent: options.rawEvent, + }); + }); + + const editor = ({ + triggerPluginEvent, + getEnvironment: () => ({}), + formatContentModel, + } as any) as IContentModelEditor; + + executionCallback(editor); + + expect(model).toEqual(result); + expect(formatContentModel).toHaveBeenCalledTimes(1); + expect(formatResult).toBe(calledTimes > 0); +} diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts similarity index 91% rename from packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts index 34bdc29154e..cde253d34d3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/handleKeyboardEventCommonTest.ts @@ -1,13 +1,12 @@ import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; -import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; -import { FormatWithContentModelContext } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { FormatWithContentModelContext } from 'roosterjs-content-model-types'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { PluginEventType } from 'roosterjs-editor-types'; import { handleKeyboardEventResult, shouldDeleteAllSegmentsBefore, shouldDeleteWord, -} from '../../../lib/editor/utils/handleKeyboardEventCommon'; +} from '../../lib/edit/handleKeyboardEventCommon'; describe('handleKeyboardEventResult', () => { let mockedEditor: IContentModelEditor; @@ -38,7 +37,7 @@ describe('handleKeyboardEventResult', () => { spyOn(normalizeContentModel, 'normalizeContentModel'); }); - it('DeleteResult.SingleChar', () => { + it('singleChar', () => { const mockedModel = 'MODEL' as any; const which = 'WHICH' as any; (mockedEvent).which = which; @@ -51,7 +50,7 @@ describe('handleKeyboardEventResult', () => { mockedEditor, mockedModel, mockedEvent, - DeleteResult.SingleChar, + 'singleChar', context ); @@ -64,9 +63,10 @@ describe('handleKeyboardEventResult', () => { rawEvent: mockedEvent, }); expect(context.skipUndoSnapshot).toBeTrue(); + expect(context.clearModelCache).toBeFalsy(); }); - it('DeleteResult.NotDeleted', () => { + it('notDeleted', () => { const mockedModel = 'MODEL' as any; const context: FormatWithContentModelContext = { newEntities: [], @@ -77,7 +77,7 @@ describe('handleKeyboardEventResult', () => { mockedEditor, mockedModel, mockedEvent, - DeleteResult.NotDeleted, + 'notDeleted', context ); @@ -88,9 +88,10 @@ describe('handleKeyboardEventResult', () => { expect(cacheContentModel).not.toHaveBeenCalledWith(null); expect(triggerPluginEvent).not.toHaveBeenCalled(); expect(context.skipUndoSnapshot).toBeTrue(); + expect(context.clearModelCache).toBeTruthy(); }); - it('DeleteResult.Range', () => { + it('range', () => { const mockedModel = 'MODEL' as any; const context: FormatWithContentModelContext = { newEntities: [], @@ -101,7 +102,7 @@ describe('handleKeyboardEventResult', () => { mockedEditor, mockedModel, mockedEvent, - DeleteResult.Range, + 'range', context ); @@ -114,9 +115,10 @@ describe('handleKeyboardEventResult', () => { rawEvent: mockedEvent, }); expect(context.skipUndoSnapshot).toBeFalse(); + expect(context.clearModelCache).toBeFalsy(); }); - it('DeleteResult.NothingToDelete', () => { + it('nothingToDelete', () => { const mockedModel = 'MODEL' as any; const context: FormatWithContentModelContext = { newEntities: [], @@ -127,7 +129,7 @@ describe('handleKeyboardEventResult', () => { mockedEditor, mockedModel, mockedEvent, - DeleteResult.NothingToDelete, + 'nothingToDelete', context ); @@ -138,6 +140,7 @@ describe('handleKeyboardEventResult', () => { expect(cacheContentModel).not.toHaveBeenCalled(); expect(triggerPluginEvent).not.toHaveBeenCalled(); expect(context.skipUndoSnapshot).toBeTrue(); + expect(context.clearModelCache).toBeFalsy(); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/keyboardDeleteTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts similarity index 84% rename from packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/keyboardDeleteTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts index 0cb566e5f5f..902d3502c8b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/keyboardDeleteTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/keyboardDeleteTest.ts @@ -1,25 +1,21 @@ -import * as deleteSelection from '../../../lib/modelApi/edit/deleteSelection'; -import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; -import * as handleKeyboardEventResult from '../../../lib/editor/utils/handleKeyboardEventCommon'; -import keyboardDelete from '../../../lib/publicApi/editing/keyboardDelete'; -import { ChangeSource } from '../../../lib/publicTypes/event/ContentModelContentChangedEvent'; +import * as deleteSelection from 'roosterjs-content-model-core/lib/publicApi/selection/deleteSelection'; +import * as handleKeyboardEventResult from '../../lib/edit/handleKeyboardEventCommon'; +import { ChangeSource } from 'roosterjs-content-model-core'; import { ContentModelDocument, DOMSelection } from 'roosterjs-content-model-types'; -import { deleteAllSegmentBefore } from '../../../lib/modelApi/edit/deleteSteps/deleteAllSegmentBefore'; +import { deleteAllSegmentBefore } from '../../lib/edit/deleteSteps/deleteAllSegmentBefore'; +import { DeleteResult, DeleteSelectionStep } from 'roosterjs-content-model-types'; import { editingTestCommon } from './editingTestCommon'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; +import { keyboardDelete } from '../../lib/edit/keyboardDelete'; import { Keys } from 'roosterjs-editor-types'; import { backwardDeleteWordSelection, forwardDeleteWordSelection, -} from '../../../lib/modelApi/edit/deleteSteps/deleteWordSelection'; -import { - DeleteResult, - DeleteSelectionStep, -} from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; +} from '../../lib/edit/deleteSteps/deleteWordSelection'; import { backwardDeleteCollapsedSelection, forwardDeleteCollapsedSelection, -} from '../../../lib/modelApi/edit/deleteSteps/deleteCollapsedSelection'; +} from '../../lib/edit/deleteSteps/deleteCollapsedSelection'; describe('keyboardDelete', () => { let deleteSelectionSpy: jasmine.Spy; @@ -34,6 +30,7 @@ describe('keyboardDelete', () => { expectedResult: ContentModelDocument, expectedSteps: DeleteSelectionStep[], expectedDelete: DeleteResult, + expectedClearModelCache: boolean, calledTimes: number ) { deleteSelectionSpy.and.returnValue({ @@ -49,7 +46,7 @@ describe('keyboardDelete', () => { let editor: any; editingTestCommon( - 'handleBackspaceKey', + key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey', newEditor => { editor = newEditor; @@ -74,6 +71,7 @@ describe('keyboardDelete', () => { rawEvent: mockedEvent, newImages: [], skipUndoSnapshot: true, + clearModelCache: expectedClearModelCache, }); } @@ -89,7 +87,8 @@ describe('keyboardDelete', () => { blocks: [], }, [null!, null!, forwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -106,7 +105,8 @@ describe('keyboardDelete', () => { blocks: [], }, [null!, null!, backwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -125,7 +125,8 @@ describe('keyboardDelete', () => { blocks: [], }, [null!, forwardDeleteWordSelection, forwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -144,7 +145,8 @@ describe('keyboardDelete', () => { blocks: [], }, [null!, backwardDeleteWordSelection, backwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -163,7 +165,8 @@ describe('keyboardDelete', () => { blocks: [], }, [null!, null!, forwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -182,7 +185,8 @@ describe('keyboardDelete', () => { blocks: [], }, [deleteAllSegmentBefore, null!, backwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -223,7 +227,8 @@ describe('keyboardDelete', () => { ], }, [null!, null!, forwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -264,7 +269,8 @@ describe('keyboardDelete', () => { ], }, [null!, null!, backwardDeleteCollapsedSelection], - DeleteResult.NotDeleted, + 'notDeleted', + true, 0 ); }); @@ -315,7 +321,8 @@ describe('keyboardDelete', () => { ], }, [null!, null!, forwardDeleteCollapsedSelection], - DeleteResult.SingleChar, + 'singleChar', + false, 1 ); }); @@ -366,17 +373,17 @@ describe('keyboardDelete', () => { ], }, [null!, null!, backwardDeleteCollapsedSelection], - DeleteResult.SingleChar, + 'singleChar', + false, 1 ); }); - it('Check parameter of formatWithContentModel, forward', () => { - const spy = spyOn(formatWithContentModel, 'formatWithContentModel'); - const addUndoSnapshot = jasmine.createSpy('addUndoSnapshot'); + it('Check parameter of formatContentModel, forward', () => { + const spy = jasmine.createSpy('formatContentModel'); const editor = ({ - addUndoSnapshot, + formatContentModel: spy, getDOMSelection: () => ({ type: 'range', range: { collapsed: false }, @@ -389,18 +396,17 @@ describe('keyboardDelete', () => { keyboardDelete(editor, event); - expect(spy.calls.argsFor(0)[0]).toBe(editor); - expect(spy.calls.argsFor(0)[1]).toBe('handleDeleteKey'); - expect(addUndoSnapshot).not.toHaveBeenCalled(); - expect(spy.calls.argsFor(0)[3]?.changeSource).toBe(ChangeSource.Keyboard); - expect(spy.calls.argsFor(0)[3]?.getChangeData?.()).toBe(Keys.DELETE); + expect(spy.calls.argsFor(0)[1]!.changeSource).toBe(ChangeSource.Keyboard); + expect(spy.calls.argsFor(0)[1]!.getChangeData?.()).toBe(Keys.DELETE); + expect(spy.calls.argsFor(0)[1]!.apiName).toBe('handleDeleteKey'); }); it('Check parameter of formatWithContentModel, backward', () => { - const spy = spyOn(formatWithContentModel, 'formatWithContentModel'); + const spy = jasmine.createSpy('formatContentModel'); const preventDefault = jasmine.createSpy('preventDefault'); const editor = { + formatContentModel: spy, getDOMSelection: () => ({ type: 'range', range: { collapsed: false }, @@ -415,14 +421,14 @@ describe('keyboardDelete', () => { keyboardDelete(editor, event); - expect(spy.calls.argsFor(0)[0]).toBe(editor); - expect(spy.calls.argsFor(0)[1]).toBe('handleBackspaceKey'); - expect(spy.calls.argsFor(0)[3]?.changeSource).toBe(ChangeSource.Keyboard); - expect(spy.calls.argsFor(0)[3]?.getChangeData?.()).toBe(which); + expect(spy.calls.argsFor(0)[1]!.apiName).toBe('handleBackspaceKey'); + expect(spy.calls.argsFor(0)[1]?.changeSource).toBe(ChangeSource.Keyboard); + expect(spy.calls.argsFor(0)[1]?.getChangeData?.()).toBe(which); }); it('No need to delete - Backspace', () => { const rawEvent = { key: 'Backspace' } as any; + const formatWithContentModelSpy = jasmine.createSpy('formatContentModel'); const range: DOMSelection = { type: 'range', range: ({ @@ -432,9 +438,9 @@ describe('keyboardDelete', () => { } as any) as Range, }; const editor = { + formatContentModel: formatWithContentModelSpy, getDOMSelection: () => range, } as any; - const formatWithContentModelSpy = spyOn(formatWithContentModel, 'formatWithContentModel'); const result = keyboardDelete(editor, rawEvent); @@ -444,6 +450,7 @@ describe('keyboardDelete', () => { it('No need to delete - Delete', () => { const rawEvent = { key: 'Delete' } as any; + const formatWithContentModelSpy = jasmine.createSpy('formatContentModel'); const range: DOMSelection = { type: 'range', range: ({ @@ -453,9 +460,9 @@ describe('keyboardDelete', () => { } as any) as Range, }; const editor = { + formatContentModel: formatWithContentModelSpy, getDOMSelection: () => range, } as any; - const formatWithContentModelSpy = spyOn(formatWithContentModel, 'formatWithContentModel'); const result = keyboardDelete(editor, rawEvent); @@ -465,6 +472,7 @@ describe('keyboardDelete', () => { it('Backspace from the beginning', () => { const rawEvent = { key: 'Backspace' } as any; + const formatWithContentModelSpy = jasmine.createSpy('formatContentModel'); const range: DOMSelection = { type: 'range', range: ({ @@ -475,9 +483,9 @@ describe('keyboardDelete', () => { }; const editor = { + formatContentModel: formatWithContentModelSpy, getDOMSelection: () => range, } as any; - const formatWithContentModelSpy = spyOn(formatWithContentModel, 'formatWithContentModel'); const result = keyboardDelete(editor, rawEvent); @@ -487,6 +495,7 @@ describe('keyboardDelete', () => { it('Delete from the last', () => { const rawEvent = { key: 'Delete' } as any; + const formatWithContentModelSpy = jasmine.createSpy('formatContentModel'); const range: DOMSelection = { type: 'range', range: ({ @@ -497,9 +506,9 @@ describe('keyboardDelete', () => { }; const editor = { + formatContentModel: formatWithContentModelSpy, getDOMSelection: () => range, } as any; - const formatWithContentModelSpy = spyOn(formatWithContentModel, 'formatWithContentModel'); const result = keyboardDelete(editor, rawEvent); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/block/getLeafSiblingBlockTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/edit/utils/getLeafSiblingBlockTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/modelApi/block/getLeafSiblingBlockTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/edit/utils/getLeafSiblingBlockTest.ts index 0e0498f6dcd..1a8d1154a21 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/block/getLeafSiblingBlockTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/edit/utils/getLeafSiblingBlockTest.ts @@ -1,4 +1,4 @@ -import { getLeafSiblingBlock } from '../../../lib/modelApi/block/getLeafSiblingBlock'; +import { getLeafSiblingBlock } from '../../../lib/edit/utils/getLeafSiblingBlock'; import { createContentModelDocument, createFormatContainer, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelPastePluginTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelPastePluginTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index cdf8cf512af..b9d35c35a6d 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -1,15 +1,15 @@ -import * as addParser from '../../../lib/editor/plugins/PastePlugin/utils/addParser'; +import * as addParser from '../../lib/paste/utils/addParser'; import * as chainSanitizerCallbackFile from 'roosterjs-editor-dom/lib/htmlSanitizer/chainSanitizerCallback'; -import * as ExcelFile from '../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; -import * as getPasteSource from '../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import * as PowerPointFile from '../../../lib/editor/plugins/PastePlugin/PowerPoint/processPastedContentFromPowerPoint'; -import * as setProcessor from '../../../lib/editor/plugins/PastePlugin/utils/setProcessor'; -import * as WacFile from '../../../lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents'; -import * as WordDesktopFile from '../../../lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop'; -import ContentModelBeforePasteEvent from '../../../lib/publicTypes/event/ContentModelBeforePasteEvent'; -import ContentModelPastePlugin from '../../../lib/editor/plugins/PastePlugin/ContentModelPastePlugin'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; -import { PastePropertyNames } from '../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/constants'; +import * as ExcelFile from '../../lib/paste/Excel/processPastedContentFromExcel'; +import * as getPasteSource from '../../lib/paste/pasteSourceValidations/getPasteSource'; +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 * as WordDesktopFile from '../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; +import { ContentModelBeforePasteEvent } 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 = 'mock'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/deprecatedColorParserTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/deprecatedColorParserTest.ts similarity index 88% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/deprecatedColorParserTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/deprecatedColorParserTest.ts index 23fe6db6960..55e8efa4263 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/deprecatedColorParserTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/deprecatedColorParserTest.ts @@ -1,4 +1,4 @@ -import { deprecatedBorderColorParser } from '../../../../lib/editor/plugins/PastePlugin/utils/deprecatedColorParser'; +import { deprecatedBorderColorParser } from '../../lib/paste/utils/deprecatedColorParser'; const DeprecatedColors: string[] = [ 'activeborder', diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromExcelOnlineTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromExcelOnlineTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts index 786f8263b4a..55e5a6a68fd 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromExcelOnlineTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelOnlineTest.ts @@ -1,9 +1,9 @@ -import * as processPastedContentFromExcel from '../../../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; -import paste from '../../../../../lib/publicApi/utils/paste'; +import * as processPastedContentFromExcel from '../../../lib/paste/Excel/processPastedContentFromExcel'; import { ClipboardData } from 'roosterjs-editor-types'; import { expectEqual, initEditor } from './testUtils'; -import { IContentModelEditor } from '../../../../../lib/publicTypes/IContentModelEditor'; +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_From_ExcelOnline_E2E'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromExcelTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromExcelTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index 3c79c0625bd..dfd1cc6ad7b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -1,10 +1,10 @@ -import * as processPastedContentFromExcel from '../../../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; -import paste from '../../../../../lib/publicApi/utils/paste'; +import * as processPastedContentFromExcel from '../../../lib/paste/Excel/processPastedContentFromExcel'; import { Browser } from 'roosterjs-editor-dom'; import { ClipboardData } from 'roosterjs-editor-types'; import { expectEqual, initEditor } from './testUtils'; -import { IContentModelEditor } from '../../../../../lib/publicTypes/IContentModelEditor'; +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_From_Excel_E2E'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromWacTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts index cc5eddaedb3..9f2f0391918 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWacTest.ts @@ -1,9 +1,9 @@ -import * as processPastedContentWacComponents from '../../../../../lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents'; -import paste from '../../../../../lib/publicApi/utils/paste'; +import * as processPastedContentWacComponents from '../../../lib/paste/WacComponents/processPastedContentWacComponents'; import { ClipboardData } from 'roosterjs-editor-types'; import { DomToModelOption } from 'roosterjs-content-model-types'; import { expectEqual, initEditor } from './testUtils'; -import { IContentModelEditor } from '../../../../../lib/publicTypes/IContentModelEditor'; +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'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromWordTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromWordTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts index 7a44986901e..ba61bd45854 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteFromWordTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromWordTest.ts @@ -1,10 +1,9 @@ -import * as wordFile from '../../../../../lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop'; -import paste from '../../../../../lib/publicApi/utils/paste'; +import * as wordFile from '../../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { ClipboardData } from 'roosterjs-editor-types'; -import { cloneModel } from '../../../../../lib/modelApi/common/cloneModel'; +import { cloneModel, paste } from 'roosterjs-content-model-core'; import { DomToModelOption } from 'roosterjs-content-model-types'; import { expectEqual, initEditor } from './testUtils'; -import { IContentModelEditor } from '../../../../../lib/publicTypes/IContentModelEditor'; +import { IContentModelEditor } from 'roosterjs-content-model-editor'; import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; import { tableProcessor } from 'roosterjs-content-model-dom'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts index 0e2de9639cf..387cfe53e97 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/cmPasteTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/cmPasteTest.ts @@ -1,10 +1,10 @@ -import * as wordFile from '../../../../../lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop'; -import paste from '../../../../../lib/publicApi/utils/paste'; +import * as wordFile from '../../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { ClipboardData } from 'roosterjs-editor-types'; import { DomToModelOption } from 'roosterjs-content-model-types'; import { expectEqual, initEditor } from './testUtils'; -import { IContentModelEditor } from '../../../../../lib/publicTypes/IContentModelEditor'; +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'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/testUtils.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts similarity index 70% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/testUtils.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts index a1e3f1d7d77..025c0d7882e 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/e2e/testUtils.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/e2e/testUtils.ts @@ -1,13 +1,13 @@ -import ContentModelEditor from '../../../../../lib/editor/ContentModelEditor'; -import ContentModelPastePlugin from '../../../../../lib/editor/plugins/PastePlugin/ContentModelPastePlugin'; -import { cloneModel } from '../../../../../lib/modelApi/common/cloneModel'; +import { cloneModel } from 'roosterjs-content-model-core'; import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { ContentModelPastePlugin } from '../../../lib/paste/ContentModelPastePlugin'; import { ContentModelEditorOptions, + ContentModelEditor, IContentModelEditor, -} from '../../../../../lib/publicTypes/IContentModelEditor'; +} from 'roosterjs-content-model-editor'; -export function initEditor(id: string) { +export function initEditor(id: string): IContentModelEditor { let node = document.createElement('div'); node.id = id; document.body.insertBefore(node, document.body.childNodes[0]); @@ -26,7 +26,7 @@ export function initEditor(id: string) { let editor = new ContentModelEditor(node as HTMLDivElement, options); - return editor as IContentModelEditor; + return editor; } export function expectEqual(model1: ContentModelDocument, model2: ContentModelDocument) { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/linkParserTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/linkParserTest.ts index f55d8ea8888..a1be1151bf5 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/linkParserTest.ts @@ -1,6 +1,6 @@ import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createBeforePasteEventMock } from './processPastedContentFromWordDesktopTest'; -import { parseLink } from '../../../../lib/editor/plugins/PastePlugin/utils/linkParser'; +import { parseLink } from '../../lib/paste/utils/linkParser'; import { contentModelToDom, createDomToModelContext, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/documentContainWacElementsTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/documentContainWacElementsTest.ts similarity index 91% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/documentContainWacElementsTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/documentContainWacElementsTest.ts index 8075be62b34..e499ab2dc66 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/documentContainWacElementsTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/documentContainWacElementsTest.ts @@ -1,5 +1,5 @@ -import { documentContainWacElements } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/documentContainWacElements'; -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; +import { documentContainWacElements } from '../../../lib/paste/pasteSourceValidations/documentContainWacElements'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; import { getWacElement } from './pasteTestUtils'; describe('documentContainWacElements |', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/getPasteSourceTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts similarity index 94% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/getPasteSourceTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts index 67baf9f23f2..d1d82fc682f 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/getPasteSourceTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/getPasteSourceTest.ts @@ -1,6 +1,6 @@ import { BeforePasteEvent, ClipboardData } from 'roosterjs-editor-types'; -import { getPasteSource } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import { PastePropertyNames } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/constants'; +import { getPasteSource } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; +import { PastePropertyNames } from '../../../lib/paste/pasteSourceValidations/constants'; import { EXCEL_ATTRIBUTE_VALUE, getWacElement, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts similarity index 83% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts index 8a338712815..ef93fabee8c 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isExcelDesktopDocumentTest.ts @@ -1,6 +1,6 @@ import { EXCEL_ATTRIBUTE_VALUE } from './pasteTestUtils'; -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import { isExcelDesktopDocument } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/isExcelDesktopDocument'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; +import { isExcelDesktopDocument } from '../../../lib/paste/pasteSourceValidations/isExcelDesktopDocument'; const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts similarity index 79% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts index 50bab7a70ad..f44a719c38a 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isExcelOnlineDocumentTest.ts @@ -1,6 +1,6 @@ import { EXCEL_ATTRIBUTE_VALUE } from './pasteTestUtils'; -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import { isExcelOnlineDocument } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/isExcelOnlineDocument'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; +import { isExcelOnlineDocument } from '../../../lib/paste/pasteSourceValidations/isExcelOnlineDocument'; const EXCEL_ONLINE_ATTRIBUTE_VALUE = 'Excel.Sheet'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts similarity index 71% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts index 4aec82dae22..16471e31f03 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isGoogleSheetDocumentTest.ts @@ -1,7 +1,7 @@ -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; import { getWacElement } from './pasteTestUtils'; -import { isGoogleSheetDocument } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/isGoogleSheetDocument'; -import { PastePropertyNames } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/constants'; +import { isGoogleSheetDocument } from '../../../lib/paste/pasteSourceValidations/isGoogleSheetDocument'; +import { PastePropertyNames } from '../../../lib/paste/pasteSourceValidations/constants'; describe('isGoogleSheetDocument |', () => { it('Is from Google Sheets', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts similarity index 68% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts index d9f1180db46..194f1d2a539 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isPowerPointDesktopDocumentTest.ts @@ -1,5 +1,5 @@ -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import { isPowerPointDesktopDocument } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/isPowerPointDesktopDocument'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; +import { isPowerPointDesktopDocument } from '../../../lib/paste/pasteSourceValidations/isPowerPointDesktopDocument'; import { POWERPOINT_ATTRIBUTE_VALUE } from './pasteTestUtils'; describe('isPowerPointDesktopDocument |', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts similarity index 82% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts index e6414c4830b..7fe63ffea12 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/isWordDesktopDocumentTest.ts @@ -1,5 +1,5 @@ -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import { isWordDesktopDocument } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/isWordDesktopDocument'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; +import { isWordDesktopDocument } from '../../../lib/paste/pasteSourceValidations/isWordDesktopDocument'; import { WORD_ATTRIBUTE_VALUE } from './pasteTestUtils'; const WORD_PROG_ID = 'Word.Document'; diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/pasteTestUtils.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/pasteTestUtils.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/pasteTestUtils.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/pasteTestUtils.ts diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts similarity index 82% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts index 5efba972efa..df6b78663c3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/pasteSourceValidations/shouldConvertToSingleImageTest.ts @@ -1,6 +1,6 @@ import { ClipboardData } from 'roosterjs-editor-types'; -import { GetSourceInputParams } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/getPasteSource'; -import { shouldConvertToSingleImage } from '../../../../../lib/editor/plugins/PastePlugin/pasteSourceValidations/shouldConvertToSingleImage'; +import { GetSourceInputParams } from '../../../lib/paste/pasteSourceValidations/getPasteSource'; +import { shouldConvertToSingleImage } from '../../../lib/paste/pasteSourceValidations/shouldConvertToSingleImage'; describe('shouldConvertToSingleImage |', () => { it('Is Single Image', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts similarity index 98% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts index 8d73b24ae72..ecd95a2316d 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromExcelTest.ts @@ -1,8 +1,8 @@ -import * as PastePluginFile from '../../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; +import * as PastePluginFile from '../../lib/paste/Excel/processPastedContentFromExcel'; import { Browser } from 'roosterjs-editor-dom'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { createBeforePasteEventMock } from './processPastedContentFromWordDesktopTest'; -import { processPastedContentFromExcel } from '../../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; +import { processPastedContentFromExcel } from '../../lib/paste/Excel/processPastedContentFromExcel'; import { contentModelToDom, createDomToModelContext, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromPowerPointTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts similarity index 96% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromPowerPointTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts index d945fc8adc5..db4976d67b7 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromPowerPointTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromPowerPointTest.ts @@ -1,6 +1,6 @@ import * as moveChildNodes from 'roosterjs-content-model-dom/lib/domUtils/moveChildNodes'; import { createDefaultHtmlSanitizerOptions } from 'roosterjs-editor-dom'; -import { processPastedContentFromPowerPoint } from '../../../../lib/editor/plugins/PastePlugin/PowerPoint/processPastedContentFromPowerPoint'; +import { processPastedContentFromPowerPoint } from '../../lib/paste/PowerPoint/processPastedContentFromPowerPoint'; import { BeforePasteEvent, ClipboardData, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index 7f91c7c4c1c..e505f48d5a0 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -2,11 +2,12 @@ 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 { processPastedContentWacComponents } from '../../../../lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents'; +import { processPastedContentWacComponents } from '../../lib/paste/WacComponents/processPastedContentWacComponents'; import { listItemMetadataApplier, listLevelMetadataApplier, -} from '../../../../lib/domUtils/metadata/updateListMetadata'; +} from 'roosterjs-content-model-core/lib/metadata/updateListMetadata'; + import { contentModelToDom, createDomToModelContext, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts similarity index 99% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts index ee44ce2be0b..8c063b74d74 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/processPastedContentFromWordDesktopTest.ts @@ -1,12 +1,11 @@ -import ContentModelBeforePasteEvent from '../../../../lib/publicTypes/event/ContentModelBeforePasteEvent'; import { ClipboardData, PluginEventType } from 'roosterjs-editor-types'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { ContentModelBeforePasteEvent, ContentModelDocument } from 'roosterjs-content-model-types'; import { expectHtml } from 'roosterjs-editor-api/test/TestHelper'; -import { processPastedContentFromWordDesktop } from '../../../../lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop'; +import { processPastedContentFromWordDesktop } from '../../lib/paste/WordDesktop/processPastedContentFromWordDesktop'; import { listItemMetadataApplier, listLevelMetadataApplier, -} from '../../../../lib/domUtils/metadata/updateListMetadata'; +} from 'roosterjs-content-model-core/lib/metadata/updateListMetadata'; import { contentModelToDom, createDomToModelContext, diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/utils/getStylesTest.ts b/packages-content-model/roosterjs-content-model-plugins/test/paste/utils/getStylesTest.ts similarity index 92% rename from packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/utils/getStylesTest.ts rename to packages-content-model/roosterjs-content-model-plugins/test/paste/utils/getStylesTest.ts index 8b684b5742c..921655d20a4 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/utils/getStylesTest.ts +++ b/packages-content-model/roosterjs-content-model-plugins/test/paste/utils/getStylesTest.ts @@ -1,4 +1,4 @@ -import { getStyles } from '../../../../../lib/editor/plugins/PastePlugin/utils/getStyles'; +import { getStyles } from '../../../lib/paste/utils/getStyles'; describe('getStyles', () => { function runTest(style: string, expected: Record) { 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 new file mode 100644 index 00000000000..c5911a87ac3 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IStandaloneEditor.ts @@ -0,0 +1,139 @@ +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'; +import type { DomToModelOption } from '../context/DomToModelOption'; +import type { EditorEnvironment } from '../parameter/EditorEnvironment'; +import type { ModelToDomOption } from '../context/ModelToDomOption'; +import type { OnNodeCreated } from '../context/ModelToDomSettings'; +import type { + ContentModelFormatter, + FormatWithContentModelOptions, +} from '../parameter/FormatWithContentModelOptions'; +import type { + EditorUndoState, + PluginEventData, + PluginEventFromType, + PluginEventType, +} from 'roosterjs-editor-types'; + +/** + * An interface of standalone Content Model editor. + * (This interface is still under development, and may still be changed in the future with some breaking changes) + */ +export interface IStandaloneEditor { + /** + * Create Content Model from DOM tree in this editor + * @param rootNode Optional start node. If provided, Content Model will be created from this node (including itself), + * otherwise it will create Content Model for the whole content in editor. + * @param option The options to customize the behavior of DOM to Content Model conversion + * @param selectionOverride When specified, use this selection to override existing selection inside editor + */ + createContentModel( + option?: DomToModelOption, + selectionOverride?: DOMSelection + ): ContentModelDocument; + + /** + * 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; + + /** + * Get current running environment, such as if editor is running on Mac + */ + getEnvironment(): EditorEnvironment; + + /** + * Get current DOM selection. + * This is the replacement of IEditor.getSelectionRangeEx. + */ + getDOMSelection(): DOMSelection | null; + + /** + * Set DOMSelection into editor content. + * This is the replacement of IEditor.select. + * @param selection The selection to set + */ + setDOMSelection(selection: DOMSelection): void; + + /** + * 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; + + /** + * Get pending format of editor if any, or return null + */ + 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 + */ + isDisposed(): boolean; + + /** + * Get document which contains this editor + * @returns The HTML document which contains this editor + */ + getDocument(): Document; + + /** + * Focus to this editor, the selection was restored to where it was before, no unexpected scroll. + */ + focus(): void; + + /** + * 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 + ): PluginEventFromType; + + /** + * Whether there is an available undo/redo snapshot + */ + getUndoState(): EditorUndoState; + + /** + * Check if the editor is in dark mode + * @returns True if the editor is in dark mode, otherwise false + */ + isDarkMode(): boolean; + + /** + * 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; + + //#endregion +} 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 new file mode 100644 index 00000000000..8be1e5cf3d1 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorCore.ts @@ -0,0 +1,178 @@ +import type { EditorCore, SwitchShadowEdit } from 'roosterjs-editor-types'; +import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelPluginState } from '../pluginState/ContentModelPluginState'; +import type { DOMSelection } from '../selection/DOMSelection'; +import type { DomToModelOption } from '../context/DomToModelOption'; +import type { DomToModelSettings } from '../context/DomToModelSettings'; +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 { + ContentModelFormatter, + FormatWithContentModelOptions, +} from '../parameter/FormatWithContentModelOptions'; + +/** + * Create a EditorContext object used by ContentModel API + * @param core The StandaloneEditorCore object + */ +export type CreateEditorContext = (core: StandaloneEditorCore & EditorCore) => EditorContext; + +/** + * Create Content Model from DOM tree in this editor + * @param core The StandaloneEditorCore object + * @param option The option to customize the behavior of DOM to Content Model conversion + * @param selectionOverride When passed, use this selection range instead of current selection in editor + */ +export type CreateContentModel = ( + core: StandaloneEditorCore & EditorCore, + option?: DomToModelOption, + selectionOverride?: DOMSelection +) => ContentModelDocument; + +/** + * Get current DOM selection from editor + * @param core The StandaloneEditorCore object + */ +export type GetDOMSelection = (core: StandaloneEditorCore & EditorCore) => DOMSelection | null; + +/** + * Set content with content model. This is the replacement of core API getSelectionRangeEx + * @param core The StandaloneEditorCore object + * @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 + */ +export type SetContentModel = ( + core: StandaloneEditorCore & EditorCore, + model: ContentModelDocument, + option?: ModelToDomOption, + onNodeCreated?: OnNodeCreated +) => DOMSelection | null; + +/** + * Set current DOM selection from editor. This is the replacement of core API select + * @param core The StandaloneEditorCore object + * @param selection The selection to set + */ +export type SetDOMSelection = ( + core: StandaloneEditorCore & EditorCore, + selection: DOMSelection +) => void; + +/** + * 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 core The StandaloneEditorCore object + * @param formatter Formatter function, see ContentModelFormatter + * @param options More options, see FormatWithContentModelOptions + */ +export type FormatContentModel = ( + core: StandaloneEditorCore & EditorCore, + formatter: ContentModelFormatter, + options?: FormatWithContentModelOptions +) => void; + +/** + * 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 { + /** + * Create a EditorContext object used by ContentModel API + * @param core The StandaloneEditorCore object + */ + createEditorContext: CreateEditorContext; + + /** + * Create Content Model from DOM tree in this editor + * @param core The StandaloneEditorCore object + * @param option The option to customize the behavior of DOM to Content Model conversion + */ + createContentModel: CreateContentModel; + + /** + * Get current DOM selection from editor + * @param core The StandaloneEditorCore object + */ + getDOMSelection: GetDOMSelection; + + /** + * Set content with content model + * @param core The StandaloneEditorCore object + * @param model The content model to set + * @param option Additional options to customize the behavior of Content Model to DOM conversion + */ + setContentModel: SetContentModel; + + /** + * Set current DOM selection from editor. This is the replacement of core API select + * @param core The StandaloneEditorCore object + * @param selection The selection to set + */ + setDOMSelection: SetDOMSelection; + + /** + * 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 core The StandaloneEditorCore object + * @param formatter Formatter function, see ContentModelFormatter + * @param options More options, see FormatWithContentModelOptions + */ + formatContentModel: FormatContentModel; + + // TODO: This is copied from legacy editor core, will be ported to use new types later + switchShadowEdit: SwitchShadowEdit; +} + +/** + * Represents the core data structure of a Content Model editor + */ +export interface StandaloneEditorCore extends ContentModelPluginState { + /** + * The content DIV element of this editor + */ + readonly contentDiv: HTMLDivElement; + + /** + * Core API map of this editor + */ + readonly api: StandaloneCoreApiMap; + + /** + * 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: StandaloneCoreApiMap; + + /** + * Default DOM to Content Model options + */ + defaultDomToModelOptions: (DomToModelOption | undefined)[]; + + /** + * Default Content Model to DOM options + */ + defaultModelToDomOptions: (ModelToDomOption | undefined)[]; + + /** + * Default DOM to Content Model config, calculated from defaultDomToModelOptions, + * will be used for creating content model if there is no other customized options + */ + defaultDomToModelConfig: DomToModelSettings; + + /** + * Default Content Model to DOM config, calculated from defaultModelToDomOptions, + * will be used for setting content model if there is no other customized options + */ + defaultModelToDomConfig: ModelToDomSettings; + + /** + * Editor running environment + */ + environment: EditorEnvironment; +} 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 new file mode 100644 index 00000000000..3f17c706d55 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/StandaloneEditorOptions.ts @@ -0,0 +1,22 @@ +import type { DomToModelOption } from '../context/DomToModelOption'; +import type { ModelToDomOption } from '../context/ModelToDomOption'; + +/** + * Options for Content Model editor + */ +export interface StandaloneEditorOptions { + /** + * Default options used for DOM to Content Model conversion + */ + defaultDomToModelOptions?: DomToModelOption; + + /** + * Default options used for Content Model to DOM conversion + */ + defaultModelToDomOptions?: ModelToDomOption; + + /** + * Reuse existing DOM structure if possible, and update the model when content or selection is changed + */ + cacheModel?: boolean; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/enum/BorderOperations.ts b/packages-content-model/roosterjs-content-model-types/lib/enum/BorderOperations.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/enum/BorderOperations.ts rename to packages-content-model/roosterjs-content-model-types/lib/enum/BorderOperations.ts diff --git a/packages-content-model/roosterjs-content-model-types/lib/enum/DeleteResult.ts b/packages-content-model/roosterjs-content-model-types/lib/enum/DeleteResult.ts new file mode 100644 index 00000000000..50c4e68abdb --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/enum/DeleteResult.ts @@ -0,0 +1,23 @@ +/** + * Delete selection result + */ +export type DeleteResult = + /** + * Content Model could not finish deletion, need to let browser handle it + */ + | 'notDeleted' + + /** + * Deleted a single char, no need to let browser keep handling + */ + | 'singleChar' + + /** + * Deleted a range, no need to let browser keep handling + */ + | 'range' + + /** + * There is nothing to delete, no need to let browser keep handling + */ + | 'nothingToDelete'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/enum/EntityOperation.ts b/packages-content-model/roosterjs-content-model-types/lib/enum/EntityOperation.ts new file mode 100644 index 00000000000..d6a768f4dfc --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/enum/EntityOperation.ts @@ -0,0 +1,52 @@ +/** + * Define entity lifecycle related operations + */ +export type EntityLifecycleOperation = + /** + * Notify plugins that there is a new plugin was added into editor. + * Plugin can handle this event to entity hydration. + * This event will be only fired once for each entity DOM node. + * After undo, or copy/paste, since new DOM nodes were added, this event will be fired + * for those entities represented by newly added nodes. + */ + | 'newEntity' + + /** + * Notify plugins that editor is generating HTML content for save. + * Plugin should use this event to remove any temporary content, and only leave DOM nodes that + * should be saved as HTML string. + * This event will provide a cloned DOM tree for each entity, do NOT compare the DOM nodes with cached nodes + * because it will always return false. + */ + | 'replaceTemporaryContent' + /** + * Notify plugins that a new entity state need to be updated to an entity. + * This is normally happened when user undo/redo the content with an entity snapshot added by a plugin that handles entity + */ + | 'UpdateEntityState'; + +/** + * Define entity removal related operations + */ +export type EntityRemovalOperation = + /** + * Notify plugins that user is removing an entity from its start position using DELETE key + */ + | 'removeFromStart' + + /** + * Notify plugins that user is remove an entity from its end position using BACKSPACE key + */ + | 'removeFromEnd' + + /** + * Notify plugins that an entity is being overwritten. + * This can be caused by key in, cut, paste, delete, backspace ... on a selection + * which contains some entities. + */ + | 'overwrite'; + +/** + * Define possible operations to an entity + */ +export type EntityOperation = EntityLifecycleOperation | EntityRemovalOperation; diff --git a/packages-content-model/roosterjs-content-model-types/lib/enum/InsertEntityPosition.ts b/packages-content-model/roosterjs-content-model-types/lib/enum/InsertEntityPosition.ts new file mode 100644 index 00000000000..e590ad6090b --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/enum/InsertEntityPosition.ts @@ -0,0 +1,8 @@ +/** + * Define the position of the entity to insert. It can be: + * "focus": insert at current focus. If insert a block entity, it will be inserted under the paragraph where focus is + * "begin": insert at beginning of content. When insert an inline entity, it will be wrapped with a paragraph. + * "end": insert at end of content. When insert an inline entity, it will be wrapped with a paragraph. + * "root": insert at the root level of current region + */ +export type InsertEntityPosition = 'focus' | 'begin' | 'end' | 'root'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/PasteType.ts b/packages-content-model/roosterjs-content-model-types/lib/enum/PasteType.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/PasteType.ts rename to packages-content-model/roosterjs-content-model-types/lib/enum/PasteType.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/TableOperation.ts b/packages-content-model/roosterjs-content-model-types/lib/enum/TableOperation.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/TableOperation.ts rename to packages-content-model/roosterjs-content-model-types/lib/enum/TableOperation.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelBeforePasteEvent.ts b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts similarity index 85% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelBeforePasteEvent.ts rename to packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts index 742850f1489..5e28cc48be8 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/event/ContentModelBeforePasteEvent.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelBeforePasteEvent.ts @@ -1,5 +1,6 @@ +import type { DomToModelOption } from '../context/DomToModelOption'; +import type { ContentModelDocument } from '../group/ContentModelDocument'; import type { InsertPoint } from '../selection/InsertPoint'; -import type { ContentModelDocument, DomToModelOption } from 'roosterjs-content-model-types'; import type { BeforePasteEvent, BeforePasteEventData, @@ -26,7 +27,7 @@ export interface ContentModelBeforePasteEventData extends BeforePasteEventData { /** * Provides a chance for plugin to change the content before it is pasted into editor. */ -export default interface ContentModelBeforePasteEvent +export interface ContentModelBeforePasteEvent extends ContentModelBeforePasteEventData, BeforePasteEvent {} 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 new file mode 100644 index 00000000000..7c425792fcc --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/event/ContentModelContentChangedEvent.ts @@ -0,0 +1,36 @@ +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 + */ + contentModel?: ContentModelDocument; + + /** + * Selection range applied to the document + */ + selection?: DOMSelection; +} + +/** + * 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/format/formatParts/MarginFormat.ts b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/MarginFormat.ts index 7377b7f3588..36deed9dcbc 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/MarginFormat.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/MarginFormat.ts @@ -21,4 +21,14 @@ export type MarginFormat = { * Margin left value */ marginLeft?: string; + + /** + * Margin-block start value + */ + marginBlockStart?: string; + + /** + * Margin-block end value + */ + marginBlockEnd?: string; }; 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 e7d36c0362e..851d8cdd922 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -67,6 +67,28 @@ export { TableCellMetadataFormat } from './format/metadata/TableCellMetadataForm export { ContentModelBlockGroupType } from './enum/BlockGroupType'; export { ContentModelBlockType } from './enum/BlockType'; export { ContentModelSegmentType } from './enum/SegmentType'; +export { + EntityLifecycleOperation, + EntityOperation, + EntityRemovalOperation, +} from './enum/EntityOperation'; +export { + TableOperation, + TableVerticalInsertOperation, + TableHorizontalInsertOperation, + TableDeleteOperation, + TableVerticalMergeOperation, + TableHorizontalMergeOperation, + TableCellMergeOperation, + TableSplitOperation, + TableAlignOperation, + TableCellHorizontalAlignOperation, + TableCellVerticalAlignOperation, +} from './enum/TableOperation'; +export { PasteType } from './enum/PasteType'; +export { BorderOperations } from './enum/BorderOperations'; +export { DeleteResult } from './enum/DeleteResult'; +export { InsertEntityPosition } from './enum/InsertEntityPosition'; export { ContentModelBlock } from './block/ContentModelBlock'; export { ContentModelParagraph } from './block/ContentModelParagraph'; @@ -109,6 +131,8 @@ export { RangeSelection, TableSelection, } from './selection/DOMSelection'; +export { InsertPoint } from './selection/InsertPoint'; +export { TableSelectionContext } from './selection/TableSelectionContext'; export { ContentModelHandlerMap, @@ -172,3 +196,54 @@ export { Definition, } from './metadata/Definition'; export { ColorManager, Colors } from './context/ColorManager'; + +export { IStandaloneEditor } from './editor/IStandaloneEditor'; +export { StandaloneEditorOptions } from './editor/StandaloneEditorOptions'; +export { + CreateContentModel, + CreateEditorContext, + GetDOMSelection, + SetContentModel, + SetDOMSelection, + FormatContentModel, + StandaloneCoreApiMap, + StandaloneEditorCore, +} from './editor/StandaloneEditorCore'; + +export { ContentModelCachePluginState } from './pluginState/ContentModelCachePluginState'; +export { ContentModelPluginState } from './pluginState/ContentModelPluginState'; +export { + ContentModelFormatPluginState, + PendingFormat, +} from './pluginState/ContentModelFormatPluginState'; + +export { EditorEnvironment } from './parameter/EditorEnvironment'; +export { + DeletedEntity, + FormatWithContentModelContext, +} from './parameter/FormatWithContentModelContext'; +export { + FormatWithContentModelOptions, + ContentModelFormatter, +} from './parameter/FormatWithContentModelOptions'; +export { ContentModelFormatState } from './parameter/ContentModelFormatState'; +export { ImageFormatState } from './parameter/ImageFormatState'; +export { Border } from './parameter/Border'; +export { InsertEntityOptions } from './parameter/InsertEntityOptions'; +export { + DeleteSelectionContext, + DeleteSelectionResult, + DeleteSelectionStep, + ValidDeleteSelectionContext, +} from './parameter/DeleteSelectionStep'; + +export { + ContentModelBeforePasteEvent, + ContentModelBeforePasteEventData, + CompatibleContentModelBeforePasteEvent, +} from './event/ContentModelBeforePasteEvent'; +export { + ContentModelContentChangedEvent, + CompatibleContentModelContentChangedEvent, + ContentModelContentChangedEventData, +} from './event/ContentModelContentChangedEvent'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/interface/Border.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/Border.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/interface/Border.ts rename to packages-content-model/roosterjs-content-model-types/lib/parameter/Border.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/format/formatState/ContentModelFormatState.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts similarity index 97% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/format/formatState/ContentModelFormatState.ts rename to packages-content-model/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts index 09d910cd73f..e3ae48f49ef 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/format/formatState/ContentModelFormatState.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/ContentModelFormatState.ts @@ -1,4 +1,4 @@ -import type { TableMetadataFormat } from 'roosterjs-content-model-types'; +import type { TableMetadataFormat } from '../format/metadata/TableMetadataFormat'; import type { ImageFormatState } from './ImageFormatState'; /** diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts new file mode 100644 index 00000000000..84a0976f326 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/DeleteSelectionStep.ts @@ -0,0 +1,56 @@ +import type { ContentModelParagraph } from '../block/ContentModelParagraph'; +import type { DeleteResult } from '../enum/DeleteResult'; +import type { FormatWithContentModelContext } from './FormatWithContentModelContext'; +import type { InsertPoint } from '../selection/InsertPoint'; +import type { TableSelectionContext } from '../selection/TableSelectionContext'; + +/** + * Result of deleteSelection API + */ +export interface DeleteSelectionResult { + /** + * Insert point position after delete, or null if there is no insert point + */ + insertPoint: InsertPoint | null; + + /** + * Delete result + */ + deleteResult: DeleteResult; +} + +/** + * A context object used by DeleteSelectionStep + */ +export interface DeleteSelectionContext extends DeleteSelectionResult { + /** + * Last paragraph after previous step + */ + lastParagraph?: ContentModelParagraph; + + /** + * Last table context after previous step + */ + lastTableContext?: TableSelectionContext; + + /** + * Format context provided by formatContentModel API + */ + formatContext?: FormatWithContentModelContext; +} + +/** + * DeleteSelectionContext with a valid insert point that can be handled by next step + */ +export interface ValidDeleteSelectionContext extends DeleteSelectionContext { + /** + * Insert point position after delete + */ + insertPoint: InsertPoint; +} + +/** + * Represents a step function for deleteSelection API + * @param context The valid delete selection context object returned from previous step + */ +export type DeleteSelectionStep = (context: ValidDeleteSelectionContext) => void; diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/EditorEnvironment.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/EditorEnvironment.ts new file mode 100644 index 00000000000..242c2145bf8 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/EditorEnvironment.ts @@ -0,0 +1,14 @@ +/** + * Current running environment + */ +export interface EditorEnvironment { + /** + * Whether editor is running on Mac + */ + isMac?: boolean; + + /** + * Whether editor is running on Android + */ + isAndroid?: boolean; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelContext.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelContext.ts new file mode 100644 index 00000000000..8278725ae51 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelContext.ts @@ -0,0 +1,66 @@ +import type { ContentModelEntity } from '../entity/ContentModelEntity'; +import type { ContentModelImage } from '../segment/ContentModelImage'; +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import type { EntityRemovalOperation } from '../enum/EntityOperation'; + +/** + * Represents an entity that is deleted by a specified entity operation + */ +export interface DeletedEntity { + /** + * The deleted entity + */ + entity: ContentModelEntity; + + /** + * The operation that causes this entity to be deleted + */ + operation: EntityRemovalOperation; +} + +/** + * Context object for API formatWithContentModel + */ +export interface FormatWithContentModelContext { + /** + * New entities added during the format process + */ + readonly newEntities: ContentModelEntity[]; + + /** + * Entities got deleted during formatting. Need to be set by the formatter function + */ + readonly deletedEntities: DeletedEntity[]; + + /** + * Images inserted in the editor that needs to have their size adjusted + */ + readonly newImages: ContentModelImage[]; + + /** + * Raw Event that triggers this format call + */ + readonly rawEvent?: Event; + + /** + * @optional + * When pass true, skip adding undo snapshot when write Content Model back to DOM. + * Need to be set by the formatter function + */ + skipUndoSnapshot?: boolean; + + /** + * @optional + * When set to true, formatWithContentModel API will not keep cached Content Model. Next time when we need a Content Model, a new one will be created + */ + clearModelCache?: boolean; + + /** + * @optional + * Specify new pending format. + * To keep current format event selection position is changed, set this value to "preserved", editor will update pending format position to the new position + * To set a new pending format, set this property to the format object + * Otherwise, leave it there and editor will automatically decide if the original pending format is still available + */ + newPendingFormat?: ContentModelSegmentFormat | 'preserve'; +} diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelOptions.ts new file mode 100644 index 00000000000..cfcb8736a7c --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/FormatWithContentModelOptions.ts @@ -0,0 +1,52 @@ +import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { DOMSelection } from '../selection/DOMSelection'; +import type { FormatWithContentModelContext } from './FormatWithContentModelContext'; +import type { OnNodeCreated } from '../context/ModelToDomSettings'; + +/** + * Options for API formatWithContentModel + */ +export interface FormatWithContentModelOptions { + /** + * Name of the format API + */ + apiName?: string; + + /** + * Raw event object that triggers this call + */ + rawEvent?: Event; + + /** + * Change source used for triggering a ContentChanged event. @default ChangeSource.Format. + */ + changeSource?: string; + + /** + * An optional callback that will be called when a DOM node is created + * @param modelElement The related Content Model element + * @param node The node created for this model element + */ + onNodeCreated?: OnNodeCreated; + + /** + * Optional callback to get an object used for change data in ContentChangedEvent + */ + getChangeData?: () => any; + + /** + * When specified, use this selection range to override current selection inside editor + */ + selectionOverride?: DOMSelection; +} + +/** + * Type of formatter used for format Content Model. + * @param model The source Content Model to format + * @param context A context object used for pass in and out more parameters + * @returns True means the model is changed and need to write back to editor, otherwise false + */ +export type ContentModelFormatter = ( + model: ContentModelDocument, + context: FormatWithContentModelContext +) => boolean; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/format/formatState/ImageFormatState.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/ImageFormatState.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/format/formatState/ImageFormatState.ts rename to packages-content-model/roosterjs-content-model-types/lib/parameter/ImageFormatState.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/InsertEntityOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts similarity index 54% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/InsertEntityOptions.ts rename to packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts index bac29b89210..e936fd88255 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/InsertEntityOptions.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts @@ -22,12 +22,3 @@ export interface InsertEntityOptions { */ skipUndoSnapshot?: boolean; } - -/** - * Define the position of the entity to insert. It can be: - * "focus": insert at current focus. If insert a block entity, it will be inserted under the paragraph where focus is - * "begin": insert at beginning of content. When insert an inline entity, it will be wrapped with a paragraph. - * "end": insert at end of content. When insert an inline entity, it will be wrapped with a paragraph. - * "root": insert at the root level of current region - */ -export type InsertEntityPosition = 'focus' | 'begin' | 'end' | 'root'; diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelCachePluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelCachePluginState.ts similarity index 70% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelCachePluginState.ts rename to packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelCachePluginState.ts index 807d2bb7de2..b9fbc4befcb 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelCachePluginState.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelCachePluginState.ts @@ -1,8 +1,6 @@ -import type { - ContentModelDocument, - ContentModelDomIndexer, - DOMSelection, -} from 'roosterjs-content-model-types'; +import type { ContentModelDocument } from '../group/ContentModelDocument'; +import type { ContentModelDomIndexer } from '../context/ContentModelDomIndexer'; +import type { DOMSelection } from '../selection/DOMSelection'; /** * Plugin state for ContentModelEditPlugin diff --git a/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelFormatPluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelFormatPluginState.ts new file mode 100644 index 00000000000..13a058373ab --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelFormatPluginState.ts @@ -0,0 +1,36 @@ +import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; + +/** + * Pending format holder interface + */ +export interface PendingFormat { + /** + * The pending format + */ + format: ContentModelSegmentFormat; + + /** + * Container node of pending format + */ + posContainer: Node; + + /** + * Offset under container node of pending format + */ + posOffset: number; +} + +/** + * Plugin state for ContentModelFormatPlugin + */ +export interface ContentModelFormatPluginState { + /** + * Default format of this editor + */ + defaultFormat: ContentModelSegmentFormat; + + /** + * Pending format + */ + pendingFormat: PendingFormat | null; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelPluginState.ts b/packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelPluginState.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/pluginState/ContentModelPluginState.ts rename to packages-content-model/roosterjs-content-model-types/lib/pluginState/ContentModelPluginState.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/selection/InsertPoint.ts b/packages-content-model/roosterjs-content-model-types/lib/selection/InsertPoint.ts similarity index 71% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/selection/InsertPoint.ts rename to packages-content-model/roosterjs-content-model-types/lib/selection/InsertPoint.ts index 6b740d2278d..e7de09ee7d2 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/selection/InsertPoint.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/selection/InsertPoint.ts @@ -1,9 +1,7 @@ +import type { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; +import type { ContentModelParagraph } from '../block/ContentModelParagraph'; +import type { ContentModelSelectionMarker } from '../segment/ContentModelSelectionMarker'; import type { TableSelectionContext } from './TableSelectionContext'; -import type { - ContentModelBlockGroup, - ContentModelParagraph, - ContentModelSelectionMarker, -} from 'roosterjs-content-model-types'; /** * Represent all related information of an insert point diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/selection/TableSelectionContext.ts b/packages-content-model/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts similarity index 86% rename from packages-content-model/roosterjs-content-model-editor/lib/publicTypes/selection/TableSelectionContext.ts rename to packages-content-model/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts index 0cfc059f5ba..af2befb6bd8 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/selection/TableSelectionContext.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/selection/TableSelectionContext.ts @@ -1,4 +1,4 @@ -import type { ContentModelTable } from 'roosterjs-content-model-types'; +import type { ContentModelTable } from '../block/ContentModelTable'; /** * Context object for table in a selection diff --git a/packages-content-model/roosterjs-content-model-types/package.json b/packages-content-model/roosterjs-content-model-types/package.json index 632c7495fda..83f2dc53f2b 100644 --- a/packages-content-model/roosterjs-content-model-types/package.json +++ b/packages-content-model/roosterjs-content-model-types/package.json @@ -1,7 +1,9 @@ { "name": "roosterjs-content-model-types", "description": "Types for Content Model for roosterjs (Under development)", - "dependencies": {}, + "dependencies": { + "roosterjs-editor-types": "" + }, "version": "0.0.0", "main": "./lib/index.ts" } diff --git a/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts b/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts index 14d85491728..821d628f9e6 100644 --- a/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts +++ b/packages-content-model/roosterjs-content-model/lib/createContentModelEditor.ts @@ -1,4 +1,5 @@ -import { ContentModelEditor, ContentModelPastePlugin } from 'roosterjs-content-model-editor'; +import { ContentModelEditor } from 'roosterjs-content-model-editor'; +import { ContentModelEditPlugin, ContentModelPastePlugin } from 'roosterjs-content-model-plugins'; import { getDarkColor } from 'roosterjs-color-utils'; import type { EditorPlugin } from 'roosterjs-editor-types'; import type { @@ -19,11 +20,8 @@ export function createContentModelEditor( additionalPlugins?: EditorPlugin[], initialContent?: string ): IContentModelEditor { - let plugins: EditorPlugin[] = [new ContentModelPastePlugin()]; - - if (additionalPlugins) { - plugins = plugins.concat(additionalPlugins); - } + const plugins = additionalPlugins ? [...additionalPlugins] : []; + plugins.push(new ContentModelPastePlugin(), new ContentModelEditPlugin()); const options: ContentModelEditorOptions = { plugins: plugins, diff --git a/packages-content-model/roosterjs-content-model/lib/index.ts b/packages-content-model/roosterjs-content-model/lib/index.ts index 2954c80afd4..ef7a866c977 100644 --- a/packages-content-model/roosterjs-content-model/lib/index.ts +++ b/packages-content-model/roosterjs-content-model/lib/index.ts @@ -1,4 +1,7 @@ export { createContentModelEditor } from './createContentModelEditor'; export * from 'roosterjs-content-model-types'; export * from 'roosterjs-content-model-dom'; +export * from 'roosterjs-content-model-core'; +export * from 'roosterjs-content-model-api'; export * from 'roosterjs-content-model-editor'; +export * from 'roosterjs-content-model-plugins'; diff --git a/packages-content-model/roosterjs-content-model/package.json b/packages-content-model/roosterjs-content-model/package.json index b1de4e99b1b..698c3df6db1 100644 --- a/packages-content-model/roosterjs-content-model/package.json +++ b/packages-content-model/roosterjs-content-model/package.json @@ -4,11 +4,12 @@ "dependencies": { "tslib": "^2.3.1", "roosterjs-editor-types": "", - "roosterjs-editor-dom": "", - "roosterjs-editor-core": "", "roosterjs-content-model-types": "", "roosterjs-content-model-dom": "", + "roosterjs-content-model-core": "", + "roosterjs-content-model-api": "", "roosterjs-content-model-editor": "", + "roosterjs-content-model-plugins": "", "roosterjs-color-utils": "" }, "version": "0.0.0", diff --git a/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx b/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx index ba6ee5eb0ec..9de0ffd7345 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx +++ b/packages-ui/roosterjs-react/lib/contextMenu/menus/createImageEditMenuProvider.tsx @@ -1,8 +1,13 @@ import createContextMenuProvider from '../utils/createContextMenuProvider'; import showInputDialog from '../../inputDialog/utils/showInputDialog'; -import { canRegenerateImage, resetImage, resizeByPercentage } from 'roosterjs-editor-plugins'; -import { DocumentCommand, ImageEditOperation } from 'roosterjs-editor-types'; -import { safeInstanceOf } from 'roosterjs-editor-dom'; +import { + canRegenerateImage, + isResizedTo, + resetImage, + resizeByPercentage, +} from 'roosterjs-editor-plugins'; +import { DocumentCommand, ImageEditOperation, SelectionRangeTypes } from 'roosterjs-editor-types'; +import { getObjectKeys } from 'roosterjs-editor-dom'; import { setImageAltText } from 'roosterjs-editor-api'; import type ContextMenuItem from '../types/ContextMenuItem'; import type { EditorPlugin, IEditor } from 'roosterjs-editor-types'; @@ -40,6 +45,13 @@ const ImageAltTextMenuItem: ContextMenuItem = { key: 'menuNameImageResize', unlocalizedText: 'Size', @@ -49,34 +61,40 @@ const ImageResizeMenuItem: ContextMenuItem { + onClick: (key, editor, _) => { + const selection = editor.getSelectionRangeEx(); + if (selection.type !== SelectionRangeTypes.ImageSelection) { + return; + } editor.addUndoSnapshot(() => { - let percentage = 0; - switch (key) { - case 'menuNameImageSizeSmall': - percentage = 0.25; - break; - case 'menuNameImageSizeMedium': - percentage = 0.5; - break; - case 'menuNameImageSizeOriginal': - percentage = 1; - break; - } + const percentage = sizeMap[key]; - if (percentage > 0) { + if (percentage != undefined && percentage > 0) { resizeByPercentage( editor, - node as HTMLImageElement, + selection.image, percentage, 10 /*minWidth*/, 10 /*minHeight*/ ); } else { - resetImage(editor, node as HTMLImageElement); + resetImage(editor, selection.image); } }); }, + getSelectedId: (editor, _) => { + const selection = editor.getSelectionRangeEx(); + return ( + (selection.type === SelectionRangeTypes.ImageSelection && + getObjectKeys(sizeMap).find(key => { + return key == 'menuNameImageSizeBestFit' + ? !selection.image.hasAttribute('width') && + !selection.image.hasAttribute('height') + : isResizedTo(selection.image, sizeMap[key]!); + })) || + null + ); + }, }; const ImageRotateMenuItem: ContextMenuItem = { @@ -184,8 +202,9 @@ const ImageCutMenuItem: ContextMenuItem = }, }; -function shouldShowImageEditItems(editor: IEditor, node: Node) { - return safeInstanceOf(node, 'HTMLImageElement') && node.isContentEditable; +function shouldShowImageEditItems(editor: IEditor, _: Node) { + const selection = editor.getSelectionRangeEx(); + return selection.type === SelectionRangeTypes.ImageSelection && !!selection.image; } /** diff --git a/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts b/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts index b1728ac2bda..52cb0d0e204 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts +++ b/packages-ui/roosterjs-react/lib/contextMenu/types/ContextMenuItem.ts @@ -42,6 +42,14 @@ export default interface ContextMenuItem boolean; + /** + * A callback function to verify which subitem ID should have a checkmark + * @param editor The editor object that triggers this event + * @param targetNode The node that user is clicking onto + * @returns ID to be shown as selected, null for none + */ + getSelectedId?: (editor: IEditor, targetNode: Node) => TString | null; + /** * A key-value map for sub menu items, key is the key of menu item, value is unlocalized string * When click on a child item, onClick handler will be triggered with the key of the clicked child item passed in as the second parameter diff --git a/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts b/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts index 375c8e92f0e..f98737a8d7b 100644 --- a/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts +++ b/packages-ui/roosterjs-react/lib/contextMenu/utils/createContextMenuProvider.ts @@ -59,7 +59,7 @@ class ContextMenuProviderImpl .filter( item => !item.shouldShow || item.shouldShow(this.editor!, node, this.context) ) - .map(item => this.convertMenuItems(item)) + .map(item => this.convertMenuItems(item, node)) : []; } @@ -67,7 +67,11 @@ class ContextMenuProviderImpl this.uiUtilities = uiUtilities; } - private convertMenuItems(item: ContextMenuItem): IContextualMenuItem { + private convertMenuItems( + item: ContextMenuItem, + node: Node + ): IContextualMenuItem { + const selectedId = item.getSelectedId?.(this.editor!, node); return { key: item.key, data: item, @@ -85,6 +89,12 @@ class ContextMenuProviderImpl onRender: item.itemRender ? subItem => item.itemRender?.(subItem, () => this.onClick(item, key)) : undefined, + iconProps: + key == selectedId + ? { + iconName: 'Checkmark', + } + : undefined, })), ...(item.commandBarSubMenuProperties || {}), } diff --git a/packages/roosterjs-editor-api/lib/utils/toggleListType.ts b/packages/roosterjs-editor-api/lib/utils/toggleListType.ts index a502bc769c8..0f0c3572c24 100644 --- a/packages/roosterjs-editor-api/lib/utils/toggleListType.ts +++ b/packages/roosterjs-editor-api/lib/utils/toggleListType.ts @@ -1,6 +1,7 @@ import blockFormat from '../utils/blockFormat'; import { createVListFromRegion, getBlockElementAtNode } from 'roosterjs-editor-dom'; import { ExperimentalFeatures } from 'roosterjs-editor-types'; +import type { VList } from 'roosterjs-editor-dom'; import type { BulletListType, IEditor, ListType, NumberingListType } from 'roosterjs-editor-types'; import type { CompatibleBulletListType, @@ -49,6 +50,7 @@ export default function toggleListType( if (!block) { return; } + const vList = chain && end && start?.equalTo(end) ? chain.createVListAtBlock(block, startNumber) @@ -60,6 +62,9 @@ export default function toggleListType( if (vList && start && end) { vList.changeListType(start, end, listType); vList.setListStyleType(orderedStyle, unorderedStyle); + if (isNewList(vList)) { + vList.removeMargins(); + } vList.writeBack( editor.isFeatureEnabled(ExperimentalFeatures.ReuseAllAncestorListElements), editor.isFeatureEnabled(ExperimentalFeatures.DisableListChain) @@ -70,3 +75,11 @@ export default function toggleListType( apiNameOverride || 'toggleListType' ); } + +function isNewList(vList: VList | null) { + const list = vList?.rootList; + if (list) { + return list.childElementCount === 0; + } + return false; +} diff --git a/packages/roosterjs-editor-api/test/format/setIndentationTest.ts b/packages/roosterjs-editor-api/test/format/setIndentationTest.ts index db2c21de36f..ef245fa8be1 100644 --- a/packages/roosterjs-editor-api/test/format/setIndentationTest.ts +++ b/packages/roosterjs-editor-api/test/format/setIndentationTest.ts @@ -45,6 +45,19 @@ describe('setIndentation()', () => { ); }); + it('Indent the first list item in a list with margin-block', () => { + runTest( + '
  1. Text
', + () => { + const range = new Range(); + range.setStart(editor.getDocument().getElementById('test'), 0); + editor.select(range); + }, + Indentation.Increase, + '
  1. Text
' + ); + }); + it('Outdent the first list item in a list', () => { runTest( '
  1. Text
', @@ -58,6 +71,19 @@ describe('setIndentation()', () => { ); }); + it('Outdent the first list item in a list with margin-block', () => { + runTest( + '
  1. Text
', + () => { + const range = new Range(); + range.setStart(editor.getDocument().getElementById('test'), 0); + editor.select(range); + }, + Indentation.Decrease, + '
  1. Text
' + ); + }); + it('Outdent whole table selected, when no Blockquote wraping table', () => { runTest( '




', diff --git a/packages/roosterjs-editor-api/test/utils/toggleListTypeTest.ts b/packages/roosterjs-editor-api/test/utils/toggleListTypeTest.ts index db43cb22ac7..74373a682e8 100644 --- a/packages/roosterjs-editor-api/test/utils/toggleListTypeTest.ts +++ b/packages/roosterjs-editor-api/test/utils/toggleListTypeTest.ts @@ -98,4 +98,24 @@ describe('toggleListTypeTest()', () => { '
default format

  • test

  • test
' ); }); + + it('Do not set margin-block when in the middle', () => { + // Arrange + const originalContent = + '
default format
' + + '

' + + '
' + + '
  • test

  • test
' + + '
'; + editor.setContent(originalContent); + editor.focus(); + editor.select(document.getElementById('focusNode'), PositionType.Begin); + + // Act + toggleListType(editor, ListType.Unordered); + + // Assert + const ul = editor.getDocument().getElementById('list'); + expect(ul?.style.marginBlock).toBe(''); + }); }); diff --git a/packages/roosterjs-editor-core/lib/corePlugins/ImageSelection.ts b/packages/roosterjs-editor-core/lib/corePlugins/ImageSelection.ts index 6cde4d9d736..0041b247fc9 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/ImageSelection.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/ImageSelection.ts @@ -1,10 +1,10 @@ import { PluginEventType, PositionType, SelectionRangeTypes } from 'roosterjs-editor-types'; -import { Position, safeInstanceOf } from 'roosterjs-editor-dom'; +import { safeInstanceOf } from 'roosterjs-editor-dom'; import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-editor-types'; const Escape = 'Escape'; const Delete = 'Delete'; -const mouseLeftButton = 0; +const mouseMiddleButton = 1; /** * Detect image selection and help highlight the image @@ -43,7 +43,7 @@ export default class ImageSelection implements EditorPlugin { if ( safeInstanceOf(target, 'HTMLImageElement') && target.isContentEditable && - event.rawEvent.button === mouseLeftButton + event.rawEvent.button != mouseMiddleButton ) { this.editor.select(target); } @@ -70,20 +70,16 @@ export default class ImageSelection implements EditorPlugin { !rawEvent.metaKey && keyDownSelection.type === SelectionRangeTypes.ImageSelection ) { - if (key === Escape) { + const imageParent = keyDownSelection.image?.parentNode; + if (key === Escape && imageParent) { this.editor.select(keyDownSelection.image, PositionType.Before); this.editor.getSelectionRange()?.collapse(); event.rawEvent.stopPropagation(); } else if (key === Delete) { this.editor.deleteNode(keyDownSelection.image); event.rawEvent.preventDefault(); - } else { - const position = new Position( - keyDownSelection.image, - PositionType.Before - ); - - this.editor.select(position); + } else if (imageParent) { + this.editor.select(keyDownSelection.image, PositionType.Before); } } break; diff --git a/packages/roosterjs-editor-dom/lib/list/VList.ts b/packages/roosterjs-editor-dom/lib/list/VList.ts index ab2f3b94ab9..a8022ba1662 100644 --- a/packages/roosterjs-editor-dom/lib/list/VList.ts +++ b/packages/roosterjs-editor-dom/lib/list/VList.ts @@ -309,7 +309,6 @@ export default class VList { * @param end End position to operate to * @param alignment Align items left, center or right */ - setAlignment( start: NodePosition, end: NodePosition, @@ -328,6 +327,16 @@ export default class VList { }); } + /** + * Remove margins of a new list + */ + removeMargins() { + if (!this.rootList.style.marginTop && !this.rootList.style.marginBottom) { + this.rootList.style.marginBlockStart = '0px'; + this.rootList.style.marginBlockEnd = '0px'; + } + } + /** * Change list type of the given range of this list. * If some of the items are not real list item yet, this will make them to be list item with given type diff --git a/packages/roosterjs-editor-dom/lib/list/VListItem.ts b/packages/roosterjs-editor-dom/lib/list/VListItem.ts index 38bd1b241f4..cb165d0a060 100644 --- a/packages/roosterjs-editor-dom/lib/list/VListItem.ts +++ b/packages/roosterjs-editor-dom/lib/list/VListItem.ts @@ -473,6 +473,14 @@ function createListElement( result = doc.createElement(listType == ListType.Ordered ? 'ol' : 'ul'); } + if ( + originalRoot?.style.marginBlockStart == '0px' && + originalRoot?.style.marginBlockEnd == '0px' + ) { + result.style.marginBlockStart = '0px'; + result.style.marginBlockEnd = '0px'; + } + // Always maintain the metadata saved in the list if (originalRoot && nextLevel == 1 && listType != getListTypeFromNode(originalRoot)) { const style = getMetadata(originalRoot, ListStyleDefinitionMetadata); diff --git a/packages/roosterjs-editor-dom/test/list/VListTest.ts b/packages/roosterjs-editor-dom/test/list/VListTest.ts index dd3af36b065..0090990035e 100644 --- a/packages/roosterjs-editor-dom/test/list/VListTest.ts +++ b/packages/roosterjs-editor-dom/test/list/VListTest.ts @@ -1285,6 +1285,14 @@ describe('VList.split', () => { 9 ); }); + + it('split List 4 with margin-block', () => { + runTest( + `
  1. 1
    1. 1
    2. 2
    3. 3
  2. 3
  3. 4
`, + '
  1. 1
    1. 1
    2. 2
    3. 3
  2. 3
  3. 4
', + 9 + ); + }); }); describe('VList.setListStyleType', () => { @@ -1514,3 +1522,47 @@ describe('VList.setListStyleType', () => { ); }); }); + +describe('VList.removeMargins', () => { + const testId = 'VList_changeListType'; + const ListRoot = 'listRoot'; + + afterEach(() => { + DomTestHelper.removeElement(testId); + }); + + function runTest(source: string, shouldNotRemoveMargin: boolean = false) { + DomTestHelper.createElementFromContent(testId, source); + const list = document.getElementById(ListRoot) as HTMLOListElement; + + if (!list) { + throw new Error('No root node'); + } + const vList = new VList(list); + + // Act + vList.removeMargins(); + if (shouldNotRemoveMargin) { + expect(list.style.marginBlock).toEqual(''); + } else { + expect(list.style.marginBlock).toEqual('0px'); + } + + DomTestHelper.removeElement(testId); + } + + it('remove list margins OL list', () => { + const list = `
    `; + runTest(list); + }); + + it('remove list margins UL list', () => { + const list = `
      `; + runTest(list); + }); + + it('do not remove list margins UL list', () => { + const list = `
      • test
      `; + runTest(list, true /** shouldNotRemoveMargin */); + }); +}); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts index 3d4cf2ec963..b09edea9687 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/api/isResizedTo.ts @@ -5,14 +5,21 @@ import { getEditInfoFromImage } from '../editInfoUtils/editInfo'; * Check if the image is already resized to the given percentage * @param image The image to check * @param percentage The percentage to check + * @param maxError Maximum difference of pixels to still be considered the same size */ -export default function isResizedTo(image: HTMLImageElement, percentage: number): boolean { +export default function isResizedTo( + image: HTMLImageElement, + percentage: number, + maxError: number = 1 +): boolean { const editInfo = getEditInfoFromImage(image); + //Image selection will sometimes return an image which is currently hidden and wrapped. Use HTML attributes as backup + const visibleHeight = editInfo.heightPx || image.height; + const visibleWidth = editInfo.widthPx || image.width; if (editInfo) { const { width, height } = getTargetSizeByPercentage(editInfo, percentage); return ( - Math.round(width) == Math.round(editInfo.widthPx) && - Math.round(height) == Math.round(editInfo.heightPx) + Math.abs(width - visibleWidth) < maxError && Math.abs(height - visibleHeight) < maxError ); } return false; diff --git a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts index 41a7966f87e..0650005599b 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/Picker/PickerPlugin.ts @@ -260,12 +260,14 @@ export default class PickerPlugin { + if (currentNode) { + this.editor?.deleteNode(currentNode); + } + if (replacementNode) { + this.editor?.insertNode(replacementNode); + } + }, ChangeSource.Keyboard); } private getRangeUntilAt(event: PluginKeyboardEvent | null): Range | null { @@ -497,7 +499,7 @@ export default class PickerPlugin this.onMouseOut(e), }); + const scrollContainer = this.editor.getScrollContainer(); + scrollContainer.addEventListener('mouseout', this.onMouseOut); } - private onMouseOut = (ev: Event) => { + private onMouseOut = ({ relatedTarget, currentTarget }: MouseEvent) => { if ( - isMouseEvent(ev) && - safeInstanceOf(ev.relatedTarget, 'HTMLElement') && + safeInstanceOf(relatedTarget, 'HTMLElement') && + safeInstanceOf(currentTarget, 'HTMLElement') && this.tableEditor && - !this.tableEditor.isOwnedElement(ev.relatedTarget) && - !this.editor?.contains(ev.relatedTarget) + !this.tableEditor.isOwnedElement(relatedTarget) && + !contains(currentTarget, relatedTarget) ) { this.setTableEditor(null); } @@ -72,6 +73,8 @@ export default class TableResize implements EditorPlugin { * Dispose this plugin */ dispose() { + const scrollContainer = this.editor?.getScrollContainer(); + scrollContainer?.removeEventListener('mouseout', this.onMouseOut); this.onMouseMoveDisposer?.(); this.invalidateTableRects(); this.disposeTableEditor(); @@ -129,7 +132,12 @@ export default class TableResize implements EditorPlugin { this.tableEditor?.onMouseMove(x, y); }; - private setTableEditor(table: HTMLTableElement | null, e?: MouseEvent) { + /** + * @internal Public only for unit test + * @param table Table to use when setting the Editors + * @param event (Optional) Mouse event + */ + public setTableEditor(table: HTMLTableElement | null, event?: MouseEvent) { if (this.tableEditor && !this.tableEditor.isEditing() && table != this.tableEditor.table) { this.disposeTableEditor(); } @@ -145,7 +153,7 @@ export default class TableResize implements EditorPlugin { this.invalidateTableRects, this.onShowHelperElement, safeInstanceOf(container, 'HTMLElement') ? container : undefined, - e?.currentTarget + event?.currentTarget ); } } @@ -176,7 +184,3 @@ export default class TableResize implements EditorPlugin { } } } - -function isMouseEvent(e: Event): e is MouseEvent { - return !!(e as MouseEvent).pageX; -} diff --git a/packages/roosterjs-editor-plugins/test/TableResize/tableResizeTest.ts b/packages/roosterjs-editor-plugins/test/TableResize/tableResizeTest.ts index 3073b91896d..7a68f29e871 100644 --- a/packages/roosterjs-editor-plugins/test/TableResize/tableResizeTest.ts +++ b/packages/roosterjs-editor-plugins/test/TableResize/tableResizeTest.ts @@ -1,4 +1,6 @@ +import * as Contains from 'roosterjs-editor-dom/lib/utils/contains'; import * as TestHelper from 'roosterjs-editor-api/test/TestHelper'; +import { createElement } from 'roosterjs-editor-dom'; import { DEFAULT_TABLE, DEFAULT_TABLE_MERGED, EXCEL_TABLE, WORD_TABLE } from './tableData'; import { TableResize } from '../../lib/TableResize'; import { @@ -715,3 +717,265 @@ xdescribe('Table Resizer/Inserter tests', () => { expect(pluginName).toBe(expectedName); }); }); + +describe('TableResize', () => { + let editor: IEditor; + let plugin: TableResize; + const TEST_ID = 'inserterTest'; + + let mouseOutListener: undefined | ((this: HTMLElement, ev: MouseEvent) => any); + + beforeEach(() => { + editor = TestHelper.initEditor(TEST_ID); + plugin = new TableResize(); + + spyOn(editor, 'getScrollContainer').and.returnValue(({ + addEventListener: ( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | AddEventListenerOptions + ) => { + if (type == 'mouseout') { + mouseOutListener = listener as (this: HTMLElement, ev: MouseEvent) => any; + } + }, + removeEventListener: ( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventListenerOptions + ) => { + if (type == 'mouseout') { + mouseOutListener = undefined; + } + }, + })); + plugin.initialize(editor); + }); + + afterEach(() => { + plugin.dispose(); + editor.dispose(); + TestHelper.removeElement(TEST_ID); + document.body = document.createElement('body'); + }); + + it('Dismiss table editor on mouse out', () => { + const ele = createElement( + { + tag: 'div', + children: [ + { + tag: 'div', + children: ['asd'], + }, + ], + }, + editor.getDocument() + ); + const table = createElement( + { + tag: 'table', + children: [ + { + tag: 'tr', + children: [ + { + tag: 'td', + children: ['Test'], + }, + ], + }, + ], + }, + editor.getDocument() + ) as HTMLTableElement; + editor.insertNode(table); + + spyOn(plugin, 'setTableEditor').and.callThrough(); + + plugin.setTableEditor(table); + + if (mouseOutListener) { + const boundedListener = mouseOutListener.bind(ele); + spyOn(Contains, 'default').and.returnValue(false); + boundedListener(({ + currentTarget: ele, + relatedTarget: ele, + })); + + expect(plugin.setTableEditor).toHaveBeenCalledWith(null); + } + }); + + it('Do not dismiss table editor on mouse out, related target is contained in scroll container', () => { + const ele = createElement( + { + tag: 'div', + children: [ + { + tag: 'div', + children: ['asd'], + }, + ], + }, + editor.getDocument() + ); + const table = createElement( + { + tag: 'table', + children: [ + { + tag: 'tr', + children: [ + { + tag: 'td', + children: ['Test'], + }, + ], + }, + ], + }, + editor.getDocument() + ) as HTMLTableElement; + editor.insertNode(table); + + spyOn(plugin, 'setTableEditor').and.callThrough(); + + plugin.setTableEditor(table); + + if (mouseOutListener) { + const boundedListener = mouseOutListener.bind(ele); + spyOn(Contains, 'default').and.returnValue(true); + boundedListener(({ + currentTarget: ele, + relatedTarget: ele, + })); + + expect(plugin.setTableEditor).not.toHaveBeenCalledWith(null); + } + }); + + it('Do not dismiss table editor on mouse out, table editor not', () => { + const ele = createElement( + { + tag: 'div', + children: [ + { + tag: 'div', + children: ['asd'], + }, + ], + }, + editor.getDocument() + ); + + spyOn(plugin, 'setTableEditor').and.callThrough(); + + if (mouseOutListener) { + const boundedListener = mouseOutListener.bind(ele); + spyOn(Contains, 'default').and.returnValue(false); + boundedListener(({ + currentTarget: ele, + relatedTarget: ele, + })); + + expect(plugin.setTableEditor).not.toHaveBeenCalledWith(null); + } + }); + + it('Do not dismiss table editor on mouse out, related target null', () => { + const ele = createElement( + { + tag: 'div', + children: [ + { + tag: 'div', + children: ['asd'], + }, + ], + }, + editor.getDocument() + ); + const table = createElement( + { + tag: 'table', + children: [ + { + tag: 'tr', + children: [ + { + tag: 'td', + children: ['Test'], + }, + ], + }, + ], + }, + editor.getDocument() + ) as HTMLTableElement; + editor.insertNode(table); + + spyOn(plugin, 'setTableEditor').and.callThrough(); + + plugin.setTableEditor(table); + + if (mouseOutListener) { + const boundedListener = mouseOutListener.bind(ele); + spyOn(Contains, 'default').and.returnValue(false); + boundedListener(({ + currentTarget: ele, + relatedTarget: null, + })); + + expect(plugin.setTableEditor).not.toHaveBeenCalledWith(null); + } + }); + + it('Do not dismiss table editor on mouse out, currentTarget null', () => { + const ele = createElement( + { + tag: 'div', + children: [ + { + tag: 'div', + children: ['asd'], + }, + ], + }, + editor.getDocument() + ); + const table = createElement( + { + tag: 'table', + children: [ + { + tag: 'tr', + children: [ + { + tag: 'td', + children: ['Test'], + }, + ], + }, + ], + }, + editor.getDocument() + ) as HTMLTableElement; + editor.insertNode(table); + + spyOn(plugin, 'setTableEditor').and.callThrough(); + + plugin.setTableEditor(table); + + if (mouseOutListener) { + const boundedListener = mouseOutListener.bind(ele); + spyOn(Contains, 'default').and.returnValue(false); + boundedListener(({ + currentTarget: null, + relatedTarget: ele, + })); + + expect(plugin.setTableEditor).not.toHaveBeenCalledWith(null); + } + }); +}); diff --git a/tools/buildTools/normalize.js b/tools/buildTools/normalize.js index 8821c196e99..740489870a3 100644 --- a/tools/buildTools/normalize.js +++ b/tools/buildTools/normalize.js @@ -46,6 +46,7 @@ function normalize() { packageJson.typings = './lib/index.d.ts'; packageJson.main = './lib/index.js'; + packageJson.module = './lib-mjs/index.js'; packageJson.license = 'MIT'; packageJson.repository = { type: 'git', diff --git a/versions.json b/versions.json index e349b5995e6..1317dbfc02c 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "packages": "8.58.1", - "packages-ui": "8.53.0", - "packages-content-model": "0.19.0" + "packages": "8.59.0", + "packages-ui": "8.54.0", + "packages-content-model": "0.20.0" }