From fcd106128d36221e6e5bb4356bc7a0f5f2a4e21f Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 9 Feb 2023 19:14:09 -0600 Subject: [PATCH] Bump Rooster to 8.42.0 (#1571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * convert alpha to decimals * fix auto format list * add null and refactor * Content Model Selection API step 4: Refactor existing table API (#1479) * Selection API step 1 * Selection API 2 * New selection API * Refactor table API * add test * Support element with namespace (#1489) * Content Model: Fix a bug when process margin (#1493) * Fix margin issue * Fix test * Fix A tag without href (#1495) * Fix Cut/Copy page scroll issue (#1496) * Fix Cut/Copy page scroll issue * Fix test * fix image plugin z-index calc * Content Model Format State Step 1: Refactor formatSegmentWithContentModel() (#1490) * Selection API step 1 * Selection API 2 * New selection API * Refactor table API * add test * Format state step 1 * Improve * update condition per comments * Content Model Format State Step 2: Allow retrieving metadata directly (#1491) * Selection API step 1 * Selection API 2 * New selection API * Refactor table API * add test * Format state step 1 * FormatState step 2 * Improve * Content Model Format State Step 3: Add getFormatState API and ContentModelPlugin (#1492) * Selection API step 1 * Selection API 2 * New selection API * Refactor table API * add test * Format state step 1 * FormatState step 2 * FormatState step 3: Add getFormatState API and ContentModel plugin * Improve * Improve * Improve * fix test * improve, fix safari issue * fix test * wip * WIP * wip * Content Model: Add API clearFormat (#1497) * Selection API step 1 * Selection API 2 * New selection API * Refactor table API * add test * Format state step 1 * FormatState step 2 * FormatState step 3: Add getFormatState API and ContentModel plugin * Improve * Content Model: clearFormat * fix build * Improve * Improve * fix test * improve, fix safari issue * fix test * remove wrapper when content change * fix * Content Model: Move format API: link, image, captalization, ... (#1506) * Selection API step 1 * Selection API 2 * New selection API * Refactor table API * add test * Format state step 1 * FormatState step 2 * FormatState step 3: Add getFormatState API and ContentModel plugin * Improve * Content Model: clearFormat * fix build * Improve * Improve * fix test * improve, fix safari issue * fix test * ContentModel: Support insertLink and removeLink * changeCapitalization and setImageAltText * fix for image selection * refactor * refactor * Test image edit with ShadowDOM * improve * Fix #1509 (#1511) * ContentModel: Improve Divider (#1513) * ContentModel: Improve Divider * Add BorderFormat to ContentModelBlockFormat * Add test * fix build * Content Model: Support "no color" when set color (#1514) * Content Model: Support "no color" when set color * improve * Content Model: Use Entity handle readonly element (#1515) * image wrapper using shadow dom * Content Model: Support get and apply segment format (#1518) * Do not merge table when insert a table (#1519) * wip * Content Model: Fix #1239 (#1521) * WIP AND fix for span height * stop dragging * comment * prototype * remove change * prevent drag * remove new max-width * Load fluent ui from cdnjs (#1525) * small changes * Content Model: Improve selection (#1526) * Apply format to word where cursor is located (#1367) * attempt with traversers * attempt using splitTextNode * Return to original implementation * Fix build * implementation with content model * Implement word selection with new content model * removed selectWordFromCollapsedRange.ts * optimization fixes and file changes * standardize function and remove castings * fix paragraph and pending state * fix pending state, name change * Added test cases, disabled end or start of word * fixed dependency * fix pending state * more tests * fixed tests * End of word format fix (#1528) * End of word format fix Fix scenario where format was wrongly applied where the cursor was located at the end of a word * add tests * Variable based dark color (#1531) * Variable based dark color * fix test * improve * Improve * Fix comment * Fix #1532: Support isCode in FormatState (#1533) * Fix #1532 * add comment * fix build * fix comment * Bump ua-parser-js from 0.7.31 to 0.7.33 (#1535) Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix #1529 #1530 and 187095 (#1534) * WIP * test and fixes * unit tests * fix list trigger * Content Model: Adjust selection for link to select a word (#1538) * Content Model: Adjust word selection for link * add test * fix build * Fix table format (#1541) * refactor * move comment * Content Model code simplify 1 (#1544) * Content Model code simplify 2: Clean up EditorContext (#1545) * Content Model code simplify 1 * Content Model: Clean up EditorContext * improve * Content Model code simplify 3: Remove get/setPendingFormat from editor interface (#1546) * Content Model code simplify 1 * Content Model: Clean up EditorContext * improve * Content Model: Remove get/setPendingFormat * Add support for paragraph line spacing for content model (#1543) * Fetch line height from children * Create new content model api * Spacing btn * Fix tests * Remove key from roosterjs-react * testing * Allow segment to hold lineHeight format * Remove lineHeight from segments whenever possible * Fix imports * Remove normalization * Add todo for edge case * Render segment line height * Content Model Editor 1: Rename interface IExperimentalContentModelEditor (#1547) * Content Model code simplify 1 * Content Model: Clean up EditorContext * improve * Content Model: Remove get/setPendingFormat * Content Model: Rename IExperimentalContentModelEditor * fix build * Content Model Editor 2: Publish ContentModelEditor class (#1548) * Content Model code simplify 1 * Content Model: Clean up EditorContext * improve * Content Model: Remove get/setPendingFormat * Content Model: Rename IExperimentalContentModelEditor * Publish ContentModelEditor class * fix build * add test * fix build * fix build * Reorganize Contente Model code (#1555) * blur issue * Fix some block format issues (#1554) * Fix some block format issues * Improve * Content Model: Fix image size for outlook (#1556) * Content Model: Fix image size for outlook * Fix build * fix build * improve * add filter to client rects * Content Model: Support align table to center (#1557) * simply code * Content Model: Fix PRE tag (#1559) * Content Model: Support insert image with src (#1563) * Content Model: Get init segment format from root container (#1567) * Content Model: Get init segment format from root container * Improve * add test * Add missing setSpacing comment (#1564) * do not call updateHandle * check viewport * Content Model: Improve pending format implementation (#1570) * Disable list indentation on mac (#1552) * Disable list indentation on mac * Switch to default disabled * Content Model: Improve hyperlink handling (#1569) * backu * remove unneeded changes * bump RoosterJS version * Add support for adding/removing space before/after paragraphs in content model (#1565) * Create api for block margins * Create space before after buttons * Update testing * Fix by using decorator * Replace positive value with none * Treat no margin as space removed * Fix testing * Improve tag name selecting * testing * rename and improve logic * Add support for formatState on onClick call * Update button impl * Revert "Update button impl" This reverts commit edce3ad11d1bb6841461f40e494b3df3207459e2. * Revert "Add support for formatState on onClick call" This reverts commit 3d6b83d0f22e1224868f7730eface582014e68f0. * use getFormatState() * Update test name * Bump Content Model version * Fix a pending format issue in firefox (#1572) --------- Signed-off-by: dependabot[bot] Co-authored-by: JĂșlia Roldi Co-authored-by: Jiuqing Song Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> Co-authored-by: Shai Petel Co-authored-by: Shai Petel Co-authored-by: Andres-CT98 <107568016+Andres-CT98@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ian Elizondo --- demo/scripts/controls/MainPane.tsx | 5 +- .../components/format/LinkFormatView.tsx | 8 +- .../components/format/SegmentFormatView.tsx | 2 + .../formatPart/TextColorFormatRenderer.ts | 8 +- .../model/ContentModelImageView.tsx | 2 +- .../model/ContentModelTableCellView.tsx | 2 +- .../model/ContentModelTableView.tsx | 22 +- .../plugins/FormatPainterPlugin.ts | 8 +- .../editor/ExperimentalContentModelEditor.ts | 110 ------- .../controls/editor/isContentModelEditor.ts | 10 - .../contentModel/ContentModelRibbon.tsx | 15 + .../contentModel/ContentModelRibbonPlugin.ts | 6 +- .../contentModel/alignCenterButton.ts | 2 +- .../contentModel/alignLeftButton.ts | 2 +- .../contentModel/alignRightButton.ts | 2 +- .../contentModel/backgroundColorButton.ts | 2 +- .../contentModel/blockQuoteButton.ts | 2 +- .../ribbonButtons/contentModel/boldButton.ts | 2 +- .../contentModel/bulletedListButton.ts | 2 +- .../contentModel/changeImageButton.ts | 46 +++ .../contentModel/clearFormatButton.ts | 2 +- .../contentModel/decreaseFontSizeButton.ts | 2 +- .../contentModel/decreaseIndentButton.ts | 2 +- .../ribbonButtons/contentModel/fontButton.ts | 2 +- .../contentModel/fontSizeButton.ts | 2 +- .../contentModel/formatPainterButton.ts | 2 +- .../contentModel/formatTableButton.ts | 2 +- .../contentModel/imageBorderColorButton.ts | 25 ++ .../contentModel/imageBorderStyleButton.ts | 35 ++ .../contentModel/imageBorderWidthButton.ts | 35 ++ .../contentModel/imageBoxShadowButton.ts | 50 +++ .../contentModel/increaseFontSizeButton.ts | 2 +- .../contentModel/increaseIndentButton.ts | 2 +- .../contentModel/insertImageButton.ts | 2 +- .../contentModel/insertLinkButton.ts | 3 +- .../contentModel/insertTableButton.ts | 2 +- .../contentModel/italicButton.ts | 2 +- .../contentModel/listStartNumberButton.ts | 2 +- .../ribbonButtons/contentModel/ltrButton.ts | 2 +- .../contentModel/numberedListButton.ts | 2 +- .../contentModel/removeLinkButton.ts | 2 +- .../ribbonButtons/contentModel/rtlButton.ts | 2 +- .../setBulletedListStyleButton.ts | 2 +- .../contentModel/setHeaderLevelButton.ts | 2 +- .../setNumberedListStyleButton.ts | 2 +- .../contentModel/setTableCellShadeButton.ts | 2 +- .../contentModel/setTableHeaderButton.ts | 2 +- .../contentModel/spaceBeforeAfterButtons.ts | 49 +++ .../contentModel/spacingButton.ts | 37 +++ .../contentModel/strikethroughButton.ts | 2 +- .../contentModel/subscriptButton.ts | 2 +- .../contentModel/superscriptButton.ts | 2 +- .../contentModel/tableEditButtons.ts | 2 +- .../contentModel/textColorButton.ts | 2 +- .../contentModel/underlineButton.ts | 2 +- .../contentModel/ContentModelPanePlugin.ts | 2 +- .../contentModel/buttons/exportButton.ts | 2 +- .../sidePane/formatState/FormatStatePlugin.ts | 6 +- package.json | 2 +- .../context/createDomToModelContext.ts | 9 +- .../lib/domToModel/domToContentModel.ts | 45 +++ .../processors/knownElementProcessor.ts | 24 +- .../domToModel/processors/listProcessor.ts | 2 +- .../domToModel/processors/quoteProcessor.ts | 4 +- .../domToModel/processors/tableProcessor.ts | 212 ++++++------ .../lib/domToModel/utils/getDefaultStyle.ts | 5 - .../lib/domToModel/utils/stackFormat.ts | 94 ++++-- .../lib/domUtils/borderValues.ts | 21 +- .../lib/domUtils/index.ts | 6 + .../metadata/updateImageMetadata.ts | 0 .../metadata/updateListMetadata.ts | 0 .../metadata/updateMetadata.ts | 0 .../metadata/updateTableCellMetadata.ts | 0 .../metadata/updateTableMetadata.ts | 0 .../lib/editor/ContentModelEditor.ts | 73 ++++ .../lib/editor/ContentModelPlugin.ts | 86 +++++ .../lib/editor/index.ts | 3 + .../lib/editor/isContentModelEditor.ts | 8 + .../block/whiteSpaceFormatHandler.ts | 7 +- .../common/borderFormatHandler.ts | 1 + .../common/boxShadowFormatHandler.ts | 18 + .../formatHandlers/defaultFormatHandlers.ts | 7 +- .../root/rootDirectionFormatHandler.ts | 16 + .../root/zoomScaleFormatHandler.ts | 18 + .../segment/computedSegmentFormatHandler.ts | 28 ++ .../lib/formatHandlers/utils/defaultStyles.ts | 7 - .../utils/parseValueWithUnit.ts | 14 +- packages/roosterjs-content-model/lib/index.ts | 178 +--------- .../lib/modelApi/common/clearModelFormat.ts | 4 +- .../lib/modelApi/common/insertContent.ts | 4 +- .../lib/modelApi/common/mergeModel.ts | 4 +- .../common/retrieveModelFormatState.ts | 6 +- .../lib/modelApi/creators/createTable.ts | 5 +- .../lib/modelApi/format/pendingFormat.ts | 74 +++++ .../modelApi/image/applyImageBorderFormat.ts | 53 +++ .../lib/modelApi/list/setListType.ts | 9 +- .../modelApi/selection/collectSelections.ts | 6 +- .../modelApi/selection/deleteSelections.ts | 2 +- .../modelApi/selection/iterateSelections.ts | 19 +- .../lib/modelApi/table/applyTableFormat.ts | 4 +- .../table/setTableCellBackgroundColor.ts | 2 +- .../contentModelToDom.ts | 6 +- .../context/createModelToDomContext.ts | 5 +- .../context/defaultContentModelHandlers.ts | 2 + .../lib/modelToDom/handlers/handleEntity.ts | 2 +- .../modelToDom/handlers/handleGeneralModel.ts | 44 +-- .../lib/modelToDom/handlers/handleImage.ts | 49 ++- .../lib/modelToDom/handlers/handleLink.ts | 17 + .../lib/modelToDom/handlers/handleList.ts | 2 +- .../lib/modelToDom/handlers/handleText.ts | 16 +- .../lib/publicApi/block/setAlignment.ts | 4 +- .../lib/publicApi/block/setDirection.ts | 7 +- .../lib/publicApi/block/setHeaderLevel.ts | 4 +- .../lib/publicApi/block/setIndentation.ts | 4 +- .../lib/publicApi/block/setParagraphMargin.ts | 34 ++ .../lib/publicApi/block/setSpacing.ts | 18 + .../lib/publicApi/block/toggleBlockQuote.ts | 4 +- .../lib/publicApi/domToContentModel.ts | 30 -- .../publicApi/format/applyPendingFormat.ts | 50 +++ .../lib/publicApi/format/clearFormat.ts | 4 +- .../lib/publicApi/format/getFormatState.ts | 7 +- .../lib/publicApi/format/getSegmentFormat.ts | 7 +- .../publicApi/image/adjustImageSelection.ts | 4 +- .../lib/publicApi/image/changeImage.ts | 22 ++ .../lib/publicApi/image/insertImage.ts | 31 ++ .../lib/publicApi/image/setImageAltText.ts | 16 +- .../lib/publicApi/image/setImageBorder.ts | 22 ++ .../lib/publicApi/image/setImageBoxShadow.ts | 14 + .../lib/publicApi/index.ts | 44 +++ .../lib/publicApi/insert/insertImage.ts | 23 -- .../lib/publicApi/link/adjustLinkSelection.ts | 9 +- .../lib/publicApi/link/insertLink.ts | 31 +- .../lib/publicApi/link/removeLink.ts | 13 +- .../lib/publicApi/list/setListStartNumber.ts | 4 +- .../lib/publicApi/list/setListStyle.ts | 7 +- .../lib/publicApi/list/toggleBullet.ts | 4 +- .../lib/publicApi/list/toggleNumbering.ts | 4 +- .../publicApi/segment/applySegmentFormat.ts | 4 +- .../publicApi/segment/changeCapitalization.ts | 4 +- .../lib/publicApi/segment/changeFontSize.ts | 29 +- .../publicApi/segment/setBackgroundColor.ts | 4 +- .../lib/publicApi/segment/setFontName.ts | 4 +- .../lib/publicApi/segment/setFontSize.ts | 4 +- .../lib/publicApi/segment/setTextColor.ts | 19 +- .../lib/publicApi/segment/toggleBold.ts | 4 +- .../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 | 10 +- .../lib/publicApi/table/editTable.ts | 9 +- .../lib/publicApi/table/formatTable.ts | 4 +- .../lib/publicApi/table/insertTable.ts | 4 +- .../lib/publicApi/table/setTableCellShade.ts | 4 +- .../utils/formatImageWithContentModel.ts | 24 ++ .../utils/formatParagraphWithContentModel.ts | 4 +- .../utils/formatSegmentWithContentModel.ts | 16 +- .../publicApi/utils/formatWithContentModel.ts | 9 +- .../lib/publicPlugin/ContentModelPlugin.ts | 116 ------- ...tModelEditor.ts => IContentModelEditor.ts} | 45 +-- .../context/ContentModelHandler.ts | 3 +- .../context/DomToModelFormatContext.ts | 6 + .../lib/publicTypes/context/EditorContext.ts | 10 - .../context/ModelToDomEntityContext.ts | 5 - .../publicTypes/context/ModelToDomSettings.ts | 6 + .../publicTypes/decorator/ContentModelLink.ts | 4 +- .../format/ContentModelHyperLinkFormat.ts | 8 + .../format/ContentModelImageFormat.ts | 6 +- .../format/ContentModelSegmentFormat.ts | 4 +- .../format/FormatHandlerTypeMap.ts | 6 + .../format/formatParts/BorderFormat.ts | 5 + .../format/formatParts/BoxShadowFormat.ts | 9 + .../format/formatParts/ZoomScaleFormat.ts | 9 + .../lib/publicTypes/index.ts | 126 +++++++ .../lib/publicTypes/interface/Border.ts | 20 ++ packages/roosterjs-content-model/package.json | 5 +- .../context/createDomToModelContextTest.ts | 31 +- .../context/createModelToDomContextTest.ts | 7 - .../processors/knownElementProcessorTest.ts | 19 +- .../processors/quoteProcessorTest.ts | 51 ++- .../processors/tableProcessorTest.ts | 43 ++- .../test/domToModel/utils/parseFormatTest.ts | 5 +- .../test/domToModel/utils/stackFormatTest.ts | 26 ++ .../metadata/updateImageMetadataTest.ts | 2 +- .../metadata/updateListlMetadataTest.ts | 2 +- .../metadata/updateMetadataTest.ts | 2 +- .../metadata/updateTableCellMetadataTest.ts | 2 +- .../metadata/updateTableMetadataTest.ts | 2 +- .../test/editor/ContentModelEditorTest.ts | 142 ++++++++ .../ContentModelPluginTest.ts | 192 ++++++----- .../test/editor/isContentModelEditorTest.ts | 24 ++ .../block/directionFormatHandlerTest.ts | 2 +- .../block/whiteSpaceFormatHandlerTest.ts | 16 +- .../common/borderFormatHandlerTest.ts | 2 +- .../common/boxShadowFormatHandlerTest.ts | 64 ++++ .../root/rootDirectionFormatHandlerTest.ts | 84 +++++ .../root/zoomScaleFormatHandlerTest.ts | 94 ++++++ .../computedSegmentFormatHandlerTest.ts | 52 +++ .../segment/textColorFormatHandlerTest.ts | 7 +- .../segment/underlineFormatHandlerTest.ts | 10 +- .../utils/parseValueWithUnitTest.ts | 12 + .../test/modelApi/common/mergeModelTest.ts | 283 +++++++++++++--- .../common/retrieveModelFormatStateTest.ts | 32 ++ .../test/modelApi/format/pendingFormatTest.ts | 170 ++++++++++ .../image/applyImageBorderFormatTest.ts | 158 +++++++++ .../selection/iterateSelectionsTest.ts | 66 +++- .../handlers/handleGeneralModelTest.ts | 3 +- .../modelToDom/handlers/handleImageTest.ts | 45 ++- .../modelToDom/handlers/handleLinkTest.ts | 73 ++++ .../handlers/handleParagraphTest.ts | 3 +- .../modelToDom/handlers/handleTextTest.ts | 12 +- .../test/modelToDom/utils/stackFormatTest.ts | 2 - .../publicApi/block/paragraphTestCommon.ts | 9 +- .../publicApi/block/setIndentationTest.ts | 9 +- .../publicApi/block/setParagraphMarginTest.ts | 242 ++++++++++++++ .../test/publicApi/block/setSpacingTest.ts | 221 +++++++++++++ .../publicApi/block/toggleBlockQuoteTest.ts | 9 +- .../test/publicApi/domToContentModelTest.ts | 87 +++++ .../format/applyPendingFormatTest.ts | 256 ++++++++++++++ .../test/publicApi/format/clearFormatTest.ts | 4 +- .../publicApi/format/getFormatStateTest.ts | 13 +- .../publicApi/format/getSegmentFormatTest.ts | 12 +- .../test/publicApi/image/changeImageTest.ts | 145 ++++++++ .../{insert => image}/insertImageTest.ts | 51 ++- .../publicApi/image/setImageBorderTest.ts | 311 ++++++++++++++++++ .../publicApi/image/setImageBoxShadowTest.ts | 132 ++++++++ .../publicApi/link/adjustLinkSelectionTest.ts | 10 +- .../test/publicApi/link/insertLinkTest.ts | 51 +-- .../test/publicApi/link/removeLinkTest.ts | 26 +- .../test/publicApi/list/toggleBulletTest.ts | 9 +- .../publicApi/list/toggleNumberingTest.ts | 9 +- .../publicApi/segment/changeFontSizeTest.ts | 73 +++- .../publicApi/segment/segmentTestCommon.ts | 16 +- .../publicApi/segment/setTextColorTest.ts | 103 ++++++ .../publicApi/segment/toggleUnderlineTest.ts | 98 ++++++ .../publicApi/table/setTableCellShadeTest.ts | 10 +- .../utils/formatImageWithContentModelTest.ts | 147 +++++++++ .../formatParagraphWithContentModelTest.ts | 9 +- .../formatSegmentWithContentModelTest.ts | 32 +- .../utils/formatWithContentModelTest.ts | 9 +- .../tsconfig.child.json | 3 +- .../lib/format/getFormatState.ts | 1 + .../lib/format/insertImage.ts | 29 +- .../lib/coreApi/getStyleBasedFormatState.ts | 16 +- .../corePlugins/PendingFormatStatePlugin.ts | 2 +- .../lib/editor/Editor.ts | 2 +- .../lib/utils/getIntersectedRect.ts | 4 +- .../ContentEdit/features/listFeatures.ts | 4 +- .../lib/plugins/ImageEdit/ImageEdit.ts | 44 ++- .../plugins/ImageEdit/imageEditors/Rotator.ts | 27 +- .../lib/interface/FormatState.ts | 15 + 251 files changed, 5282 insertions(+), 1475 deletions(-) delete mode 100644 demo/scripts/controls/editor/ExperimentalContentModelEditor.ts delete mode 100644 demo/scripts/controls/editor/isContentModelEditor.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts create mode 100644 demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts create mode 100644 packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts create mode 100644 packages/roosterjs-content-model/lib/domUtils/index.ts rename packages/roosterjs-content-model/lib/{modelApi => domUtils}/metadata/updateImageMetadata.ts (100%) rename packages/roosterjs-content-model/lib/{modelApi => domUtils}/metadata/updateListMetadata.ts (100%) rename packages/roosterjs-content-model/lib/{modelApi => domUtils}/metadata/updateMetadata.ts (100%) rename packages/roosterjs-content-model/lib/{modelApi => domUtils}/metadata/updateTableCellMetadata.ts (100%) rename packages/roosterjs-content-model/lib/{modelApi => domUtils}/metadata/updateTableMetadata.ts (100%) create mode 100644 packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts create mode 100644 packages/roosterjs-content-model/lib/editor/ContentModelPlugin.ts create mode 100644 packages/roosterjs-content-model/lib/editor/index.ts create mode 100644 packages/roosterjs-content-model/lib/editor/isContentModelEditor.ts create mode 100644 packages/roosterjs-content-model/lib/formatHandlers/common/boxShadowFormatHandler.ts create mode 100644 packages/roosterjs-content-model/lib/formatHandlers/root/rootDirectionFormatHandler.ts create mode 100644 packages/roosterjs-content-model/lib/formatHandlers/root/zoomScaleFormatHandler.ts create mode 100644 packages/roosterjs-content-model/lib/formatHandlers/segment/computedSegmentFormatHandler.ts create mode 100644 packages/roosterjs-content-model/lib/modelApi/format/pendingFormat.ts create mode 100644 packages/roosterjs-content-model/lib/modelApi/image/applyImageBorderFormat.ts rename packages/roosterjs-content-model/lib/{publicApi => modelToDom}/contentModelToDom.ts (94%) create mode 100644 packages/roosterjs-content-model/lib/modelToDom/handlers/handleLink.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/block/setParagraphMargin.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/block/setSpacing.ts delete mode 100644 packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/format/applyPendingFormat.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/image/changeImage.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/image/insertImage.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/image/setImageBorder.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/image/setImageBoxShadow.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/index.ts delete mode 100644 packages/roosterjs-content-model/lib/publicApi/insert/insertImage.ts create mode 100644 packages/roosterjs-content-model/lib/publicApi/utils/formatImageWithContentModel.ts delete mode 100644 packages/roosterjs-content-model/lib/publicPlugin/ContentModelPlugin.ts rename packages/roosterjs-content-model/lib/publicTypes/{IExperimentalContentModelEditor.ts => IContentModelEditor.ts} (59%) create mode 100644 packages/roosterjs-content-model/lib/publicTypes/format/ContentModelHyperLinkFormat.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BoxShadowFormat.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/format/formatParts/ZoomScaleFormat.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/index.ts create mode 100644 packages/roosterjs-content-model/lib/publicTypes/interface/Border.ts rename packages/roosterjs-content-model/test/{modelApi => domUtils}/metadata/updateImageMetadataTest.ts (99%) rename packages/roosterjs-content-model/test/{modelApi => domUtils}/metadata/updateListlMetadataTest.ts (98%) rename packages/roosterjs-content-model/test/{modelApi => domUtils}/metadata/updateMetadataTest.ts (98%) rename packages/roosterjs-content-model/test/{modelApi => domUtils}/metadata/updateTableCellMetadataTest.ts (99%) rename packages/roosterjs-content-model/test/{modelApi => domUtils}/metadata/updateTableMetadataTest.ts (99%) create mode 100644 packages/roosterjs-content-model/test/editor/ContentModelEditorTest.ts rename packages/roosterjs-content-model/test/{publicPlugin => editor}/ContentModelPluginTest.ts (61%) create mode 100644 packages/roosterjs-content-model/test/editor/isContentModelEditorTest.ts create mode 100644 packages/roosterjs-content-model/test/formatHandlers/common/boxShadowFormatHandlerTest.ts create mode 100644 packages/roosterjs-content-model/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts create mode 100644 packages/roosterjs-content-model/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts create mode 100644 packages/roosterjs-content-model/test/formatHandlers/segment/computedSegmentFormatHandlerTest.ts create mode 100644 packages/roosterjs-content-model/test/modelApi/format/pendingFormatTest.ts create mode 100644 packages/roosterjs-content-model/test/modelApi/image/applyImageBorderFormatTest.ts create mode 100644 packages/roosterjs-content-model/test/modelToDom/handlers/handleLinkTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/block/setParagraphMarginTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/block/setSpacingTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/domToContentModelTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/format/applyPendingFormatTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/image/changeImageTest.ts rename packages/roosterjs-content-model/test/publicApi/{insert => image}/insertImageTest.ts (70%) create mode 100644 packages/roosterjs-content-model/test/publicApi/image/setImageBorderTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/image/setImageBoxShadowTest.ts create mode 100644 packages/roosterjs-content-model/test/publicApi/utils/formatImageWithContentModelTest.ts diff --git a/demo/scripts/controls/MainPane.tsx b/demo/scripts/controls/MainPane.tsx index 5907e670704..62c8843f921 100644 --- a/demo/scripts/controls/MainPane.tsx +++ b/demo/scripts/controls/MainPane.tsx @@ -6,7 +6,6 @@ import ContentModelPanePlugin from './sidePane/contentModel/ContentModelPanePlug import ContentModelRibbon from './ribbonButtons/contentModel/ContentModelRibbon'; import EditorOptionsPlugin from './sidePane/editorOptions/EditorOptionsPlugin'; import EventViewPlugin from './sidePane/eventViewer/EventViewPlugin'; -import ExperimentalContentModelEditor from './editor/ExperimentalContentModelEditor'; import FormatPainterPlugin from './contentModel/plugins/FormatPainterPlugin'; import FormatStatePlugin from './sidePane/formatState/FormatStatePlugin'; import getToggleablePlugins from './getToggleablePlugins'; @@ -15,7 +14,7 @@ import SidePane from './sidePane/SidePane'; import SnapshotPlugin from './sidePane/snapshot/SnapshotPlugin'; import TitleBar from './titleBar/TitleBar'; import { arrayPush } from 'roosterjs-editor-dom'; -import { ContentModelPlugin } from 'roosterjs-content-model'; +import { ContentModelEditor, ContentModelPlugin } from 'roosterjs-content-model'; import { ContentModelRibbonPlugin } from './ribbonButtons/contentModel/ContentModelRibbonPlugin'; import { darkMode, DarkModeButtonStringKey } from './ribbonButtons/darkMode'; import { EditorOptions, EditorPlugin } from 'roosterjs-editor-types'; @@ -456,7 +455,7 @@ class MainPane extends MainPaneBase { this.toggleablePlugins = null; this.setState({ editorCreator: (div: HTMLDivElement, options: EditorOptions) => - new ExperimentalContentModelEditor(div, options), + new ContentModelEditor(div, options), }); } } diff --git a/demo/scripts/controls/contentModel/components/format/LinkFormatView.tsx b/demo/scripts/controls/contentModel/components/format/LinkFormatView.tsx index d91d4c892a0..6efc0fd2700 100644 --- a/demo/scripts/controls/contentModel/components/format/LinkFormatView.tsx +++ b/demo/scripts/controls/contentModel/components/format/LinkFormatView.tsx @@ -1,10 +1,12 @@ import * as React from 'react'; +import { ContentModelHyperLinkFormat, LinkFormat } from 'roosterjs-content-model'; import { createTextFormatRenderer } from './utils/createTextFormatRenderer'; import { FormatRenderer } from './utils/FormatRenderer'; import { FormatView } from './FormatView'; -import { LinkFormat } from 'roosterjs-content-model'; +import { TextColorFormatRenderer } from './formatPart/TextColorFormatRenderer'; +import { UnderlineFormatRenderer } from './formatPart/BasicFormatRenderers'; -const LinkFormatRenderers: FormatRenderer[] = [ +const LinkFormatRenderers: FormatRenderer[] = [ createTextFormatRenderer( 'Name', format => format.name, @@ -40,6 +42,8 @@ const LinkFormatRenderers: FormatRenderer[] = [ format => format.relationship, (format, value) => (format.relationship = value) ), + TextColorFormatRenderer, + UnderlineFormatRenderer, ]; export function LinkFormatView(props: { format: LinkFormat }) { diff --git a/demo/scripts/controls/contentModel/components/format/SegmentFormatView.tsx b/demo/scripts/controls/contentModel/components/format/SegmentFormatView.tsx index 8cf9d845502..a28c3bf9553 100644 --- a/demo/scripts/controls/contentModel/components/format/SegmentFormatView.tsx +++ b/demo/scripts/controls/contentModel/components/format/SegmentFormatView.tsx @@ -13,6 +13,7 @@ import { UnderlineFormatRenderer, SuperOrSubScriptFormatRenderer, } from './formatPart/BasicFormatRenderers'; +import { LineHeightFormatRenderer } from './formatPart/LineHeightFormatRenderer'; const SegmentFormatRenders: FormatRenderer[] = [ TextColorFormatRenderer, @@ -24,6 +25,7 @@ const SegmentFormatRenders: FormatRenderer[] = [ UnderlineFormatRenderer, StrikeFormatRenderer, SuperOrSubScriptFormatRenderer, + LineHeightFormatRenderer, ]; export function SegmentFormatView(props: { format: ContentModelSegmentFormat }) { diff --git a/demo/scripts/controls/contentModel/components/format/formatPart/TextColorFormatRenderer.ts b/demo/scripts/controls/contentModel/components/format/formatPart/TextColorFormatRenderer.ts index 3e0f3482aed..c7521b7b47c 100644 --- a/demo/scripts/controls/contentModel/components/format/formatPart/TextColorFormatRenderer.ts +++ b/demo/scripts/controls/contentModel/components/format/formatPart/TextColorFormatRenderer.ts @@ -1,19 +1,13 @@ import * as Color from 'color'; import { createColorFormatRenderer } from '../utils/createColorFormatRender'; import { FormatRenderer } from '../utils/FormatRenderer'; -import { HyperLinkColorPlaceholder } from 'roosterjs-content-model/lib/formatHandlers/utils/defaultStyles'; import { TextColorFormat } from 'roosterjs-content-model'; export const TextColorFormatRenderer: FormatRenderer = createColorFormatRenderer< TextColorFormat >( 'Text color', - format => - format.textColor == HyperLinkColorPlaceholder - ? format.textColor - : format.textColor - ? Color(format.textColor).hex() - : '', + format => (format.textColor ? Color(format.textColor).hex() : ''), (format, value) => { format.textColor = value; return undefined; diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx index 1be3a347626..509cbc870d9 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelImageView.tsx @@ -11,7 +11,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/lib/modelApi/metadata/updateImageMetadata'; +import { updateImageMetadata } from 'roosterjs-content-model'; import { useProperty } from '../../hooks/useProperty'; const styles = require('./ContentModelImageView.scss'); diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx index 6d3df3a0acd..a0dde41d597 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableCellView.tsx @@ -11,7 +11,7 @@ import { MetadataView } from '../format/MetadataView'; import { PaddingFormatRenderer } from '../format/formatPart/PaddingFormatRenderer'; import { TableCellMetadataFormatRender } from '../format/formatPart/TableCellMetadataFormatRender'; import { TextColorFormatRenderer } from '../format/formatPart/TextColorFormatRenderer'; -import { updateTableCellMetadata } from 'roosterjs-content-model/lib/modelApi/metadata/updateTableCellMetadata'; +import { updateTableCellMetadata } from 'roosterjs-content-model'; 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/ContentModelTableView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx index b68644cf9c2..b40037d8b94 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelTableView.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { applyTableFormat } from 'roosterjs-content-model/lib/modelApi/table/applyTableFormat'; import { BackgroundColorFormatRenderer } from '../format/formatPart/BackgroundColorFormatRenderer'; import { BorderBoxFormatRenderer } from '../format/formatPart/BorderBoxFormatRenderer'; import { BorderFormatRenderers } from '../format/formatPart/BorderFormatRenderers'; @@ -13,7 +12,7 @@ import { MarginFormatRenderer } from '../format/formatPart/MarginFormatRenderer' import { MetadataView } from '../format/MetadataView'; import { SpacingFormatRenderer } from '../format/formatPart/SpacingFormatRenderer'; import { TableMetadataFormatRenders } from '../format/formatPart/TableMetadataFormatRenders'; -import { updateTableMetadata } from 'roosterjs-content-model/lib/modelApi/metadata/updateTableMetadata'; +import { updateTableMetadata } from 'roosterjs-content-model'; import { useProperty } from '../../hooks/useProperty'; import { ContentModelTable, @@ -61,26 +60,17 @@ export function ContentModelTableView(props: { table: ContentModelTable }) { ); }, [table]); - const onApplyTableFormat = React.useCallback(() => { - applyTableFormat(table, undefined, true); - }, [table]); - const getFormat = React.useCallback(() => { return ; }, [table.format]); const getMetadata = React.useCallback(() => { return ( - <> - -
- -
- + ); }, [table]); diff --git a/demo/scripts/controls/contentModel/plugins/FormatPainterPlugin.ts b/demo/scripts/controls/contentModel/plugins/FormatPainterPlugin.ts index b2a380b5c63..941c72e4462 100644 --- a/demo/scripts/controls/contentModel/plugins/FormatPainterPlugin.ts +++ b/demo/scripts/controls/contentModel/plugins/FormatPainterPlugin.ts @@ -3,7 +3,7 @@ import { applySegmentFormat, ContentModelSegmentFormat, getSegmentFormat, - IExperimentalContentModelEditor, + IContentModelEditor, } from 'roosterjs-content-model'; const FORMATPAINTERCURSOR_SVG = require('./formatpaintercursor.svg'); @@ -15,14 +15,14 @@ interface FormatPainterFormatHolder { } export default class FormatPainterPlugin implements EditorPlugin { - private editor: IExperimentalContentModelEditor | null = null; + private editor: IContentModelEditor | null = null; getName() { return 'FormatPainter'; } initialize(editor: IEditor) { - this.editor = editor as IExperimentalContentModelEditor; + this.editor = editor as IContentModelEditor; } dispose() { @@ -42,7 +42,7 @@ export default class FormatPainterPlugin implements EditorPlugin { } } - static startFormatPainter(editor: IExperimentalContentModelEditor) { + static startFormatPainter(editor: IContentModelEditor) { const formatHolder = getFormatHolder(editor); const format = getSegmentFormat(editor); diff --git a/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts b/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts deleted file mode 100644 index 4bc47edd64e..00000000000 --- a/demo/scripts/controls/editor/ExperimentalContentModelEditor.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Editor } from 'roosterjs-editor-core'; -import { EditorOptions, SelectionRangeTypes } from 'roosterjs-editor-types'; -import { - ContentModelDocument, - ContentModelSegmentFormat, - contentModelToDom, - domToContentModel, - DomToModelOption, - EditorContext, - IExperimentalContentModelEditor, - ModelToDomOption, -} from 'roosterjs-content-model'; -import { - getComputedStyles, - Position, - restoreContentWithEntityPlaceholder, -} from 'roosterjs-editor-dom'; - -/** - * !!! This is a temporary interface and will be removed in the future !!! - * - * Experimental editor to support Content Model - */ -export default class ExperimentalContentModelEditor extends Editor - implements IExperimentalContentModelEditor { - private getDarkColor: ((lightColor: string) => string) | undefined; - private pendingFormat: ContentModelSegmentFormat | null = null; - - /** - * Creates an instance of ExperimentalContentModelEditor - * @param contentDiv The DIV HTML element which will be the container element of editor - * @param options An optional options object to customize the editor - */ - constructor(private contentDiv: HTMLDivElement, options?: EditorOptions) { - super(contentDiv, options); - this.getDarkColor = options?.getDarkColor; - } - - /** - * Create a EditorContext object used by ContentModel API - */ - createEditorContext(): EditorContext { - return { - isDarkMode: this.isDarkMode(), - zoomScale: this.getZoomScale(), - isRightToLeft: getComputedStyles(this.contentDiv, 'direction')[0] == 'rtl', - getDarkColor: this.getDarkColor, - darkColorHandler: this.getDarkColorHandler(), - }; - } - - /** - * Create Content Model from DOM tree in this editor - * @param startNode 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 option to customize the behavior of DOM to Content Model conversion - */ - createContentModel(startNode?: HTMLElement, option?: DomToModelOption): ContentModelDocument { - return domToContentModel(startNode || this.contentDiv, this.createEditorContext(), { - includeRoot: !!startNode, - selectionRange: this.getSelectionRangeEx(), - alwaysNormalizeTable: true, - ...(option || {}), - }); - } - - /** - * Set content with content model - * @param model The content model to set - * @param mergingCallback A callback to indicate how should the new content be integrated into existing content - * @param option Additional options to customize the behavior of Content Model to DOM conversion - */ - setContentModel(model: ContentModelDocument, option?: ModelToDomOption) { - const [fragment, range, entityPairs] = contentModelToDom( - this.getDocument(), - model, - this.createEditorContext(), - option - ); - const mergingCallback = option?.mergingCallback || restoreContentWithEntityPlaceholder; - - if (range?.type == SelectionRangeTypes.Normal) { - // Need to get start and end from range position before merge because range can be changed during merging - const start = Position.getStart(range.ranges[0]); - const end = Position.getEnd(range.ranges[0]); - - mergingCallback(fragment, this.contentDiv, entityPairs); - this.select(start, end); - } else { - mergingCallback(fragment, this.contentDiv, entityPairs); - this.select(range); - } - } - - /** - * Get current pending format if any. A pending format is a format that user set when selection is collapsed, - * it will be applied when next time user input something - */ - getPendingFormat(): ContentModelSegmentFormat | null { - return this.pendingFormat; - } - - /** - * Set current pending format if any. A pending format is a format that user set when selection is collapsed, - * it will be applied when next time user input something - */ - setPendingFormat(format: ContentModelSegmentFormat | null) { - this.pendingFormat = format; - } -} diff --git a/demo/scripts/controls/editor/isContentModelEditor.ts b/demo/scripts/controls/editor/isContentModelEditor.ts deleted file mode 100644 index e0aa9709076..00000000000 --- a/demo/scripts/controls/editor/isContentModelEditor.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IEditor } from 'roosterjs-editor-types'; -import { IExperimentalContentModelEditor } from 'roosterjs-content-model'; - -export default function isContentModelEditor( - editor: IEditor -): editor is IExperimentalContentModelEditor { - const experimentalEditor = editor as IExperimentalContentModelEditor; - - return !!experimentalEditor.createEditorContext && 'contentDiv' in experimentalEditor; -} diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx index 4988fe18fe4..7d542020de3 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbon.tsx @@ -6,6 +6,7 @@ import { backgroundColorButton } from './backgroundColorButton'; import { blockQuoteButton } from './blockQuoteButton'; import { boldButton } from './boldButton'; import { bulletedListButton } from './bulletedListButton'; +import { changeImageButton } from './changeImageButton'; import { clearFormatButton } from './clearFormatButton'; import { decreaseFontSizeButton } from './decreaseFontSizeButton'; import { decreaseIndentButton } from './decreaseIndentButton'; @@ -13,6 +14,10 @@ import { fontButton } from './fontButton'; import { fontSizeButton } from './fontSizeButton'; import { formatPainterButton } from './formatPainterButton'; import { formatTableButton } from './formatTableButton'; +import { imageBorderColorButton } from './imageBorderColorButton'; +import { imageBorderStyleButton } from './imageBorderStyleButton'; +import { imageBorderWidthButton } from './imageBorderWidthButton'; +import { imageBoxShadowButton } from './imageBoxShadowButton'; import { increaseFontSizeButton } from './increaseFontSizeButton'; import { increaseIndentButton } from './increaseIndentButton'; import { insertImageButton } from './insertImageButton'; @@ -43,6 +48,8 @@ import { tableMergeButton, tableSplitButton, } from './tableEditButtons'; +import { spacingButton } from './spacingButton'; +import { spaceAfterButton, spaceBeforeButton } from './spaceBeforeAfterButtons'; const buttons = [ formatPainterButton, @@ -86,6 +93,14 @@ const buttons = [ tableSplitButton, tableAlignCellButton, tableAlignTableButton, + imageBorderColorButton, + imageBorderWidthButton, + imageBorderStyleButton, + changeImageButton, + imageBoxShadowButton, + spacingButton, + spaceBeforeButton, + spaceAfterButton, ]; export default function ContentModelRibbon(props: { ribbonPlugin: RibbonPlugin; isRtl: boolean }) { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts index ecb78f3c353..0a298ecf4bd 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ContentModelRibbonPlugin.ts @@ -1,10 +1,10 @@ import { FormatState, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; -import { getFormatState, IExperimentalContentModelEditor } from 'roosterjs-content-model'; +import { getFormatState, IContentModelEditor } from 'roosterjs-content-model'; import { getObjectKeys } from 'roosterjs-editor-dom'; import { LocalizedStrings, RibbonButton, RibbonPlugin, UIUtilities } from 'roosterjs-react'; export class ContentModelRibbonPlugin implements RibbonPlugin { - private editor: IExperimentalContentModelEditor | null = null; + private editor: IContentModelEditor | null = null; private onFormatChanged: ((formatState: FormatState) => void) | null = null; private timer = 0; private formatState: FormatState | null = null; @@ -27,7 +27,7 @@ export class ContentModelRibbonPlugin implements RibbonPlugin { * Initialize this plugin * @param editor The editor instance */ - initialize(editor: IExperimentalContentModelEditor) { + initialize(editor: IContentModelEditor) { this.editor = editor; } diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts index f3dd69501ce..d655de0a1c2 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignCenterButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { AlignCenterButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setAlignment } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts index 52cd1db7e2d..901c47d0a10 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignLeftButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { AlignLeftButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setAlignment } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts index ccfcb5f9f24..ebbbf495401 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/alignRightButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { AlignRightButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setAlignment } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts index a3aba65d16f..7c27cc1a8c5 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/backgroundColorButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setBackgroundColor } from 'roosterjs-content-model'; import { BackgroundColorButtonStringKey, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts index 370517521c8..245826715fc 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/blockQuoteButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { QuoteButtonStringKey, RibbonButton } from 'roosterjs-react'; import { toggleBlockQuote } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts index d3d4583cb6a..666a7e30b58 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/boldButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { BoldButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { toggleBold } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts index 27afa71f1e9..48e0ed1c72e 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/bulletedListButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { BulletedListButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { toggleBullet } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts new file mode 100644 index 00000000000..9cbcba58e19 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/changeImageButton.ts @@ -0,0 +1,46 @@ +import { changeImage } from 'roosterjs-content-model'; +import { createElement } from 'roosterjs-editor-dom'; +import { CreateElementData } from 'roosterjs-editor-types'; +import { isContentModelEditor } from 'roosterjs-content-model'; +import { RibbonButton } from 'roosterjs-react'; + +const FileInput: CreateElementData = { + tag: 'input', + attributes: { + type: 'file', + accept: 'image/*', + display: 'none', + }, +}; + +/** + * @internal + * "Change Image" button on the format ribbon + */ +export const changeImageButton: RibbonButton<'buttonNameChangeImage'> = { + key: 'buttonNameChangeImage', + unlocalizedText: 'Change Image', + iconName: 'ImageSearch', + isDisabled: formatState => !formatState.canAddImageAltText, + onClick: editor => { + if (isContentModelEditor(editor)) { + const document = editor.getDocument(); + const fileInput = createElement(FileInput, document) as HTMLInputElement; + document.body.appendChild(fileInput); + + fileInput.addEventListener('change', () => { + if (fileInput.files) { + for (let i = 0; i < fileInput.files.length; i++) { + changeImage(editor, fileInput.files[i]); + } + } + }); + + try { + fileInput.click(); + } finally { + document.body.removeChild(fileInput); + } + } + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts index a63ebcf1f08..8a207142918 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/clearFormatButton.ts @@ -1,6 +1,6 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { clearFormat } from 'roosterjs-content-model'; import { ClearFormatButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; /** * "Clear format" button on the format ribbon diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts index 79e0f5bab6b..513f788f9a8 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseFontSizeButton.ts @@ -1,6 +1,6 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { changeFontSize } from 'roosterjs-content-model'; import { DecreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts index 601cb70ea51..03fe0d42957 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/decreaseIndentButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { DecreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setIndentation } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts index 8406a1dba54..b7d741e743c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { FontButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setFontName } from 'roosterjs-content-model'; interface FontName { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts index e95539a6224..3f55d31627f 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/fontSizeButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { FontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setFontSize } from 'roosterjs-content-model'; 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/formatPainterButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts index 2ed2a024e6a..0172d1f952b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatPainterButton.ts @@ -1,5 +1,5 @@ import FormatPainterPlugin from '../../contentModel/plugins/FormatPainterPlugin'; -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton } from 'roosterjs-react'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts index 1a6d8926037..ce865ad0b24 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/formatTableButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { formatTable } from 'roosterjs-content-model'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { PREDEFINED_STYLES } from '../../sidePane/shared/PredefinedTableStyles'; import { RibbonButton } from 'roosterjs-react'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts new file mode 100644 index 00000000000..e66bd870afb --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderColorButton.ts @@ -0,0 +1,25 @@ +import { getButtons, getTextColorValue, KnownRibbonButtonKey } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; +import { RibbonButton } from 'roosterjs-react'; +import { setImageBorder } from 'roosterjs-content-model'; + +const originalButton = getButtons([KnownRibbonButtonKey.TextColor])[0] as RibbonButton< + 'buttonNameImageBorderColor' +>; + +/** + * @internal + * "Image Border Color" button on the format ribbon + */ +export const imageBorderColorButton: RibbonButton<'buttonNameImageBorderColor'> = { + ...originalButton, + unlocalizedText: 'Image Border Color', + iconName: 'Photo2', + isDisabled: formatState => !formatState.canAddImageAltText, + onClick: (editor, key) => { + // This check will always be true, add it here just to satisfy compiler + if (key != 'buttonNameImageBorderColor' && isContentModelEditor(editor)) { + setImageBorder(editor, { color: getTextColorValue(key).lightModeColor }, '5px'); + } + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts new file mode 100644 index 00000000000..93239482f5e --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderStyleButton.ts @@ -0,0 +1,35 @@ +import { isContentModelEditor } from 'roosterjs-content-model'; +import { RibbonButton } from 'roosterjs-react'; +import { setImageBorder } from 'roosterjs-content-model'; + +const STYLES: Record = { + dashed: 'dashed', + dotted: 'dotted', + solid: 'solid', + double: 'doubled', + groove: 'groove', + ridge: 'ridge', + inset: 'inset', + outset: 'outset', +}; + +/** + * @internal + * "Image Border Style" button on the format ribbon + */ +export const imageBorderStyleButton: RibbonButton<'buttonNameImageBorderStyle'> = { + key: 'buttonNameImageBorderStyle', + unlocalizedText: 'Image Border Style', + iconName: 'BorderDash', + isDisabled: formatState => !formatState.canAddImageAltText, + dropDownMenu: { + items: STYLES, + allowLivePreview: true, + }, + onClick: (editor, style) => { + if (isContentModelEditor(editor)) { + setImageBorder(editor, { style: style }, '5px'); + } + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts new file mode 100644 index 00000000000..dafa8feabac --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBorderWidthButton.ts @@ -0,0 +1,35 @@ +import { isContentModelEditor } from 'roosterjs-content-model'; +import { RibbonButton } from 'roosterjs-react'; +import { setImageBorder } from 'roosterjs-content-model'; + +const WIDTH = [8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72]; + +/** + * @internal + * "Image Border Width" button on the format ribbon + */ +export const imageBorderWidthButton: RibbonButton<'buttonNameImageBorderWidth'> = { + key: 'buttonNameImageBorderWidth', + unlocalizedText: 'Image Border Width', + iconName: 'Photo2', + isDisabled: formatState => !formatState.canAddImageAltText, + dropDownMenu: { + items: WIDTH.reduce((map, size) => { + map[size + 'pt'] = size.toString(); + return map; + }, >{}), + allowLivePreview: true, + }, + onClick: (editor, size) => { + if (isContentModelEditor(editor)) { + setImageBorder( + editor, + { + width: size, + }, + '5px' + ); + } + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts new file mode 100644 index 00000000000..27db40340e0 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/imageBoxShadowButton.ts @@ -0,0 +1,50 @@ +import { isContentModelEditor } from 'roosterjs-content-model'; +import { RibbonButton } from 'roosterjs-react'; +import { setImageBoxShadow } from 'roosterjs-content-model'; + +const STYLES_NAMES: Record = { + noShadow: 'noShadow', + bottomRight: 'bottomRight', + bottom: 'bottom', + bottomLeft: 'bottomLeft', + right: 'right', + center: 'center', + left: 'left', + topRight: 'topRight', + top: 'top', + topLeft: 'topLeft', +}; + +const STYLES: Record = { + noShadow: '', + bottomRight: '4px 4px 3px #aaaaaa', + bottom: '0px 4px 3px 0px #aaaaaa', + bottomLeft: '-4px 4px 3px 3px #aaaaaa', + right: '4px 0px 3px 0px #aaaaaa', + center: '0px 0px 3px 3px #aaaaaa', + left: '-4px 0px 3px 0px #aaaaaa', + topRight: '4px -4px 3px 3px #aaaaaa', + top: '0px -4px 3px 0px #aaaaaa', + topLeft: '-4px -4px 3px 0px #aaaaaa', +}; + +/** + * @internal + * "Image Shadow" button on the format ribbon + */ +export const imageBoxShadowButton: RibbonButton<'buttonNameImageBoxSHadow'> = { + key: 'buttonNameImageBoxSHadow', + unlocalizedText: 'Image Shadow', + iconName: 'Photo2', + isDisabled: formatState => !formatState.canAddImageAltText, + dropDownMenu: { + items: STYLES_NAMES, + allowLivePreview: true, + }, + onClick: (editor, size) => { + if (isContentModelEditor(editor)) { + setImageBoxShadow(editor, STYLES[size]); + } + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts index 6bf0e1fb0ed..9f67d9bf64c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseFontSizeButton.ts @@ -1,6 +1,6 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { changeFontSize } from 'roosterjs-content-model'; import { IncreaseFontSizeButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; /** * @internal diff --git a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts index 5d69bbf40fe..496fe187c20 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/increaseIndentButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { IncreaseIndentButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setIndentation } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts index 5609f981518..6b9fc0a2aa3 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertImageButton.ts @@ -1,8 +1,8 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { createElement } from 'roosterjs-editor-dom'; import { CreateElementData } from 'roosterjs-editor-types'; import { insertImage } from 'roosterjs-content-model'; import { InsertImageButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; const FileInput: CreateElementData = { tag: 'input', diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts index 6fcdefa0b68..5118772e8b3 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertLinkButton.ts @@ -1,7 +1,7 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import showInputDialog from 'roosterjs-react/lib/inputDialog/utils/showInputDialog'; import { adjustLinkSelection, insertLink } from 'roosterjs-content-model'; import { InsertLinkButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; /** * @internal @@ -49,7 +49,6 @@ export const insertLinkButton: RibbonButton = { if ( result && - result.displayText && result.url && (result.displayText != displayText || result.url != url) ) { diff --git a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts index 15a4cd39d64..040bea7bde6 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/insertTableButton.ts @@ -1,7 +1,7 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { getButtons, KnownRibbonButtonKey } from 'roosterjs-react'; import { insertTable } from 'roosterjs-content-model'; import { InsertTableButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; 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 2adfbacc702..bbfe280449c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/italicButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { ItalicButtonStringKey, RibbonButton } from 'roosterjs-react'; import { toggleItalic } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts index 9febc897676..16ed180f8da 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/listStartNumberButton.ts @@ -1,6 +1,6 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import showInputDialog from 'roosterjs-react/lib/inputDialog/utils/showInputDialog'; import { CancelButtonStringKey, OkButtonStringKey, RibbonButton } from 'roosterjs-react'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setListStartNumber } from 'roosterjs-content-model'; /** diff --git a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts index add372d5a50..057bff31762 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/ltrButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { LtrButtonStringKey, RibbonButton } from 'roosterjs-react'; import { setDirection } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts index 80dd6471486..b47c97fce0b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/numberedListButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { NumberedListButtonStringKey, RibbonButton } from 'roosterjs-react'; import { toggleNumbering } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts index f2de6b5163b..2e4c012c34b 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/removeLinkButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { removeLink } from 'roosterjs-content-model'; 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 459b44b7378..7b1c5c73509 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/rtlButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton, RtlButtonStringKey } from 'roosterjs-react'; import { setDirection } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts index 70b2f4fef37..eba5299e6e1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setBulletedListStyleButton.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { BulletListType } from 'roosterjs-editor-types'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton } from 'roosterjs-react'; import { setListStyle } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setHeaderLevelButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setHeaderLevelButton.ts index ee57c48889f..4bb209aab20 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setHeaderLevelButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setHeaderLevelButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setHeaderLevel } from 'roosterjs-content-model'; import { getButtons, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts index 6a27fa1a556..07861d71beb 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setNumberedListStyleButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { NumberingListType } from 'roosterjs-editor-types'; import { RibbonButton } from 'roosterjs-react'; import { setListStyle } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts index 307e371542e..40a47ec5cf1 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableCellShadeButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setTableCellShade } from 'roosterjs-content-model'; import { BackgroundColorKeys, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts index bc81645603f..827d9089b0d 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/setTableHeaderButton.ts @@ -1,6 +1,6 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { formatTable } from 'roosterjs-content-model'; import { getFormatState } from 'roosterjs-editor-api'; +import { isContentModelEditor } from 'roosterjs-content-model'; 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 new file mode 100644 index 00000000000..27c0094a265 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/spaceBeforeAfterButtons.ts @@ -0,0 +1,49 @@ +import { RibbonButton } from 'roosterjs-react'; +import { getFormatState, isContentModelEditor, setParagraphMargin } from 'roosterjs-content-model'; + +const spaceAfterButtonKey = 'buttonNameSpaceAfter'; +const spaceBeforeButtonKey = 'buttonNameSpaceBefore'; + +/** + * @internal + * "Add space after" button on the format ribbon + */ +export const spaceAfterButton: RibbonButton = { + key: spaceAfterButtonKey, + unlocalizedText: 'Remove space after', + iconName: 'CaretDown8', + isChecked: formatState => !formatState.marginBottom || parseInt(formatState.marginBottom) <= 0, + onClick: editor => { + if (isContentModelEditor(editor)) { + const marginBottom = getFormatState(editor).marginBottom; + setParagraphMargin( + editor, + undefined /* marginTop */, + parseInt(marginBottom) ? null : '8pt' + ); + } + return true; + }, +}; + +/** + * @internal + * "Add space before" button on the format ribbon + */ +export const spaceBeforeButton: RibbonButton = { + key: spaceBeforeButtonKey, + unlocalizedText: 'Add space before', + iconName: 'CaretUp8', + isChecked: formatState => parseInt(formatState.marginTop) > 0, + onClick: editor => { + if (isContentModelEditor(editor)) { + const marginTop = getFormatState(editor).marginTop; + setParagraphMargin( + editor, + parseInt(marginTop) ? null : '12pt', + undefined /* marginBottom */ + ); + } + return true; + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts new file mode 100644 index 00000000000..84ce6255cb3 --- /dev/null +++ b/demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts @@ -0,0 +1,37 @@ +import { isContentModelEditor, setSpacing } from 'roosterjs-content-model'; +import type { RibbonButton } from 'roosterjs-react'; + +const SPACING_OPTIONS = ['1.0', '1.15', '1.5', '2.0']; +const NORMAL_SPACING = 1.2; +const spacingButtonKey = 'buttonNameSpacing'; + +function findClosest(lineHeight?: string) { + if (Number.isNaN(+lineHeight)) { + return ''; + } + const query = +lineHeight / NORMAL_SPACING; + return SPACING_OPTIONS.find(opt => Math.abs(query - +opt) < 0.05); +} + +/** + * @internal + * "Spacing" button on the format ribbon + */ +export const spacingButton: RibbonButton = { + key: spacingButtonKey, + unlocalizedText: 'Spacing', + iconName: 'LineSpacing', + dropDownMenu: { + items: SPACING_OPTIONS.reduce((map, size) => { + map[size] = size; + return map; + }, >{}), + getSelectedItemKey: formatState => findClosest(formatState.lineHeight), + allowLivePreview: true, + }, + onClick: (editor, size) => { + if (isContentModelEditor(editor)) { + setSpacing(editor, +size * NORMAL_SPACING); + } + }, +}; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts index 21413b1e1e3..0e5fb706331 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/strikethroughButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton, StrikethroughButtonStringKey } from 'roosterjs-react'; import { toggleStrikethrough } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts index a501e8ec862..897e28deb5c 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/subscriptButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton, SubscriptButtonStringKey } from 'roosterjs-react'; import { toggleSubscript } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts index 8da958758b2..24368d37075 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/superscriptButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton, SuperscriptButtonStringKey } from 'roosterjs-react'; import { toggleSuperscript } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts index 82276a8ab2d..ebfcc836ec5 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/tableEditButtons.ts @@ -1,5 +1,5 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; import { editTable } from 'roosterjs-content-model'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { TableOperation } from 'roosterjs-editor-types'; import { RibbonButton, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts index cfc02a9079b..7c68c1581b4 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/textColorButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setTextColor } from 'roosterjs-content-model'; import { getButtons, diff --git a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts index 7fd42af1ece..776f5a06e37 100644 --- a/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts +++ b/demo/scripts/controls/ribbonButtons/contentModel/underlineButton.ts @@ -1,4 +1,4 @@ -import isContentModelEditor from '../../editor/isContentModelEditor'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton, UnderlineButtonStringKey } from 'roosterjs-react'; import { toggleUnderline } from 'roosterjs-content-model'; diff --git a/demo/scripts/controls/sidePane/contentModel/ContentModelPanePlugin.ts b/demo/scripts/controls/sidePane/contentModel/ContentModelPanePlugin.ts index 53a821dd744..cab6812383f 100644 --- a/demo/scripts/controls/sidePane/contentModel/ContentModelPanePlugin.ts +++ b/demo/scripts/controls/sidePane/contentModel/ContentModelPanePlugin.ts @@ -1,8 +1,8 @@ import ContentModelPane, { ContentModelPaneProps } from './ContentModelPane'; -import isContentModelEditor from '../../editor/isContentModelEditor'; import SidePanePluginImpl from '../SidePanePluginImpl'; import { createRibbonPlugin, RibbonPlugin } from 'roosterjs-react'; import { IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { setCurrentContentModel } from './currentModel'; import { SidePaneElementProps } from '../SidePaneElement'; diff --git a/demo/scripts/controls/sidePane/contentModel/buttons/exportButton.ts b/demo/scripts/controls/sidePane/contentModel/buttons/exportButton.ts index 4b45e811572..ae5f239124a 100644 --- a/demo/scripts/controls/sidePane/contentModel/buttons/exportButton.ts +++ b/demo/scripts/controls/sidePane/contentModel/buttons/exportButton.ts @@ -1,6 +1,6 @@ -import isContentModelEditor from '../../../editor/isContentModelEditor'; import { ChangeSource } from 'roosterjs-editor-types'; import { getCurrentContentModel } from '../currentModel'; +import { isContentModelEditor } from 'roosterjs-content-model'; import { RibbonButton } from 'roosterjs-react'; export const exportButton: RibbonButton<'buttonNameExport'> = { diff --git a/demo/scripts/controls/sidePane/formatState/FormatStatePlugin.ts b/demo/scripts/controls/sidePane/formatState/FormatStatePlugin.ts index 23a238f7fee..56764134905 100644 --- a/demo/scripts/controls/sidePane/formatState/FormatStatePlugin.ts +++ b/demo/scripts/controls/sidePane/formatState/FormatStatePlugin.ts @@ -1,11 +1,13 @@ import FormatStatePane, { FormatStatePaneProps } from './FormatStatePane'; -import isContentModelEditor from '../../editor/isContentModelEditor'; import SidePanePluginImpl from '../SidePanePluginImpl'; import { getFormatState } from 'roosterjs-editor-api'; -import { getFormatState as getFormatStateFromContentModel } from 'roosterjs-content-model'; import { getPositionRect } from 'roosterjs-editor-dom'; import { IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; import { SidePaneElementProps } from '../SidePaneElement'; +import { + getFormatState as getFormatStateFromContentModel, + isContentModelEditor, +} from 'roosterjs-content-model'; export default class FormatStatePlugin extends SidePanePluginImpl< FormatStatePane, diff --git a/package.json b/package.json index e527154b996..df4d6e413b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roosterjs", - "version": "8.41.0", + "version": "8.42.0", "description": "Framework-independent javascript editor", "repository": { "type": "git", diff --git a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts index 38653d33f6a..45f02ed3b11 100644 --- a/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts +++ b/packages/roosterjs-content-model/lib/domToModel/context/createDomToModelContext.ts @@ -2,7 +2,7 @@ import { defaultFormatParsers, getFormatParsers } from '../../formatHandlers/def import { defaultProcessorMap } from './defaultProcessors'; import { defaultStyleMap } from '../../formatHandlers/utils/defaultStyles'; import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; -import { DomToModelOption } from '../../publicTypes/IExperimentalContentModelEditor'; +import { DomToModelOption } from '../../publicTypes/IContentModelEditor'; import { EditorContext } from '../../publicTypes/context/EditorContext'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; @@ -16,13 +16,12 @@ export function createDomToModelContext( const context: DomToModelContext = { ...(editorContext || { isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, getDarkColor: undefined, }), blockFormat: {}, segmentFormat: {}, + zoomScaleFormat: {}, isInSelection: false, listFormat: { @@ -53,10 +52,6 @@ export function createDomToModelContext( defaultFormatParsers: defaultFormatParsers, }; - if (editorContext?.isRightToLeft) { - context.blockFormat.direction = 'rtl'; - } - if (options?.alwaysNormalizeTable) { context.alwaysNormalizeTable = true; } diff --git a/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts b/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts new file mode 100644 index 00000000000..3e61e867c4e --- /dev/null +++ b/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts @@ -0,0 +1,45 @@ +import { computedSegmentFormatHandler } from '../formatHandlers/segment/computedSegmentFormatHandler'; +import { ContentModelDocument } from '../publicTypes/group/ContentModelDocument'; +import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument'; +import { createDomToModelContext } from './context/createDomToModelContext'; +import { DomToModelOption } from '../publicTypes/IContentModelEditor'; +import { EditorContext } from '../publicTypes/context/EditorContext'; +import { normalizeContentModel } from '../modelApi/common/normalizeContentModel'; +import { parseFormat } from './utils/parseFormat'; +import { rootDirectionFormatHandler } from '../formatHandlers/root/rootDirectionFormatHandler'; +import { zoomScaleFormatHandler } from '../formatHandlers/root/zoomScaleFormatHandler'; + +/** + * Create Content Model from DOM tree in this editor + * @param root Root element of DOM tree to create Content Model from + * @param editorContext Context of content model editor + * @param option The option to customize the behavior of DOM to Content Model conversion + * @returns A ContentModelDocument object that contains all the models created from the give root element + */ +export default function domToContentModel( + root: HTMLElement, + editorContext: EditorContext, + option: DomToModelOption +): ContentModelDocument { + const model = createContentModelDocument(); + const context = createDomToModelContext(editorContext, option); + + // For root element, use computed style as initial value of segment formats + parseFormat(root, [computedSegmentFormatHandler.parse], context.segmentFormat, context); + + // Need to calculate direction (ltr or rtl), use it as initial value + parseFormat(root, [rootDirectionFormatHandler.parse], context.blockFormat, context); + + // Need to calculate zoom scale value from root element, use this value to calculate sizes for elements + parseFormat(root, [zoomScaleFormatHandler.parse], context.zoomScaleFormat, context); + + const processor = option.includeRoot + ? context.elementProcessors.element + : context.elementProcessors.child; + + processor(model, root, context); + + normalizeContentModel(model); + + return model; +} diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts index 3a08acbd2e6..d1502f9d4d3 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/knownElementProcessor.ts @@ -16,24 +16,18 @@ import { stackFormat } from '../utils/stackFormat'; */ export const knownElementProcessor: ElementProcessor = (group, element, context) => { const isBlock = isBlockElement(element, context); - const isLink = element.tagName == 'A'; + const isLink = element.tagName == 'A' && element.hasAttribute('href'); stackFormat( context, { segment: isBlock ? 'shallowCloneForBlock' : 'shallowClone', paragraph: 'shallowClone', - link: isLink ? 'empty' : undefined, }, () => { let topDivider: ContentModelDivider | undefined; let bottomDivider: ContentModelDivider | undefined; - if (isLink) { - parseFormat(element, context.formatParsers.link, context.link.format, context); - parseFormat(element, context.formatParsers.dataset, context.link.dataset, context); - } - if (isBlock) { parseFormat(element, context.formatParsers.block, context.blockFormat, context); parseFormat( @@ -84,7 +78,21 @@ export const knownElementProcessor: ElementProcessor = (group, elem parseFormat(element, context.formatParsers.segment, context.segmentFormat, context); } - context.elementProcessors.child(group, element, context); + if (isLink) { + stackFormat(context, { link: 'linkDefault' }, () => { + parseFormat(element, context.formatParsers.link, context.link.format, context); + parseFormat( + element, + context.formatParsers.dataset, + context.link.dataset, + context + ); + + context.elementProcessors.child(group, element, context); + }); + } else { + context.elementProcessors.child(group, element, context); + } if (bottomDivider) { if (context.isInSelection) { diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/listProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/listProcessor.ts index 9b8a565541b..5387bb17b08 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/listProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/listProcessor.ts @@ -4,7 +4,7 @@ import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; import { ElementProcessor } from '../../publicTypes/context/ElementProcessor'; import { parseFormat } from '../utils/parseFormat'; import { stackFormat } from '../utils/stackFormat'; -import { updateListMetadata } from '../../modelApi/metadata/updateListMetadata'; +import { updateListMetadata } from '../../domUtils/metadata/updateListMetadata'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/quoteProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/quoteProcessor.ts index 5a72ee1338f..b3d8d51ca0b 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/quoteProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/quoteProcessor.ts @@ -16,11 +16,11 @@ export const quoteProcessor: ElementProcessor = (group, elemen stackFormat( context, { - paragraph: 'empty', + paragraph: 'shallowCopyInherit', segment: 'shallowCloneForBlock', }, () => { - const quoteFormat: ContentModelBlockFormat = {}; + const quoteFormat: ContentModelBlockFormat = { ...context.blockFormat }; const segmentFormat: ContentModelSegmentFormat = {}; parseFormat(element, context.formatParsers.block, quoteFormat, context); diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts index dfa13ab56fe..e94eb48abc4 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/tableProcessor.ts @@ -26,118 +26,126 @@ export const tableProcessor: ElementProcessor = ( tableElement, context ) => { - const table = createTable(tableElement.rows.length); - const { table: selectedTable, firstCell, lastCell } = context.tableSelection || {}; - const hasTableSelection = selectedTable == tableElement && !!firstCell && !!lastCell; - - stackFormat(context, { segment: 'shallowCloneForBlock', paragraph: 'empty' }, () => { - parseFormat(tableElement, context.formatParsers.table, table.format, context); - parseFormat( - tableElement, - context.formatParsers.segmentOnBlock, - context.segmentFormat, - context - ); - parseFormat(tableElement, context.formatParsers.dataset, table.dataset, context); - addBlock(group, table); - - const columnPositions: number[] = [0]; - const rowPositions: number[] = [0]; - const zoomScale = context.zoomScale; - - for (let row = 0; row < tableElement.rows.length; row++) { - const tr = tableElement.rows[row]; - for (let sourceCol = 0, targetCol = 0; sourceCol < tr.cells.length; sourceCol++) { - for (; table.cells[row][targetCol]; targetCol++) {} - - const td = tr.cells[sourceCol]; - const hasSelectionBeforeCell = context.isInSelection; - const colEnd = targetCol + td.colSpan; - const rowEnd = row + td.rowSpan; - const needCalcWidth = columnPositions[colEnd] === undefined; - const needCalcHeight = rowPositions[rowEnd] === undefined; - - if (needCalcWidth || needCalcHeight) { - const rect = getBoundingClientRect(td); - - if (rect.width > 0 || rect.height > 0) { - if (needCalcWidth) { - columnPositions[colEnd] = - columnPositions[targetCol] + rect.width / zoomScale; - } - - if (needCalcHeight) { - rowPositions[rowEnd] = rowPositions[row] + rect.height / zoomScale; + stackFormat( + context, + { segment: 'shallowCloneForBlock', paragraph: 'shallowCopyInherit' }, + () => { + const table = createTable(tableElement.rows.length, context.blockFormat); + const { table: selectedTable, firstCell, lastCell } = context.tableSelection || {}; + const hasTableSelection = selectedTable == tableElement && !!firstCell && !!lastCell; + + parseFormat(tableElement, context.formatParsers.table, table.format, context); + parseFormat( + tableElement, + context.formatParsers.segmentOnBlock, + context.segmentFormat, + context + ); + parseFormat(tableElement, context.formatParsers.dataset, table.dataset, context); + addBlock(group, table); + + const columnPositions: number[] = [0]; + const rowPositions: number[] = [0]; + const zoomScale = context.zoomScaleFormat.zoomScale || 1; + + for (let row = 0; row < tableElement.rows.length; row++) { + const tr = tableElement.rows[row]; + for (let sourceCol = 0, targetCol = 0; sourceCol < tr.cells.length; sourceCol++) { + for (; table.cells[row][targetCol]; targetCol++) {} + + const td = tr.cells[sourceCol]; + const hasSelectionBeforeCell = context.isInSelection; + const colEnd = targetCol + td.colSpan; + const rowEnd = row + td.rowSpan; + const needCalcWidth = columnPositions[colEnd] === undefined; + const needCalcHeight = rowPositions[rowEnd] === undefined; + + if (needCalcWidth || needCalcHeight) { + const rect = getBoundingClientRect(td); + + if (rect.width > 0 || rect.height > 0) { + if (needCalcWidth) { + columnPositions[colEnd] = + columnPositions[targetCol] + rect.width / zoomScale; + } + + if (needCalcHeight) { + rowPositions[rowEnd] = rowPositions[row] + rect.height / zoomScale; + } } } - } - for (let colSpan = 1; colSpan <= td.colSpan; colSpan++, targetCol++) { - for (let rowSpan = 1; rowSpan <= td.rowSpan; rowSpan++) { - const hasTd = colSpan == 1 && rowSpan == 1; - const cell = createTableCell(colSpan > 1, rowSpan > 1, td.tagName == 'TH'); - - table.cells[row + rowSpan - 1][targetCol] = cell; - - if (hasTd) { - stackFormat(context, { segment: 'shallowClone' }, () => { - parseFormat( - td, - context.formatParsers.tableCell, - cell.format, - context - ); - parseFormat( - td, - context.formatParsers.segmentOnTableCell, - context.segmentFormat, - context - ); - parseFormat( - td, - context.formatParsers.dataset, - cell.dataset, - context - ); - - const { listParent, levels } = context.listFormat; - - context.listFormat.listParent = undefined; - context.listFormat.levels = []; - - try { - context.elementProcessors.child(cell, td, context); - } finally { - context.listFormat.listParent = listParent; - context.listFormat.levels = levels; - } - }); - } - - const hasSelectionAfterCell = context.isInSelection; - - if ( - (hasSelectionBeforeCell && hasSelectionAfterCell) || - (hasTableSelection && - row >= firstCell.y && - row <= lastCell.y && - targetCol >= firstCell.x && - targetCol <= lastCell.x) - ) { - cell.isSelected = true; + for (let colSpan = 1; colSpan <= td.colSpan; colSpan++, targetCol++) { + for (let rowSpan = 1; rowSpan <= td.rowSpan; rowSpan++) { + const hasTd = colSpan == 1 && rowSpan == 1; + const cell = createTableCell( + colSpan > 1, + rowSpan > 1, + td.tagName == 'TH' + ); + + table.cells[row + rowSpan - 1][targetCol] = cell; + + if (hasTd) { + stackFormat(context, { segment: 'shallowClone' }, () => { + parseFormat( + td, + context.formatParsers.tableCell, + cell.format, + context + ); + parseFormat( + td, + context.formatParsers.segmentOnTableCell, + context.segmentFormat, + context + ); + parseFormat( + td, + context.formatParsers.dataset, + cell.dataset, + context + ); + + const { listParent, levels } = context.listFormat; + + context.listFormat.listParent = undefined; + context.listFormat.levels = []; + + try { + context.elementProcessors.child(cell, td, context); + } finally { + context.listFormat.listParent = listParent; + context.listFormat.levels = levels; + } + }); + } + + const hasSelectionAfterCell = context.isInSelection; + + if ( + (hasSelectionBeforeCell && hasSelectionAfterCell) || + (hasTableSelection && + row >= firstCell.y && + row <= lastCell.y && + targetCol >= firstCell.x && + targetCol <= lastCell.x) + ) { + cell.isSelected = true; + } } } } } - } - table.widths = calcSizes(columnPositions); - table.heights = calcSizes(rowPositions); + table.widths = calcSizes(columnPositions); + table.heights = calcSizes(rowPositions); - if (context.alwaysNormalizeTable) { - normalizeTable(table); + if (context.alwaysNormalizeTable) { + normalizeTable(table); + } } - }); + ); }; function calcSizes(positions: number[]): number[] { diff --git a/packages/roosterjs-content-model/lib/domToModel/utils/getDefaultStyle.ts b/packages/roosterjs-content-model/lib/domToModel/utils/getDefaultStyle.ts index bef61a8e267..ab4f5bb8330 100644 --- a/packages/roosterjs-content-model/lib/domToModel/utils/getDefaultStyle.ts +++ b/packages/roosterjs-content-model/lib/domToModel/utils/getDefaultStyle.ts @@ -14,10 +14,5 @@ export function getDefaultStyle( ): Partial { let tag = element.tagName.toLowerCase() as keyof DefaultStyleMap; - if (tag == 'a' && !element.hasAttribute('href')) { - // For A tag without Href, treat it as SPAN since it will not be rendered as a link - tag = 'span'; - } - return context.defaultStyles[tag] || {}; } diff --git a/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts b/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts index 6847f81ddda..bf18a85eb27 100644 --- a/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts +++ b/packages/roosterjs-content-model/lib/domToModel/utils/stackFormat.ts @@ -1,24 +1,17 @@ +import { ContentModelBlockFormat } from '../../publicTypes/format/ContentModelBlockFormat'; import { ContentModelFormatBase } from '../../publicTypes/format/ContentModelFormatBase'; +import { ContentModelLink } from '../../publicTypes/decorator/ContentModelLink'; import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { DomToModelContext } from '../../publicTypes/context/DomToModelContext'; - -/** - * @internal - */ -export type ObjectStackType = 'empty'; - -/** - * @internal - */ -export type ShallowObjectStackType = 'shallowClone' | 'shallowCloneForBlock' | ObjectStackType; +import { getObjectKeys } from 'roosterjs-editor-dom'; /** * @internal */ export interface StackFormatOptions { - segment?: ShallowObjectStackType; - paragraph?: ShallowObjectStackType; - link?: ObjectStackType; + segment?: 'shallowClone' | 'shallowCloneForBlock' | 'empty'; + paragraph?: 'shallowClone' | 'shallowCopyInherit' | 'empty'; + link?: 'linkDefault' | 'empty'; } // Some styles, such as background color, won't be inherited by block element if it was originally @@ -30,6 +23,15 @@ export interface StackFormatOptions { // const SkippedStylesForBlock: (keyof ContentModelSegmentFormat)[] = ['backgroundColor']; +const CopiedStylesForBlockInherit: (keyof ContentModelBlockFormat)[] = [ + 'backgroundColor', + 'direction', + 'textAlign', + 'isTextAlignFromAttr', + 'lineHeight', + 'whiteSpace', +]; + /** * @internal */ @@ -44,13 +46,7 @@ export function stackFormat( try { context.segmentFormat = stackFormatInternal(segmentFormat, segment); context.blockFormat = stackFormatInternal(blockFormat, paragraph); - context.link = - link == 'empty' - ? { - format: {}, - dataset: {}, - } - : linkFormat; + context.link = stackLinkInternal(linkFormat, link); callback(); } finally { @@ -60,22 +56,54 @@ export function stackFormat( } } +function stackLinkInternal(linkFormat: ContentModelLink, link?: 'linkDefault' | 'empty') { + switch (link) { + case 'linkDefault': + return { + format: { + underline: true, + }, + dataset: {}, + }; + + case 'empty': + return { + format: {}, + dataset: {}, + }; + + default: + return linkFormat; + } +} + function stackFormatInternal( format: T, - processType: ShallowObjectStackType | undefined + processType?: 'shallowClone' | 'shallowCloneForBlock' | 'shallowCopyInherit' | 'empty' ): T | {} { - const result = - processType == 'empty' - ? {} - : processType == 'shallowClone' || processType == 'shallowCloneForBlock' - ? { ...format } - : format; + switch (processType) { + case 'empty': + return {}; - if (processType == 'shallowCloneForBlock') { - SkippedStylesForBlock.forEach(key => { - delete (result as ContentModelSegmentFormat)[key]; - }); - } + case undefined: + return format; + + default: + const result = { ...format }; - return result; + getObjectKeys(format).forEach(key => { + if ( + (processType == 'shallowCloneForBlock' && + SkippedStylesForBlock.indexOf(key as keyof ContentModelSegmentFormat) >= + 0) || + (processType == 'shallowCopyInherit' && + CopiedStylesForBlockInherit.indexOf(key as keyof ContentModelBlockFormat) < + 0) + ) { + delete result[key]; + } + }); + + return result; + } } diff --git a/packages/roosterjs-content-model/lib/domUtils/borderValues.ts b/packages/roosterjs-content-model/lib/domUtils/borderValues.ts index 6d7946ebe17..1391b09b900 100644 --- a/packages/roosterjs-content-model/lib/domUtils/borderValues.ts +++ b/packages/roosterjs-content-model/lib/domUtils/borderValues.ts @@ -1,23 +1,4 @@ -/** - * A combination of CSS border value. - * See https://developer.mozilla.org/en-US/docs/Web/CSS/border for more information - */ -export interface Border { - /** - * Width of the border - */ - width?: string; - - /** - * Style of the border - */ - style?: string; - - /** - * Color of the border - */ - color?: string; -} +import { Border } from '../publicTypes/interface/Border'; const BorderStyles = [ 'none', diff --git a/packages/roosterjs-content-model/lib/domUtils/index.ts b/packages/roosterjs-content-model/lib/domUtils/index.ts new file mode 100644 index 00000000000..6d9db842fe8 --- /dev/null +++ b/packages/roosterjs-content-model/lib/domUtils/index.ts @@ -0,0 +1,6 @@ +export { combineBorderValue, extractBorderValues } from './borderValues'; +export { updateImageMetadata } from './metadata/updateImageMetadata'; +export { updateListMetadata } from './metadata/updateListMetadata'; +export { updateMetadata } from './metadata/updateMetadata'; +export { updateTableCellMetadata } from './metadata/updateTableCellMetadata'; +export { updateTableMetadata } from './metadata/updateTableMetadata'; diff --git a/packages/roosterjs-content-model/lib/modelApi/metadata/updateImageMetadata.ts b/packages/roosterjs-content-model/lib/domUtils/metadata/updateImageMetadata.ts similarity index 100% rename from packages/roosterjs-content-model/lib/modelApi/metadata/updateImageMetadata.ts rename to packages/roosterjs-content-model/lib/domUtils/metadata/updateImageMetadata.ts diff --git a/packages/roosterjs-content-model/lib/modelApi/metadata/updateListMetadata.ts b/packages/roosterjs-content-model/lib/domUtils/metadata/updateListMetadata.ts similarity index 100% rename from packages/roosterjs-content-model/lib/modelApi/metadata/updateListMetadata.ts rename to packages/roosterjs-content-model/lib/domUtils/metadata/updateListMetadata.ts diff --git a/packages/roosterjs-content-model/lib/modelApi/metadata/updateMetadata.ts b/packages/roosterjs-content-model/lib/domUtils/metadata/updateMetadata.ts similarity index 100% rename from packages/roosterjs-content-model/lib/modelApi/metadata/updateMetadata.ts rename to packages/roosterjs-content-model/lib/domUtils/metadata/updateMetadata.ts diff --git a/packages/roosterjs-content-model/lib/modelApi/metadata/updateTableCellMetadata.ts b/packages/roosterjs-content-model/lib/domUtils/metadata/updateTableCellMetadata.ts similarity index 100% rename from packages/roosterjs-content-model/lib/modelApi/metadata/updateTableCellMetadata.ts rename to packages/roosterjs-content-model/lib/domUtils/metadata/updateTableCellMetadata.ts diff --git a/packages/roosterjs-content-model/lib/modelApi/metadata/updateTableMetadata.ts b/packages/roosterjs-content-model/lib/domUtils/metadata/updateTableMetadata.ts similarity index 100% rename from packages/roosterjs-content-model/lib/modelApi/metadata/updateTableMetadata.ts rename to packages/roosterjs-content-model/lib/domUtils/metadata/updateTableMetadata.ts diff --git a/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts b/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts new file mode 100644 index 00000000000..a7ca749c569 --- /dev/null +++ b/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts @@ -0,0 +1,73 @@ +import contentModelToDom from '../modelToDom/contentModelToDom'; +import domToContentModel from '../domToModel/domToContentModel'; +import { ContentModelDocument } from '../publicTypes/group/ContentModelDocument'; +import { Editor } from 'roosterjs-editor-core'; +import { EditorContext } from '../publicTypes/context/EditorContext'; +import { Position, restoreContentWithEntityPlaceholder } from 'roosterjs-editor-dom'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; + +import { + DomToModelOption, + IContentModelEditor, + ModelToDomOption, +} from '../publicTypes/IContentModelEditor'; + +/** + * Editor for Content Model. + * (This class is still under development, and may still be changed in the future with some breaking changes) + */ +export default class ContentModelEditor extends Editor implements IContentModelEditor { + /** + * Create Content Model from DOM tree in this editor + * @param option The option to customize the behavior of DOM to Content Model conversion + */ + createContentModel(option?: DomToModelOption): ContentModelDocument { + const core = this.getCore(); + + return domToContentModel(core.contentDiv, this.createEditorContext(), { + selectionRange: this.getSelectionRangeEx(), + alwaysNormalizeTable: true, + ...(option || {}), + }); + } + + /** + * 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 + */ + setContentModel(model: ContentModelDocument, option?: ModelToDomOption) { + const [fragment, range, entityPairs] = contentModelToDom( + this.getDocument(), + model, + this.createEditorContext(), + option + ); + const core = this.getCore(); + + if (range?.type == SelectionRangeTypes.Normal) { + // Need to get start and end from range position before merge because range can be changed during merging + const start = Position.getStart(range.ranges[0]); + const end = Position.getEnd(range.ranges[0]); + + restoreContentWithEntityPlaceholder(fragment, core.contentDiv, entityPairs); + this.select(start, end); + } else { + restoreContentWithEntityPlaceholder(fragment, core.contentDiv, entityPairs); + this.select(range); + } + } + + /** + * Create a EditorContext object used by ContentModel API + */ + private createEditorContext(): EditorContext { + const core = this.getCore(); + + return { + isDarkMode: this.isDarkMode(), + getDarkColor: core.lifecycle.getDarkColor, + darkColorHandler: this.getDarkColorHandler(), + }; + } +} diff --git a/packages/roosterjs-content-model/lib/editor/ContentModelPlugin.ts b/packages/roosterjs-content-model/lib/editor/ContentModelPlugin.ts new file mode 100644 index 00000000000..c1e6a17842b --- /dev/null +++ b/packages/roosterjs-content-model/lib/editor/ContentModelPlugin.ts @@ -0,0 +1,86 @@ +import applyPendingFormat from '../publicApi/format/applyPendingFormat'; +import { canApplyPendingFormat, clearPendingFormat } from '../modelApi/format/pendingFormat'; +import { EditorPlugin, IEditor, Keys, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; +import { IContentModelEditor } from '../publicTypes/IContentModelEditor'; + +/** + * ContentModel plugins helps editor to do editing operation on top of content model. + * This includes: + * 1. Handle pending format changes when selection is collapsed + */ +export default class ContentModelPlugin implements EditorPlugin { + private editor: IContentModelEditor | null = null; + + /** + * Get name of this plugin + */ + getName() { + return 'ContentModel'; + } + + /** + * The first method that editor will call to a plugin when editor is initializing. + * It will pass in the editor instance, plugin should take this chance to save the + * editor reference so that it can call to any editor method or format API later. + * @param editor The editor object + */ + initialize(editor: IEditor) { + // TODO: Later we may need a different interface for Content Model editor plugin + this.editor = editor as IContentModelEditor; + } + + /** + * The last method that editor will call to a plugin before it is disposed. + * Plugin can take this chance to clear the reference to editor. After this method is + * called, plugin should not call to any editor method since it will result in error. + */ + dispose() { + this.editor = null; + } + + /** + * Core method for a plugin. Once an event happens in editor, editor will call this + * method of each plugin to handle the event as long as the event is not handled + * exclusively by another plugin. + * @param event The event to handle: + */ + onPluginEvent(event: PluginEvent) { + if (!this.editor) { + return; + } + + switch (event.eventType) { + case PluginEventType.Input: + // In Safari, isComposing will be undefined but isInIME() works + if (!event.rawEvent.isComposing && !this.editor.isInIME()) { + this.checkAndApplyPendingFormat(event.rawEvent.data); + } + + break; + + case PluginEventType.CompositionEnd: + this.checkAndApplyPendingFormat(event.rawEvent.data); + break; + + case PluginEventType.KeyDown: + if (event.rawEvent.which >= Keys.PAGEUP && event.rawEvent.which <= Keys.DOWN) { + clearPendingFormat(this.editor); + } + break; + + case PluginEventType.MouseUp: + case PluginEventType.ContentChanged: + if (!canApplyPendingFormat(this.editor)) { + clearPendingFormat(this.editor); + } + break; + } + } + + private checkAndApplyPendingFormat(data: string | null) { + if (this.editor && data) { + applyPendingFormat(this.editor, data); + clearPendingFormat(this.editor); + } + } +} diff --git a/packages/roosterjs-content-model/lib/editor/index.ts b/packages/roosterjs-content-model/lib/editor/index.ts new file mode 100644 index 00000000000..4eee12cf5d8 --- /dev/null +++ b/packages/roosterjs-content-model/lib/editor/index.ts @@ -0,0 +1,3 @@ +export { default as ContentModelEditor } from './ContentModelEditor'; +export { default as isContentModelEditor } from './isContentModelEditor'; +export { default as ContentModelPlugin } from './ContentModelPlugin'; diff --git a/packages/roosterjs-content-model/lib/editor/isContentModelEditor.ts b/packages/roosterjs-content-model/lib/editor/isContentModelEditor.ts new file mode 100644 index 00000000000..7891d4976c5 --- /dev/null +++ b/packages/roosterjs-content-model/lib/editor/isContentModelEditor.ts @@ -0,0 +1,8 @@ +import { IContentModelEditor } from '../publicTypes/IContentModelEditor'; +import { IEditor } from 'roosterjs-editor-types'; + +export default function isContentModelEditor(editor: IEditor): editor is IContentModelEditor { + const contentModelEditor = editor as IContentModelEditor; + + return !!contentModelEditor.createContentModel; +} diff --git a/packages/roosterjs-content-model/lib/formatHandlers/block/whiteSpaceFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/block/whiteSpaceFormatHandler.ts index 46dacfbd709..8cc178ff2c9 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/block/whiteSpaceFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/block/whiteSpaceFormatHandler.ts @@ -13,7 +13,12 @@ export const whiteSpaceFormatHandler: FormatHandler = { } }, apply: (format, element) => { - if (format.whiteSpace) { + if (format.whiteSpace == 'pre') { + const pre = element.ownerDocument.createElement('pre'); + + element.parentNode?.appendChild(pre); + pre.appendChild(element); + } else if (format.whiteSpace) { element.style.whiteSpace = format.whiteSpace; } }, diff --git a/packages/roosterjs-content-model/lib/formatHandlers/common/borderFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/common/borderFormatHandler.ts index 06e92c22649..98f9dac609f 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/common/borderFormatHandler.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/common/borderFormatHandler.ts @@ -9,6 +9,7 @@ export const BorderKeys: (keyof BorderFormat & keyof CSSStyleDeclaration)[] = [ 'borderRight', 'borderBottom', 'borderLeft', + 'borderRadius', ]; /** diff --git a/packages/roosterjs-content-model/lib/formatHandlers/common/boxShadowFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/common/boxShadowFormatHandler.ts new file mode 100644 index 00000000000..aac8557c86a --- /dev/null +++ b/packages/roosterjs-content-model/lib/formatHandlers/common/boxShadowFormatHandler.ts @@ -0,0 +1,18 @@ +import { BoxShadowFormat } from '../../publicTypes/format/formatParts/BoxShadowFormat'; +import { FormatHandler } from '../FormatHandler'; + +/** + * @internal + */ +export const boxShadowFormatHandler: FormatHandler = { + parse: (format, element) => { + if (element.style?.boxShadow) { + format.boxShadow = element.style.boxShadow; + } + }, + apply: (format, element) => { + if (format.boxShadow) { + element.style.boxShadow = format.boxShadow; + } + }, +}; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts b/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts index c9e7a64a849..8a66f1ad841 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/defaultFormatHandlers.ts @@ -2,6 +2,7 @@ import { backgroundColorFormatHandler } from './common/backgroundColorFormatHand import { boldFormatHandler } from './segment/boldFormatHandler'; import { borderBoxFormatHandler } from './common/borderBoxFormatHandler'; import { borderFormatHandler } from './common/borderFormatHandler'; +import { boxShadowFormatHandler } from './common/boxShadowFormatHandler'; import { ContentModelFormatMap } from '../publicTypes/format/ContentModelFormatMap'; import { datasetFormatHandler } from './common/datasetFormatHandler'; import { directionFormatHandler } from './block/directionFormatHandler'; @@ -51,6 +52,7 @@ const defaultFormatHandlerMap: FormatHandlers = { bold: boldFormatHandler, border: borderFormatHandler, borderBox: borderBoxFormatHandler, + boxShadow: boxShadowFormatHandler, dataset: datasetFormatHandler, direction: directionFormatHandler, display: displayFormatHandler, @@ -104,6 +106,7 @@ const defaultFormatKeysPerCategory: { 'bold', 'textColor', 'backgroundColor', + 'lineHeight', ], segmentOnBlock: ['fontFamily', 'fontSize', 'underline', 'italic', 'bold', 'textColor'], segmentOnTableCell: ['fontFamily', 'fontSize', 'underline', 'italic', 'bold'], @@ -127,8 +130,8 @@ const defaultFormatKeysPerCategory: { 'display', 'direction', ], - image: ['id', 'size', 'margin', 'padding', 'borderBox'], - link: ['link'], + image: ['id', 'size', 'margin', 'padding', 'borderBox', 'border', 'boxShadow'], + link: ['link', 'textColor', 'underline'], dataset: ['dataset'], divider: [...blockFormatHandlers, 'display', 'size'], }; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/root/rootDirectionFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/root/rootDirectionFormatHandler.ts new file mode 100644 index 00000000000..c05b9780a08 --- /dev/null +++ b/packages/roosterjs-content-model/lib/formatHandlers/root/rootDirectionFormatHandler.ts @@ -0,0 +1,16 @@ +import { DirectionFormat } from '../../publicTypes/format/formatParts/DirectionFormat'; +import { FormatHandler } from '../FormatHandler'; + +/** + * @internal + */ +export const rootDirectionFormatHandler: FormatHandler = { + parse: (format, element) => { + const style = element.ownerDocument.defaultView?.getComputedStyle(element); + + if (style?.direction == 'rtl') { + format.direction = 'rtl'; + } + }, + apply: () => {}, +}; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/root/zoomScaleFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/root/zoomScaleFormatHandler.ts new file mode 100644 index 00000000000..a5c81825c73 --- /dev/null +++ b/packages/roosterjs-content-model/lib/formatHandlers/root/zoomScaleFormatHandler.ts @@ -0,0 +1,18 @@ +import { FormatHandler } from '../FormatHandler'; +import { ZoomScaleFormat } from '../../publicTypes/format/formatParts/ZoomScaleFormat'; + +/** + * @internal + */ +export const zoomScaleFormatHandler: FormatHandler = { + parse: (format, element) => { + const originalWidth = element.getBoundingClientRect().width; + const visualWidth = element.offsetWidth; + + format.zoomScale = + visualWidth > 0 && originalWidth > 0 + ? Math.round((originalWidth / visualWidth) * 100) / 100 + : 1; + }, + apply: () => {}, +}; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/segment/computedSegmentFormatHandler.ts b/packages/roosterjs-content-model/lib/formatHandlers/segment/computedSegmentFormatHandler.ts new file mode 100644 index 00000000000..6d52162e62d --- /dev/null +++ b/packages/roosterjs-content-model/lib/formatHandlers/segment/computedSegmentFormatHandler.ts @@ -0,0 +1,28 @@ +import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; +import { FormatHandler } from '../FormatHandler'; +import { parseValueWithUnit } from '../utils/parseValueWithUnit'; + +/** + * @internal + */ +export const computedSegmentFormatHandler: FormatHandler = { + parse: (format, element) => { + const computedStyles = element.ownerDocument.defaultView?.getComputedStyle(element); + + if (computedStyles) { + const { fontFamily, fontSize } = computedStyles; + + // Only read font family and size from root container. + // For others, keep them undefined since it is not normal to have bold/italic/... in root container + // and we skip colors on purpose since we will get inverted color in dark mode which is not right. + if (fontFamily) { + format.fontFamily = fontFamily; + } + + if (fontSize) { + format.fontSize = parseValueWithUnit(fontSize, undefined /*element*/, 'pt') + 'pt'; + } + } + }, + apply: () => {}, +}; diff --git a/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts b/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts index 0860b37df18..ca0086f443d 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/utils/defaultStyles.ts @@ -5,16 +5,10 @@ const blockElement: Partial = { display: 'block', }; -export const HyperLinkColorPlaceholder = '__hyperLinkColor'; - /** * @internal */ export const defaultStyleMap: DefaultStyleMap = { - a: { - textDecoration: 'underline', - color: HyperLinkColorPlaceholder, - }, address: blockElement, article: blockElement, aside: blockElement, @@ -135,7 +129,6 @@ export const defaultStyleMap: DefaultStyleMap = { export const defaultImplicitFormatMap: DefaultImplicitFormatMap = { a: { underline: true, - textColor: HyperLinkColorPlaceholder, }, blockquote: { marginTop: '1em', diff --git a/packages/roosterjs-content-model/lib/formatHandlers/utils/parseValueWithUnit.ts b/packages/roosterjs-content-model/lib/formatHandlers/utils/parseValueWithUnit.ts index f84505baabc..9d7be4273ec 100644 --- a/packages/roosterjs-content-model/lib/formatHandlers/utils/parseValueWithUnit.ts +++ b/packages/roosterjs-content-model/lib/formatHandlers/utils/parseValueWithUnit.ts @@ -5,7 +5,11 @@ const MarginValueRegex = /(-?\d+(\.\d+)?)([a-z]+|%)/; /** * @internal */ -export function parseValueWithUnit(value: string = '', element?: HTMLElement): number { +export function parseValueWithUnit( + value: string = '', + element?: HTMLElement, + resultUnit: 'px' | 'pt' = 'px' +): number { const match = MarginValueRegex.exec(value); let result = 0; @@ -35,6 +39,10 @@ export function parseValueWithUnit(value: string = '', element?: HTMLElement): n } } + if (result > 0 && resultUnit == 'pt') { + result = pxToPt(result); + } + return result; } @@ -49,3 +57,7 @@ function getFontSize(element: HTMLElement) { function ptToPx(pt: number): number { return Math.round((pt * 4000) / 3) / 1000; } + +function pxToPt(px: number) { + return Math.round((px * 3000) / 4) / 1000; +} diff --git a/packages/roosterjs-content-model/lib/index.ts b/packages/roosterjs-content-model/lib/index.ts index 9f1c7076af1..1c09bf3054a 100644 --- a/packages/roosterjs-content-model/lib/index.ts +++ b/packages/roosterjs-content-model/lib/index.ts @@ -1,174 +1,4 @@ -export { default as domToContentModel } from './publicApi/domToContentModel'; -export { default as contentModelToDom } from './publicApi/contentModelToDom'; -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 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/insert/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 setHeaderLevel } from './publicApi/block/setHeaderLevel'; -export { default as toggleBlockQuote } from './publicApi/block/toggleBlockQuote'; -export { default as getFormatState } from './publicApi/format/getFormatState'; -export { default as getSegmentFormat } from './publicApi/format/getSegmentFormat'; -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 ContentModelPlugin } from './publicPlugin/ContentModelPlugin'; - -export { combineBorderValue, extractBorderValues, Border } from './domUtils/borderValues'; - -export { ContentModelBlockGroupType } from './publicTypes/enum/BlockGroupType'; -export { ContentModelBlockType } from './publicTypes/enum/BlockType'; -export { ContentModelSegmentType } from './publicTypes/enum/SegmentType'; -export { Selectable } from './publicTypes/selection/Selectable'; - -export { ContentModelBlockBase } from './publicTypes/block/ContentModelBlockBase'; -export { ContentModelTable } from './publicTypes/block/ContentModelTable'; -export { ContentModelBlockGroupBase } from './publicTypes/group/ContentModelBlockGroupBase'; -export { ContentModelDocument } from './publicTypes/group/ContentModelDocument'; -export { ContentModelQuote } from './publicTypes/group/ContentModelQuote'; -export { ContentModelListItem } from './publicTypes/group/ContentModelListItem'; -export { ContentModelTableCell } from './publicTypes/group/ContentModelTableCell'; -export { ContentModelGeneralBlock } from './publicTypes/group/ContentModelGeneralBlock'; -export { ContentModelBlockGroup } from './publicTypes/group/ContentModelBlockGroup'; -export { ContentModelBlock } from './publicTypes/block/ContentModelBlock'; -export { ContentModelParagraph } from './publicTypes/block/ContentModelParagraph'; -export { ContentModelSegmentBase } from './publicTypes/segment/ContentModelSegmentBase'; -export { ContentModelSelectionMarker } from './publicTypes/segment/ContentModelSelectionMarker'; -export { ContentModelText } from './publicTypes/segment/ContentModelText'; -export { ContentModelBr } from './publicTypes/segment/ContentModelBr'; -export { ContentModelImage } from './publicTypes/segment/ContentModelImage'; -export { ContentModelGeneralSegment } from './publicTypes/segment/ContentModelGeneralSegment'; -export { ContentModelSegment } from './publicTypes/segment/ContentModelSegment'; -export { ContentModelEntity } from './publicTypes/entity/ContentModelEntity'; -export { ContentModelDivider } from './publicTypes/block/ContentModelDivider'; - -export { ContentModelParagraphDecorator } from './publicTypes/decorator/ContentModelParagraphDecorator'; -export { ContentModelLink } from './publicTypes/decorator/ContentModelLink'; - -export { FormatHandlerTypeMap, FormatKey } from './publicTypes/format/FormatHandlerTypeMap'; -export { ContentModelTableFormat } from './publicTypes/format/ContentModelTableFormat'; -export { ContentModelTableCellFormat } from './publicTypes/format/ContentModelTableCellFormat'; -export { ContentModelBlockFormat } from './publicTypes/format/ContentModelBlockFormat'; -export { ContentModelSegmentFormat } from './publicTypes/format/ContentModelSegmentFormat'; -export { ContentModelListItemLevelFormat } from './publicTypes/format/ContentModelListItemLevelFormat'; -export { ContentModelImageFormat } from './publicTypes/format/ContentModelImageFormat'; -export { ContentModelWithFormat } from './publicTypes/format/ContentModelWithFormat'; -export { ContentModelWithDataset } from './publicTypes/format/ContentModelWithDataset'; -export { ContentModelDividerFormat } from './publicTypes/format/ContentModelDividerFormat'; - -export { VerticalAlignFormat } from './publicTypes/format/formatParts/VerticalAlignFormat'; -export { BackgroundColorFormat } from './publicTypes/format/formatParts/BackgroundColorFormat'; -export { BorderFormat } from './publicTypes/format/formatParts/BorderFormat'; -export { BorderBoxFormat } from './publicTypes/format/formatParts/BorderBoxFormat'; -export { IdFormat } from './publicTypes/format/formatParts/IdFormat'; -export { SizeFormat } from './publicTypes/format/formatParts/SizeFormat'; -export { SpacingFormat } from './publicTypes/format/formatParts/SpacingFormat'; -export { DirectionFormat } from './publicTypes/format/formatParts/DirectionFormat'; -export { TextColorFormat } from './publicTypes/format/formatParts/TextColorFormat'; -export { FontSizeFormat } from './publicTypes/format/formatParts/FontSizeFormat'; -export { FontFamilyFormat } from './publicTypes/format/formatParts/FontFamilyFormat'; -export { BoldFormat } from './publicTypes/format/formatParts/BoldFormat'; -export { ItalicFormat } from './publicTypes/format/formatParts/ItalicFormat'; -export { UnderlineFormat } from './publicTypes/format/formatParts/UnderlineFormat'; -export { StrikeFormat } from './publicTypes/format/formatParts/StrikeFormat'; -export { SuperOrSubScriptFormat } from './publicTypes/format/formatParts/SuperOrSubScriptFormat'; -export { TableMetadataFormat } from './publicTypes/format/formatParts/TableMetadataFormat'; -export { ContentModelFormatBase } from './publicTypes/format/ContentModelFormatBase'; -export { MarginFormat } from './publicTypes/format/formatParts/MarginFormat'; -export { PaddingFormat } from './publicTypes/format/formatParts/PaddingFormat'; -export { DisplayFormat } from './publicTypes/format/formatParts/DisplayFormat'; -export { LineHeightFormat } from './publicTypes/format/formatParts/LineHeightFormat'; -export { LinkFormat } from './publicTypes/format/formatParts/LinkFormat'; -export { ListTypeFormat } from './publicTypes/format/formatParts/ListTypeFormat'; -export { ListThreadFormat } from './publicTypes/format/formatParts/ListThreadFormat'; -export { ListMetadataFormat } from './publicTypes/format/formatParts/ListMetadataFormat'; -export { - ImageResizeMetadataFormat, - ImageCropMetadataFormat, - ImageMetadataFormat, - ImageRotateMetadataFormat, -} from './publicTypes/format/formatParts/ImageMetadataFormat'; -export { DatasetFormat } from './publicTypes/format/formatParts/DatasetFormat'; -export { WhiteSpaceFormat } from './publicTypes/format/formatParts/WhiteSpaceFormat'; -export { WordBreakFormat } from './publicTypes/format/formatParts/WordBreakFormat'; - -export { ContentModelFormatMap } from './publicTypes/format/ContentModelFormatMap'; - -export { EditorContext } from './publicTypes/context/EditorContext'; -export { - DomToModelListFormat, - DomToModelFormatContext, -} from './publicTypes/context/DomToModelFormatContext'; -export { - DomToModelRegularSelection, - DomToModelTableSelection, - DomToModelImageSelection, - DomToModelSelectionContext, -} from './publicTypes/context/DomToModelSelectionContext'; -export { - DomToModelSettings, - DefaultStyleMap, - ElementProcessorMap, - FormatParser, - FormatParsers, - FormatParsersPerCategory, -} from './publicTypes/context/DomToModelSettings'; -export { DomToModelContext } from './publicTypes/context/DomToModelContext'; -export { ModelToDomContext } from './publicTypes/context/ModelToDomContext'; -export { - ModelToDomListStackItem, - ModelToDomListContext, - ModelToDomFormatContext, -} from './publicTypes/context/ModelToDomFormatContext'; -export { - ModelToDomBlockAndSegmentNode, - ModelToDomRegularSelection, - ModelToDomTableSelection, - ModelToDomImageSelection, - ModelToDomSelectionContext, -} from './publicTypes/context/ModelToDomSelectionContext'; -export { - ModelToDomSettings, - FormatApplier, - FormatAppliers, - FormatAppliersPerCategory, - ContentModelHandlerMap, - ContentModelHandlerTypeMap, - DefaultImplicitFormatMap, -} from './publicTypes/context/ModelToDomSettings'; -export { ModelToDomEntityContext } from './publicTypes/context/ModelToDomEntityContext'; -export { ElementProcessor } from './publicTypes/context/ElementProcessor'; -export { ContentModelHandler } from './publicTypes/context/ContentModelHandler'; - -export { - IExperimentalContentModelEditor, - DomToModelOption, - ModelToDomOption, -} from './publicTypes/IExperimentalContentModelEditor'; +export * from './publicTypes/index'; +export * from './editor/index'; +export * from './publicApi/index'; +export * from './domUtils/index'; diff --git a/packages/roosterjs-content-model/lib/modelApi/common/clearModelFormat.ts b/packages/roosterjs-content-model/lib/modelApi/common/clearModelFormat.ts index ab75e9a809d..b6a22fe5655 100644 --- a/packages/roosterjs-content-model/lib/modelApi/common/clearModelFormat.ts +++ b/packages/roosterjs-content-model/lib/modelApi/common/clearModelFormat.ts @@ -12,8 +12,8 @@ import { createQuote } from '../creators/createQuote'; import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; import { iterateSelections, TableSelectionContext } from '../selection/iterateSelections'; import { Selectable } from '../../publicTypes/selection/Selectable'; -import { updateTableCellMetadata } from '../metadata/updateTableCellMetadata'; -import { updateTableMetadata } from '../metadata/updateTableMetadata'; +import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; +import { updateTableMetadata } from '../../domUtils/metadata/updateTableMetadata'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts b/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts index 2b4a16a73fa..1e5a458dec6 100644 --- a/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts +++ b/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts @@ -1,4 +1,4 @@ -import domToContentModel from '../../publicApi/domToContentModel'; +import domToContentModel from '../../domToModel/domToContentModel'; import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument'; import { mergeModel } from '../../modelApi/common/mergeModel'; import { safeInstanceOf, wrap } from 'roosterjs-editor-dom'; @@ -21,8 +21,6 @@ export function insertContent( htmlContent, { isDarkMode: !!isFromDarkMode, - zoomScale: 1, - isRightToLeft: false, }, { includeRoot: true, diff --git a/packages/roosterjs-content-model/lib/modelApi/common/mergeModel.ts b/packages/roosterjs-content-model/lib/modelApi/common/mergeModel.ts index a51b6258d76..6b9c31b86e7 100644 --- a/packages/roosterjs-content-model/lib/modelApi/common/mergeModel.ts +++ b/packages/roosterjs-content-model/lib/modelApi/common/mergeModel.ts @@ -105,7 +105,7 @@ function mergeTable( const newCell = newTable.cells[i][j]; if (i == 0 && colIndex + j >= table.cells[0].length) { - for (let k = 0; k < rowIndex; k++) { + for (let k = 0; k < table.cells.length; k++) { const leftCell = table.cells[k]?.[colIndex + j - 1]; table.cells[k][colIndex + j] = createTableCell( false /*spanLeft*/, @@ -121,7 +121,7 @@ function mergeTable( table.cells[rowIndex + i] = []; } - for (let k = 0; k < colIndex; k++) { + for (let k = 0; k < table.cells[rowIndex].length; k++) { const aboveCell = table.cells[rowIndex + i - 1]?.[k]; table.cells[rowIndex + i][k] = createTableCell( false /*spanLeft*/, diff --git a/packages/roosterjs-content-model/lib/modelApi/common/retrieveModelFormatState.ts b/packages/roosterjs-content-model/lib/modelApi/common/retrieveModelFormatState.ts index c816b22eae5..de58641cd93 100644 --- a/packages/roosterjs-content-model/lib/modelApi/common/retrieveModelFormatState.ts +++ b/packages/roosterjs-content-model/lib/modelApi/common/retrieveModelFormatState.ts @@ -8,7 +8,7 @@ import { FormatState } from 'roosterjs-editor-types'; import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex'; import { isBold } from '../../publicApi/segment/toggleBold'; import { iterateSelections, TableSelectionContext } from '../selection/iterateSelections'; -import { updateTableMetadata } from '../metadata/updateTableMetadata'; +import { updateTableMetadata } from '../../domUtils/metadata/updateTableMetadata'; /** * @internal @@ -82,6 +82,10 @@ function retrieveFormatStateInternal( result.fontSize = format.fontSize; result.backgroundColor = format.backgroundColor; result.textColor = format.textColor; + //TODO: handle block owning segments with different line-heights + result.lineHeight = paragraph.format.lineHeight || format.lineHeight; + result.marginBottom = paragraph.format.marginBottom; + result.marginTop = paragraph.format.marginTop; result.isBold = isBold(format.fontWeight); result.isItalic = format.italic; diff --git a/packages/roosterjs-content-model/lib/modelApi/creators/createTable.ts b/packages/roosterjs-content-model/lib/modelApi/creators/createTable.ts index 70cc1c2972e..d26beb3d944 100644 --- a/packages/roosterjs-content-model/lib/modelApi/creators/createTable.ts +++ b/packages/roosterjs-content-model/lib/modelApi/creators/createTable.ts @@ -1,10 +1,11 @@ +import { ContentModelBlockFormat } from '../../publicTypes/format/ContentModelBlockFormat'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; import { ContentModelTableCell } from '../../publicTypes/group/ContentModelTableCell'; /** * @internal */ -export function createTable(rowCount: number): ContentModelTable { +export function createTable(rowCount: number, format?: ContentModelBlockFormat): ContentModelTable { const rows: ContentModelTableCell[][] = []; for (let i = 0; i < rowCount; i++) { @@ -14,7 +15,7 @@ export function createTable(rowCount: number): ContentModelTable { return { blockType: 'Table', cells: rows, - format: {}, + format: { ...(format || {}) }, widths: [], heights: [], dataset: {}, diff --git a/packages/roosterjs-content-model/lib/modelApi/format/pendingFormat.ts b/packages/roosterjs-content-model/lib/modelApi/format/pendingFormat.ts new file mode 100644 index 00000000000..5938ff75aac --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelApi/format/pendingFormat.ts @@ -0,0 +1,74 @@ +import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { NodePosition } from 'roosterjs-editor-types'; + +/** + * @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 position Cursor position when set this format + */ +export function setPendingFormat( + editor: IContentModelEditor, + format: ContentModelSegmentFormat, + position: NodePosition +) { + const holder = getPendingFormatHolder(editor); + + holder.format = format; + holder.position = position; +} + +/** + * @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.position = 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.position) { + const position = editor.getFocusedPosition(); + + if (position?.equalTo(holder.position)) { + result = true; + } + } + + return result; +} +interface PendingFormatHolder { + format: ContentModelSegmentFormat | null; + position: NodePosition | null; +} + +const PendingFormatHolderKey = '__ContentModelPendingFormat'; + +function getPendingFormatHolder(editor: IContentModelEditor): PendingFormatHolder { + return editor.getCustomData(PendingFormatHolderKey, () => ({ + format: null, + position: null, + })); +} diff --git a/packages/roosterjs-content-model/lib/modelApi/image/applyImageBorderFormat.ts b/packages/roosterjs-content-model/lib/modelApi/image/applyImageBorderFormat.ts new file mode 100644 index 00000000000..83ba25e2479 --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelApi/image/applyImageBorderFormat.ts @@ -0,0 +1,53 @@ +import { Border } from '../../publicTypes/interface/Border'; +import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; +import { extractBorderValues } from '../../domUtils/borderValues'; +import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit'; + +/** + * @internal + */ +export default function applyImageBorderFormat( + image: ContentModelImage, + border: Border, + borderRadius?: string +) { + const format = image.format; + const { width, style, color } = border; + const borderKey = 'borderTop'; + const extractedBorder = extractBorderValues(format[borderKey]); + const borderColor = extractedBorder.color; + const borderWidth = extractedBorder.width; + const borderStyle = extractedBorder.style; + let borderFormat = ''; + + 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 (borderRadius) { + image.format.borderRadius = borderRadius; + } + + image.format.borderLeft = borderFormat; + image.format.borderTop = borderFormat; + image.format.borderBottom = borderFormat; + image.format.borderRight = borderFormat; +} diff --git a/packages/roosterjs-content-model/lib/modelApi/list/setListType.ts b/packages/roosterjs-content-model/lib/modelApi/list/setListType.ts index ad0dbf20762..e7269818245 100644 --- a/packages/roosterjs-content-model/lib/modelApi/list/setListType.ts +++ b/packages/roosterjs-content-model/lib/modelApi/list/setListType.ts @@ -30,12 +30,13 @@ export function setListType(model: ContentModelDocument, listType: 'OL' | 'UL') } else if (block.blocks.length == 1) { setParagraphNotImplicit(block.blocks[0]); } - } else if (block.blockType == 'Paragraph') { + } else { const index = parent.blocks.indexOf(block); if (index >= 0) { const prevBlock = parent.blocks[index - 1]; - const segmentFormat = block.segments[0]?.format || {}; + const segmentFormat = + (block.blockType == 'Paragraph' && block.segments[0]?.format) || {}; const newListItem = createListItem( [ { @@ -61,7 +62,9 @@ export function setListType(model: ContentModelDocument, listType: 'OL' | 'UL') // Since there is only one paragraph under the list item, no need to keep its paragraph element (DIV). // TODO: Do we need to keep the CSS styles applied to original DIV? - block.isImplicit = true; + if (block.blockType == 'Paragraph') { + block.isImplicit = true; + } newListItem.blocks.push(block); diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/collectSelections.ts b/packages/roosterjs-content-model/lib/modelApi/selection/collectSelections.ts index 9df0fb4fecf..0c848bb41e7 100644 --- a/packages/roosterjs-content-model/lib/modelApi/selection/collectSelections.ts +++ b/packages/roosterjs-content-model/lib/modelApi/selection/collectSelections.ts @@ -8,6 +8,7 @@ import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParag import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; import { isBlockGroupOfType } from '..//common/isBlockGroupOfType'; + import { iterateSelections, IterateSelectionsOption, @@ -79,7 +80,10 @@ export function getOperationalBlocks( ): OperationalBlocks[] { const result: OperationalBlocks[] = []; const findSequence = deepFirst ? blockGroupTypes.map(type => [type]) : [blockGroupTypes]; - const selections = collectSelections(model, { includeListFormatHolder: 'never' }); + const selections = collectSelections(model, { + includeListFormatHolder: 'never', + contentUnderSelectedTableCell: 'ignoreForTable', // When whole table is selected, we treat the table as a single block + }); removeUnmeaningfulSelections(selections); diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/deleteSelections.ts b/packages/roosterjs-content-model/lib/modelApi/selection/deleteSelections.ts index ebcfb1a7b41..1a4a5fe1ee7 100644 --- a/packages/roosterjs-content-model/lib/modelApi/selection/deleteSelections.ts +++ b/packages/roosterjs-content-model/lib/modelApi/selection/deleteSelections.ts @@ -77,7 +77,7 @@ export function deleteSelection(model: ContentModelDocument): InsertPosition | n } }, { - ignoreContentUnderSelectedTableCell: true, + contentUnderSelectedTableCell: 'ignoreForTableOrCell', // When a table cell is selected, we replace all content for this cell, so no need to go into its content includeListFormatHolder: 'never', } ); diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/iterateSelections.ts b/packages/roosterjs-content-model/lib/modelApi/selection/iterateSelections.ts index 5204e38f8d7..0f33d69b388 100644 --- a/packages/roosterjs-content-model/lib/modelApi/selection/iterateSelections.ts +++ b/packages/roosterjs-content-model/lib/modelApi/selection/iterateSelections.ts @@ -18,9 +18,15 @@ export interface TableSelectionContext { */ export interface IterateSelectionsOption { /** - * When set to true, and a table cell is marked as selected, all content under this table cell will not be included in result + * For selected table cell, this property determines how do we handle its content. + * include: No matter if table cell is selected, always invoke callback function for selected content (default value) + * ignoreForTable: When the whole table is selected we invoke callback for the table (using block parameter) but skip + * all its cells and content, otherwise keep invoking callback for table cell and content + * ignoreForTableOrCell: If whole table is selected, same with ignoreForTable, or if a table cell is selected, only + * invoke callback for the table cell itself but not for its content, otherwise keep invoking callback for content. + * @default include */ - ignoreContentUnderSelectedTableCell?: boolean; + contentUnderSelectedTableCell?: 'include' | 'ignoreForTable' | 'ignoreForTableOrCell'; /** * Whether call the callback for the list item format holder segment @@ -56,6 +62,8 @@ export function iterateSelections( ): boolean { const parent = path[0]; const includeListFormatHolder = option?.includeListFormatHolder || 'allSegments'; + const contentUnderSelectedTableCell = option?.contentUnderSelectedTableCell || 'include'; + let hasSelectedSegment = false; let hasUnselectedSegment = false; @@ -74,7 +82,7 @@ export function iterateSelections( const cells = block.cells; const isWholeTableSelected = cells.every(row => row.every(cell => cell.isSelected)); - if (option?.ignoreContentUnderSelectedTableCell && isWholeTableSelected) { + if (contentUnderSelectedTableCell != 'include' && isWholeTableSelected) { if (callback(path, table, block)) { return true; } @@ -96,7 +104,10 @@ export function iterateSelections( return true; } - if (!cell.isSelected || !option?.ignoreContentUnderSelectedTableCell) { + if ( + !cell.isSelected || + contentUnderSelectedTableCell != 'ignoreForTableOrCell' + ) { const newPath = [cell, ...path]; const isSelected = treatAllAsSelect || cell.isSelected; diff --git a/packages/roosterjs-content-model/lib/modelApi/table/applyTableFormat.ts b/packages/roosterjs-content-model/lib/modelApi/table/applyTableFormat.ts index 4e415adfd0d..aa8320cf2fa 100644 --- a/packages/roosterjs-content-model/lib/modelApi/table/applyTableFormat.ts +++ b/packages/roosterjs-content-model/lib/modelApi/table/applyTableFormat.ts @@ -6,8 +6,8 @@ import { ContentModelTableCell } from '../../publicTypes/group/ContentModelTable import { setTableCellBackgroundColor } from './setTableCellBackgroundColor'; import { TableBorderFormat } from 'roosterjs-editor-types'; import { TableMetadataFormat } from '../../publicTypes/format/formatParts/TableMetadataFormat'; -import { updateTableCellMetadata } from '../metadata/updateTableCellMetadata'; -import { updateTableMetadata } from '../metadata/updateTableMetadata'; +import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; +import { updateTableMetadata } from '../../domUtils/metadata/updateTableMetadata'; const DEFAULT_FORMAT: Required = { topBorderColor: '#ABABAB', diff --git a/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts b/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts index 857c2c1ac25..f35f70ce132 100644 --- a/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts +++ b/packages/roosterjs-content-model/lib/modelApi/table/setTableCellBackgroundColor.ts @@ -1,5 +1,5 @@ import { ContentModelTableCell } from '../../publicTypes/group/ContentModelTableCell'; -import { updateTableCellMetadata } from '../metadata/updateTableCellMetadata'; +import { updateTableCellMetadata } from '../../domUtils/metadata/updateTableCellMetadata'; // Using the HSL (hue, saturation and lightness) representation for RGB color values. // If the value of the lightness is less than 20, the color is dark. diff --git a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts b/packages/roosterjs-content-model/lib/modelToDom/contentModelToDom.ts similarity index 94% rename from packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts rename to packages/roosterjs-content-model/lib/modelToDom/contentModelToDom.ts index 898f430df6a..320fe42642b 100644 --- a/packages/roosterjs-content-model/lib/publicApi/contentModelToDom.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/contentModelToDom.ts @@ -1,12 +1,12 @@ import { ContentModelDocument } from '../publicTypes/group/ContentModelDocument'; -import { createModelToDomContext } from '../modelToDom/context/createModelToDomContext'; +import { createModelToDomContext } from './context/createModelToDomContext'; import { createRange, Position, toArray } from 'roosterjs-editor-dom'; import { EditorContext } from '../publicTypes/context/EditorContext'; import { isNodeOfType } from '../domUtils/isNodeOfType'; import { ModelToDomBlockAndSegmentNode } from '../publicTypes/context/ModelToDomSelectionContext'; import { ModelToDomContext } from '../publicTypes/context/ModelToDomContext'; -import { ModelToDomOption } from '../publicTypes/IExperimentalContentModelEditor'; -import { optimize } from '../modelToDom/optimizers/optimize'; +import { ModelToDomOption } from '../publicTypes/IContentModelEditor'; +import { optimize } from './optimizers/optimize'; import { NodePosition, NodeType, diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts index 88275067818..3c9988d2262 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts @@ -2,7 +2,7 @@ import { defaultContentModelHandlers } from './defaultContentModelHandlers'; import { defaultImplicitFormatMap } from '../../formatHandlers/utils/defaultStyles'; import { EditorContext } from '../../publicTypes/context/EditorContext'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; -import { ModelToDomOption } from '../../publicTypes/IExperimentalContentModelEditor'; +import { ModelToDomOption } from '../../publicTypes/IContentModelEditor'; import { defaultFormatAppliers, getFormatAppliers, @@ -20,8 +20,6 @@ export function createModelToDomContext( return { ...(editorContext || { isDarkMode: false, - isRightToLeft: false, - zoomScale: 1, getDarkColor: undefined, }), regularSelection: { @@ -51,6 +49,5 @@ export function createModelToDomContext( defaultModelHandlers: defaultContentModelHandlers, defaultFormatAppliers: defaultFormatAppliers, - doNotReuseEntityDom: !!options?.doNotReuseEntityDom, }; } diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts b/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts index 2cc8620aa5c..9b0a91adcc8 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts @@ -7,6 +7,7 @@ import { handleDivider } from '../handlers/handleDivider'; import { handleEntity } from '../handlers/handleEntity'; import { handleGeneralModel } from '../handlers/handleGeneralModel'; import { handleImage } from '../handlers/handleImage'; +import { handleLink } from '../handlers/handleLink'; import { handleList } from '../handlers/handleList'; import { handleListItem } from '../handlers/handleListItem'; import { handleParagraph } from '../handlers/handleParagraph'; @@ -27,6 +28,7 @@ export const defaultContentModelHandlers: ContentModelHandlerMap = { general: handleGeneralModel, divider: handleDivider, image: handleImage, + link: handleLink, list: handleList, listItem: handleListItem, paragraph: handleParagraph, diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts index 898549a9c05..c10ea7e6503 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts @@ -38,7 +38,7 @@ export const handleEntity: ContentModelHandler = ( parent = span; } - if (context.doNotReuseEntityDom || !entity) { + if (!entity) { parent.appendChild(wrapper); } else { // Create a comment as placeholder and insert into DOM tree. diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts index 8a678ff85c5..3292484a33a 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts @@ -5,7 +5,6 @@ import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandl import { isNodeOfType } from '../../domUtils/isNodeOfType'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; import { NodeType } from 'roosterjs-editor-types'; -import { stackFormat } from '../utils/stackFormat'; /** * @internal @@ -16,46 +15,23 @@ export const handleGeneralModel: ContentModelHandler = group: ContentModelGeneralBlock, context: ModelToDomContext ) => { - const newParent = group.element.cloneNode(); + const element = group.element.cloneNode(); - if (isGeneralSegment(group) && isNodeOfType(newParent, NodeType.Element)) { + parent.appendChild(element); + + if (isGeneralSegment(group) && isNodeOfType(element, NodeType.Element)) { if (!group.element.firstChild) { - context.regularSelection.current.segment = newParent; + context.regularSelection.current.segment = element; } - stackFormat(context, group.link ? 'a' : null, () => { - let segmentElement: HTMLElement; - - if (group.link) { - segmentElement = doc.createElement('a'); - - parent.appendChild(segmentElement); - segmentElement.appendChild(newParent); + applyFormat(element, context.formatAppliers.segment, group.format, context); - applyFormat( - segmentElement, - context.formatAppliers.link, - group.link.format, - context - ); - applyFormat( - segmentElement, - context.formatAppliers.dataset, - group.link.dataset, - context - ); - } else { - segmentElement = newParent; - parent.appendChild(newParent); - } - - applyFormat(segmentElement, context.formatAppliers.segment, group.format, context); - }); - } else { - parent.appendChild(newParent); + if (group.link) { + context.modelHandlers.link(doc, element, group.link, context); + } } - context.modelHandlers.blockGroupChildren(doc, newParent, group, context); + context.modelHandlers.blockGroupChildren(doc, element, group, context); }; function isGeneralSegment(block: ContentModelGeneralBlock): block is ContentModelGeneralSegment { diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts index f20305a0ebe..94c857be85d 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts @@ -2,7 +2,7 @@ import { applyFormat } from '../utils/applyFormat'; import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; -import { stackFormat } from '../utils/stackFormat'; +import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit'; /** * @internal @@ -14,6 +14,11 @@ export const handleImage: ContentModelHandler = ( context: ModelToDomContext ) => { const img = doc.createElement('img'); + const element = document.createElement('span'); + + parent.appendChild(element); + element.appendChild(img); + img.src = imageModel.src; if (imageModel.alt) { @@ -24,36 +29,26 @@ export const handleImage: ContentModelHandler = ( img.title = imageModel.title; } - stackFormat(context, imageModel.link ? 'a' : null, () => { - let segmentElement: HTMLElement; + applyFormat(img, context.formatAppliers.image, imageModel.format, context); + applyFormat(img, context.formatAppliers.dataset, imageModel.dataset, context); + + applyFormat(element, context.formatAppliers.segment, imageModel.format, context); - if (imageModel.link) { - segmentElement = doc.createElement('a'); + const { width, height } = imageModel.format; + const widthNum = width ? parseValueWithUnit(width) : 0; + const heightNum = height ? parseValueWithUnit(height) : 0; - parent.appendChild(segmentElement); - segmentElement.appendChild(img); + if (widthNum > 0) { + img.width = widthNum; + } - applyFormat( - segmentElement, - context.formatAppliers.link, - imageModel.link.format, - context - ); - applyFormat( - segmentElement, - context.formatAppliers.dataset, - imageModel.link.dataset, - context - ); - } else { - segmentElement = img; - parent.appendChild(img); - } + if (heightNum > 0) { + img.height = heightNum; + } - applyFormat(img, context.formatAppliers.image, imageModel.format, context); - applyFormat(segmentElement, context.formatAppliers.segment, imageModel.format, context); - applyFormat(img, context.formatAppliers.dataset, imageModel.dataset, context); - }); + if (imageModel.link) { + context.modelHandlers.link(doc, img, imageModel.link, context); + } context.regularSelection.current.segment = img; diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleLink.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleLink.ts new file mode 100644 index 00000000000..d8581fa610c --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleLink.ts @@ -0,0 +1,17 @@ +import { applyFormat } from '../utils/applyFormat'; +import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; +import { ContentModelLink } from '../../publicTypes/decorator/ContentModelLink'; +import { stackFormat } from '../utils/stackFormat'; +import { wrap } from 'roosterjs-editor-dom'; + +/** + * @internal + */ +export const handleLink: ContentModelHandler = (doc, parent, link, context) => { + stackFormat(context, 'a', () => { + const a = wrap(parent, 'a'); + + applyFormat(a, context.formatAppliers.link, link.format, context); + applyFormat(a, context.formatAppliers.dataset, link.dataset, context); + }); +}; diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts index 2d95a95ba2f..91ad8db0808 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts @@ -4,7 +4,7 @@ import { ContentModelListItem } from '../../publicTypes/group/ContentModelListIt import { ContentModelListItemLevelFormat } from '../../publicTypes/format/ContentModelListItemLevelFormat'; import { DatasetFormat } from '../../publicTypes/format/formatParts/DatasetFormat'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; -import { updateListMetadata } from '../../modelApi/metadata/updateListMetadata'; +import { updateListMetadata } from '../../domUtils/metadata/updateListMetadata'; /** * @internal diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts index 0203fdb8ae9..fbef08016e4 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts @@ -2,7 +2,6 @@ import { applyFormat } from '../utils/applyFormat'; import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; import { ContentModelText } from '../../publicTypes/segment/ContentModelText'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; -import { stackFormat } from '../utils/stackFormat'; /** * @internal @@ -14,19 +13,16 @@ export const handleText: ContentModelHandler = ( context: ModelToDomContext ) => { const txt = doc.createTextNode(segment.text); - const element = doc.createElement(segment.link ? 'a' : 'span'); + const element = doc.createElement('span'); - element.appendChild(txt); parent.appendChild(element); + element.appendChild(txt); context.regularSelection.current.segment = txt; - stackFormat(context, segment.link ? 'a' : null, () => { - applyFormat(element, context.formatAppliers.segment, segment.format, context); + applyFormat(element, context.formatAppliers.segment, segment.format, context); - if (segment.link) { - applyFormat(element, context.formatAppliers.link, segment.link.format, context); - applyFormat(element, context.formatAppliers.dataset, segment.link.dataset, context); - } - }); + if (segment.link) { + context.modelHandlers.link(doc, txt, segment.link, context); + } }; diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setAlignment.ts b/packages/roosterjs-content-model/lib/publicApi/block/setAlignment.ts index 9b3e93730ba..382307b2d78 100644 --- a/packages/roosterjs-content-model/lib/publicApi/block/setAlignment.ts +++ b/packages/roosterjs-content-model/lib/publicApi/block/setAlignment.ts @@ -1,5 +1,5 @@ import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; const ResultMap: Record< 'left' | 'center' | 'right', @@ -25,7 +25,7 @@ const ResultMap: Record< * @param alignment Alignment value: left, center or right */ export default function setAlignment( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, alignment: 'left' | 'center' | 'right' ) { formatParagraphWithContentModel(editor, 'setAlignment', para => { diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setDirection.ts b/packages/roosterjs-content-model/lib/publicApi/block/setDirection.ts index 52cfe3f0dad..3a11c205bf1 100644 --- a/packages/roosterjs-content-model/lib/publicApi/block/setDirection.ts +++ b/packages/roosterjs-content-model/lib/publicApi/block/setDirection.ts @@ -1,15 +1,12 @@ import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { 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: IExperimentalContentModelEditor, - direction: 'ltr' | 'rtl' -) { +export default function setDirection(editor: IContentModelEditor, direction: 'ltr' | 'rtl') { formatParagraphWithContentModel(editor, 'setDirection', para => { const isOldValueRtl = para.format.direction == 'rtl'; const isNewValueRtl = direction == 'rtl'; diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts b/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts index 80a639c378a..0d87e3ee69b 100644 --- a/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/block/setHeaderLevel.ts @@ -3,7 +3,7 @@ import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModel import { defaultImplicitFormatMap } from '../../formatHandlers/utils/defaultStyles'; import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; import { getObjectKeys } from 'roosterjs-editor-dom'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; type HeaderLevelTags = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; @@ -13,7 +13,7 @@ type HeaderLevelTags = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; * @param headerLevel Level of header, from 1 to 6. Set to 0 means set it back to a regular paragraph */ export default function setHeaderLevel( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, headerLevel: 0 | 1 | 2 | 3 | 4 | 5 | 6 ) { formatParagraphWithContentModel(editor, 'setHeaderLevel', para => { diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setIndentation.ts b/packages/roosterjs-content-model/lib/publicApi/block/setIndentation.ts index 95207d6127e..84958490248 100644 --- a/packages/roosterjs-content-model/lib/publicApi/block/setIndentation.ts +++ b/packages/roosterjs-content-model/lib/publicApi/block/setIndentation.ts @@ -1,5 +1,5 @@ import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { setModelIndentation } from '../../modelApi/block/setModelIndentation'; /** @@ -9,7 +9,7 @@ import { setModelIndentation } from '../../modelApi/block/setModelIndentation'; * @param length The length of pixel to indent/outdent @default 40 */ export default function setIndentation( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, indentation: 'indent' | 'outdent', length?: number ) { diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setParagraphMargin.ts b/packages/roosterjs-content-model/lib/publicApi/block/setParagraphMargin.ts new file mode 100644 index 00000000000..91088224203 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/block/setParagraphMargin.ts @@ -0,0 +1,34 @@ +import { createParagraphDecorator } from '../../modelApi/creators/createParagraphDecorator'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; + +/** + * Toggles the current block(s) margin properties. + * null deletes any existing value, undefined is ignored + * @param editor The editor to operate on + * @param marginTop value for top margin + * @param marginBottom value for bottom margin + */ +export default function setParagraphMargin( + editor: IContentModelEditor, + marginTop?: string | null, + marginBottom?: string | null +) { + formatParagraphWithContentModel(editor, 'setParagraphMargin', para => { + if (!para.decorator) { + para.decorator = createParagraphDecorator('p'); + } + + if (marginTop) { + para.format.marginTop = marginTop; + } else if (marginTop === null) { + delete para.format.marginTop; + } + + if (marginBottom) { + para.format.marginBottom = marginBottom; + } else if (marginBottom === null) { + delete para.format.marginBottom; + } + }); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/block/setSpacing.ts b/packages/roosterjs-content-model/lib/publicApi/block/setSpacing.ts new file mode 100644 index 00000000000..5e7002e716a --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/block/setSpacing.ts @@ -0,0 +1,18 @@ +import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; + +/** + * 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) { + formatParagraphWithContentModel(editor, 'setSpacing', paragraph => { + paragraph.format.lineHeight = spacing.toString(); + paragraph.segments.forEach(segment => { + if (segment.format.lineHeight) { + delete segment.format.lineHeight; + } + }); + }); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/block/toggleBlockQuote.ts b/packages/roosterjs-content-model/lib/publicApi/block/toggleBlockQuote.ts index fc4dacd9587..178dd4bda0d 100644 --- a/packages/roosterjs-content-model/lib/publicApi/block/toggleBlockQuote.ts +++ b/packages/roosterjs-content-model/lib/publicApi/block/toggleBlockQuote.ts @@ -1,7 +1,7 @@ import { ContentModelBlockFormat } from '../../publicTypes/format/ContentModelBlockFormat'; import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { toggleModelBlockQuote } from '../../modelApi/block/toggleModelBlockQuote'; const DefaultQuoteFormat: ContentModelBlockFormat = { @@ -27,7 +27,7 @@ const BuildInQuoteFormat: ContentModelBlockFormat = { * @param segmentFormat @optional Segment format for the content of model */ export default function toggleBlockQuote( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, quoteFormat: ContentModelBlockFormat = DefaultQuoteFormat, segmentFormat: ContentModelSegmentFormat = DefaultSegmentFormat ) { diff --git a/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts deleted file mode 100644 index a0a862fcd07..00000000000 --- a/packages/roosterjs-content-model/lib/publicApi/domToContentModel.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ContentModelDocument } from '../publicTypes/group/ContentModelDocument'; -import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument'; -import { createDomToModelContext } from '../domToModel/context/createDomToModelContext'; -import { DomToModelOption } from '../publicTypes/IExperimentalContentModelEditor'; -import { EditorContext } from '../publicTypes/context/EditorContext'; -import { normalizeContentModel } from '../modelApi/common/normalizeContentModel'; - -/** - * Create Content Model from DOM tree in this editor - * @param root Root element of DOM tree to create Content Model from - * @param editorContext Context of content model editor - * @param option The option to customize the behavior of DOM to Content Model conversion - * @returns A ContentModelDocument object that contains all the models created from the give root element - */ -export default function domToContentModel( - root: HTMLElement, - editorContext: EditorContext, - option: DomToModelOption -): ContentModelDocument { - const model = createContentModelDocument(); - const domToModelContext = createDomToModelContext(editorContext, option); - const { element, child } = domToModelContext.elementProcessors; - const processor = option.includeRoot ? element : child; - - processor(model, root, domToModelContext); - - normalizeContentModel(model); - - return model; -} diff --git a/packages/roosterjs-content-model/lib/publicApi/format/applyPendingFormat.ts b/packages/roosterjs-content-model/lib/publicApi/format/applyPendingFormat.ts new file mode 100644 index 00000000000..a9979233512 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/format/applyPendingFormat.ts @@ -0,0 +1,50 @@ +import { createText } from '../../modelApi/creators/createText'; +import { formatWithContentModel } from '../utils/formatWithContentModel'; +import { getPendingFormat } from '../../modelApi/format/pendingFormat'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { iterateSelections } from '../../modelApi/selection/iterateSelections'; + +/** + * 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); + + if (format) { + let isChanged = false; + + formatWithContentModel(editor, 'applyPendingFormat', model => { + iterateSelections([model], (_, __, block, segments) => { + if ( + block?.blockType == 'Paragraph' && + segments?.length == 1 && + segments[0].segmentType == 'SelectionMarker' + ) { + const index = block.segments.indexOf(segments[0]); + const previousSegment = block.segments[index - 1]; + + if (previousSegment?.segmentType == 'Text') { + const text = previousSegment.text; + + if (text.substr(-data.length, data.length) == data) { + previousSegment.text = text.substring(0, text.length - data.length); + + const newText = createText(data, { + ...previousSegment.format, + ...format, + }); + + block.segments.splice(index, 0, newText); + isChanged = true; + } + } + } + return true; + }); + + return isChanged; + }); + } +} diff --git a/packages/roosterjs-content-model/lib/publicApi/format/clearFormat.ts b/packages/roosterjs-content-model/lib/publicApi/format/clearFormat.ts index 4ea673421bb..32f1a3a94b7 100644 --- a/packages/roosterjs-content-model/lib/publicApi/format/clearFormat.ts +++ b/packages/roosterjs-content-model/lib/publicApi/format/clearFormat.ts @@ -5,7 +5,7 @@ import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegme import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { ContentModelTable } from '../../publicTypes/block/ContentModelTable'; import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { normalizeContentModel } from '../../modelApi/common/normalizeContentModel'; /** @@ -13,7 +13,7 @@ import { normalizeContentModel } from '../../modelApi/common/normalizeContentMod * @param editor The editor to clear format from */ export default function clearFormat( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, defaultSegmentFormat?: ContentModelSegmentFormat ) { formatWithContentModel(editor, 'clearFormat', model => { diff --git a/packages/roosterjs-content-model/lib/publicApi/format/getFormatState.ts b/packages/roosterjs-content-model/lib/publicApi/format/getFormatState.ts index caaa66c6414..0ec38b8e05c 100644 --- a/packages/roosterjs-content-model/lib/publicApi/format/getFormatState.ts +++ b/packages/roosterjs-content-model/lib/publicApi/format/getFormatState.ts @@ -1,6 +1,7 @@ import { FormatState } from 'roosterjs-editor-types'; import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { getPendingFormat } from '../../modelApi/format/pendingFormat'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { reducedModelChildProcessor } from '../../domToModel/processors/reducedModelChildProcessor'; import { retrieveModelFormatState } from '../../modelApi/common/retrieveModelFormatState'; @@ -8,7 +9,7 @@ import { retrieveModelFormatState } from '../../modelApi/common/retrieveModelFor * Get current format state * @param editor The editor to get format from */ -export default function getFormatState(editor: IExperimentalContentModelEditor): FormatState { +export default function getFormatState(editor: IContentModelEditor): FormatState { let result: FormatState = { ...editor.getUndoState(), @@ -20,7 +21,7 @@ export default function getFormatState(editor: IExperimentalContentModelEditor): editor, 'getFormatState', model => { - const pendingFormat = editor.getPendingFormat(); + const pendingFormat = getPendingFormat(editor); retrieveModelFormatState(model, pendingFormat, result); diff --git a/packages/roosterjs-content-model/lib/publicApi/format/getSegmentFormat.ts b/packages/roosterjs-content-model/lib/publicApi/format/getSegmentFormat.ts index 8fe81ae6a95..e9e92ba099f 100644 --- a/packages/roosterjs-content-model/lib/publicApi/format/getSegmentFormat.ts +++ b/packages/roosterjs-content-model/lib/publicApi/format/getSegmentFormat.ts @@ -1,6 +1,7 @@ import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { getPendingFormat } from '../../modelApi/format/pendingFormat'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { iterateSelections } from '../../modelApi/selection/iterateSelections'; import { reducedModelChildProcessor } from '../../domToModel/processors/reducedModelChildProcessor'; @@ -9,9 +10,9 @@ import { reducedModelChildProcessor } from '../../domToModel/processors/reducedM * @param editor The editor to get format from */ export default function getSegmentFormat( - editor: IExperimentalContentModelEditor + editor: IContentModelEditor ): ContentModelSegmentFormat | null { - let result = editor.getPendingFormat() || null; + let result = getPendingFormat(editor); if (!result) { formatWithContentModel( diff --git a/packages/roosterjs-content-model/lib/publicApi/image/adjustImageSelection.ts b/packages/roosterjs-content-model/lib/publicApi/image/adjustImageSelection.ts index 847182d3660..f8ce9f09452 100644 --- a/packages/roosterjs-content-model/lib/publicApi/image/adjustImageSelection.ts +++ b/packages/roosterjs-content-model/lib/publicApi/image/adjustImageSelection.ts @@ -1,14 +1,14 @@ import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { 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: IExperimentalContentModelEditor + editor: IContentModelEditor ): ContentModelImage | null { let image: ContentModelImage | null = null; diff --git a/packages/roosterjs-content-model/lib/publicApi/image/changeImage.ts b/packages/roosterjs-content-model/lib/publicApi/image/changeImage.ts new file mode 100644 index 00000000000..d3029316c8d --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/image/changeImage.ts @@ -0,0 +1,22 @@ +import formatImageWithContentModel from '../utils/formatImageWithContentModel'; +import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { readFile } from 'roosterjs-editor-dom'; + +/** + * Change the selected image src + * @param editor The editor instance + * @param file The image file + */ +export default function changeImage(editor: IContentModelEditor, file: File) { + readFile(file, dataUrl => { + if (dataUrl && !editor.isDisposed()) { + formatImageWithContentModel(editor, 'changeImage', (image: ContentModelImage) => { + image.src = dataUrl; + image.dataset = {}; + image.format.width = ''; + image.format.height = ''; + }); + } + }); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/image/insertImage.ts b/packages/roosterjs-content-model/lib/publicApi/image/insertImage.ts new file mode 100644 index 00000000000..4a28e9b6e73 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/image/insertImage.ts @@ -0,0 +1,31 @@ +import { formatWithContentModel } from '../utils/formatWithContentModel'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { insertContent } from '../../modelApi/common/insertContent'; +import { readFile } from 'roosterjs-editor-dom'; + +/** + * 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) { + 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 => { + const image = editor.getDocument().createElement('img'); + + image.src = src; + insertContent(model, image); + return true; + }); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/image/setImageAltText.ts b/packages/roosterjs-content-model/lib/publicApi/image/setImageAltText.ts index 56a8fdcc8b3..25da1d8ae72 100644 --- a/packages/roosterjs-content-model/lib/publicApi/image/setImageAltText.ts +++ b/packages/roosterjs-content-model/lib/publicApi/image/setImageAltText.ts @@ -1,19 +1,15 @@ -import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import formatImageWithContentModel from '../utils/formatImageWithContentModel'; +import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Set image alt text for all selected images at selection. If no images is contained * in selection, do nothing. - * The alt attribute provides alternative information for an image if a user for some reason - * cannot view it (because of slow connection, an error in the src attribute, or if the user - * uses a screen reader). See https://www.w3schools.com/tags/att_img_alt.asp * @param editor The editor instance * @param altText The image alt text */ -export default function setImageAltText(editor: IExperimentalContentModelEditor, altText: string) { - formatSegmentWithContentModel(editor, 'setImageAltText', (_, __, segment) => { - if (segment?.segmentType == 'Image') { - segment.alt = altText; - } +export default function setImageAltText(editor: IContentModelEditor, altText: string) { + formatImageWithContentModel(editor, 'setImageAltText', (image: ContentModelImage) => { + image.alt = altText; }); } diff --git a/packages/roosterjs-content-model/lib/publicApi/image/setImageBorder.ts b/packages/roosterjs-content-model/lib/publicApi/image/setImageBorder.ts new file mode 100644 index 00000000000..f255e1c66a1 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/image/setImageBorder.ts @@ -0,0 +1,22 @@ +import applyImageBorderFormat from '../../modelApi/image/applyImageBorderFormat'; +import formatImageWithContentModel from '../utils/formatImageWithContentModel'; +import { Border } from '../../publicTypes/interface/Border'; +import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; + +/** + * Set image border style for all selected images at selection. + * @param editor The editor instance + * @param border the border format object. Ex: { color: 'red', width: '10px', style: 'solid'}, if one of the value in object is undefined + * its value will not be changed + * @param borderRadius the border radius value, if undefined, the border radius will keep the actual value + */ +export default function setImageBorder( + editor: IContentModelEditor, + border: Border, + borderRadius?: string +) { + formatImageWithContentModel(editor, 'setImageBorder', (image: ContentModelImage) => { + applyImageBorderFormat(image, border, borderRadius); + }); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/image/setImageBoxShadow.ts b/packages/roosterjs-content-model/lib/publicApi/image/setImageBoxShadow.ts new file mode 100644 index 00000000000..95b04ac1584 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/image/setImageBoxShadow.ts @@ -0,0 +1,14 @@ +import formatImageWithContentModel from '../utils/formatImageWithContentModel'; +import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; + +/** + * Set image box shadow for all selected images at selection. + * @param editor The editor instance + * @param boxShadow The image box shadow + */ +export default function setImageBoxShadow(editor: IContentModelEditor, boxShadow: string) { + formatImageWithContentModel(editor, 'setImageBoxShadow', (image: ContentModelImage) => { + image.format.boxShadow = boxShadow; + }); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/index.ts b/packages/roosterjs-content-model/lib/publicApi/index.ts new file mode 100644 index 00000000000..4038f135caa --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/index.ts @@ -0,0 +1,44 @@ +export { default as insertTable } from './table/insertTable'; +export { default as formatTable } from './table/formatTable'; +export { default as setTableCellShade } from './table/setTableCellShade'; +export { default as editTable } from './table/editTable'; +export { default as toggleBullet } from './list/toggleBullet'; +export { default as toggleNumbering } from './list/toggleNumbering'; +export { default as toggleBold } from './segment/toggleBold'; +export { default as toggleItalic } from './segment/toggleItalic'; +export { default as toggleUnderline } from './segment/toggleUnderline'; +export { default as toggleStrikethrough } from './segment/toggleStrikethrough'; +export { default as toggleSubscript } from './segment/toggleSubscript'; +export { default as toggleSuperscript } from './segment/toggleSuperscript'; +export { default as setBackgroundColor } from './segment/setBackgroundColor'; +export { default as setFontName } from './segment/setFontName'; +export { default as setFontSize } from './segment/setFontSize'; +export { default as setTextColor } from './segment/setTextColor'; +export { default as changeFontSize } from './segment/changeFontSize'; +export { default as applySegmentFormat } from './segment/applySegmentFormat'; +export { default as changeCapitalization } from './segment/changeCapitalization'; +export { default as insertImage } from './image/insertImage'; +export { default as setListStyle } from './list/setListStyle'; +export { default as setListStartNumber } from './list/setListStartNumber'; +export { default as hasSelectionInBlock } from './selection/hasSelectionInBlock'; +export { default as hasSelectionInSegment } from './selection/hasSelectionInSegment'; +export { default as hasSelectionInBlockGroup } from './selection/hasSelectionInBlockGroup'; +export { default as setIndentation } from './block/setIndentation'; +export { default as setAlignment } from './block/setAlignment'; +export { default as setDirection } from './block/setDirection'; +export { default as setHeaderLevel } from './block/setHeaderLevel'; +export { default as toggleBlockQuote } from './block/toggleBlockQuote'; +export { default as setSpacing } from './block/setSpacing'; +export { default as setImageBorder } from './image/setImageBorder'; +export { default as setImageBoxShadow } from './image/setImageBoxShadow'; +export { default as changeImage } from './image/changeImage'; +export { default as getFormatState } from './format/getFormatState'; +export { default as getSegmentFormat } from './format/getSegmentFormat'; +export { default as applyPendingFormat } from './format/applyPendingFormat'; +export { default as clearFormat } from './format/clearFormat'; +export { default as insertLink } from './link/insertLink'; +export { default as removeLink } from './link/removeLink'; +export { default as adjustLinkSelection } from './link/adjustLinkSelection'; +export { default as setImageAltText } from './image/setImageAltText'; +export { default as adjustImageSelection } from './image/adjustImageSelection'; +export { default as setParagraphMargin } from './block/setParagraphMargin'; diff --git a/packages/roosterjs-content-model/lib/publicApi/insert/insertImage.ts b/packages/roosterjs-content-model/lib/publicApi/insert/insertImage.ts deleted file mode 100644 index ea1d804621f..00000000000 --- a/packages/roosterjs-content-model/lib/publicApi/insert/insertImage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; -import { insertContent } from '../../modelApi/common/insertContent'; -import { readFile } from 'roosterjs-editor-dom'; - -/** - * Insert an image into current selected position - * @param editor The editor to operate on - * @param file Image Blob file - */ -export default function insertImage(editor: IExperimentalContentModelEditor, file: File) { - readFile(file, dataUrl => { - if (dataUrl && !editor.isDisposed()) { - formatWithContentModel(editor, 'insertImage', model => { - const image = editor.getDocument().createElement('img'); - - image.src = dataUrl; - insertContent(model, image); - return true; - }); - } - }); -} diff --git a/packages/roosterjs-content-model/lib/publicApi/link/adjustLinkSelection.ts b/packages/roosterjs-content-model/lib/publicApi/link/adjustLinkSelection.ts index 25948fbc9fd..f6672103ef1 100644 --- a/packages/roosterjs-content-model/lib/publicApi/link/adjustLinkSelection.ts +++ b/packages/roosterjs-content-model/lib/publicApi/link/adjustLinkSelection.ts @@ -1,18 +1,15 @@ import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; -import { areSameFormats } from '../../domToModel/utils/areSameFormats'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getSelectedSegments } from '../../modelApi/selection/collectSelections'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { setSelection } from '../../modelApi/selection/setSelection'; /** * 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: IExperimentalContentModelEditor -): [string, string | null] { +export default function adjustLinkSelection(editor: IContentModelEditor): [string, string | null] { let text = ''; let url: string | null = null; @@ -20,7 +17,7 @@ export default function adjustLinkSelection( let changed = adjustSegmentSelection( model, target => !!target.isSelected && !!target.link, - (target, ref) => !!target.link && areSameFormats(target.link.format, ref.link!.format) + (target, ref) => !!target.link && target.link.format.href == ref.link!.format.href ); let segments = getSelectedSegments(model, false /*includingFormatHolder*/); const firstSegment = segments[0]; diff --git a/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts b/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts index 29eb5c1c77d..d5688fedbfb 100644 --- a/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts +++ b/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts @@ -1,14 +1,12 @@ import { addLink } from '../../modelApi/common/addLink'; import { addSegment } from '../../modelApi/common/addSegment'; import { ContentModelLink } from '../../publicTypes/decorator/ContentModelLink'; -import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { createContentModelDocument } from '../../modelApi/creators/createContentModelDocument'; import { createText } from '../../modelApi/creators/createText'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getSelectedSegments } from '../../modelApi/selection/collectSelections'; import { HtmlSanitizer, matchLink } from 'roosterjs-editor-dom'; -import { HyperLinkColorPlaceholder } from '../../formatHandlers/utils/defaultStyles'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { mergeModel } from '../../modelApi/common/mergeModel'; // Regex matching Uri scheme @@ -32,7 +30,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: IExperimentalContentModelEditor, + editor: IContentModelEditor, link: string, anchorTitle?: string, displayText?: string, @@ -47,6 +45,7 @@ export default function insertLink( href: linkData ? linkData.normalizedUrl : applyLinkPrefix(url), anchorTitle, target, + underline: true, }, }; @@ -55,25 +54,26 @@ export default function insertLink( const originalText = segments .map(x => (x.segmentType == 'Text' ? x.text : '')) .join(''); - const text = displayText || originalText || (linkData ? linkData.originalUrl : url); + const text = displayText || originalText || ''; - if ( + if (segments.some(x => x.segmentType != 'SelectionMarker') && originalText == text) { + segments.forEach(x => { + addLink(x, link); + }); + } else if ( segments.every(x => x.segmentType == 'SelectionMarker') || (!!text && text != originalText) ) { - const segment = createText(text, segments[0]?.format); + const segment = createText( + text || (linkData ? linkData.originalUrl : url), + segments[0]?.format + ); const doc = createContentModelDocument(); addLink(segment, link); addSegment(doc, segment); - updateLinkSegmentFormat(segment.format); mergeModel(model, doc); - } else if (text == originalText || !text) { - segments.forEach(x => { - addLink(x, link); - updateLinkSegmentFormat(x.format); - }); } return segments.length > 0; @@ -81,11 +81,6 @@ export default function insertLink( } } -function updateLinkSegmentFormat(format: ContentModelSegmentFormat) { - format.underline = true; - format.textColor = HyperLinkColorPlaceholder; -} - // TODO: This is copied from original code. We may need to integrate this logic into matchLink() later. function applyLinkPrefix(url: string): string { if (!url) { diff --git a/packages/roosterjs-content-model/lib/publicApi/link/removeLink.ts b/packages/roosterjs-content-model/lib/publicApi/link/removeLink.ts index 224611e2aaf..3e04ba0ab2c 100644 --- a/packages/roosterjs-content-model/lib/publicApi/link/removeLink.ts +++ b/packages/roosterjs-content-model/lib/publicApi/link/removeLink.ts @@ -1,9 +1,7 @@ import { adjustSegmentSelection } from '../../modelApi/selection/adjustSegmentSelection'; -import { areSameFormats } from '../../domToModel/utils/areSameFormats'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getSelectedSegments } from '../../modelApi/selection/collectSelections'; -import { HyperLinkColorPlaceholder } from '../../formatHandlers/utils/defaultStyles'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Remove link at selection. If no links at selection, do nothing. @@ -11,14 +9,14 @@ import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimental * 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: IExperimentalContentModelEditor) { +export default function removeLink(editor: IContentModelEditor) { 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 && areSameFormats(target.link.format, ref.link!.format)) + (!!target.link && target.link.format.href == ref.link!.format.href) ); const segments = getSelectedSegments(model, false /*includingFormatHolder*/); @@ -28,11 +26,6 @@ export default function removeLink(editor: IExperimentalContentModelEditor) { if (segment.link) { isChanged = true; - if (segment.format.textColor == HyperLinkColorPlaceholder) { - delete segment.format.textColor; - } - - segment.format.underline = false; delete segment.link; } }); diff --git a/packages/roosterjs-content-model/lib/publicApi/list/setListStartNumber.ts b/packages/roosterjs-content-model/lib/publicApi/list/setListStartNumber.ts index 035eaef509b..aae1b79d3ea 100644 --- a/packages/roosterjs-content-model/lib/publicApi/list/setListStartNumber.ts +++ b/packages/roosterjs-content-model/lib/publicApi/list/setListStartNumber.ts @@ -1,13 +1,13 @@ import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getFirstSelectedListItem } from '../../modelApi/selection/collectSelections'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { 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: IExperimentalContentModelEditor, value: number) { +export default function setListStartNumber(editor: IContentModelEditor, value: number) { formatWithContentModel(editor, 'setListStartNumber', model => { const listItem = getFirstSelectedListItem(model); const level = listItem?.levels[listItem?.levels.length - 1]; diff --git a/packages/roosterjs-content-model/lib/publicApi/list/setListStyle.ts b/packages/roosterjs-content-model/lib/publicApi/list/setListStyle.ts index 7f564e751d0..3df6c6b1aeb 100644 --- a/packages/roosterjs-content-model/lib/publicApi/list/setListStyle.ts +++ b/packages/roosterjs-content-model/lib/publicApi/list/setListStyle.ts @@ -1,7 +1,7 @@ import { findListItemsInSameThread } from '../../modelApi/list/findListItemsInSameThread'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getFirstSelectedListItem } from '../../modelApi/selection/collectSelections'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { ListMetadataFormat } from '../../publicTypes/format/formatParts/ListMetadataFormat'; /** @@ -9,10 +9,7 @@ import { ListMetadataFormat } from '../../publicTypes/format/formatParts/ListMet * @param editor The editor to operate on * @param style The target list item style to set */ -export default function setListStyle( - editor: IExperimentalContentModelEditor, - style: ListMetadataFormat -) { +export default function setListStyle(editor: IContentModelEditor, style: ListMetadataFormat) { formatWithContentModel(editor, 'setListStyle', model => { const listItem = getFirstSelectedListItem(model); diff --git a/packages/roosterjs-content-model/lib/publicApi/list/toggleBullet.ts b/packages/roosterjs-content-model/lib/publicApi/list/toggleBullet.ts index ee8681a62d0..5daee5af867 100644 --- a/packages/roosterjs-content-model/lib/publicApi/list/toggleBullet.ts +++ b/packages/roosterjs-content-model/lib/publicApi/list/toggleBullet.ts @@ -1,5 +1,5 @@ import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { setListType } from '../../modelApi/list/setListType'; /** @@ -8,6 +8,6 @@ import { setListType } from '../../modelApi/list/setListType'; * - 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: IExperimentalContentModelEditor) { +export default function toggleBullet(editor: IContentModelEditor) { formatWithContentModel(editor, 'toggleBullet', model => setListType(model, 'UL')); } diff --git a/packages/roosterjs-content-model/lib/publicApi/list/toggleNumbering.ts b/packages/roosterjs-content-model/lib/publicApi/list/toggleNumbering.ts index d23c1a9ac3f..216c0061162 100644 --- a/packages/roosterjs-content-model/lib/publicApi/list/toggleNumbering.ts +++ b/packages/roosterjs-content-model/lib/publicApi/list/toggleNumbering.ts @@ -1,5 +1,5 @@ import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { setListType } from '../../modelApi/list/setListType'; /** @@ -8,6 +8,6 @@ import { setListType } from '../../modelApi/list/setListType'; * - 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: IExperimentalContentModelEditor) { +export default function toggleNumbering(editor: IContentModelEditor) { formatWithContentModel(editor, 'toggleNumbering', model => setListType(model, 'OL')); } diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/applySegmentFormat.ts b/packages/roosterjs-content-model/lib/publicApi/segment/applySegmentFormat.ts index 2268fa5b374..7b0af7a5c45 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/applySegmentFormat.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/applySegmentFormat.ts @@ -1,6 +1,6 @@ import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Bulk apply segment format to all selected content. This is usually used for format painter. @@ -8,7 +8,7 @@ import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimental * @param newFormat The segment format to apply */ export default function applySegmentFormat( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, newFormat: ContentModelSegmentFormat ) { formatSegmentWithContentModel( diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/changeCapitalization.ts b/packages/roosterjs-content-model/lib/publicApi/segment/changeCapitalization.ts index e2644202c54..a3900553351 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/changeCapitalization.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/changeCapitalization.ts @@ -1,5 +1,5 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Change the capitalization of text in the selection @@ -10,7 +10,7 @@ import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimental * Default is the host environment’s current locale. */ export default function changeCapitalization( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, capitalization: 'sentence' | 'lowerCase' | 'upperCase' | 'capitalize', language?: string ) { diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/changeFontSize.ts b/packages/roosterjs-content-model/lib/publicApi/segment/changeFontSize.ts index 6cf4eb033db..7beddc0359d 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/changeFontSize.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/changeFontSize.ts @@ -1,9 +1,7 @@ import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; -import { FontSizeFormat } from '../../publicTypes/format/formatParts/FontSizeFormat'; -import { FormatParser } from '../../publicTypes/context/DomToModelSettings'; import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { getComputedStyle } from 'roosterjs-editor-dom'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; +import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit'; /** * Default font size sequence, in pt. Suggest editor UI use this sequence as your font size list, @@ -20,7 +18,7 @@ const MAX_FONT_SIZE = 1000; * @param fontSizes A sorted font size array, in pt. Default value is FONT_SIZES */ export default function changeFontSize( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, change: 'increase' | 'decrease' ) { formatSegmentWithContentModel( @@ -28,32 +26,19 @@ export default function changeFontSize( 'changeFontSize', format => changeFontSizeInternal(format, change), undefined /* segmentHasStyleCallback*/, - true /*includingFormatHandler*/, - { - formatParserOverride: { - fontSize: fontSizeHandler, - }, - } + true /*includingFormatHandler*/ ); } -const fontSizeHandler: FormatParser = (format, element, context, defaultStyle) => { - // Superscript and subscript will have "smaller" font size, - // we should keep using its parent element's font size since SUB/SUP tag will auto make font smaller - if (!format.fontSize || defaultStyle.fontSize != 'smaller') { - format.fontSize = getComputedStyle(element, 'font-size'); - } -}; - function changeFontSizeInternal( format: ContentModelSegmentFormat, change: 'increase' | 'decrease' ) { if (format.fontSize) { - let sizeNumber = parseFloat(format.fontSize); + let sizeInPt = parseValueWithUnit(format.fontSize, undefined /*element*/, 'pt'); - if (sizeNumber > 0) { - const newSize = getNewFontSize(sizeNumber, change == 'increase' ? 1 : -1, FONT_SIZES); + if (sizeInPt > 0) { + const newSize = getNewFontSize(sizeInPt, change == 'increase' ? 1 : -1, FONT_SIZES); format.fontSize = newSize + 'pt'; } diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/setBackgroundColor.ts b/packages/roosterjs-content-model/lib/publicApi/segment/setBackgroundColor.ts index b7aea12320c..2dcb2c46b14 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/setBackgroundColor.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/setBackgroundColor.ts @@ -1,5 +1,5 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Set background color @@ -7,7 +7,7 @@ import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimental * @param backgroundColor The color to set. Pass null to remove existing color. */ export default function setBackgroundColor( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, backgroundColor: string | null ) { formatSegmentWithContentModel( diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/setFontName.ts b/packages/roosterjs-content-model/lib/publicApi/segment/setFontName.ts index c02f31790bc..48b9f0dc9d1 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/setFontName.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/setFontName.ts @@ -1,12 +1,12 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Set font name * @param editor The editor to operate on * @param fontName The font name to set */ -export default function setFontName(editor: IExperimentalContentModelEditor, fontName: string) { +export default function setFontName(editor: IContentModelEditor, fontName: string) { formatSegmentWithContentModel( editor, 'setFontName', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/setFontSize.ts b/packages/roosterjs-content-model/lib/publicApi/segment/setFontSize.ts index 4d03a3b8aa0..87fe60c764f 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/setFontSize.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/setFontSize.ts @@ -1,12 +1,12 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { 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: IExperimentalContentModelEditor, fontSize: string) { +export default function setFontSize(editor: IContentModelEditor, fontSize: string) { formatSegmentWithContentModel( editor, 'setFontSize', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/setTextColor.ts b/packages/roosterjs-content-model/lib/publicApi/segment/setTextColor.ts index 42109d648bb..fe2df53f0e8 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/setTextColor.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/setTextColor.ts @@ -1,24 +1,29 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * 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: IExperimentalContentModelEditor, - textColor: string | null -) { +export default function setTextColor(editor: IContentModelEditor, textColor: string | null) { formatSegmentWithContentModel( editor, 'setTextColor', textColor === null - ? format => { + ? (format, _, segment) => { delete format.textColor; + + if (segment?.link) { + delete segment.link.format.textColor; + } } - : format => { + : (format, _, segment) => { format.textColor = textColor; + + if (segment?.link) { + segment.link.format.textColor = textColor; + } }, undefined /* segmentHasStyleCallback*/, true /*includingFormatHandler*/ diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/toggleBold.ts b/packages/roosterjs-content-model/lib/publicApi/segment/toggleBold.ts index ba2f7bc3702..0e97ad76ed8 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/toggleBold.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/toggleBold.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Toggle bold style * @param editor The editor to operate on */ -export default function toggleBold(editor: IExperimentalContentModelEditor) { +export default function toggleBold(editor: IContentModelEditor) { formatSegmentWithContentModel( editor, 'toggleBold', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/toggleItalic.ts b/packages/roosterjs-content-model/lib/publicApi/segment/toggleItalic.ts index 6c5b8ad2b43..8c62d361122 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/toggleItalic.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/toggleItalic.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Toggle italic style * @param editor The editor to operate on */ -export default function toggleItalic(editor: IExperimentalContentModelEditor) { +export default function toggleItalic(editor: IContentModelEditor) { formatSegmentWithContentModel( editor, 'toggleItalic', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/toggleStrikethrough.ts b/packages/roosterjs-content-model/lib/publicApi/segment/toggleStrikethrough.ts index 29b4645431b..aa227e891d3 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/toggleStrikethrough.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/toggleStrikethrough.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Toggle strikethrough style * @param editor The editor to operate on */ -export default function toggleStrikethrough(editor: IExperimentalContentModelEditor) { +export default function toggleStrikethrough(editor: IContentModelEditor) { formatSegmentWithContentModel( editor, 'toggleStrikethrough', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/toggleSubscript.ts b/packages/roosterjs-content-model/lib/publicApi/segment/toggleSubscript.ts index fe138169b7f..9e9c569fa15 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/toggleSubscript.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/toggleSubscript.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Toggle subscript style * @param editor The editor to operate on */ -export default function toggleSubscript(editor: IExperimentalContentModelEditor) { +export default function toggleSubscript(editor: IContentModelEditor) { formatSegmentWithContentModel( editor, 'toggleSubscript', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/toggleSuperscript.ts b/packages/roosterjs-content-model/lib/publicApi/segment/toggleSuperscript.ts index 093c8c2d79f..4ebbbe2127d 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/toggleSuperscript.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/toggleSuperscript.ts @@ -1,11 +1,11 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Toggle superscript style * @param editor The editor to operate on */ -export default function toggleSuperscript(editor: IExperimentalContentModelEditor) { +export default function toggleSuperscript(editor: IContentModelEditor) { formatSegmentWithContentModel( editor, 'toggleSuperscript', diff --git a/packages/roosterjs-content-model/lib/publicApi/segment/toggleUnderline.ts b/packages/roosterjs-content-model/lib/publicApi/segment/toggleUnderline.ts index 640103f8e86..b654b4e8bd8 100644 --- a/packages/roosterjs-content-model/lib/publicApi/segment/toggleUnderline.ts +++ b/packages/roosterjs-content-model/lib/publicApi/segment/toggleUnderline.ts @@ -1,16 +1,20 @@ import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * Toggle underline style * @param editor The editor to operate on */ -export default function toggleUnderline(editor: IExperimentalContentModelEditor) { +export default function toggleUnderline(editor: IContentModelEditor) { formatSegmentWithContentModel( editor, 'toggleUnderline', - (format, isTurningOn) => { + (format, isTurningOn, segment) => { format.underline = !!isTurningOn; + + if (segment?.link) { + segment.link.format.underline = !!isTurningOn; + } }, format => !!format.underline ); diff --git a/packages/roosterjs-content-model/lib/publicApi/table/editTable.ts b/packages/roosterjs-content-model/lib/publicApi/table/editTable.ts index 5b6e4375c3b..d986735dfa3 100644 --- a/packages/roosterjs-content-model/lib/publicApi/table/editTable.ts +++ b/packages/roosterjs-content-model/lib/publicApi/table/editTable.ts @@ -6,8 +6,8 @@ import { deleteTableColumn } from '../../modelApi/table/deleteTableColumn'; import { deleteTableRow } from '../../modelApi/table/deleteTableRow'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { hasMetadata } from '../../modelApi/metadata/updateMetadata'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { hasMetadata } from '../../domUtils/metadata/updateMetadata'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { insertTableColumn } from '../../modelApi/table/insertTableColumn'; import { insertTableRow } from '../../modelApi/table/insertTableRow'; import { mergeTableCells } from '../../modelApi/table/mergeTableCells'; @@ -23,10 +23,7 @@ import { TableOperation } from 'roosterjs-editor-types'; * @param editor The editor instance * @param operation The table operation to apply */ -export default function editTable( - editor: IExperimentalContentModelEditor, - operation: TableOperation -) { +export default function editTable(editor: IContentModelEditor, operation: TableOperation) { formatWithContentModel(editor, 'editTable', model => { const tableModel = getFirstSelectedTable(model); diff --git a/packages/roosterjs-content-model/lib/publicApi/table/formatTable.ts b/packages/roosterjs-content-model/lib/publicApi/table/formatTable.ts index 617250695e5..9e527888a8a 100644 --- a/packages/roosterjs-content-model/lib/publicApi/table/formatTable.ts +++ b/packages/roosterjs-content-model/lib/publicApi/table/formatTable.ts @@ -1,7 +1,7 @@ import { applyTableFormat } from '../../modelApi/table/applyTableFormat'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { TableMetadataFormat } from '../../publicTypes/format/formatParts/TableMetadataFormat'; /** @@ -11,7 +11,7 @@ import { TableMetadataFormat } from '../../publicTypes/format/formatParts/TableM * @param keepCellShade Whether keep existing shade color when apply format if there is a manually set shade color */ export default function formatTable( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, format: TableMetadataFormat, keepCellShade?: boolean ) { diff --git a/packages/roosterjs-content-model/lib/publicApi/table/insertTable.ts b/packages/roosterjs-content-model/lib/publicApi/table/insertTable.ts index eabcd44cc44..3c9b793e7dc 100644 --- a/packages/roosterjs-content-model/lib/publicApi/table/insertTable.ts +++ b/packages/roosterjs-content-model/lib/publicApi/table/insertTable.ts @@ -3,7 +3,7 @@ import { createContentModelDocument } from '../../modelApi/creators/createConten import { createSelectionMarker } from '../../modelApi/creators/createSelectionMarker'; import { createTableStructure } from '../../modelApi/table/createTableStructure'; import { formatWithContentModel } from '../utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { mergeModel } from '../../modelApi/common/mergeModel'; import { normalizeTable } from '../../modelApi/table/normalizeTable'; import { setSelection } from '../../modelApi/selection/setSelection'; @@ -19,7 +19,7 @@ import { TableMetadataFormat } from '../../publicTypes/format/formatParts/TableM * background color: #FFF; border color: #ABABAB */ export default function insertTable( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, columns: number, rows: number, format?: TableMetadataFormat diff --git a/packages/roosterjs-content-model/lib/publicApi/table/setTableCellShade.ts b/packages/roosterjs-content-model/lib/publicApi/table/setTableCellShade.ts index 83f6c8f4d1c..dcf9a85f145 100644 --- a/packages/roosterjs-content-model/lib/publicApi/table/setTableCellShade.ts +++ b/packages/roosterjs-content-model/lib/publicApi/table/setTableCellShade.ts @@ -1,7 +1,7 @@ import hasSelectionInBlockGroup from '../selection/hasSelectionInBlockGroup'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { getFirstSelectedTable } from '../../modelApi/selection/collectSelections'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { normalizeTable } from '../../modelApi/table/normalizeTable'; import { setTableCellBackgroundColor } from '../../modelApi/table/setTableCellBackgroundColor'; @@ -10,7 +10,7 @@ import { setTableCellBackgroundColor } from '../../modelApi/table/setTableCellBa * @param editor The editor instance * @param color The color to set */ -export default function setTableCellShade(editor: IExperimentalContentModelEditor, color: string) { +export default function setTableCellShade(editor: IContentModelEditor, color: string) { formatWithContentModel(editor, 'setTableCellShade', model => { const table = getFirstSelectedTable(model); diff --git a/packages/roosterjs-content-model/lib/publicApi/utils/formatImageWithContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/utils/formatImageWithContentModel.ts new file mode 100644 index 00000000000..a6153d37ca6 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicApi/utils/formatImageWithContentModel.ts @@ -0,0 +1,24 @@ +import { ContentModelImage } from '../../publicTypes/segment/ContentModelImage'; +import { formatSegmentWithContentModel } from './formatSegmentWithContentModel'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; + +/** + * @internal + */ +export default function formatImageWithContentModel( + editor: IContentModelEditor, + apiName: string, + callback: (segment: ContentModelImage) => void +) { + formatSegmentWithContentModel( + editor, + apiName, + (_, __, segment) => { + if (segment?.segmentType == 'Image') { + callback(segment); + } + }, + undefined /** segmentHasStyleCallback **/, + undefined /** includingFormatHolder */ + ); +} diff --git a/packages/roosterjs-content-model/lib/publicApi/utils/formatParagraphWithContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/utils/formatParagraphWithContentModel.ts index b509a32fdd3..aa68f490f77 100644 --- a/packages/roosterjs-content-model/lib/publicApi/utils/formatParagraphWithContentModel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/utils/formatParagraphWithContentModel.ts @@ -1,13 +1,13 @@ import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParagraph'; import { formatWithContentModel } from './formatWithContentModel'; import { getSelectedParagraphs } from '../../modelApi/selection/collectSelections'; -import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * @internal */ export function formatParagraphWithContentModel( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, apiName: string, setStyleCallback: (paragraph: ContentModelParagraph) => void ) { diff --git a/packages/roosterjs-content-model/lib/publicApi/utils/formatSegmentWithContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/utils/formatSegmentWithContentModel.ts index 8e1e6548a34..f6699d500ce 100644 --- a/packages/roosterjs-content-model/lib/publicApi/utils/formatSegmentWithContentModel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/utils/formatSegmentWithContentModel.ts @@ -1,17 +1,15 @@ import { adjustWordSelection } from '../../modelApi/selection/adjustWordSelection'; import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat'; +import { DomToModelOption, IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { formatWithContentModel } from './formatWithContentModel'; +import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat'; import { getSelectedSegments } from '../../modelApi/selection/collectSelections'; -import { - DomToModelOption, - IExperimentalContentModelEditor, -} from '../../publicTypes/IExperimentalContentModelEditor'; /** * @internal */ export function formatSegmentWithContentModel( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, apiName: string, toggleStyleCallback: ( format: ContentModelSegmentFormat, @@ -30,7 +28,7 @@ export function formatSegmentWithContentModel( apiName, model => { let segments = getSelectedSegments(model, !!includingFormatHolder); - const pendingFormat = editor.getPendingFormat(); + const pendingFormat = getPendingFormat(editor); let isCollapsedSelection = segments.length == 1 && segments[0].segmentType == 'SelectionMarker'; @@ -59,7 +57,11 @@ export function formatSegmentWithContentModel( ); if (!pendingFormat && isCollapsedSelection) { - editor.setPendingFormat(segments[0].format); + const pos = editor.getFocusedPosition(); + + if (pos) { + setPendingFormat(editor, segments[0].format, pos); + } } if (isCollapsedSelection) { diff --git a/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts index e34b0937f14..4c242e2cb00 100644 --- a/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts +++ b/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts @@ -1,20 +1,17 @@ import { ChangeSource } from 'roosterjs-editor-types'; import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument'; -import { - DomToModelOption, - IExperimentalContentModelEditor, -} from '../../publicTypes/IExperimentalContentModelEditor'; +import { DomToModelOption, IContentModelEditor } from '../../publicTypes/IContentModelEditor'; /** * @internal */ export function formatWithContentModel( - editor: IExperimentalContentModelEditor, + editor: IContentModelEditor, apiName: string, callback: (model: ContentModelDocument) => boolean, domToModelOptions?: DomToModelOption ) { - const model = editor.createContentModel(undefined /*rootNode*/, domToModelOptions); + const model = editor.createContentModel(domToModelOptions); if (callback(model)) { editor.addUndoSnapshot( diff --git a/packages/roosterjs-content-model/lib/publicPlugin/ContentModelPlugin.ts b/packages/roosterjs-content-model/lib/publicPlugin/ContentModelPlugin.ts deleted file mode 100644 index a3550d55e0c..00000000000 --- a/packages/roosterjs-content-model/lib/publicPlugin/ContentModelPlugin.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { ContentModelSegmentFormat } from '../publicTypes/format/ContentModelSegmentFormat'; -import { createText } from '../modelApi/creators/createText'; -import { EditorPlugin, IEditor, Keys, PluginEvent, PluginEventType } from 'roosterjs-editor-types'; -import { IExperimentalContentModelEditor } from '../publicTypes/IExperimentalContentModelEditor'; -import { iterateSelections } from '../modelApi/selection/iterateSelections'; - -/** - * ContentModel plugins helps editor to do editing operation on top of content model. - * This includes: - * 1. Handle pending format changes when selection is collapsed - */ -export default class ContentModelPlugin implements EditorPlugin { - private editor: IExperimentalContentModelEditor | null = null; - - /** - * Get name of this plugin - */ - getName() { - return 'ContentModel'; - } - - /** - * The first method that editor will call to a plugin when editor is initializing. - * It will pass in the editor instance, plugin should take this chance to save the - * editor reference so that it can call to any editor method or format API later. - * @param editor The editor object - */ - initialize(editor: IEditor) { - // TODO: Later we may need a different interface for Content Model editor plugin - this.editor = editor as IExperimentalContentModelEditor; - } - - /** - * The last method that editor will call to a plugin before it is disposed. - * Plugin can take this chance to clear the reference to editor. After this method is - * called, plugin should not call to any editor method since it will result in error. - */ - dispose() { - this.editor = null; - } - - /** - * Core method for a plugin. Once an event happens in editor, editor will call this - * method of each plugin to handle the event as long as the event is not handled - * exclusively by another plugin. - * @param event The event to handle: - */ - onPluginEvent(event: PluginEvent) { - if (this.editor) { - let format: ContentModelSegmentFormat | null; - - if ( - ((event.eventType == PluginEventType.Input && - !event.rawEvent.isComposing && - !this.editor.isInIME()) || // In Safari, isComposing will be undfined but isInIME() works - event.eventType == PluginEventType.CompositionEnd) && - event.rawEvent.data && - (format = this.editor.getPendingFormat()) - ) { - applyPendingFormat(this.editor, event.rawEvent.data, format); - this.editor.setPendingFormat(null); - } - - if ( - (event.eventType == PluginEventType.KeyDown && - event.rawEvent.which >= Keys.PAGEUP && - event.rawEvent.which <= Keys.DOWN) || - event.eventType == PluginEventType.MouseDown || - event.eventType == PluginEventType.ContentChanged - ) { - this.editor.setPendingFormat(null); - } - } - } -} - -function applyPendingFormat( - editor: IExperimentalContentModelEditor, - data: string, - format: ContentModelSegmentFormat -) { - const model = editor.createContentModel(); - let isChanged = false; - - iterateSelections([model], (_, __, block, segments) => { - if ( - block?.blockType == 'Paragraph' && - segments?.length == 1 && - segments[0].segmentType == 'SelectionMarker' - ) { - const index = block.segments.indexOf(segments[0]); - const previousSegment = block.segments[index - 1]; - - if (previousSegment?.segmentType == 'Text') { - const text = previousSegment.text; - - if (text.substr(-data.length, data.length) == data) { - previousSegment.text = text.substring(0, text.length - data.length); - - const newText = createText(data, { - ...previousSegment.format, - ...format, - }); - - block.segments.splice(index, 0, newText); - isChanged = true; - } - } - } - return true; - }); - - if (isChanged) { - editor.setContentModel(model); - } -} diff --git a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts b/packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts similarity index 59% rename from packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts rename to packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts index 4bc05a1d116..d699564e5fa 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/IExperimentalContentModelEditor.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts @@ -1,6 +1,4 @@ import { ContentModelDocument } from './group/ContentModelDocument'; -import { ContentModelSegmentFormat } from './format/ContentModelSegmentFormat'; -import { EditorContext } from './context/EditorContext'; import { IEditor, SelectionRangeEx } from 'roosterjs-editor-types'; import { ContentModelHandlerMap, @@ -61,23 +59,6 @@ export interface DomToModelOption { * Options for creating ModelToDomContext */ export interface ModelToDomOption { - /** - * A callback to specify how to merge DOM tree generated from Content Model in to existing container - * @param source Source document fragment that is generated from Content Model - * @param target Target container, usually to be editor root container - * @param entities An array of entity wrapper - placeholder pairs, used for reuse existing DOM structure for entity - */ - mergingCallback?: ( - source: DocumentFragment, - target: HTMLElement, - entities: Record - ) => void; - - /** - * When set to true, directly put entity DOM nodes into the result DOM tree when doing Content Model to DOM conversion and do not use placeholder - */ - doNotReuseEntityDom?: boolean; - /** * Overrides default format appliers */ @@ -100,23 +81,17 @@ export interface ModelToDomOption { } /** - * !!! This is a temporary interface and will be removed in the future !!! - * - * An interface of editor with Content Model support (in experiment) + * 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 IExperimentalContentModelEditor extends IEditor { - /** - * Create a EditorContext object used by ContentModel API - */ - createEditorContext(): EditorContext; - +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 */ - createContentModel(rootNode?: HTMLElement, option?: DomToModelOption): ContentModelDocument; + createContentModel(option?: DomToModelOption): ContentModelDocument; /** * Set content with content model @@ -124,16 +99,4 @@ export interface IExperimentalContentModelEditor extends IEditor { * @param option Additional options to customize the behavior of Content Model to DOM conversion */ setContentModel(model: ContentModelDocument, option?: ModelToDomOption): void; - - /** - * Get current pending format if any. A pending format is a format that user set when selection is collapsed, - * it will be applied when next time user input something - */ - getPendingFormat(): ContentModelSegmentFormat | null; - - /** - * Set current pending format if any. A pending format is a format that user set when selection is collapsed, - * it will be applied when next time user input something - */ - setPendingFormat(format: ContentModelSegmentFormat | null): void; } diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ContentModelHandler.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ContentModelHandler.ts index edb6fba34b6..6040a168fab 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ContentModelHandler.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ContentModelHandler.ts @@ -1,5 +1,6 @@ import { ContentModelBlock } from '../block/ContentModelBlock'; import { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; +import { ContentModelLink } from '../decorator/ContentModelLink'; import { ContentModelSegment } from '../segment/ContentModelSegment'; import { ModelToDomContext } from './ModelToDomContext'; @@ -11,5 +12,5 @@ import { ModelToDomContext } from './ModelToDomContext'; * @param context The context object to provide related information */ export type ContentModelHandler< - T extends ContentModelSegment | ContentModelBlock | ContentModelBlockGroup + T extends ContentModelSegment | ContentModelBlock | ContentModelBlockGroup | ContentModelLink > = (doc: Document, parent: Node, model: T, context: ModelToDomContext) => void; diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts index e37e5911581..de302ed4417 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/DomToModelFormatContext.ts @@ -3,6 +3,7 @@ import { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; import { ContentModelLink } from '../decorator/ContentModelLink'; import { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; import { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; +import { ZoomScaleFormat } from '../format/formatParts/ZoomScaleFormat'; /** * Represents the context object used when do DOM to Content Model conversion and processing a List @@ -48,6 +49,11 @@ export interface DomToModelFormatContext { */ link: ContentModelLink; + /** + * Zoom scale of the content + */ + zoomScaleFormat: ZoomScaleFormat; + /** * When process table, whether we should always normalize it. * This can help persist the size of table that is not created from Content Model diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts index ef773d3ed3c..b8a514859fd 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/EditorContext.ts @@ -9,16 +9,6 @@ export interface EditorContext { */ isDarkMode: boolean; - /** - * Zoom scale of the content - */ - zoomScale: number; - - /** - * Whether current content is from right to left - */ - isRightToLeft: boolean; - /** * Calculate color for dark mode * @param lightColor Light mode color diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomEntityContext.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomEntityContext.ts index d8e564d52cf..1dc2a28f229 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomEntityContext.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomEntityContext.ts @@ -2,11 +2,6 @@ * Represents context for entity */ export interface ModelToDomEntityContext { - /** - * When set to true, directly put entity DOM nodes into the result DOM tree when doing Content Model to DOM conversion and do not use placeholder - */ - doNotReuseEntityDom: boolean; - /** * Entities collected during DOM tree generation, used for reusing existing DOM structure of entities */ diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts index 54edd709c3c..4a3a13f2fc5 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts @@ -9,6 +9,7 @@ import { ContentModelFormatMap } from '../format/ContentModelFormatMap'; import { ContentModelGeneralBlock } from '../group/ContentModelGeneralBlock'; import { ContentModelHandler } from './ContentModelHandler'; import { ContentModelImage } from '../segment/ContentModelImage'; +import { ContentModelLink } from '../decorator/ContentModelLink'; import { ContentModelListItem } from '../group/ContentModelListItem'; import { ContentModelParagraph } from '../block/ContentModelParagraph'; import { ContentModelQuote } from '../group/ContentModelQuote'; @@ -97,6 +98,11 @@ export interface ContentModelHandlerTypeMap { */ image: ContentModelImage; + /** + * Content Model type for ContentModelLink + */ + link: ContentModelLink; + /** * Content Model type for list group of ContentModelListItem */ diff --git a/packages/roosterjs-content-model/lib/publicTypes/decorator/ContentModelLink.ts b/packages/roosterjs-content-model/lib/publicTypes/decorator/ContentModelLink.ts index 2e570235909..ec6909e54a8 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/decorator/ContentModelLink.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/decorator/ContentModelLink.ts @@ -1,6 +1,6 @@ +import { ContentModelHyperLinkFormat } from '../format/ContentModelHyperLinkFormat'; import { ContentModelWithDataset } from '../format/ContentModelWithDataset'; import { ContentModelWithFormat } from '../format/ContentModelWithFormat'; -import { LinkFormat } from '../format/formatParts/LinkFormat'; /** * Represent link info of Content Model. @@ -8,5 +8,5 @@ import { LinkFormat } from '../format/formatParts/LinkFormat'; * since link is also a kind of segment, with some extra information */ export interface ContentModelLink - extends ContentModelWithFormat, + extends ContentModelWithFormat, ContentModelWithDataset {} diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelHyperLinkFormat.ts b/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelHyperLinkFormat.ts new file mode 100644 index 00000000000..94da744c7e2 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelHyperLinkFormat.ts @@ -0,0 +1,8 @@ +import { LinkFormat } from './formatParts/LinkFormat'; +import { TextColorFormat } from './formatParts/TextColorFormat'; +import { UnderlineFormat } from './formatParts/UnderlineFormat'; + +/** + * The format object for a hyperlink in Content Model + */ +export type ContentModelHyperLinkFormat = LinkFormat & TextColorFormat & UnderlineFormat; diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelImageFormat.ts b/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelImageFormat.ts index 36603e8b05b..0c054922338 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelImageFormat.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelImageFormat.ts @@ -1,3 +1,5 @@ +import { BorderFormat } from './formatParts/BorderFormat'; +import { BoxShadowFormat } from './formatParts/BoxShadowFormat'; import { ContentModelSegmentFormat } from './ContentModelSegmentFormat'; import { IdFormat } from './formatParts/IdFormat'; import { MarginFormat } from './formatParts/MarginFormat'; @@ -11,4 +13,6 @@ export type ContentModelImageFormat = ContentModelSegmentFormat & IdFormat & SizeFormat & MarginFormat & - PaddingFormat; + PaddingFormat & + BorderFormat & + BoxShadowFormat; diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelSegmentFormat.ts b/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelSegmentFormat.ts index 0df62de69ec..bafe3bf8903 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelSegmentFormat.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/format/ContentModelSegmentFormat.ts @@ -3,6 +3,7 @@ import { BoldFormat } from './formatParts/BoldFormat'; import { FontFamilyFormat } from './formatParts/FontFamilyFormat'; import { FontSizeFormat } from './formatParts/FontSizeFormat'; import { ItalicFormat } from './formatParts/ItalicFormat'; +import { LineHeightFormat } from './formatParts/LineHeightFormat'; import { StrikeFormat } from './formatParts/StrikeFormat'; import { SuperOrSubScriptFormat } from './formatParts/SuperOrSubScriptFormat'; import { TextColorFormat } from './formatParts/TextColorFormat'; @@ -19,4 +20,5 @@ export type ContentModelSegmentFormat = TextColorFormat & ItalicFormat & UnderlineFormat & StrikeFormat & - SuperOrSubScriptFormat; + SuperOrSubScriptFormat & + LineHeightFormat; diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts b/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts index 576b8615c1b..e792d8ecb2e 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/format/FormatHandlerTypeMap.ts @@ -2,6 +2,7 @@ import { BackgroundColorFormat } from './formatParts/BackgroundColorFormat'; import { BoldFormat } from './formatParts/BoldFormat'; import { BorderBoxFormat } from './formatParts/BorderBoxFormat'; import { BorderFormat } from './formatParts/BorderFormat'; +import { BoxShadowFormat } from './formatParts/BoxShadowFormat'; import { DatasetFormat } from './formatParts/DatasetFormat'; import { DirectionFormat } from './formatParts/DirectionFormat'; import { DisplayFormat } from './formatParts/DisplayFormat'; @@ -50,6 +51,11 @@ export interface FormatHandlerTypeMap { */ borderBox: BorderBoxFormat; + /** + * Format for BoxShadowFormat + */ + boxShadow: BoxShadowFormat; + /** * Format for DatasetFormat */ diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BorderFormat.ts b/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BorderFormat.ts index e0ac66acc74..8d0b501cdfd 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BorderFormat.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BorderFormat.ts @@ -21,4 +21,9 @@ export type BorderFormat = { * Left border in format 'width style color' */ borderLeft?: string; + + /** + * Radius to be applied in all borders corners + */ + borderRadius?: string; }; diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BoxShadowFormat.ts b/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BoxShadowFormat.ts new file mode 100644 index 00000000000..1f5d4acafed --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/BoxShadowFormat.ts @@ -0,0 +1,9 @@ +/** + * Format of box shadow + */ +export type BoxShadowFormat = { + /** + * Box shadow in format "offset-x offset-y blur-radius spread-radius color" + */ + boxShadow?: string; +}; diff --git a/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/ZoomScaleFormat.ts b/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/ZoomScaleFormat.ts new file mode 100644 index 00000000000..2de9a3646a3 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/format/formatParts/ZoomScaleFormat.ts @@ -0,0 +1,9 @@ +/** + * Format of scale + */ +export type ZoomScaleFormat = { + /** + * Zoom scale number + */ + zoomScale?: number; +}; diff --git a/packages/roosterjs-content-model/lib/publicTypes/index.ts b/packages/roosterjs-content-model/lib/publicTypes/index.ts new file mode 100644 index 00000000000..6a59980530d --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/index.ts @@ -0,0 +1,126 @@ +export { ContentModelBlockGroupType } from './enum/BlockGroupType'; +export { ContentModelBlockType } from './enum/BlockType'; +export { ContentModelSegmentType } from './enum/SegmentType'; +export { Selectable } from './selection/Selectable'; + +export { ContentModelBlockBase } from './block/ContentModelBlockBase'; +export { ContentModelTable } from './block/ContentModelTable'; +export { ContentModelBlockGroupBase } from './group/ContentModelBlockGroupBase'; +export { ContentModelDocument } from './group/ContentModelDocument'; +export { ContentModelQuote } from './group/ContentModelQuote'; +export { ContentModelListItem } from './group/ContentModelListItem'; +export { ContentModelTableCell } from './group/ContentModelTableCell'; +export { ContentModelGeneralBlock } from './group/ContentModelGeneralBlock'; +export { ContentModelBlockGroup } from './group/ContentModelBlockGroup'; +export { ContentModelBlock } from './block/ContentModelBlock'; +export { ContentModelParagraph } from './block/ContentModelParagraph'; +export { ContentModelSegmentBase } from './segment/ContentModelSegmentBase'; +export { ContentModelSelectionMarker } from './segment/ContentModelSelectionMarker'; +export { ContentModelText } from './segment/ContentModelText'; +export { ContentModelBr } from './segment/ContentModelBr'; +export { ContentModelImage } from './segment/ContentModelImage'; +export { ContentModelGeneralSegment } from './segment/ContentModelGeneralSegment'; +export { ContentModelSegment } from './segment/ContentModelSegment'; +export { ContentModelEntity } from './entity/ContentModelEntity'; +export { ContentModelDivider } from './block/ContentModelDivider'; + +export { ContentModelParagraphDecorator } from './decorator/ContentModelParagraphDecorator'; +export { ContentModelLink } from './decorator/ContentModelLink'; + +export { FormatHandlerTypeMap, FormatKey } from './format/FormatHandlerTypeMap'; +export { ContentModelTableFormat } from './format/ContentModelTableFormat'; +export { ContentModelTableCellFormat } from './format/ContentModelTableCellFormat'; +export { ContentModelBlockFormat } from './format/ContentModelBlockFormat'; +export { ContentModelSegmentFormat } from './format/ContentModelSegmentFormat'; +export { ContentModelListItemLevelFormat } from './format/ContentModelListItemLevelFormat'; +export { ContentModelImageFormat } from './format/ContentModelImageFormat'; +export { ContentModelWithFormat } from './format/ContentModelWithFormat'; +export { ContentModelWithDataset } from './format/ContentModelWithDataset'; +export { ContentModelDividerFormat } from './format/ContentModelDividerFormat'; +export { ContentModelHyperLinkFormat } from './format/ContentModelHyperLinkFormat'; + +export { VerticalAlignFormat } from './format/formatParts/VerticalAlignFormat'; +export { BackgroundColorFormat } from './format/formatParts/BackgroundColorFormat'; +export { BorderFormat } from './format/formatParts/BorderFormat'; +export { BorderBoxFormat } from './format/formatParts/BorderBoxFormat'; +export { IdFormat } from './format/formatParts/IdFormat'; +export { SizeFormat } from './format/formatParts/SizeFormat'; +export { SpacingFormat } from './format/formatParts/SpacingFormat'; +export { DirectionFormat } from './format/formatParts/DirectionFormat'; +export { TextColorFormat } from './format/formatParts/TextColorFormat'; +export { FontSizeFormat } from './format/formatParts/FontSizeFormat'; +export { FontFamilyFormat } from './format/formatParts/FontFamilyFormat'; +export { BoldFormat } from './format/formatParts/BoldFormat'; +export { ItalicFormat } from './format/formatParts/ItalicFormat'; +export { UnderlineFormat } from './format/formatParts/UnderlineFormat'; +export { StrikeFormat } from './format/formatParts/StrikeFormat'; +export { SuperOrSubScriptFormat } from './format/formatParts/SuperOrSubScriptFormat'; +export { TableMetadataFormat } from './format/formatParts/TableMetadataFormat'; +export { ContentModelFormatBase } from './format/ContentModelFormatBase'; +export { MarginFormat } from './format/formatParts/MarginFormat'; +export { PaddingFormat } from './format/formatParts/PaddingFormat'; +export { DisplayFormat } from './format/formatParts/DisplayFormat'; +export { LineHeightFormat } from './format/formatParts/LineHeightFormat'; +export { LinkFormat } from './format/formatParts/LinkFormat'; +export { ListTypeFormat } from './format/formatParts/ListTypeFormat'; +export { ListThreadFormat } from './format/formatParts/ListThreadFormat'; +export { ListMetadataFormat } from './format/formatParts/ListMetadataFormat'; +export { + ImageResizeMetadataFormat, + ImageCropMetadataFormat, + ImageMetadataFormat, + ImageRotateMetadataFormat, +} from './format/formatParts/ImageMetadataFormat'; +export { DatasetFormat } from './format/formatParts/DatasetFormat'; +export { WhiteSpaceFormat } from './format/formatParts/WhiteSpaceFormat'; +export { WordBreakFormat } from './format/formatParts/WordBreakFormat'; +export { ZoomScaleFormat } from './format/formatParts/ZoomScaleFormat'; + +export { ContentModelFormatMap } from './format/ContentModelFormatMap'; + +export { EditorContext } from './context/EditorContext'; +export { DomToModelListFormat, DomToModelFormatContext } from './context/DomToModelFormatContext'; +export { + DomToModelRegularSelection, + DomToModelTableSelection, + DomToModelImageSelection, + DomToModelSelectionContext, +} from './context/DomToModelSelectionContext'; +export { + DomToModelSettings, + DefaultStyleMap, + ElementProcessorMap, + FormatParser, + FormatParsers, + FormatParsersPerCategory, +} from './context/DomToModelSettings'; +export { DomToModelContext } from './context/DomToModelContext'; +export { ModelToDomContext } from './context/ModelToDomContext'; +export { + ModelToDomListStackItem, + ModelToDomListContext, + ModelToDomFormatContext, +} from './context/ModelToDomFormatContext'; +export { + ModelToDomBlockAndSegmentNode, + ModelToDomRegularSelection, + ModelToDomTableSelection, + ModelToDomImageSelection, + ModelToDomSelectionContext, +} from './context/ModelToDomSelectionContext'; +export { + ModelToDomSettings, + FormatApplier, + FormatAppliers, + FormatAppliersPerCategory, + ContentModelHandlerMap, + ContentModelHandlerTypeMap, + DefaultImplicitFormatMap, +} from './context/ModelToDomSettings'; +export { ModelToDomEntityContext } from './context/ModelToDomEntityContext'; +export { ElementProcessor } from './context/ElementProcessor'; +export { ContentModelHandler } from './context/ContentModelHandler'; + +export { Border } from './interface/Border'; + +export { IContentModelEditor, DomToModelOption, ModelToDomOption } from './IContentModelEditor'; diff --git a/packages/roosterjs-content-model/lib/publicTypes/interface/Border.ts b/packages/roosterjs-content-model/lib/publicTypes/interface/Border.ts new file mode 100644 index 00000000000..adbc8d31d21 --- /dev/null +++ b/packages/roosterjs-content-model/lib/publicTypes/interface/Border.ts @@ -0,0 +1,20 @@ +/** + * A combination of CSS border value. + * See https://developer.mozilla.org/en-US/docs/Web/CSS/border for more information + */ +export interface Border { + /** + * Width of the border + */ + width?: string; + + /** + * Style of the border + */ + style?: string; + + /** + * Color of the border + */ + color?: string; +} diff --git a/packages/roosterjs-content-model/package.json b/packages/roosterjs-content-model/package.json index 74d507bf31d..b9db0ef268c 100644 --- a/packages/roosterjs-content-model/package.json +++ b/packages/roosterjs-content-model/package.json @@ -3,8 +3,9 @@ "description": "Content Model for roosterjs (Under development)", "dependencies": { "roosterjs-editor-types": "", - "roosterjs-editor-dom": "" + "roosterjs-editor-dom": "", + "roosterjs-editor-core": "" }, "main": "./lib/index.ts", - "version": "0.0.15" + "version": "0.1.0" } diff --git a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts index a8fed150ad9..69288caa339 100644 --- a/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/context/createDomToModelContextTest.ts @@ -12,8 +12,6 @@ import { describe('createDomToModelContext', () => { const editorContext: EditorContext = { isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, getDarkColor: undefined, }; const listFormat: DomToModelListFormat = { @@ -34,6 +32,7 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, + zoomScaleFormat: {}, isInSelection: false, listFormat, link: { @@ -47,8 +46,6 @@ describe('createDomToModelContext', () => { it('with content model context', () => { const editorContext: EditorContext = { isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, getDarkColor: () => '', }; @@ -57,9 +54,8 @@ describe('createDomToModelContext', () => { expect(context).toEqual({ ...editorContext, segmentFormat: {}, - blockFormat: { - direction: 'rtl', - }, + blockFormat: {}, + zoomScaleFormat: {}, isInSelection: false, listFormat, link: { @@ -92,6 +88,7 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, + zoomScaleFormat: {}, isInSelection: false, regularSelection: { startContainer: 'DIV 1' as any, @@ -129,6 +126,7 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, + zoomScaleFormat: {}, isInSelection: false, tableSelection: { table: mockTable, @@ -160,6 +158,7 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, + zoomScaleFormat: {}, link: { format: {}, dataset: {}, @@ -180,8 +179,6 @@ describe('createDomToModelContext', () => { const context = createDomToModelContext( { isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, getDarkColor, }, { @@ -195,13 +192,10 @@ describe('createDomToModelContext', () => { expect(context).toEqual({ isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, getDarkColor: getDarkColor, isInSelection: false, - blockFormat: { - direction: 'rtl', - }, + blockFormat: {}, + zoomScaleFormat: {}, segmentFormat: {}, listFormat, link: { @@ -218,8 +212,6 @@ describe('createDomToModelContext', () => { const context = createDomToModelContext( { isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, getDarkColor, }, { @@ -235,13 +227,10 @@ describe('createDomToModelContext', () => { expect(context).toEqual({ isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, getDarkColor: getDarkColor, isInSelection: false, - blockFormat: { - direction: 'rtl', - }, + blockFormat: {}, + zoomScaleFormat: {}, segmentFormat: {}, link: { format: {}, diff --git a/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts index 761bf479e19..059fac27524 100644 --- a/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts @@ -11,8 +11,6 @@ import { describe('createModelToDomContext', () => { const editorContext: EditorContext = { isDarkMode: false, - zoomScale: 1, - isRightToLeft: false, getDarkColor: undefined, }; const defaultResult: ModelToDomContext = { @@ -34,7 +32,6 @@ describe('createModelToDomContext', () => { entities: {}, defaultModelHandlers: defaultContentModelHandlers, defaultFormatAppliers: defaultFormatAppliers, - doNotReuseEntityDom: false, }; it('no param', () => { const context = createModelToDomContext(); @@ -45,8 +42,6 @@ describe('createModelToDomContext', () => { it('with content model context', () => { const editorContext: EditorContext = { isDarkMode: true, - zoomScale: 2, - isRightToLeft: true, getDarkColor: () => '', }; @@ -59,13 +54,11 @@ describe('createModelToDomContext', () => { }); it('with overrides', () => { - const mockedMergingCallback = 'mergingCallback' as any; const mockedBoldApplier = 'bold' as any; const mockedBlockApplier = 'block' as any; const mockedBrHandler = 'br' as any; const mockedAStyle = 'a' as any; const context = createModelToDomContext(undefined, { - mergingCallback: mockedMergingCallback, formatApplierOverride: { bold: mockedBoldApplier, }, diff --git a/packages/roosterjs-content-model/test/domToModel/processors/knownElementProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/knownElementProcessorTest.ts index a86b5b2d9d4..a532f6bf7c3 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/knownElementProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/knownElementProcessorTest.ts @@ -2,7 +2,6 @@ import * as parseFormat from '../../../lib/domToModel/utils/parseFormat'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; -import { HyperLinkColorPlaceholder } from '../../../lib/formatHandlers/utils/defaultStyles'; import { knownElementProcessor } from '../../../lib/domToModel/processors/knownElementProcessor'; describe('knownElementProcessor', () => { @@ -228,11 +227,8 @@ describe('knownElementProcessor', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, - link: { format: { href: '/test' }, dataset: {} }, + format: {}, + link: { format: { href: '/test', underline: true }, dataset: {} }, text: 'test', }, ], @@ -264,12 +260,9 @@ describe('knownElementProcessor', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, link: { - format: { href: '/test' }, + format: { href: '/test', underline: true }, dataset: { a: 'b', c: 'd', @@ -537,7 +530,9 @@ describe('knownElementProcessor', () => { { blockType: 'Divider', tagName: 'div', - format: { borderBottom: '1px solid black' }, + format: { + borderBottom: '1px solid black', + }, }, { blockType: 'Paragraph', diff --git a/packages/roosterjs-content-model/test/domToModel/processors/quoteProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/quoteProcessorTest.ts index 039a66276ba..c92e2db24a1 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/quoteProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/quoteProcessorTest.ts @@ -241,7 +241,9 @@ describe('quoteProcessor', () => { const childProcessor = jasmine .createSpy('childProcessor') .and.callFake((group, element, context) => { - expect(context.blockFormat).toEqual({}); + expect(context.blockFormat).toEqual({ + backgroundColor: 'red', + }); expect(context.segmentFormat).toEqual({ fontSize: '20px', }); @@ -269,6 +271,7 @@ describe('quoteProcessor', () => { marginBottom: '1em', marginLeft: '40px', borderLeft: '1px solid black', + backgroundColor: 'red', }, quoteSegmentFormat: { textColor: 'blue', @@ -279,4 +282,50 @@ describe('quoteProcessor', () => { expect(childProcessor).toHaveBeenCalledTimes(1); }); + + it('Verify inherited formats from context are correctly handled', () => { + const group = createContentModelDocument(); + const quote = document.createElement('blockquote'); + const childProcessor = jasmine.createSpy('childProcessor'); + + quote.style.borderLeft = 'solid 1px black'; + + context.blockFormat.backgroundColor = 'red'; + context.blockFormat.textAlign = 'center'; + context.blockFormat.isTextAlignFromAttr = true; + context.blockFormat.lineHeight = '2'; + context.blockFormat.whiteSpace = 'pre'; + context.blockFormat.direction = 'rtl'; + + context.elementProcessors.child = childProcessor; + + quoteProcessor(group, quote, context); + + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'Quote', + blocks: [], + format: { + marginTop: '1em', + marginRight: '40px', + marginBottom: '1em', + marginLeft: '40px', + borderLeft: '1px solid black', + backgroundColor: 'red', + textAlign: 'center', + isTextAlignFromAttr: true, + lineHeight: '2', + whiteSpace: 'pre', + direction: 'rtl', + }, + quoteSegmentFormat: {}, + }, + ], + }); + + expect(childProcessor).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts b/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts index 16dcd06116e..645f255c3b0 100644 --- a/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/processors/tableProcessorTest.ts @@ -318,7 +318,7 @@ describe('tableProcessor with format', () => { } as any) as HTMLTableElement; const doc = createContentModelDocument(); - context.zoomScale = 2; + context.zoomScaleFormat.zoomScale = 2; tableProcessor(doc, mockedTable, context); @@ -545,4 +545,45 @@ describe('tableProcessor', () => { ], }); }); + + it('Check inherited format from context', () => { + const group = createContentModelDocument(); + const mockedTable = ({ + tagName: 'table', + rows: [], + style: {}, + dataset: {}, + getAttribute: () => '', + } as any) as HTMLTableElement; + + context.blockFormat.backgroundColor = 'red'; + context.blockFormat.textAlign = 'center'; + context.blockFormat.isTextAlignFromAttr = true; + context.blockFormat.lineHeight = '2'; + context.blockFormat.whiteSpace = 'pre'; + context.blockFormat.direction = 'rtl'; + + tableProcessor(group, mockedTable, context); + + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + format: { + backgroundColor: 'red', + textAlign: 'center', + isTextAlignFromAttr: true, + lineHeight: '2', + whiteSpace: 'pre', + direction: 'rtl', + }, + dataset: {}, + widths: [], + heights: [], + cells: [], + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts b/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts index e75f1caac90..229a116ab96 100644 --- a/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/utils/parseFormatTest.ts @@ -87,10 +87,7 @@ describe('Default styles', () => { parseFormat(element, defaultContext.formatParsers.segment, segmentFormat, defaultContext); parseFormat(element, defaultContext.formatParsers.link, linkFormat, defaultContext); - expect(segmentFormat).toEqual({ - underline: true, - textColor: '__hyperLinkColor', - }); + expect(segmentFormat).toEqual({}); expect(linkFormat).toEqual({ href: 'http://test.com', }); diff --git a/packages/roosterjs-content-model/test/domToModel/utils/stackFormatTest.ts b/packages/roosterjs-content-model/test/domToModel/utils/stackFormatTest.ts index a15d1da634f..e0ca8f9e1af 100644 --- a/packages/roosterjs-content-model/test/domToModel/utils/stackFormatTest.ts +++ b/packages/roosterjs-content-model/test/domToModel/utils/stackFormatTest.ts @@ -43,4 +43,30 @@ describe('stackFormat', () => { backgroundColor: 'green', }); }); + + it('use default style for link', () => { + const context = createDomToModelContext(); + + context.link.format.textColor = 'red'; + context.link.format.underline = false; + + stackFormat(context, { link: 'linkDefault' }, () => { + expect(context.link).toEqual({ + format: { + underline: true, + }, + dataset: {}, + }); + + context.link.format.textColor = 'green'; + }); + + expect(context.link).toEqual({ + format: { + textColor: 'red', + underline: false, + }, + dataset: {}, + }); + }); }); diff --git a/packages/roosterjs-content-model/test/modelApi/metadata/updateImageMetadataTest.ts b/packages/roosterjs-content-model/test/domUtils/metadata/updateImageMetadataTest.ts similarity index 99% rename from packages/roosterjs-content-model/test/modelApi/metadata/updateImageMetadataTest.ts rename to packages/roosterjs-content-model/test/domUtils/metadata/updateImageMetadataTest.ts index 1a4dc8f9f4b..acf87c2bf21 100644 --- a/packages/roosterjs-content-model/test/modelApi/metadata/updateImageMetadataTest.ts +++ b/packages/roosterjs-content-model/test/domUtils/metadata/updateImageMetadataTest.ts @@ -1,6 +1,6 @@ import { ContentModelImage } from '../../../lib/publicTypes/segment/ContentModelImage'; import { ImageMetadataFormat } from '../../../lib/publicTypes/format/formatParts/ImageMetadataFormat'; -import { updateImageMetadata } from '../../../lib/modelApi/metadata/updateImageMetadata'; +import { updateImageMetadata } from '../../../lib/domUtils/metadata/updateImageMetadata'; describe('updateImageMetadataTest', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model/test/modelApi/metadata/updateListlMetadataTest.ts b/packages/roosterjs-content-model/test/domUtils/metadata/updateListlMetadataTest.ts similarity index 98% rename from packages/roosterjs-content-model/test/modelApi/metadata/updateListlMetadataTest.ts rename to packages/roosterjs-content-model/test/domUtils/metadata/updateListlMetadataTest.ts index 260b8be697e..85a1a499e35 100644 --- a/packages/roosterjs-content-model/test/modelApi/metadata/updateListlMetadataTest.ts +++ b/packages/roosterjs-content-model/test/domUtils/metadata/updateListlMetadataTest.ts @@ -1,6 +1,6 @@ import { ContentModelWithDataset } from '../../../lib/publicTypes/format/ContentModelWithDataset'; import { ListMetadataFormat } from '../../../lib/publicTypes/format/formatParts/ListMetadataFormat'; -import { updateListMetadata } from '../../../lib/modelApi/metadata/updateListMetadata'; +import { updateListMetadata } from '../../../lib/domUtils/metadata/updateListMetadata'; describe('updateListMetadata', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model/test/modelApi/metadata/updateMetadataTest.ts b/packages/roosterjs-content-model/test/domUtils/metadata/updateMetadataTest.ts similarity index 98% rename from packages/roosterjs-content-model/test/modelApi/metadata/updateMetadataTest.ts rename to packages/roosterjs-content-model/test/domUtils/metadata/updateMetadataTest.ts index be3e2bf3c3f..6a278f35e56 100644 --- a/packages/roosterjs-content-model/test/modelApi/metadata/updateMetadataTest.ts +++ b/packages/roosterjs-content-model/test/domUtils/metadata/updateMetadataTest.ts @@ -1,7 +1,7 @@ import * as validate from 'roosterjs-editor-dom/lib/metadata/validate'; import { ContentModelWithDataset } from '../../../lib/publicTypes/format/ContentModelWithDataset'; import { Definition } from 'roosterjs-editor-types'; -import { hasMetadata, updateMetadata } from '../../../lib/modelApi/metadata/updateMetadata'; +import { hasMetadata, updateMetadata } from '../../../lib/domUtils/metadata/updateMetadata'; describe('updateMetadata', () => { it('no metadata', () => { diff --git a/packages/roosterjs-content-model/test/modelApi/metadata/updateTableCellMetadataTest.ts b/packages/roosterjs-content-model/test/domUtils/metadata/updateTableCellMetadataTest.ts similarity index 99% rename from packages/roosterjs-content-model/test/modelApi/metadata/updateTableCellMetadataTest.ts rename to packages/roosterjs-content-model/test/domUtils/metadata/updateTableCellMetadataTest.ts index 4f43e4408e4..a77b88a87ee 100644 --- a/packages/roosterjs-content-model/test/modelApi/metadata/updateTableCellMetadataTest.ts +++ b/packages/roosterjs-content-model/test/domUtils/metadata/updateTableCellMetadataTest.ts @@ -1,6 +1,6 @@ import { ContentModelTableCell } from '../../../lib/publicTypes/group/ContentModelTableCell'; import { TableCellMetadataFormat } from 'roosterjs-editor-types'; -import { updateTableCellMetadata } from '../../../lib/modelApi/metadata/updateTableCellMetadata'; +import { updateTableCellMetadata } from '../../../lib/domUtils/metadata/updateTableCellMetadata'; describe('updateTableCellMetadata', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model/test/modelApi/metadata/updateTableMetadataTest.ts b/packages/roosterjs-content-model/test/domUtils/metadata/updateTableMetadataTest.ts similarity index 99% rename from packages/roosterjs-content-model/test/modelApi/metadata/updateTableMetadataTest.ts rename to packages/roosterjs-content-model/test/domUtils/metadata/updateTableMetadataTest.ts index bd0fb4ef03f..9a1bfd12ca0 100644 --- a/packages/roosterjs-content-model/test/modelApi/metadata/updateTableMetadataTest.ts +++ b/packages/roosterjs-content-model/test/domUtils/metadata/updateTableMetadataTest.ts @@ -1,7 +1,7 @@ import { ContentModelTable } from '../../../lib/publicTypes/block/ContentModelTable'; import { TableBorderFormat } from 'roosterjs-editor-types'; import { TableMetadataFormat } from '../../../lib/publicTypes/format/formatParts/TableMetadataFormat'; -import { updateTableMetadata } from '../../../lib/modelApi/metadata/updateTableMetadata'; +import { updateTableMetadata } from '../../../lib/domUtils/metadata/updateTableMetadata'; describe('updateTableMetadata', () => { it('No value', () => { diff --git a/packages/roosterjs-content-model/test/editor/ContentModelEditorTest.ts b/packages/roosterjs-content-model/test/editor/ContentModelEditorTest.ts new file mode 100644 index 00000000000..92d3ebf4ec5 --- /dev/null +++ b/packages/roosterjs-content-model/test/editor/ContentModelEditorTest.ts @@ -0,0 +1,142 @@ +import * as contentModelToDom from '../../lib/modelToDom/contentModelToDom'; +import * as domToContentModel from '../../lib/domToModel/domToContentModel'; +import * as entityPlaceholderUtils from 'roosterjs-editor-dom/lib/entity/entityPlaceholderUtils'; +import ContentModelEditor from '../../lib/editor/ContentModelEditor'; +import { ContentModelDocument } from '../../lib/publicTypes/group/ContentModelDocument'; +import { EditorPlugin, PluginEventType, SelectionRangeTypes } from 'roosterjs-editor-types'; + +describe('ContentModelEditor', () => { + it('domToContentModel', () => { + const div = document.createElement('div'); + const editor = new ContentModelEditor(div); + + const mockedResult = 'Result' as any; + + spyOn(domToContentModel, 'default').and.returnValue(mockedResult); + + const model = editor.createContentModel(); + + expect(model).toBe(mockedResult); + expect(domToContentModel.default).toHaveBeenCalledTimes(1); + expect(domToContentModel.default).toHaveBeenCalledWith( + div, + { + isDarkMode: false, + getDarkColor: (editor as any).core.lifecycle.getDarkColor, + darkColorHandler: null, + }, + { + selectionRange: { + type: SelectionRangeTypes.Normal, + areAllCollapsed: true, + ranges: [], + }, + alwaysNormalizeTable: true, + } + ); + }); + + it('setContentModel with normal selection', () => { + const div = document.createElement('div'); + const editor = new ContentModelEditor(div); + const mockedFragment = 'Fragment' as any; + const mockedRange = { + type: SelectionRangeTypes.Normal, + ranges: [document.createRange()], + } as any; + const mockedPairs = 'Pairs' as any; + + const mockedResult = [mockedFragment, mockedRange, mockedPairs] as any; + const mockedModel = 'MockedModel' as any; + + spyOn(contentModelToDom, 'default').and.returnValue(mockedResult); + spyOn(entityPlaceholderUtils, 'restoreContentWithEntityPlaceholder'); + + editor.setContentModel(mockedModel); + + expect(contentModelToDom.default).toHaveBeenCalledTimes(1); + expect(contentModelToDom.default).toHaveBeenCalledWith( + document, + mockedModel, + { + isDarkMode: false, + getDarkColor: (editor as any).core.lifecycle.getDarkColor, + darkColorHandler: null, + }, + undefined + ); + expect(entityPlaceholderUtils.restoreContentWithEntityPlaceholder).toHaveBeenCalledTimes(1); + expect(entityPlaceholderUtils.restoreContentWithEntityPlaceholder).toHaveBeenCalledWith( + mockedFragment, + div, + mockedPairs + ); + }); + + it('setContentModel', () => { + const div = document.createElement('div'); + const editor = new ContentModelEditor(div); + const mockedFragment = 'Fragment' as any; + const mockedRange = { + type: SelectionRangeTypes.Normal, + ranges: [document.createRange()], + } as any; + const mockedPairs = 'Pairs' as any; + + const mockedResult = [mockedFragment, mockedRange, mockedPairs] as any; + const mockedModel = 'MockedModel' as any; + + spyOn(contentModelToDom, 'default').and.returnValue(mockedResult); + spyOn(entityPlaceholderUtils, 'restoreContentWithEntityPlaceholder'); + + editor.setContentModel(mockedModel); + + expect(contentModelToDom.default).toHaveBeenCalledTimes(1); + expect(contentModelToDom.default).toHaveBeenCalledWith( + document, + mockedModel, + { + isDarkMode: false, + getDarkColor: (editor as any).core.lifecycle.getDarkColor, + darkColorHandler: null, + }, + undefined + ); + expect(entityPlaceholderUtils.restoreContentWithEntityPlaceholder).toHaveBeenCalledTimes(1); + expect(entityPlaceholderUtils.restoreContentWithEntityPlaceholder).toHaveBeenCalledWith( + mockedFragment, + div, + mockedPairs + ); + }); + + it('createContentModel in EditorReady event', () => { + let model: ContentModelDocument | undefined; + let pluginEditor: any; + + const div = document.createElement('div'); + const plugin: EditorPlugin = { + getName: () => '', + initialize: e => { + pluginEditor = e; + }, + dispose: () => { + pluginEditor = undefined; + }, + onPluginEvent: event => { + if (event.eventType == PluginEventType.EditorReady) { + model = pluginEditor.createContentModel(); + } + }, + }; + const editor = new ContentModelEditor(div, { + plugins: [plugin], + }); + editor.dispose(); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [], + }); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicPlugin/ContentModelPluginTest.ts b/packages/roosterjs-content-model/test/editor/ContentModelPluginTest.ts similarity index 61% rename from packages/roosterjs-content-model/test/publicPlugin/ContentModelPluginTest.ts rename to packages/roosterjs-content-model/test/editor/ContentModelPluginTest.ts index 3f141eda913..f196d7a0bd3 100644 --- a/packages/roosterjs-content-model/test/publicPlugin/ContentModelPluginTest.ts +++ b/packages/roosterjs-content-model/test/editor/ContentModelPluginTest.ts @@ -1,18 +1,18 @@ -import ContentModelPlugin from '../../lib/publicPlugin/ContentModelPlugin'; +import * as pendingFormat from '../../lib/modelApi/format/pendingFormat'; +import ContentModelPlugin from '../../lib/editor/ContentModelPlugin'; import { addSegment } from '../../lib/modelApi/common/addSegment'; import { createContentModelDocument } from '../../lib/modelApi/creators/createContentModelDocument'; import { createSelectionMarker } from '../../lib/modelApi/creators/createSelectionMarker'; import { createText } from '../../lib/modelApi/creators/createText'; -import { FormatState, PluginEventType } from 'roosterjs-editor-types'; -import { IExperimentalContentModelEditor } from '../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../lib/publicTypes/IContentModelEditor'; +import { PluginEventType } from 'roosterjs-editor-types'; describe('ContentModelPlugin', () => { it('no pending format, trigger key down event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); - const editor = ({ - getPendingFormat: (): FormatState | null => null, - setPendingFormat, - } as any) as IExperimentalContentModelEditor; + spyOn(pendingFormat, 'clearPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); + + const editor = ({} as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -24,22 +24,22 @@ describe('ContentModelPlugin', () => { plugin.dispose(); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); }); it('no selection, trigger input event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + spyOn(pendingFormat, 'clearPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + const setContentModel = jasmine.createSpy('setContentModel'); const editor = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, isInIME: () => false, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); const model = createContentModelDocument(); @@ -53,12 +53,15 @@ describe('ContentModelPlugin', () => { plugin.dispose(); expect(setContentModel).toHaveBeenCalledTimes(0); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); }); it('with pending format and selection, has correct text before, trigger input event with isComposing = true', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + spyOn(pendingFormat, 'clearPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const marker = createSelectionMarker(); @@ -66,13 +69,9 @@ describe('ContentModelPlugin', () => { addSegment(model, marker); const editor = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -83,11 +82,15 @@ describe('ContentModelPlugin', () => { plugin.dispose(); expect(setContentModel).toHaveBeenCalledTimes(0); - expect(setPendingFormat).toHaveBeenCalledTimes(0); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(0); }); it('with pending format and selection, no correct text before, trigger input event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + spyOn(pendingFormat, 'clearPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const marker = createSelectionMarker(); @@ -95,14 +98,10 @@ describe('ContentModelPlugin', () => { addSegment(model, marker); const editor = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, isInIME: () => false, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -113,12 +112,17 @@ describe('ContentModelPlugin', () => { plugin.dispose(); expect(setContentModel).toHaveBeenCalledTimes(0); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); }); it('with pending format and selection, has correct text before, trigger input event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + 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'); @@ -128,14 +132,14 @@ describe('ContentModelPlugin', () => { addSegment(model, marker); const editor = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, isInIME: () => false, - } as any) as IExperimentalContentModelEditor; + focus: () => {}, + addUndoSnapshot: (callback: () => void) => { + callback(); + }, + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -173,12 +177,16 @@ describe('ContentModelPlugin', () => { }, ], }); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); }); it('with pending format and selection, has correct text before, trigger CompositionEnd event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + spyOn(pendingFormat, 'clearPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const text = createText('test a test', { fontFamily: 'Arial' }); @@ -188,13 +196,13 @@ describe('ContentModelPlugin', () => { addSegment(model, marker); const editor = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, - } as any) as IExperimentalContentModelEditor; + focus: () => {}, + addUndoSnapshot: (callback: () => void) => { + callback(); + }, + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -232,23 +240,23 @@ describe('ContentModelPlugin', () => { }, ], }); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); }); it('Non-input and cursor moving key down should not trigger pending format change', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + spyOn(pendingFormat, 'clearPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + const setContentModel = jasmine.createSpy('setContentModel'); const model = createContentModelDocument(); const editor = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -259,22 +267,27 @@ describe('ContentModelPlugin', () => { plugin.dispose(); expect(setContentModel).toHaveBeenCalledTimes(0); - expect(setPendingFormat).toHaveBeenCalledTimes(0); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(0); }); it('Content changed event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + 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 = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, - } as any) as IExperimentalContentModelEditor; + addUndoSnapshot: (callback: () => void) => { + callback(); + }, + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); @@ -285,34 +298,65 @@ describe('ContentModelPlugin', () => { plugin.dispose(); expect(setContentModel).toHaveBeenCalledTimes(0); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); + expect(pendingFormat.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, + } as any) as IContentModelEditor; + const plugin = new ContentModelPlugin(); + + plugin.initialize(editor); + plugin.onPluginEvent({ + eventType: PluginEventType.MouseUp, + rawEvent: ({} as any) as MouseEvent, + }); + plugin.dispose(); + + expect(setContentModel).toHaveBeenCalledTimes(0); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1); + expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor); + expect(pendingFormat.canApplyPendingFormat).toHaveBeenCalledTimes(1); }); - it('Mouse down event', () => { - const setPendingFormat = jasmine.createSpy('setPendingFormat'); + 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 = ({ - getPendingFormat: (): FormatState | null => ({ - fontSize: '10px', - }), createContentModel: () => model, - setPendingFormat, setContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; const plugin = new ContentModelPlugin(); plugin.initialize(editor); plugin.onPluginEvent({ - eventType: PluginEventType.MouseDown, + eventType: PluginEventType.MouseUp, rawEvent: ({} as any) as MouseEvent, }); plugin.dispose(); expect(setContentModel).toHaveBeenCalledTimes(0); - expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith(null); + expect(pendingFormat.clearPendingFormat).not.toHaveBeenCalled(); + expect(pendingFormat.canApplyPendingFormat).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/roosterjs-content-model/test/editor/isContentModelEditorTest.ts b/packages/roosterjs-content-model/test/editor/isContentModelEditorTest.ts new file mode 100644 index 00000000000..505f3ca385a --- /dev/null +++ b/packages/roosterjs-content-model/test/editor/isContentModelEditorTest.ts @@ -0,0 +1,24 @@ +import ContentModelEditor from '../../lib/editor/ContentModelEditor'; +import isContentModelEditor from '../../lib/editor/isContentModelEditor'; +import { Editor } from 'roosterjs-editor-core'; +import { IEditor } from 'roosterjs-editor-types'; + +describe('isContentModelEditor', () => { + it('Legacy editor', () => { + const div = document.createElement('div'); + const editor: IEditor = new Editor(div); + + const result = isContentModelEditor(editor); + + expect(result).toBeFalse(); + }); + + it('Content Model editor', () => { + const div = document.createElement('div'); + const editor: IEditor = new ContentModelEditor(div); + + const result = isContentModelEditor(editor); + + expect(result).toBeTrue(); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/block/directionFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/block/directionFormatHandlerTest.ts index 6c543482e0c..899699586f2 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/block/directionFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/block/directionFormatHandlerTest.ts @@ -87,7 +87,7 @@ describe('directionFormatHandler.parse', () => { }); it('RTL', () => { - context.isRightToLeft = true; + context.blockFormat.direction = 'rtl'; runTest('left', null, 'rtl', null, 'end', 'rtl', undefined); runTest('center', null, 'rtl', null, 'center', 'rtl', undefined); diff --git a/packages/roosterjs-content-model/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts index 676072e02f8..62dfbb42fe5 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/block/whiteSpaceFormatHandlerTest.ts @@ -51,23 +51,33 @@ describe('whiteSpaceFormatHandler.parse', () => { describe('whiteSpaceFormatHandler.apply', () => { let div: HTMLElement; + let container: HTMLElement; let format: WhiteSpaceFormat; let context: ModelToDomContext; beforeEach(() => { + container = document.createElement('div'); div = document.createElement('div'); + container.appendChild(div); + format = {}; context = createModelToDomContext(); }); it('No white space', () => { whiteSpaceFormatHandler.apply(format, div, context); - expect(div.outerHTML).toBe('
'); + expect(container.innerHTML).toBe('
'); }); - it('Has white space', () => { + it('Has white space: pre', () => { format.whiteSpace = 'pre'; whiteSpaceFormatHandler.apply(format, div, context); - expect(div.outerHTML).toBe('
'); + expect(container.innerHTML).toBe('
'); + }); + + it('Has white space: pre-wrap', () => { + format.whiteSpace = 'pre-wrap'; + whiteSpaceFormatHandler.apply(format, div, context); + expect(container.innerHTML).toBe('
'); }); }); diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts index 41d698dcfca..d14cf5bd41e 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/common/borderFormatHandlerTest.ts @@ -69,7 +69,7 @@ describe('borderFormatHandler.parse', () => { }); it('Has border width none value only', () => { - div.style.borderStyle = 'none'; + div.style.borderStyle = ''; borderFormatHandler.parse(format, div, context, {}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/common/boxShadowFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/common/boxShadowFormatHandlerTest.ts new file mode 100644 index 00000000000..f80e274677b --- /dev/null +++ b/packages/roosterjs-content-model/test/formatHandlers/common/boxShadowFormatHandlerTest.ts @@ -0,0 +1,64 @@ +import { BoxShadowFormat } from '../../../lib/publicTypes/format/formatParts/BoxShadowFormat'; +import { boxShadowFormatHandler } from '../../../lib/formatHandlers/common/boxShadowFormatHandler'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; + +describe('boxShadowFormatHandler.parse', () => { + let div: HTMLElement; + let format: BoxShadowFormat; + let context: DomToModelContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createDomToModelContext(); + }); + + function runTest(cssValue: string | null, expectedValue: string) { + if (cssValue) { + div.style.boxShadow = cssValue; + } + + boxShadowFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + boxShadow: expectedValue, + }); + } + + it('No shadow', () => { + boxShadowFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({}); + }); + + it('Parse shadow', () => { + runTest('4px 4px 3px #aaaaaa', 'rgb(170, 170, 170) 4px 4px 3px'); + }); +}); + +describe('boxShadowFormatHandler.apply', () => { + let div: HTMLElement; + let format: BoxShadowFormat; + let context: ModelToDomContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createModelToDomContext(); + }); + + it('No shadow', () => { + boxShadowFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Apply shadow', () => { + format.boxShadow = '4px 4px 3px #aaaaaa'; + boxShadowFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe( + '
' + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts new file mode 100644 index 00000000000..c932c5b9201 --- /dev/null +++ b/packages/roosterjs-content-model/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts @@ -0,0 +1,84 @@ +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DirectionFormat } from '../../../lib/publicTypes/format/formatParts/DirectionFormat'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; +import { rootDirectionFormatHandler } from '../../../lib/formatHandlers/root/rootDirectionFormatHandler'; + +describe('rootDirectionFormatHandler.parse', () => { + let div: HTMLElement; + let context: DomToModelContext; + let format: DirectionFormat; + + beforeEach(() => { + div = document.createElement('div'); + context = createDomToModelContext(); + format = {}; + }); + + it('No direction', () => { + rootDirectionFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({}); + }); + + it('LTR from CSS', () => { + div.style.direction = 'ltr'; + + rootDirectionFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({}); + }); + + it('LTR from attribute', () => { + div.dir = 'ltr'; + + rootDirectionFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({}); + }); + + it('RTL from CSS', () => { + div.style.direction = 'rtl'; + + rootDirectionFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({}); + }); + + it('RTL from attribute', () => { + div.dir = 'rtl'; + + rootDirectionFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({}); + }); +}); + +describe('rootDirectionFormatHandler.apply', () => { + let div: HTMLElement; + let format: DirectionFormat; + let context: ModelToDomContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createModelToDomContext(); + }); + + it('ltr', () => { + format.direction = 'ltr'; + + rootDirectionFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toEqual('
'); + }); + + it('rtl', () => { + format.direction = 'rtl'; + + rootDirectionFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toEqual('
'); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts new file mode 100644 index 00000000000..e117d46d313 --- /dev/null +++ b/packages/roosterjs-content-model/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts @@ -0,0 +1,94 @@ +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; +import { ZoomScaleFormat } from '../../../lib/publicTypes/format/formatParts/ZoomScaleFormat'; +import { zoomScaleFormatHandler } from '../../../lib/formatHandlers/root/zoomScaleFormatHandler'; + +describe('zoomScaleFormatHandler.parse', () => { + let div: HTMLElement; + let context: DomToModelContext; + let format: ZoomScaleFormat; + + beforeEach(() => { + div = ({ + tagName: 'DIV', + ownerDocument: { + defaultView: { + getComputedStyle: () => ({}), + }, + }, + getBoundingClientRect: () => ({ width: 100 }), + } as any) as HTMLElement; + context = createDomToModelContext(); + format = {}; + }); + + it('No zoom scale', () => { + (div).offsetWidth = undefined; + + zoomScaleFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + zoomScale: 1, + }); + }); + + it('Zoom scale = 1', () => { + (div).offsetWidth = 100; + + zoomScaleFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + zoomScale: 1, + }); + }); + + it('Zoom scale = 2', () => { + (div).offsetWidth = 50; + + zoomScaleFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + zoomScale: 2, + }); + }); + + it('Zoom scale = 0.5', () => { + (div).offsetWidth = 200; + + zoomScaleFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + zoomScale: 0.5, + }); + }); +}); + +describe('zoomScaleFormatHandler.apply', () => { + let div: HTMLElement; + let format: ZoomScaleFormat; + let context: ModelToDomContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createModelToDomContext(); + }); + + it('zoom 1', () => { + format.zoomScale = 1; + + zoomScaleFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toEqual('
'); + }); + + it('zoom 2', () => { + format.zoomScale = 2; + + zoomScaleFormatHandler.apply(format, div, context); + + expect(div.outerHTML).toEqual('
'); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/computedSegmentFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/computedSegmentFormatHandlerTest.ts new file mode 100644 index 00000000000..f2cf24a34b1 --- /dev/null +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/computedSegmentFormatHandlerTest.ts @@ -0,0 +1,52 @@ +import { computedSegmentFormatHandler } from '../../../lib/formatHandlers/segment/computedSegmentFormatHandler'; +import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; + +describe('computedSegmentFormatHandler.parse', () => { + let div: HTMLElement; + let context: DomToModelContext; + let format: ContentModelSegmentFormat; + + beforeEach(() => { + div = document.createElement('div'); + context = createDomToModelContext(); + format = {}; + document.body.appendChild(div); + }); + + afterEach(() => { + document.body.removeChild(div); + }); + + it('no style', () => { + computedSegmentFormatHandler.parse(format, div, context, {}); + + expect(format.fontWeight).toBeUndefined(); + expect(format.fontFamily).toBeDefined(); + expect(format.fontSize).toBeDefined(); + expect(format.textColor).toBeUndefined(); + expect(format.italic).toBeUndefined(); + expect(format.strikethrough).toBeUndefined(); + expect(format.underline).toBeUndefined(); + expect(format.backgroundColor).toBeUndefined(); + }); + + it('has style', () => { + div.style.fontWeight = 'bold'; + div.style.fontFamily = 'Arial'; + div.style.fontSize = '20pt'; + div.style.color = 'red'; + div.style.backgroundColor = 'blue'; + div.style.textDecoration = 'underline'; + div.style.fontStyle = 'italic'; + div.style.verticalAlign = 'sub'; + + computedSegmentFormatHandler.parse(format, div, context, {}); + + expect(format).toEqual({ + fontFamily: 'Arial', + fontSize: '20pt', + }); + }); +}); diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts index c7d85417cd1..b7e42e06ea6 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/textColorFormatHandlerTest.ts @@ -1,7 +1,6 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { DomToModelContext } from '../../../lib/publicTypes/context/DomToModelContext'; -import { HyperLinkColorPlaceholder } from '../../../lib/formatHandlers/utils/defaultStyles'; import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; import { TextColorFormat } from '../../../lib/publicTypes/format/formatParts/TextColorFormat'; import { textColorFormatHandler } from '../../../lib/formatHandlers/segment/textColorFormatHandler'; @@ -94,11 +93,9 @@ describe('textColorFormatHandler.parse', () => { }); it('Color from hyperlink', () => { - textColorFormatHandler.parse(format, div, context, context.defaultStyles.a!); + textColorFormatHandler.parse(format, div, context, {}); - expect(format).toEqual({ - textColor: HyperLinkColorPlaceholder, - }); + expect(format).toEqual({}); }); it('Color from hyperlink with override', () => { diff --git a/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts b/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts index 25fe7e850d4..6dbc7112434 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/segment/underlineFormatHandlerTest.ts @@ -74,17 +74,9 @@ describe('underlineFormatHandler.parse', () => { }); }); - it('Hyperlink', () => { - underlineFormatHandler.parse(format, div, context, context.defaultStyles.a!); - - expect(format).toEqual({ - underline: true, - }); - }); - it('Hyperlink without underline', () => { div.style.textDecoration = 'none'; - underlineFormatHandler.parse(format, div, context, context.defaultStyles.a!); + underlineFormatHandler.parse(format, div, context, {}); expect(format).toEqual({}); }); diff --git a/packages/roosterjs-content-model/test/formatHandlers/utils/parseValueWithUnitTest.ts b/packages/roosterjs-content-model/test/formatHandlers/utils/parseValueWithUnitTest.ts index 5f2a6935661..02f95a345d7 100644 --- a/packages/roosterjs-content-model/test/formatHandlers/utils/parseValueWithUnitTest.ts +++ b/packages/roosterjs-content-model/test/formatHandlers/utils/parseValueWithUnitTest.ts @@ -52,4 +52,16 @@ describe('parseValueWithUnit', () => { it('%', () => { runTest('% ', [0, 10, 11, -11]); }); + + it('px to pt', () => { + const result = parseValueWithUnit('16px', undefined, 'pt'); + + expect(result).toBe(12); + }); + + it('pt to pt', () => { + const result = parseValueWithUnit('16pt', undefined, 'pt'); + + expect(result).toBe(16); + }); }); diff --git a/packages/roosterjs-content-model/test/modelApi/common/mergeModelTest.ts b/packages/roosterjs-content-model/test/modelApi/common/mergeModelTest.ts index 887a2c08a68..e14e4258a8a 100644 --- a/packages/roosterjs-content-model/test/modelApi/common/mergeModelTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/common/mergeModelTest.ts @@ -887,34 +887,40 @@ describe('mergeModel', () => { }); }); - it('table to table, merge table', () => { + it('table to table, merge table 1', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); const para1 = createParagraph(); const text1 = createText('test1'); - const cell11 = createTableCell(); - const cell12 = createTableCell(); - const cell21 = createTableCell(); - const cell22 = createTableCell(); - const table1 = createTable(2); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(false, false, false, { backgroundColor: '12' }); + const cell21 = createTableCell(false, false, false, { backgroundColor: '21' }); + const cell22 = createTableCell(false, false, false, { backgroundColor: '22' }); + const cell31 = createTableCell(false, false, false, { backgroundColor: '31' }); + const cell32 = createTableCell(false, false, false, { backgroundColor: '32' }); + const table1 = createTable(4); para1.segments.push(text1); text1.isSelected = true; - cell22.blocks.push(para1); + cell12.blocks.push(para1); table1.cells = [ + [cell01, cell02], [cell11, cell12], [cell21, cell22], + [cell31, cell32], ]; majorModel.blocks.push(table1); const newPara1 = createParagraph(); const newText1 = createText('newText1'); - const newCell11 = createTableCell(); - const newCell12 = createTableCell(); - const newCell21 = createTableCell(); - const newCell22 = createTableCell(); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); const newTable1 = createTable(2); newPara1.segments.push(newText1); @@ -941,44 +947,134 @@ describe('mergeModel', () => { blockType: 'Table', cells: [ [ + cell01, + cell02, { blockGroupType: 'TableCell', blocks: [], - format: {}, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }, - { - blockGroupType: 'TableCell', - blocks: [], - format: {}, + format: { + backgroundColor: '02', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + ], + [ + cell11, { blockGroupType: 'TableCell', - blocks: [], - format: {}, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + backgroundColor: 'n11', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + newCell12, ], + [cell21, newCell21, newCell22], [ + cell31, + cell32, { blockGroupType: 'TableCell', blocks: [], - format: {}, + format: { + backgroundColor: '32', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + ], + ], + format: {}, + widths: [], + heights: [], + dataset: {}, + }, + ], + }); + }); + + it('table to table, merge table 2', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell03 = createTableCell(false, false, false, { backgroundColor: '03' }); + const cell04 = createTableCell(false, false, false, { backgroundColor: '04' }); + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(false, false, false, { backgroundColor: '12' }); + const cell13 = createTableCell(false, false, false, { backgroundColor: '13' }); + const cell14 = createTableCell(false, false, false, { backgroundColor: '14' }); + const table1 = createTable(2); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.cells = [ + [cell01, cell02, cell03, cell04], + [cell11, cell12, cell13, cell14], + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell12.blocks.push(newPara1); + newTable1.cells = [ + [newCell11, newCell12], + [newCell21, newCell22], + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + mergeModel(majorModel, sourceModel, { + mergeTable: true, + }); + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(majorModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + cells: [ + [cell01, cell02, cell03, cell04], + [ + cell11, { blockGroupType: 'TableCell', blocks: [ @@ -995,62 +1091,163 @@ describe('mergeModel', () => { isImplicit: true, }, ], - format: {}, + format: { + backgroundColor: 'n11', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + newCell12, + cell14, + ], + [ { blockGroupType: 'TableCell', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'newText1', - format: {}, - }, - ], - format: {}, - }, - ], - format: {}, + blocks: [], + format: { + backgroundColor: '11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + newCell21, + newCell22, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + backgroundColor: '14', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, ], + ], + format: {}, + widths: [], + heights: [], + dataset: {}, + }, + ], + }); + }); + + it('table to table, merge table 3', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(false, false, false, { backgroundColor: '12' }); + const table1 = createTable(2); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.cells = [ + [cell01, cell02], + [cell11, cell12], + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell12.blocks.push(newPara1); + newTable1.cells = [ + [newCell11, newCell12], + [newCell21, newCell22], + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + mergeModel(majorModel, sourceModel, { + mergeTable: true, + }); + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(majorModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + cells: [ [ + cell01, + cell02, { blockGroupType: 'TableCell', blocks: [], - format: {}, + format: { + backgroundColor: '02', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + ], + [ + cell11, { blockGroupType: 'TableCell', - blocks: [], - format: {}, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + backgroundColor: 'n11', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + newCell12, + ], + [ { blockGroupType: 'TableCell', blocks: [], - format: {}, + format: { + backgroundColor: '11', + }, spanLeft: false, spanAbove: false, isHeader: false, dataset: {}, }, + newCell21, + newCell22, ], ], format: {}, diff --git a/packages/roosterjs-content-model/test/modelApi/common/retrieveModelFormatStateTest.ts b/packages/roosterjs-content-model/test/modelApi/common/retrieveModelFormatStateTest.ts index 5bbf1527c5d..a864231e7ca 100644 --- a/packages/roosterjs-content-model/test/modelApi/common/retrieveModelFormatStateTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/common/retrieveModelFormatStateTest.ts @@ -40,6 +40,9 @@ describe('retrieveModelFormatState', () => { isUnderline: true, canUnlink: false, canAddImageAltText: false, + lineHeight: undefined, + marginTop: undefined, + marginBottom: undefined, }; it('Empty model', () => { @@ -130,6 +133,29 @@ describe('retrieveModelFormatState', () => { }); }); + it('Single selection with margin format', () => { + const model = createContentModelDocument(); + const result: FormatState = {}; + const paraFormat = { + marginTop: '2px', + marginBottom: '5px', + }; + const para = createParagraph(false, paraFormat); + const marker = createSelectionMarker(segmentFormat); + + spyOn(iterateSelections, 'iterateSelections').and.callFake((path, callback) => { + callback(path, undefined, para, [marker]); + return false; + }); + + retrieveModelFormatState(model, null, result); + + expect(result).toEqual({ + ...baseFormatResult, + ...paraFormat, + }); + }); + it('Single selection with table', () => { const model = createContentModelDocument(); const result: FormatState = {}; @@ -329,6 +355,9 @@ describe('retrieveModelFormatState', () => { isSubscript: false, canUnlink: false, canAddImageAltText: false, + lineHeight: undefined, + marginTop: undefined, + marginBottom: undefined, }); }); @@ -440,6 +469,9 @@ describe('retrieveModelFormatState', () => { isItalic: undefined, isUnderline: undefined, isStrikeThrough: undefined, + lineHeight: undefined, + marginTop: undefined, + marginBottom: undefined, }); }); }); diff --git a/packages/roosterjs-content-model/test/modelApi/format/pendingFormatTest.ts b/packages/roosterjs-content-model/test/modelApi/format/pendingFormatTest.ts new file mode 100644 index 00000000000..1b61b885a1d --- /dev/null +++ b/packages/roosterjs-content-model/test/modelApi/format/pendingFormatTest.ts @@ -0,0 +1,170 @@ +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 mockedPosition = 'POSITION' as any; + + setPendingFormat(editor, mockedFormat, mockedPosition); + + expect((editor as any).core.lifecycle.customData.__ContentModelPendingFormat.value).toEqual( + { + format: mockedFormat, + position: mockedPosition, + } + ); + }); +}); + +describe('pendingFormat.clearPendingFormat', () => { + it('clear format', () => { + const div = document.createElement('div'); + const editor = new ContentModelEditor(div); + const mockedFormat = 'FORMAT' as any; + const mockedPosition = 'POSITION' as any; + + (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { + value: { + format: mockedFormat, + position: mockedPosition, + }, + }; + + clearPendingFormat(editor); + + expect((editor as any).core.lifecycle.customData.__ContentModelPendingFormat.value).toEqual( + { + format: null, + position: null, + } + ); + }); +}); + +describe('pendingFormat.canApplyPendingFormat', () => { + it('can apply format', () => { + 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); + const mockedPosition2 = { + equalTo, + }; + + editor.getFocusedPosition = () => mockedPosition2 as any; + + (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { + value: { + format: mockedFormat, + position: mockedPosition, + }, + }; + + const result = canApplyPendingFormat(editor); + + expect(result).toBeTrue(); + expect(equalTo).toHaveBeenCalledWith(mockedPosition); + }); + + 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 mockedPosition = 'POSITION' as any; + + const equalTo = jasmine.createSpy('equalto').and.returnValue(false); + const mockedPosition2 = { + equalTo, + }; + + editor.getFocusedPosition = () => mockedPosition2 as any; + + (editor as any).core.lifecycle.customData.__ContentModelPendingFormat = { + value: { + format: mockedFormat, + position: mockedPosition, + }, + }; + + const result = canApplyPendingFormat(editor); + + expect(result).toBeFalse(); + expect(equalTo).toHaveBeenCalledWith(mockedPosition); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelApi/image/applyImageBorderFormatTest.ts b/packages/roosterjs-content-model/test/modelApi/image/applyImageBorderFormatTest.ts new file mode 100644 index 00000000000..71f0105d2e1 --- /dev/null +++ b/packages/roosterjs-content-model/test/modelApi/image/applyImageBorderFormatTest.ts @@ -0,0 +1,158 @@ +import applyImageBorderFormat from '../../../lib/modelApi/image/applyImageBorderFormat'; +import { Border } from '../../../lib/publicTypes/interface/Border'; +import { ContentModelImage } from '../../../lib/publicTypes/segment/ContentModelImage'; + +describe('applyImageBorderFormat', () => { + function createImage(border?: string): ContentModelImage { + return { + src: 'test', + alt: 'test', + title: 'test', + isSelectedAsImageSelection: true, + segmentType: 'Image', + dataset: {}, + format: { + borderTop: border, + borderBottom: border, + borderLeft: border, + borderRight: border, + }, + }; + } + + function runTest(format: Border, expectedBorder: string, previousBorder?: string) { + const image = createImage(previousBorder); + applyImageBorderFormat(image, format, '5px'); + expect(image.format.borderBottom).toBe(expectedBorder); + expect(image.format.borderRadius).toBe('5px'); + } + + it('apply only color to image without format', () => { + runTest( + { + color: 'red', + }, + '1px solid red' + ); + }); + + it('apply only width to image without format', () => { + runTest( + { + width: '10px', + }, + '10px solid' + ); + }); + + it('apply only width to image without format in points', () => { + runTest( + { + width: '3/4pt', + }, + '5.333px solid' + ); + }); + + it('apply only style to image without format', () => { + runTest( + { + style: 'groove', + }, + '1px groove' + ); + }); + + it('apply only color to image with format', () => { + runTest( + { + color: 'red', + }, + '10px groove red', + '10px groove blue' + ); + }); + + it('apply only width to image with format', () => { + runTest( + { + width: '20px', + }, + '20px groove blue', + '10px groove blue' + ); + }); + + it('apply only width to image with format in points', () => { + runTest( + { + width: '3/4pt', + }, + '5.333px groove blue', + '10px groove blue' + ); + }); + + it('apply only style to image with format ', () => { + runTest( + { + style: 'dotted', + }, + '10px dotted blue', + '10px groove blue' + ); + }); + + it('apply only style to image with format ', () => { + runTest( + { + style: 'dotted', + }, + '10px dotted blue', + '10px groove blue' + ); + }); + + it('apply color and style to image with format ', () => { + runTest( + { + color: 'red', + style: 'dotted', + }, + '10px dotted red', + '10px groove blue' + ); + }); + + it('apply color, style, width to image with format ', () => { + runTest( + { + color: 'red', + style: 'dotted', + width: '20px', + }, + '20px dotted red', + '10px groove blue' + ); + }); + + it('apply color and width to image without format ', () => { + runTest( + { + color: 'red', + width: '20px', + }, + '20px solid red' + ); + }); + + it('apply width and style to image without format ', () => { + runTest( + { + style: 'dotted', + width: '20px', + }, + '20px dotted' + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelApi/selection/iterateSelectionsTest.ts b/packages/roosterjs-content-model/test/modelApi/selection/iterateSelectionsTest.ts index d1b645e2057..28bf1b4a50a 100644 --- a/packages/roosterjs-content-model/test/modelApi/selection/iterateSelectionsTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/selection/iterateSelectionsTest.ts @@ -299,7 +299,9 @@ describe('iterateSelections', () => { group.blocks.push(table); - iterateSelections([group], callback, { ignoreContentUnderSelectedTableCell: true }); + iterateSelections([group], callback, { + contentUnderSelectedTableCell: 'ignoreForTableOrCell', + }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], { @@ -310,6 +312,62 @@ describe('iterateSelections', () => { }); }); + it('Group with table selection and ignore selected table content', () => { + const group = createContentModelDocument(); + const table = createTable(1); + const cell1 = createTableCell(); + const cell2 = createTableCell(); + const para1 = createParagraph(); + const para2 = createParagraph(); + const text1 = createText('text1'); + const text2 = createText('text2'); + + cell1.isSelected = true; + + para1.segments.push(text1); + para2.segments.push(text2); + + cell1.blocks.push(para1); + cell1.blocks.push(para2); + table.cells = [[cell1, cell2]]; + + group.blocks.push(table); + + iterateSelections([group], callback, { + contentUnderSelectedTableCell: 'ignoreForTable', + }); + + expect(callback).toHaveBeenCalledTimes(3); + expect(callback).toHaveBeenCalledWith([group], { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }); + expect(callback).toHaveBeenCalledWith( + [cell1, group], + { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }, + para1, + [text1] + ); + expect(callback).toHaveBeenCalledWith( + [cell1, group], + { + table: table, + colIndex: 0, + rowIndex: 0, + isWholeTableSelected: false, + }, + para2, + [text2] + ); + }); + it('Group with whole table selection and ignore selected table cell content', () => { const group = createContentModelDocument(); const table = createTable(1); @@ -332,7 +390,7 @@ describe('iterateSelections', () => { group.blocks.push(table); - iterateSelections([group], callback, { ignoreContentUnderSelectedTableCell: true }); + iterateSelections([group], callback, { contentUnderSelectedTableCell: 'ignoreForTable' }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith([group], undefined, table); @@ -673,7 +731,9 @@ describe('iterateSelections', () => { return block == table; }); - iterateSelections([group], newCallback, { ignoreContentUnderSelectedTableCell: true }); + iterateSelections([group], newCallback, { + contentUnderSelectedTableCell: 'ignoreForTable', + }); expect(newCallback).toHaveBeenCalledTimes(1); expect(newCallback).toHaveBeenCalledWith([group], undefined, table); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts index 55dd2a191fb..645c32907f0 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts @@ -116,11 +116,12 @@ describe('handleBlockGroup', () => { cloneNode: () => clonedChild, firstChild: true, } as any) as HTMLElement; - const group = createGeneralSegment(childMock, { underline: true }); + const group = createGeneralSegment(childMock); group.link = { format: { href: '/test', + underline: true, }, dataset: {}, }; diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts index 20502ae1383..cbd75ec2acb 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts @@ -4,6 +4,7 @@ import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentMod import { ContentModelImage } from '../../../lib/publicTypes/segment/ContentModelImage'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { handleImage } from '../../../lib/modelToDom/handlers/handleImage'; +import { itChromeOnly } from 'roosterjs-editor-dom/test/DomTestHelper'; import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; describe('handleSegment', () => { @@ -41,7 +42,7 @@ describe('handleSegment', () => { dataset: {}, }; - runTest(segment, '', 0); + runTest(segment, '', 0); expect(context.imageSelection).toBeUndefined(); }); @@ -55,7 +56,7 @@ describe('handleSegment', () => { dataset: {}, }; - runTest(segment, '', 0); + runTest(segment, '', 0); expect(context.imageSelection!.image.src).toBe('http://test.com/test'); }); @@ -70,47 +71,67 @@ describe('handleSegment', () => { dataset: {}, }; - runTest(segment, 'a', 0); + runTest(segment, 'a', 0); }); it('image segment with link', () => { const segment: ContentModelImage = { segmentType: 'Image', src: 'http://test.com/test', - format: { underline: true }, - link: { format: { href: '/test' }, dataset: {} }, + format: {}, + link: { format: { href: '/test', underline: true }, dataset: {} }, dataset: {}, }; - runTest(segment, '', 0); + runTest(segment, '', 0); + }); + + itChromeOnly('image segment with size', () => { + const segment: ContentModelImage = { + segmentType: 'Image', + src: 'http://test.com/test', + format: { width: '100px', height: '200px' }, + link: { format: { href: '/test', underline: true }, dataset: {} }, + dataset: {}, + }; + + runTest( + segment, + '', + 0 + ); }); it('image segment with dataset', () => { const segment: ContentModelImage = { segmentType: 'Image', src: 'http://test.com/test', - format: { underline: true }, - link: { format: { href: '/test' }, dataset: {} }, + format: {}, + link: { format: { href: '/test', underline: true }, dataset: {} }, dataset: { a: 'b', }, }; - runTest(segment, '', 0); + runTest( + segment, + '', + 0 + ); }); it('call stackFormat', () => { const segment: ContentModelImage = { segmentType: 'Image', src: 'http://test.com/test', - format: { underline: true }, - link: { format: { href: '/test' }, dataset: {} }, + format: {}, + link: { format: { href: '/test', underline: true }, dataset: {} }, dataset: {}, }; spyOn(stackFormat, 'stackFormat').and.callThrough(); - runTest(segment, '', 0); + runTest(segment, '', 0); expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a'); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleLinkTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleLinkTest.ts new file mode 100644 index 00000000000..bd33c2b055e --- /dev/null +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleLinkTest.ts @@ -0,0 +1,73 @@ +import { ContentModelLink } from '../../../lib/publicTypes/decorator/ContentModelLink'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { handleLink } from '../../../lib/modelToDom/handlers/handleLink'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; + +describe('handleLink', () => { + let parent: HTMLElement; + let context: ModelToDomContext; + + beforeEach(() => { + context = createModelToDomContext(); + }); + + function runTest(link: ContentModelLink, expectedInnerHTML: string) { + parent = document.createElement('div'); + parent.innerHTML = 'test'; + + handleLink(document, parent.firstChild!, link, context); + + expect(parent.innerHTML).toBe(expectedInnerHTML); + } + + it('simple link', () => { + const link: ContentModelLink = { + format: { + href: 'http://test.com/test', + underline: true, + }, + dataset: {}, + }; + + runTest(link, 'test'); + }); + + it('link with color', () => { + const link: ContentModelLink = { + format: { + href: 'http://test.com/test', + textColor: 'red', + underline: true, + }, + dataset: {}, + }; + + runTest(link, 'test'); + }); + + it('link without underline', () => { + const link: ContentModelLink = { + format: { + href: 'http://test.com/test', + }, + dataset: {}, + }; + + runTest(link, 'test'); + }); + + it('link with dataset', () => { + const link: ContentModelLink = { + format: { + href: 'http://test.com/test', + underline: true, + }, + dataset: { + a: 'b', + c: 'd', + }, + }; + + runTest(link, 'test'); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts index 9a3b8e044e5..a91c0d0a873 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts @@ -364,8 +364,7 @@ describe('handleParagraph', () => { 1 ); - expect(stackFormat.stackFormat).toHaveBeenCalledTimes(2); + expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('h1'); - expect((stackFormat.stackFormat).calls.argsFor(1)[1]).toBe(null); }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts index 38f8028573c..31fa281190c 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts @@ -41,28 +41,28 @@ describe('handleSegment', () => { const text: ContentModelText = { segmentType: 'Text', text: 'test', - format: { underline: true }, - link: { format: { href: '/test' }, dataset: {} }, + format: {}, + link: { format: { href: '/test', underline: true }, dataset: {} }, }; handleText(document, parent, text, context); - expect(parent.innerHTML).toBe('test'); + expect(parent.innerHTML).toBe('test'); }); it('call stackFormat', () => { const text: ContentModelText = { segmentType: 'Text', text: 'test', - format: { underline: true }, - link: { format: { href: '/test' }, dataset: {} }, + format: {}, + link: { format: { href: '/test', underline: true }, dataset: {} }, }; spyOn(stackFormat, 'stackFormat').and.callThrough(); handleText(document, parent, text, context); - expect(parent.innerHTML).toBe('test'); + expect(parent.innerHTML).toBe('test'); expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a'); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts b/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts index 1564ab266e3..b6fc3dd3843 100644 --- a/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/utils/stackFormatTest.ts @@ -26,7 +26,6 @@ describe('stackFormat', () => { const callback = jasmine.createSpy().and.callFake(() => { expect(context.implicitFormat).toEqual({ underline: true, - textColor: '__hyperLinkColor', }); context.implicitFormat.fontSize = '10px'; }); @@ -42,7 +41,6 @@ describe('stackFormat', () => { const callback = jasmine.createSpy().and.callFake(() => { expect(context.implicitFormat).toEqual({ underline: true, - textColor: '__hyperLinkColor', }); context.implicitFormat.fontSize = '10px'; throw new Error('test'); diff --git a/packages/roosterjs-content-model/test/publicApi/block/paragraphTestCommon.ts b/packages/roosterjs-content-model/test/publicApi/block/paragraphTestCommon.ts index aaba220c1f6..6f4a454f47e 100644 --- a/packages/roosterjs-content-model/test/publicApi/block/paragraphTestCommon.ts +++ b/packages/roosterjs-content-model/test/publicApi/block/paragraphTestCommon.ts @@ -1,10 +1,9 @@ import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; export function paragraphTestCommon( apiName: string, - executionCallback: (editor: IExperimentalContentModelEditor) => void, + executionCallback: (editor: IContentModelEditor) => void, model: ContentModelDocument, result: ContentModelDocument, calledTimes: number @@ -24,9 +23,7 @@ export function paragraphTestCommon( addUndoSnapshot, focus: jasmine.createSpy(), setContentModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages/roosterjs-content-model/test/publicApi/block/setIndentationTest.ts b/packages/roosterjs-content-model/test/publicApi/block/setIndentationTest.ts index 71e1d938343..488d932812d 100644 --- a/packages/roosterjs-content-model/test/publicApi/block/setIndentationTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/block/setIndentationTest.ts @@ -1,19 +1,16 @@ import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import * as setModelIndentation from '../../../lib/modelApi/block/setModelIndentation'; import setIndentation from '../../../lib/publicApi/block/setIndentation'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('setIndentation', () => { const fakeModel: any = { a: 'b' }; - let editor: IExperimentalContentModelEditor; + let editor: IContentModelEditor; beforeEach(() => { editor = ({ createContentModel: () => fakeModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); it('indent', () => { diff --git a/packages/roosterjs-content-model/test/publicApi/block/setParagraphMarginTest.ts b/packages/roosterjs-content-model/test/publicApi/block/setParagraphMarginTest.ts new file mode 100644 index 00000000000..66c5a7df00b --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/block/setParagraphMarginTest.ts @@ -0,0 +1,242 @@ +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { paragraphTestCommon } from './paragraphTestCommon'; +import setParagraphMargin from '../../../lib/publicApi/block/setParagraphMargin'; + +describe('setParagraphMargin', () => { + function runTest( + model: ContentModelDocument, + result: ContentModelDocument, + marginTop?: string | null, + marginBottom?: string | null, + calledTimes: number = 1 + ) { + paragraphTestCommon( + 'setParagraphMargin', + editor => setParagraphMargin(editor, marginTop, marginBottom), + model, + result, + calledTimes + ); + } + + it('empty content', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + { + blockGroupType: 'Document', + blocks: [], + }, + '8px' /*marginTop */, + undefined /**marginBottom */, + 0 + ); + }); + + it('no selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + }, + ], + }, + '8px' /*marginTop */, + undefined /**marginBottom */, + 0 + ); + }); + + it('Collapsed selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + marginTop: '8px', + }, + decorator: { + tagName: 'p', + format: {}, + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + '8px' /*marginTop */, + undefined /**marginBottom */, + 1 + ); + }); + + it('With selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + marginTop: '8px', + }, + decorator: { + tagName: 'p', + format: {}, + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + '8px' /*marginTop */, + undefined /**marginBottom */, + 1 + ); + }); + + it('Deletes and ignores properties correctly', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + marginTop: '2px', + marginRight: '3px', + marginBottom: '8pt', + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + marginRight: '3px', + marginBottom: '8pt', + }, + decorator: { + tagName: 'p', + format: {}, + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + null /*marginTop */, + undefined /**marginBottom */, + 1 + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/block/setSpacingTest.ts b/packages/roosterjs-content-model/test/publicApi/block/setSpacingTest.ts new file mode 100644 index 00000000000..bf7af9a1dd7 --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/block/setSpacingTest.ts @@ -0,0 +1,221 @@ +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { paragraphTestCommon } from './paragraphTestCommon'; +import setSpacing from '../../../lib/publicApi/block/setSpacing'; + +describe('setSpacing', () => { + function runTest( + model: ContentModelDocument, + result: ContentModelDocument, + spacing: number, + calledTimes: number = 1 + ) { + paragraphTestCommon( + 'setSpacing', + editor => setSpacing(editor, spacing), + model, + result, + calledTimes + ); + } + + it('empty content', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [], + }, + { + blockGroupType: 'Document', + blocks: [], + }, + 1.5, + 0 + ); + }); + + it('no selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + }, + ], + }, + 1.5, + 0 + ); + }); + + it('Collapsed selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + lineHeight: '1.5', + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1.5, + 1 + ); + }); + + it('With selection', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + lineHeight: '1.5', + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1.5, + 1 + ); + }); + + it('Removes line-height from segment children', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + lineHeight: '123', + }, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: { + lineHeight: '1.5', + }, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1.5, + 1 + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/block/toggleBlockQuoteTest.ts b/packages/roosterjs-content-model/test/publicApi/block/toggleBlockQuoteTest.ts index 7e3e5aef496..9aed7bd2ba4 100644 --- a/packages/roosterjs-content-model/test/publicApi/block/toggleBlockQuoteTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/block/toggleBlockQuoteTest.ts @@ -1,19 +1,16 @@ import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import * as toggleModelBlockQuote from '../../../lib/modelApi/block/toggleModelBlockQuote'; import toggleBlockQuote from '../../../lib/publicApi/block/toggleBlockQuote'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('toggleBlockQuote', () => { const fakeModel: any = { a: 'b' }; - let editor: IExperimentalContentModelEditor; + let editor: IContentModelEditor; beforeEach(() => { editor = ({ createContentModel: () => fakeModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); it('toggleBlockQuote', () => { diff --git a/packages/roosterjs-content-model/test/publicApi/domToContentModelTest.ts b/packages/roosterjs-content-model/test/publicApi/domToContentModelTest.ts new file mode 100644 index 00000000000..98cd0d142e0 --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/domToContentModelTest.ts @@ -0,0 +1,87 @@ +import * as createDomToModelContext from '../../lib/domToModel/context/createDomToModelContext'; +import * as normalizeContentModel from '../../lib/modelApi/common/normalizeContentModel'; +import domToContentModel from '../../lib/domToModel/domToContentModel'; +import { ContentModelDocument } from '../../lib/publicTypes/group/ContentModelDocument'; +import { DomToModelContext } from '../../lib/publicTypes/context/DomToModelContext'; + +describe('domToContentModel', () => { + it('Not include root', () => { + const elementProcessor = jasmine.createSpy('elementProcessor'); + const childProcessor = jasmine.createSpy('childProcessor'); + const mockContext = ({ + elementProcessors: { + element: elementProcessor, + child: childProcessor, + }, + defaultStyles: {}, + zoomScaleFormat: {}, + segmentFormat: {}, + } as any) as DomToModelContext; + + spyOn(createDomToModelContext, 'createDomToModelContext').and.returnValue(mockContext); + spyOn(normalizeContentModel, 'normalizeContentModel'); + + const rootElement = document.createElement('div'); + const options = { + includeRoot: false, + }; + const editorContext = { isDarkMode: false }; + const model = domToContentModel(rootElement, editorContext, options); + const result: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [], + }; + + expect(model).toEqual(result); + expect(createDomToModelContext.createDomToModelContext).toHaveBeenCalledTimes(1); + expect(createDomToModelContext.createDomToModelContext).toHaveBeenCalledWith( + editorContext, + options + ); + expect(elementProcessor).not.toHaveBeenCalled(); + expect(childProcessor).toHaveBeenCalledTimes(1); + expect(childProcessor).toHaveBeenCalledWith(result, rootElement, mockContext); + expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1); + expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(result); + }); + + it('Include root', () => { + const elementProcessor = jasmine.createSpy('elementProcessor'); + const childProcessor = jasmine.createSpy('childProcessor'); + const mockContext = ({ + elementProcessors: { + element: elementProcessor, + child: childProcessor, + }, + defaultStyles: {}, + zoomScaleFormat: {}, + segmentFormat: {}, + } as any) as DomToModelContext; + + spyOn(createDomToModelContext, 'createDomToModelContext').and.returnValue(mockContext); + spyOn(normalizeContentModel, 'normalizeContentModel'); + + const rootElement = document.createElement('div'); + const options = { + includeRoot: true, + }; + const editorContext = { isDarkMode: false }; + const model = domToContentModel(rootElement, editorContext, options); + const result: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [], + }; + + expect(model).toEqual(result); + expect(createDomToModelContext.createDomToModelContext).toHaveBeenCalledTimes(1); + expect(createDomToModelContext.createDomToModelContext).toHaveBeenCalledWith( + editorContext, + options + ); + expect(childProcessor).not.toHaveBeenCalled(); + expect(elementProcessor).toHaveBeenCalledTimes(1); + expect(elementProcessor).toHaveBeenCalledWith(result, rootElement, mockContext); + expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1); + expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(result); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/format/applyPendingFormatTest.ts b/packages/roosterjs-content-model/test/publicApi/format/applyPendingFormatTest.ts new file mode 100644 index 00000000000..f2d900d444b --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/format/applyPendingFormatTest.ts @@ -0,0 +1,256 @@ +import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; +import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelections'; +import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; +import applyPendingFormat from '../../../lib/publicApi/format/applyPendingFormat'; +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { ContentModelParagraph } from '../../../lib/publicTypes/block/ContentModelParagraph'; +import { ContentModelSelectionMarker } from '../../../lib/publicTypes/segment/ContentModelSelectionMarker'; +import { ContentModelText } from '../../../lib/publicTypes/segment/ContentModelText'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; + +describe('applyPendingFormat', () => { + it('Has pending format', () => { + const editor = ({} as any) as IContentModelEditor; + const text: ContentModelText = { + segmentType: 'Text', + text: 'abc', + format: {}, + }; + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [text, marker], + format: {}, + }; + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [paragraph], + }; + + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + + spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( + (_, apiName, callback) => { + expect(apiName).toEqual('applyPendingFormat'); + callback(model); + } + ); + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { + callback([model], undefined, paragraph, [marker]); + return false; + }); + + applyPendingFormat(editor, 'c'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'ab', + format: {}, + }, + { + segmentType: 'Text', + text: 'c', + format: { + fontSize: '10px', + }, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Has pending format but wrong text', () => { + const editor = ({} as any) as IContentModelEditor; + const text: ContentModelText = { + segmentType: 'Text', + text: 'abc', + format: {}, + }; + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [text, marker], + format: {}, + }; + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [paragraph], + }; + + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + + spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( + (_, apiName, callback) => { + expect(apiName).toEqual('applyPendingFormat'); + callback(model); + } + ); + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { + callback([model], undefined, paragraph, [marker]); + return false; + }); + + applyPendingFormat(editor, 'd'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'abc', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('No pending format', () => { + const editor = ({} as any) as IContentModelEditor; + const text: ContentModelText = { + segmentType: 'Text', + text: 'abc', + format: {}, + }; + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [text, marker], + format: {}, + }; + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [paragraph], + }; + + spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); + + spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( + (_, apiName, callback) => { + expect(apiName).toEqual('applyPendingFormat'); + callback(model); + } + ); + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { + callback([model], undefined, paragraph, [marker]); + return false; + }); + + applyPendingFormat(editor, 'd'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'abc', + format: {}, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); + + it('Selection is not collapsed', () => { + const editor = ({} as any) as IContentModelEditor; + const text: ContentModelText = { + segmentType: 'Text', + text: 'abc', + format: {}, + isSelected: true, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [text], + format: {}, + }; + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [paragraph], + }; + + spyOn(pendingFormat, 'getPendingFormat').and.returnValue({ + fontSize: '10px', + }); + + spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( + (_, apiName, callback) => { + expect(apiName).toEqual('applyPendingFormat'); + callback(model); + } + ); + spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { + callback([model], undefined, paragraph, [text]); + return false; + }); + + applyPendingFormat(editor, 'd'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'abc', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/format/clearFormatTest.ts b/packages/roosterjs-content-model/test/publicApi/format/clearFormatTest.ts index 823ed06b6c1..2e9ea7b7d53 100644 --- a/packages/roosterjs-content-model/test/publicApi/format/clearFormatTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/format/clearFormatTest.ts @@ -3,11 +3,11 @@ import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWith import * as normalizeContentModel from '../../../lib/modelApi/common/normalizeContentModel'; import clearFormat from '../../../lib/publicApi/format/clearFormat'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('clearFormat', () => { it('Clear format', () => { - const editor = ({} as any) as IExperimentalContentModelEditor; + const editor = ({} as any) as IContentModelEditor; const model = ('Model' as any) as ContentModelDocument; spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( diff --git a/packages/roosterjs-content-model/test/publicApi/format/getFormatStateTest.ts b/packages/roosterjs-content-model/test/publicApi/format/getFormatStateTest.ts index 77e37f741b4..f3371b13989 100644 --- a/packages/roosterjs-content-model/test/publicApi/format/getFormatStateTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/format/getFormatStateTest.ts @@ -1,3 +1,4 @@ +import * as getPendingFormat from '../../../lib/modelApi/format/pendingFormat'; import * as retrieveModelFormatState from '../../../lib/modelApi/common/retrieveModelFormatState'; import getFormatState from '../../../lib/publicApi/format/getFormatState'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; @@ -9,8 +10,8 @@ import { normalizeContentModel } from '../../../lib/modelApi/common/normalizeCon import { DomToModelOption, - IExperimentalContentModelEditor, -} from '../../../lib/publicTypes/IExperimentalContentModelEditor'; + IContentModelEditor, +} from '../../../lib/publicTypes/IContentModelEditor'; const selectedNodeId = 'Selected'; @@ -32,8 +33,7 @@ describe('getFormatState', () => { }), isDarkMode: () => false, getZoomScale: () => 1, - getPendingFormat: () => pendingFormat, - createContentModel: (root: Node, options: DomToModelOption) => { + createContentModel: (options: DomToModelOption) => { const model = createContentModelDocument(); const editorDiv = document.createElement('div'); @@ -54,7 +54,10 @@ describe('getFormatState', () => { return model; }, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; + + spyOn(getPendingFormat, 'getPendingFormat').and.returnValue(pendingFormat); + const result = getFormatState(editor); expect(retrieveModelFormatState.retrieveModelFormatState).toHaveBeenCalledTimes(1); diff --git a/packages/roosterjs-content-model/test/publicApi/format/getSegmentFormatTest.ts b/packages/roosterjs-content-model/test/publicApi/format/getSegmentFormatTest.ts index 64e8257ed2e..f5c4349880f 100644 --- a/packages/roosterjs-content-model/test/publicApi/format/getSegmentFormatTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/format/getSegmentFormatTest.ts @@ -1,3 +1,4 @@ +import * as getPendingFormat from '../../../lib/modelApi/format/pendingFormat'; import getSegmentFormat from '../../../lib/publicApi/format/getSegmentFormat'; import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; @@ -8,8 +9,8 @@ import { PositionType, SelectionRangeTypes } from 'roosterjs-editor-types'; import { DomToModelOption, - IExperimentalContentModelEditor, -} from '../../../lib/publicTypes/IExperimentalContentModelEditor'; + IContentModelEditor, +} from '../../../lib/publicTypes/IContentModelEditor'; const selectedNodeId = 'Selected'; @@ -19,6 +20,8 @@ describe('getSegmentFormat', () => { pendingFormat: ContentModelSegmentFormat | null, expectedFormat: ContentModelSegmentFormat | null ) { + spyOn(getPendingFormat, 'getPendingFormat').and.returnValue(pendingFormat); + const editor = ({ getUndoState: () => ({ canUndo: false, @@ -26,8 +29,7 @@ describe('getSegmentFormat', () => { }), isDarkMode: () => false, getZoomScale: () => 1, - getPendingFormat: () => pendingFormat, - createContentModel: (root: Node, options: DomToModelOption) => { + createContentModel: (options: DomToModelOption) => { const model = createContentModelDocument(); const editorDiv = document.createElement('div'); @@ -62,7 +64,7 @@ describe('getSegmentFormat', () => { return model; }, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; const result = getSegmentFormat(editor); expect(result).toEqual(expectedFormat); diff --git a/packages/roosterjs-content-model/test/publicApi/image/changeImageTest.ts b/packages/roosterjs-content-model/test/publicApi/image/changeImageTest.ts new file mode 100644 index 00000000000..1f6c5803845 --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/image/changeImageTest.ts @@ -0,0 +1,145 @@ +import * as readFile from 'roosterjs-editor-dom/lib/utils/readFile'; +import changeImage from '../../../lib/publicApi/image/changeImage'; +import { addSegment } from '../../../lib/modelApi/common/addSegment'; +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createImage } from '../../../lib/modelApi/creators/createImage'; +import { createText } from '../../../lib/modelApi/creators/createText'; +import { segmentTestCommon } from '../segment/segmentTestCommon'; + +describe('changeImage', () => { + const testUrl = 'http://test.com/test'; + const blob = ({ a: 1 } as any) as File; + function runTest( + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number + ) { + segmentTestCommon( + 'changeImage', + editor => changeImage(editor, blob), + model, + result, + calledTimes + ); + } + + beforeEach(() => { + spyOn(readFile, 'default').and.callFake((_, callback) => { + callback(testUrl); + }); + }); + + it('Empty doc', () => { + runTest( + createContentModelDocument(), + { + blockGroupType: 'Document', + blocks: [], + }, + 0 + ); + }); + + it('Doc without selection', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + dataset: {}, + format: {}, + }, + ], + }, + ], + }, + 0 + ); + }); + + it('Doc with selection, but no image', () => { + const doc = createContentModelDocument(); + const text = createText('test'); + + text.isSelected = true; + + addSegment(doc, text); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1 + ); + }); + + it('Doc with selection and image', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + img.format.width = '10px'; + img.format.height = '10px'; + img.format.boxShadow = '0px 0px 3px 3px #aaaaaa'; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: testUrl, + isSelected: true, + dataset: {}, + format: { + boxShadow: '0px 0px 3px 3px #aaaaaa', + width: '', + height: '', + }, + }, + ], + }, + ], + }, + 1 + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/insert/insertImageTest.ts b/packages/roosterjs-content-model/test/publicApi/image/insertImageTest.ts similarity index 70% rename from packages/roosterjs-content-model/test/publicApi/insert/insertImageTest.ts rename to packages/roosterjs-content-model/test/publicApi/image/insertImageTest.ts index eafe2295e18..e9bda8ebdb4 100644 --- a/packages/roosterjs-content-model/test/publicApi/insert/insertImageTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/image/insertImageTest.ts @@ -1,18 +1,17 @@ import * as readFile from 'roosterjs-editor-dom/lib/utils/readFile'; -import insertImage from '../../../lib/publicApi/insert/insertImage'; +import insertImage from '../../../lib/publicApi/image/insertImage'; import { addSegment } from '../../../lib/modelApi/common/addSegment'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('insertImage', () => { const testUrl = 'http://test.com/test'; function runTest( apiName: string, - executionCallback: (editor: IExperimentalContentModelEditor) => void, + executionCallback: (editor: IContentModelEditor) => void, model: ContentModelDocument, result: ContentModelDocument, calledTimes: number @@ -36,9 +35,7 @@ describe('insertImage', () => { setContentModel, isDisposed: () => false, getDocument: () => document, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; executionCallback(editor); @@ -111,4 +108,44 @@ describe('insertImage', () => { 1 ); }); + + it('Insert image with src', () => { + const model = createContentModelDocument(); + const marker = createSelectionMarker(); + + addSegment(model, marker); + + runTest( + 'insertImage', + editor => { + insertImage(editor, testUrl); + }, + model, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: testUrl, + format: {}, + dataset: {}, + isSelectedAsImageSelection: false, + }, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1 + ); + }); }); diff --git a/packages/roosterjs-content-model/test/publicApi/image/setImageBorderTest.ts b/packages/roosterjs-content-model/test/publicApi/image/setImageBorderTest.ts new file mode 100644 index 00000000000..63efc3d0905 --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/image/setImageBorderTest.ts @@ -0,0 +1,311 @@ +import setImageBorder from '../../../lib/publicApi/image/setImageBorder'; +import { addSegment } from '../../../lib/modelApi/common/addSegment'; +import { Border } from '../../../lib/publicTypes/interface/Border'; +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createImage } from '../../../lib/modelApi/creators/createImage'; +import { createText } from '../../../lib/modelApi/creators/createText'; +import { segmentTestCommon } from '../segment/segmentTestCommon'; + +describe('setImageBorder', () => { + function runTest( + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number, + border: Border, + borderRadius?: string + ) { + segmentTestCommon( + 'setImageBorder', + editor => setImageBorder(editor, border, borderRadius), + model, + result, + calledTimes + ); + } + + it('Empty doc', () => { + runTest( + createContentModelDocument(), + { + blockGroupType: 'Document', + blocks: [], + }, + 0, + {} + ); + }); + + it('Doc without selection', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + dataset: {}, + format: {}, + }, + ], + }, + ], + }, + 0, + {} + ); + }); + + it('Doc with selection, but no image', () => { + const doc = createContentModelDocument(); + const text = createText('test'); + + text.isSelected = true; + + addSegment(doc, text); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1, + {} + ); + }); + + it('Doc with selection and image - set color', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + borderBottom: '1px solid red', + borderLeft: '1px solid red', + borderRight: '1px solid red', + borderTop: '1px solid red', + borderRadius: '5px', + }, + }, + ], + }, + ], + }, + 1, + { color: 'red' }, + '5px' + ); + }); + + it('Doc with selection and image - set style', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + borderBottom: '1px groove', + borderLeft: '1px groove', + borderRight: '1px groove', + borderTop: '1px groove', + borderRadius: '5px', + }, + }, + ], + }, + ], + }, + 1, + { style: 'groove' }, + '5px' + ); + }); + + it('Doc with selection and image - set width ', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + borderBottom: '10px solid', + borderLeft: '10px solid', + borderRight: '10px solid', + borderTop: '10px solid', + borderRadius: '5px', + }, + }, + ], + }, + ], + }, + 1, + { width: '10px' }, + '5px' + ); + }); + + it('Doc with selection and image - all formats', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + borderBottom: ptToPx(10) + 'px solid', + borderLeft: ptToPx(10) + 'px solid', + borderRight: ptToPx(10) + 'px solid', + borderTop: ptToPx(10) + 'px solid', + borderRadius: '5px', + }, + }, + ], + }, + ], + }, + 1, + { width: '10pt' }, + '5px' + ); + }); + + it('Doc with selection and image - set width ', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + borderBottom: `${ptToPx(10)}px groove red`, + borderLeft: `${ptToPx(10)}px groove red`, + borderRight: `${ptToPx(10)}px groove red`, + borderTop: `${ptToPx(10)}px groove red`, + borderRadius: '5px', + }, + }, + ], + }, + ], + }, + 1, + { color: 'red', style: 'groove', width: '10pt' }, + '5px' + ); + }); +}); + +function ptToPx(pt: number): number { + return Math.round((pt * 4000) / 3) / 1000; +} diff --git a/packages/roosterjs-content-model/test/publicApi/image/setImageBoxShadowTest.ts b/packages/roosterjs-content-model/test/publicApi/image/setImageBoxShadowTest.ts new file mode 100644 index 00000000000..dc78661f283 --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/image/setImageBoxShadowTest.ts @@ -0,0 +1,132 @@ +import setImageBoxShadow from '../../../lib/publicApi/image/setImageBoxShadow'; +import { addSegment } from '../../../lib/modelApi/common/addSegment'; +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createImage } from '../../../lib/modelApi/creators/createImage'; +import { createText } from '../../../lib/modelApi/creators/createText'; +import { segmentTestCommon } from '../segment/segmentTestCommon'; + +describe('setImageBoxShadow', () => { + const style = '0px 0px 3px 3px #aaaaaa'; + function runTest( + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number + ) { + segmentTestCommon( + 'setImageBoxShadow', + editor => setImageBoxShadow(editor, style), + model, + result, + calledTimes + ); + } + + it('Empty doc', () => { + runTest( + createContentModelDocument(), + { + blockGroupType: 'Document', + blocks: [], + }, + 0 + ); + }); + + it('Doc without selection', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + dataset: {}, + format: {}, + }, + ], + }, + ], + }, + 0 + ); + }); + + it('Doc with selection, but no image', () => { + const doc = createContentModelDocument(); + const text = createText('test'); + + text.isSelected = true; + + addSegment(doc, text); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1 + ); + }); + + it('Doc with selection and image', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + boxShadow: '0px 0px 3px 3px #aaaaaa', + }, + }, + ], + }, + ], + }, + 1 + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts b/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts index 6e8979be2bf..719085bd6ce 100644 --- a/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts @@ -8,12 +8,12 @@ import { createImage } from '../../../lib/modelApi/creators/createImage'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; import { createText } from '../../../lib/modelApi/creators/createText'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('adjustLinkSelection', () => { - let editor: IExperimentalContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; + let editor: IContentModelEditor; + let setContentModel: jasmine.Spy; + let createContentModel: jasmine.Spy; beforeEach(() => { setContentModel = jasmine.createSpy('setContentModel'); @@ -24,7 +24,7 @@ describe('adjustLinkSelection', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); function runTest( diff --git a/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts b/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts index fcf4ed67866..6510ca08750 100644 --- a/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts @@ -6,13 +6,12 @@ import { createContentModelDocument } from '../../../lib/modelApi/creators/creat import { createImage } from '../../../lib/modelApi/creators/createImage'; import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; import { createText } from '../../../lib/modelApi/creators/createText'; -import { HyperLinkColorPlaceholder } from '../../../lib/formatHandlers/utils/defaultStyles'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('insertLink', () => { - let editor: IExperimentalContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; + let editor: IContentModelEditor; + let setContentModel: jasmine.Spy; + let createContentModel: jasmine.Spy; beforeEach(() => { setContentModel = jasmine.createSpy('setContentModel'); @@ -23,7 +22,7 @@ describe('insertLink', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); function runTest( @@ -63,10 +62,7 @@ describe('insertLink', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, text: 'http://test.com', link: { dataset: {}, @@ -74,6 +70,7 @@ describe('insertLink', () => { href: 'http://test.com', anchorTitle: undefined, target: undefined, + underline: true, }, }, }, @@ -108,10 +105,7 @@ describe('insertLink', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, text: 'test', link: { dataset: {}, @@ -119,6 +113,7 @@ describe('insertLink', () => { href: 'http://test.com', anchorTitle: 'title', target: undefined, + underline: true, }, }, isSelected: true, @@ -151,10 +146,7 @@ describe('insertLink', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, text: 'linkText', link: { dataset: {}, @@ -162,6 +154,7 @@ describe('insertLink', () => { href: 'http://test.com', anchorTitle: 'title', target: 'target', + underline: true, }, }, }, @@ -200,6 +193,7 @@ describe('insertLink', () => { href: 'http://test.com', anchorTitle: 'title', target: undefined, + underline: true, }, }; @@ -216,20 +210,14 @@ describe('insertLink', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, text: 'test1', link, isSelected: true, }, { segmentType: 'Image', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, src: 'test', dataset: {}, link, @@ -237,10 +225,7 @@ describe('insertLink', () => { }, { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, text: 'test2', link, isSelected: true, @@ -274,6 +259,7 @@ describe('insertLink', () => { href: 'http://test.com', anchorTitle: 'title', target: undefined, + underline: true, }, }; @@ -290,10 +276,7 @@ describe('insertLink', () => { segments: [ { segmentType: 'Text', - format: { - underline: true, - textColor: HyperLinkColorPlaceholder, - }, + format: {}, text: 'new text', link, }, diff --git a/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts b/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts index 5aded16554b..9848cdf0ef2 100644 --- a/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts @@ -6,12 +6,12 @@ import { ContentModelLink } from '../../../lib/publicTypes/decorator/ContentMode import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createImage } from '../../../lib/modelApi/creators/createImage'; import { createText } from '../../../lib/modelApi/creators/createText'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('removeLink', () => { - let editor: IExperimentalContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; + let editor: IContentModelEditor; + let setContentModel: jasmine.Spy; + let createContentModel: jasmine.Spy; beforeEach(() => { setContentModel = jasmine.createSpy('setContentModel'); @@ -22,7 +22,7 @@ describe('removeLink', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); function runTest(model: ContentModelDocument, expectedModel: ContentModelDocument | null) { @@ -78,7 +78,7 @@ describe('removeLink', () => { { segmentType: 'Text', text: 'test', - format: { underline: false }, + format: {}, isSelected: true, }, ], @@ -124,7 +124,7 @@ describe('removeLink', () => { { segmentType: 'Text', text: 'test1', - format: { underline: false }, + format: {}, isSelected: true, }, { @@ -137,7 +137,7 @@ describe('removeLink', () => { segmentType: 'Image', src: 'test', dataset: {}, - format: { underline: false }, + format: {}, isSelected: true, isSelectedAsImageSelection: false, }, @@ -191,25 +191,25 @@ describe('removeLink', () => { { segmentType: 'Text', text: 'test1', - format: { underline: false }, + format: {}, isSelected: true, }, { segmentType: 'Text', text: 'test2', - format: { underline: false }, + format: {}, isSelected: true, }, { segmentType: 'Text', text: 'test3', - format: { underline: false }, + format: {}, isSelected: true, }, { segmentType: 'Text', text: 'test4', - format: { underline: false }, + format: {}, isSelected: true, }, ], @@ -258,7 +258,7 @@ describe('removeLink', () => { { segmentType: 'Text', text: 'test2', - format: { underline: false }, + format: {}, isSelected: true, }, { diff --git a/packages/roosterjs-content-model/test/publicApi/list/toggleBulletTest.ts b/packages/roosterjs-content-model/test/publicApi/list/toggleBulletTest.ts index 8f0264a0985..fbb27ebd9d3 100644 --- a/packages/roosterjs-content-model/test/publicApi/list/toggleBulletTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/list/toggleBulletTest.ts @@ -1,11 +1,10 @@ import * as setListType from '../../../lib/modelApi/list/setListType'; import toggleBullet from '../../../lib/publicApi/list/toggleBullet'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('toggleBullet', () => { - let editor = ({} as any) as IExperimentalContentModelEditor; + let editor = ({} as any) as IContentModelEditor; let addUndoSnapshot: jasmine.Spy; let createContentModel: jasmine.Spy; let setContentModel: jasmine.Spy; @@ -25,9 +24,7 @@ describe('toggleBullet', () => { addUndoSnapshot, createContentModel, setContentModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; spyOn(setListType, 'setListType').and.returnValue(true); }); diff --git a/packages/roosterjs-content-model/test/publicApi/list/toggleNumberingTest.ts b/packages/roosterjs-content-model/test/publicApi/list/toggleNumberingTest.ts index 783544c5187..2b44628334f 100644 --- a/packages/roosterjs-content-model/test/publicApi/list/toggleNumberingTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/list/toggleNumberingTest.ts @@ -1,11 +1,10 @@ import * as setListType from '../../../lib/modelApi/list/setListType'; import toggleNumbering from '../../../lib/publicApi/list/toggleNumbering'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('toggleNumbering', () => { - let editor = ({} as any) as IExperimentalContentModelEditor; + let editor = ({} as any) as IContentModelEditor; let addUndoSnapshot: jasmine.Spy; let createContentModel: jasmine.Spy; let setContentModel: jasmine.Spy; @@ -25,9 +24,7 @@ describe('toggleNumbering', () => { addUndoSnapshot, createContentModel, setContentModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; spyOn(setListType, 'setListType').and.returnValue(true); }); diff --git a/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts b/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts index b6466f36dc1..5853baf0e09 100644 --- a/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts @@ -1,9 +1,9 @@ -import * as getComputedStyles from 'roosterjs-editor-dom/lib/utils/getComputedStyles'; +import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; import changeFontSize from '../../../lib/publicApi/segment/changeFontSize'; -import domToContentModel from '../../../lib/publicApi/domToContentModel'; +import domToContentModel from '../../../lib/domToModel/domToContentModel'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; import { createRange } from 'roosterjs-editor-dom'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; import { segmentTestCommon } from './segmentTestCommon'; import { SelectionRangeTypes } from 'roosterjs-editor-types'; @@ -258,30 +258,78 @@ describe('changeFontSize', () => { ); } - it('increase pt', () => { + it('increase pt 1', () => { testSizeChange('7pt', '8pt', 'increase'); + }); + + it('increase pt 2', () => { testSizeChange('8pt', '9pt', 'increase'); + }); + + it('increase pt 3', () => { testSizeChange('28pt', '36pt', 'increase'); + }); + + it('increase pt 4', () => { testSizeChange('37pt', '48pt', 'increase'); + }); + + it('increase pt 5', () => { testSizeChange('72pt', '80pt', 'increase'); + }); + + it('increase pt 6', () => { testSizeChange('80pt', '90pt', 'increase'); + }); + + it('increase pt 7', () => { testSizeChange('990pt', '1000pt', 'increase'); + }); + + it('increase pt 8', () => { testSizeChange('1000pt', '1000pt', 'increase'); }); - it('decrease pt', () => { + it('decrease pt 1', () => { testSizeChange('1pt', '1pt', 'decrease'); + }); + + it('decrease pt 2', () => { testSizeChange('7pt', '6pt', 'decrease'); + }); + + it('decrease pt 3', () => { testSizeChange('8pt', '7pt', 'decrease'); + }); + + it('decrease pt 4', () => { testSizeChange('28pt', '26pt', 'decrease'); + }); + + it('decrease pt 5', () => { testSizeChange('37pt', '36pt', 'decrease'); + }); + + it('decrease pt 6', () => { testSizeChange('72pt', '48pt', 'decrease'); + }); + + it('decrease pt 7', () => { testSizeChange('80pt', '72pt', 'decrease'); + }); + + it('decrease pt 8', () => { testSizeChange('990pt', '980pt', 'decrease'); + }); + + it('decrease pt 9', () => { testSizeChange('1000pt', '990pt', 'decrease'); }); it('Test format parser', () => { + spyOn(pendingFormat, 'setPendingFormat'); + spyOn(pendingFormat, 'getPendingFormat').and.returnValue(null); + const addUndoSnapshot = jasmine.createSpy().and.callFake((callback: () => void) => { callback(); }); @@ -294,7 +342,7 @@ describe('changeFontSize', () => { div.style.fontSize = '20pt'; const editor = ({ - createContentModel: (startNode: any, option: any) => + createContentModel: (option: any) => domToContentModel(div, null!, { selectionRange: { type: SelectionRangeTypes.Normal, @@ -307,15 +355,7 @@ describe('changeFontSize', () => { addUndoSnapshot, focus: jasmine.createSpy(), setContentModel, - getPendingFormat: () => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; - - spyOn(getComputedStyles, 'getComputedStyle').and.callFake( - (node: HTMLElement, style: string) => { - return node.style.fontSize; - } - ); + } as any) as IContentModelEditor; changeFontSize(editor, 'increase'); @@ -339,8 +379,5 @@ describe('changeFontSize', () => { }, ], }); - - expect(getComputedStyles.getComputedStyle).toHaveBeenCalledTimes(1); - expect(getComputedStyles.getComputedStyle).toHaveBeenCalledWith(div, 'font-size'); }); }); diff --git a/packages/roosterjs-content-model/test/publicApi/segment/segmentTestCommon.ts b/packages/roosterjs-content-model/test/publicApi/segment/segmentTestCommon.ts index d4d9be74c91..ac22a59382f 100644 --- a/packages/roosterjs-content-model/test/publicApi/segment/segmentTestCommon.ts +++ b/packages/roosterjs-content-model/test/publicApi/segment/segmentTestCommon.ts @@ -1,14 +1,18 @@ +import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { NodePosition } from 'roosterjs-editor-types'; export function segmentTestCommon( apiName: string, - executionCallback: (editor: IExperimentalContentModelEditor) => void, + 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) => { @@ -24,9 +28,9 @@ export function segmentTestCommon( addUndoSnapshot, focus: jasmine.createSpy(), setContentModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + isDisposed: () => false, + getFocusedPosition: () => null as NodePosition, + } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages/roosterjs-content-model/test/publicApi/segment/setTextColorTest.ts b/packages/roosterjs-content-model/test/publicApi/segment/setTextColorTest.ts index ad08b9471b3..8b1001325ef 100644 --- a/packages/roosterjs-content-model/test/publicApi/segment/setTextColorTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/segment/setTextColorTest.ts @@ -360,4 +360,107 @@ describe('setTextColor', () => { null ); }); + + it('Set color with link', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + link: { + format: {}, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'red', + }, + link: { + format: { + textColor: 'red', + }, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + 1 + ); + }); + + it('Remove color with link', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + textColor: 'red', + }, + link: { + format: { textColor: 'red' }, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + link: { + format: {}, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + 1, + null + ); + }); }); diff --git a/packages/roosterjs-content-model/test/publicApi/segment/toggleUnderlineTest.ts b/packages/roosterjs-content-model/test/publicApi/segment/toggleUnderlineTest.ts index 54067394a55..22dd23e3469 100644 --- a/packages/roosterjs-content-model/test/publicApi/segment/toggleUnderlineTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/segment/toggleUnderlineTest.ts @@ -312,4 +312,102 @@ describe('toggleUnderline', () => { 1 ); }); + + it('Turn on underline with link', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + link: { + format: {}, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { underline: true }, + link: { + format: { underline: true }, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + 1 + ); + }); + + it('Turn off underline with link', () => { + runTest( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { + underline: true, + }, + link: { + format: { underline: true }, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: { underline: false }, + link: { + format: { underline: false }, + dataset: {}, + }, + isSelected: true, + }, + ], + }, + ], + }, + 1 + ); + }); }); diff --git a/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts b/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts index 44acc8e9385..29ca81868dc 100644 --- a/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts @@ -2,12 +2,12 @@ import * as normalizeTable from '../../../lib/modelApi/table/normalizeTable'; import setTableCellShade from '../../../lib/publicApi/table/setTableCellShade'; import { ContentModelTable } from '../../../lib/publicTypes/block/ContentModelTable'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('setTableCellShade', () => { - let editor: IExperimentalContentModelEditor; - let setContentModel: jasmine.Spy; - let createContentModel: jasmine.Spy; + let editor: IContentModelEditor; + let setContentModel: jasmine.Spy; + let createContentModel: jasmine.Spy; beforeEach(() => { setContentModel = jasmine.createSpy('setContentModel'); @@ -20,7 +20,7 @@ describe('setTableCellShade', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); function runTest(table: ContentModelTable, expectedTable: ContentModelTable | null) { diff --git a/packages/roosterjs-content-model/test/publicApi/utils/formatImageWithContentModelTest.ts b/packages/roosterjs-content-model/test/publicApi/utils/formatImageWithContentModelTest.ts new file mode 100644 index 00000000000..a15428cb04a --- /dev/null +++ b/packages/roosterjs-content-model/test/publicApi/utils/formatImageWithContentModelTest.ts @@ -0,0 +1,147 @@ +import formatImageWithContentModel from '../../../lib/publicApi/utils/formatImageWithContentModel'; +import { addSegment } from '../../../lib/modelApi/common/addSegment'; +import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; +import { ContentModelImage } from '../../../lib/publicTypes/segment/ContentModelImage'; +import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createImage } from '../../../lib/modelApi/creators/createImage'; +import { createText } from '../../../lib/modelApi/creators/createText'; +import { segmentTestCommon } from '../segment/segmentTestCommon'; + +describe('formatImageWithContentModel', () => { + function runTest( + model: ContentModelDocument, + result: ContentModelDocument, + calledTimes: number, + callback: (image: ContentModelImage) => void + ) { + segmentTestCommon( + 'apiTest', + editor => formatImageWithContentModel(editor, 'apiTest', callback), + model, + result, + calledTimes + ); + } + + it('Empty doc', () => { + runTest( + createContentModelDocument(), + { + blockGroupType: 'Document', + blocks: [], + }, + 0, + (image: ContentModelImage) => { + return; + } + ); + }); + + it('Doc without selection', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + dataset: {}, + format: {}, + }, + ], + }, + ], + }, + 0, + (image: ContentModelImage) => { + return; + } + ); + }); + + it('Doc with selection, but no image', () => { + const doc = createContentModelDocument(); + const text = createText('test'); + + text.isSelected = true; + + addSegment(doc, text); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, + }, + ], + }, + ], + }, + 1, + (image: ContentModelImage) => { + return; + } + ); + }); + + it('Doc with selection and image - add border top ', () => { + const doc = createContentModelDocument(); + const img = createImage('test'); + + img.isSelected = true; + + addSegment(doc, img); + + runTest( + doc, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + isImplicit: true, + segments: [ + { + segmentType: 'Image', + src: 'test', + isSelected: true, + dataset: {}, + format: { + boxShadow: '0px 0px 3px 3px #aaaaaa', + borderTop: '1px solid green', + }, + }, + ], + }, + ], + }, + 1, + (image: ContentModelImage) => { + image.format.borderTop = '1px solid green'; + image.format.boxShadow = '0px 0px 3px 3px #aaaaaa'; + } + ); + }); +}); diff --git a/packages/roosterjs-content-model/test/publicApi/utils/formatParagraphWithContentModelTest.ts b/packages/roosterjs-content-model/test/publicApi/utils/formatParagraphWithContentModelTest.ts index 637265c289a..5ceba23664a 100644 --- a/packages/roosterjs-content-model/test/publicApi/utils/formatParagraphWithContentModelTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/utils/formatParagraphWithContentModelTest.ts @@ -1,13 +1,12 @@ import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { createText } from '../../../lib/modelApi/creators/createText'; import { formatParagraphWithContentModel } from '../../../lib/publicApi/utils/formatParagraphWithContentModel'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('formatParagraphWithContentModel', () => { - let editor: IExperimentalContentModelEditor; + let editor: IContentModelEditor; let addUndoSnapshot: jasmine.Spy; let setContentModel: jasmine.Spy; let focus: jasmine.Spy; @@ -25,9 +24,7 @@ describe('formatParagraphWithContentModel', () => { addUndoSnapshot, createContentModel: () => model, setContentModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); it('empty doc', () => { diff --git a/packages/roosterjs-content-model/test/publicApi/utils/formatSegmentWithContentModelTest.ts b/packages/roosterjs-content-model/test/publicApi/utils/formatSegmentWithContentModelTest.ts index 4360c24bac6..70462d9dee8 100644 --- a/packages/roosterjs-content-model/test/publicApi/utils/formatSegmentWithContentModelTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/utils/formatSegmentWithContentModelTest.ts @@ -1,3 +1,4 @@ +import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; @@ -5,10 +6,11 @@ import { createParagraph } from '../../../lib/modelApi/creators/createParagraph' import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; import { createText } from '../../../lib/modelApi/creators/createText'; import { formatSegmentWithContentModel } from '../../../lib/publicApi/utils/formatSegmentWithContentModel'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { NodePosition } from 'roosterjs-editor-types'; describe('formatSegmentWithContentModel', () => { - let editor: IExperimentalContentModelEditor; + let editor: IContentModelEditor; let addUndoSnapshot: jasmine.Spy; let setContentModel: jasmine.Spy; let focus: jasmine.Spy; @@ -22,17 +24,17 @@ describe('formatSegmentWithContentModel', () => { addUndoSnapshot = jasmine.createSpy('addUndoSnapshot').and.callFake(callback => callback()); setContentModel = jasmine.createSpy('setContentModel'); focus = jasmine.createSpy('focus'); - getPendingFormat = jasmine.createSpy('getPendingFormat'); - setPendingFormat = jasmine.createSpy('setPendingFormat'); + + setPendingFormat = spyOn(pendingFormat, 'setPendingFormat'); + getPendingFormat = spyOn(pendingFormat, 'getPendingFormat'); editor = ({ focus, addUndoSnapshot, createContentModel: () => model, setContentModel, - getPendingFormat: getPendingFormat, - setPendingFormat: setPendingFormat, - } as any) as IExperimentalContentModelEditor; + getFocusedPosition: () => null as NodePosition, + } as any) as IContentModelEditor; }); it('empty doc', () => { @@ -217,6 +219,10 @@ describe('formatSegmentWithContentModel', () => { para.segments.push(marker); model.blocks.push(para); + const mockedPosition = ('Position' as any) as NodePosition; + + editor.getFocusedPosition = () => mockedPosition; + formatSegmentWithContentModel(editor, apiName, format => (format.fontFamily = 'test')); expect(model).toEqual({ blockGroupType: 'Document', @@ -240,10 +246,14 @@ describe('formatSegmentWithContentModel', () => { expect(addUndoSnapshot).toHaveBeenCalledTimes(0); expect(getPendingFormat).toHaveBeenCalledTimes(1); expect(setPendingFormat).toHaveBeenCalledTimes(1); - expect(setPendingFormat).toHaveBeenCalledWith({ - fontSize: '10px', - fontFamily: 'test', - }); + expect(setPendingFormat).toHaveBeenCalledWith( + editor, + { + fontSize: '10px', + fontFamily: 'test', + }, + mockedPosition + ); }); it('With pending format', () => { diff --git a/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts b/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts index 1ce41cfb659..b40fc3c8909 100644 --- a/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts +++ b/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts @@ -1,11 +1,10 @@ import { ChangeSource } from 'roosterjs-editor-types'; import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument'; -import { ContentModelSegmentFormat } from '../../../lib/publicTypes/format/ContentModelSegmentFormat'; import { formatWithContentModel } from '../../../lib/publicApi/utils/formatWithContentModel'; -import { IExperimentalContentModelEditor } from '../../../lib/publicTypes/IExperimentalContentModelEditor'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('formatWithContentModel', () => { - let editor: IExperimentalContentModelEditor; + let editor: IContentModelEditor; let addUndoSnapshot: jasmine.Spy; let createContentModel: jasmine.Spy; let setContentModel: jasmine.Spy; @@ -27,9 +26,7 @@ describe('formatWithContentModel', () => { addUndoSnapshot, createContentModel, setContentModel, - getPendingFormat: (): ContentModelSegmentFormat | null => null, - setPendingFormat: () => {}, - } as any) as IExperimentalContentModelEditor; + } as any) as IContentModelEditor; }); it('Callback return false', () => { diff --git a/packages/roosterjs-content-model/tsconfig.child.json b/packages/roosterjs-content-model/tsconfig.child.json index e8ca0fecfdb..daed1559590 100644 --- a/packages/roosterjs-content-model/tsconfig.child.json +++ b/packages/roosterjs-content-model/tsconfig.child.json @@ -6,6 +6,7 @@ "include": ["./lib/**/*.ts"], "references": [ { "path": "../roosterjs-editor-types/tsconfig.child.json" }, - { "path": "../roosterjs-editor-dom/tsconfig.child.json" } + { "path": "../roosterjs-editor-dom/tsconfig.child.json" }, + { "path": "../roosterjs-editor-core/tsconfig.child.json" } ] } diff --git a/packages/roosterjs-editor-api/lib/format/getFormatState.ts b/packages/roosterjs-editor-api/lib/format/getFormatState.ts index e5dd79eb4ce..e1b922ca894 100644 --- a/packages/roosterjs-editor-api/lib/format/getFormatState.ts +++ b/packages/roosterjs-editor-api/lib/format/getFormatState.ts @@ -35,6 +35,7 @@ export function getElementBasedFormatState( const headerTag = getTagOfNode( editor.getElementAtCursor('H1,H2,H3,H4,H5,H6', null /*startFrom*/, event) ); + const table = editor.queryElements('table', QueryScope.OnSelection)[0]; const tableFormat = table ? getTableFormatInfo(table) : undefined; const hasHeader = table?.rows[0] diff --git a/packages/roosterjs-editor-api/lib/format/insertImage.ts b/packages/roosterjs-editor-api/lib/format/insertImage.ts index ec4dbc412af..9ffb1c1feb0 100644 --- a/packages/roosterjs-editor-api/lib/format/insertImage.ts +++ b/packages/roosterjs-editor-api/lib/format/insertImage.ts @@ -5,37 +5,18 @@ import { IEditor } from 'roosterjs-editor-types'; /** * Insert an image to editor at current selection * @param editor The editor instance - * @param imageFile The image file. There are at least 3 ways to obtain the file object: - * From local file, from clipboard data, from drag-and-drop + * @param imageFileOrSrc Either the image file blob or source string of the image. * @param attributes Optional image element attributes */ export default function insertImage( editor: IEditor, - imageFile: File, - attributes?: Record -): void; - -/** - * Insert an image to editor at current selection - * @param editor The editor instance - * @param url The image link - * @param attributes Optional image element attributes - */ -export default function insertImage( - editor: IEditor, - url: string, - attributes?: Record -): void; - -export default function insertImage( - editor: IEditor, - imageFile: File | string, + imageFileOrSrc: File | string, attributes?: Record ): void { - if (typeof imageFile == 'string') { - insertImageWithSrc(editor, imageFile, attributes); + if (typeof imageFileOrSrc == 'string') { + insertImageWithSrc(editor, imageFileOrSrc, attributes); } else { - readFile(imageFile, dataUrl => { + readFile(imageFileOrSrc, dataUrl => { if (dataUrl && !editor.isDisposed()) { insertImageWithSrc(editor, dataUrl, attributes); } diff --git a/packages/roosterjs-editor-core/lib/coreApi/getStyleBasedFormatState.ts b/packages/roosterjs-editor-core/lib/coreApi/getStyleBasedFormatState.ts index 12417f3c03a..6bbf2e5d7db 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/getStyleBasedFormatState.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/getStyleBasedFormatState.ts @@ -35,7 +35,17 @@ export const getStyleBasedFormatState: GetStyleBasedFormatState = ( ]; } - const styles = node ? getComputedStyles(node) : []; + const styles = node + ? getComputedStyles(node, [ + 'font-family', + 'font-size', + 'color', + 'background-color', + 'line-height', + 'margin-top', + 'margin-bottom', + ]) + : []; const { contentDiv, darkColorHandler, @@ -86,6 +96,9 @@ export const getStyleBasedFormatState: GetStyleBasedFormatState = ( darkModeColor: backColor.darkModeColor, } : undefined, + lineHeight: styles[4], + marginTop: styles[5], + marginBottom: styles[6], }; } else { const ogTextColorNode = @@ -126,6 +139,7 @@ export const getStyleBasedFormatState: GetStyleBasedFormatState = ( styles[3], } : undefined, + lineHeight: styles[4], }; } }; diff --git a/packages/roosterjs-editor-core/lib/corePlugins/PendingFormatStatePlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/PendingFormatStatePlugin.ts index da38d8a0fd2..eff1d32c626 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/PendingFormatStatePlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/PendingFormatStatePlugin.ts @@ -99,7 +99,7 @@ export default class PendingFormatStatePlugin this.editor.insertNode(this.state.pendableFormatSpan); this.editor.select( this.state.pendableFormatSpan, - PositionType.Before, + PositionType.Begin, this.state.pendableFormatSpan, PositionType.End ); diff --git a/packages/roosterjs-editor-core/lib/editor/Editor.ts b/packages/roosterjs-editor-core/lib/editor/Editor.ts index a07b5ceb45a..69479ddc5f5 100644 --- a/packages/roosterjs-editor-core/lib/editor/Editor.ts +++ b/packages/roosterjs-editor-core/lib/editor/Editor.ts @@ -1156,7 +1156,7 @@ export default class Editor implements IEditor { * @returns the current EditorCore object * @throws a standard Error if there's no core object */ - private getCore(): EditorCore { + protected getCore(): EditorCore { if (!this.core) { throw new Error('Editor is already disposed'); } diff --git a/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts b/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts index 2c09ce14a7d..7134c974259 100644 --- a/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts +++ b/packages/roosterjs-editor-dom/lib/utils/getIntersectedRect.ts @@ -32,8 +32,8 @@ export default function getIntersectedRect( ): Rect | null { const rects = elements .map(element => normalizeRect(element.getBoundingClientRect())) - .filter(element => !!element) - .concat(additionalRects) as Rect[]; + .concat(additionalRects) + .filter(element => !!element) as Rect[]; const result: Rect = { top: Math.max(...rects.map(r => r.top)), diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts index 13f27631f22..f8a02caf20d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/listFeatures.ts @@ -103,6 +103,7 @@ const IndentWhenTab: BuildInEditFeature = { shouldHandleEvent: shouldHandleIndentationEvent(true), handleEvent: handleIndentationEvent(true), allowFunctionKeys: true, + defaultDisabled: Browser.isMac, }; /** @@ -113,6 +114,7 @@ const OutdentWhenShiftTab: BuildInEditFeature = { shouldHandleEvent: shouldHandleIndentationEvent(false), handleEvent: handleIndentationEvent(false), allowFunctionKeys: true, + defaultDisabled: Browser.isMac, }; /** @@ -479,7 +481,7 @@ function shouldTriggerList( const traverser = editor.getBlockTraverser(); const text = traverser && traverser.currentBlockElement - ? traverser.currentBlockElement.getTextContent() + ? traverser.currentBlockElement.getTextContent().slice(0, textBeforeCursor.length) : null; const isATheBeginning = text && text === textBeforeCursor; const listChains = getListChains(editor); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index d045921bc42..414ce596d35 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -168,7 +168,7 @@ export default class ImageEdit implements EditorPlugin { initialize(editor: IEditor) { this.editor = editor; this.disposer = editor.addDomEventHandler({ - blur: () => this.onBlur, + blur: () => this.onBlur(), dragstart: e => { if (this.image) { e.preventDefault(); @@ -450,18 +450,16 @@ export default class ImageEdit implements EditorPlugin { const cropBottomPx = originalHeight * bottomPercent; // Update size and margin of the wrapper - wrapper.style.width = getPx(visibleWidth); - wrapper.style.height = getPx(visibleHeight); wrapper.style.margin = `${marginVertical}px ${marginHorizontal}px`; wrapper.style.transform = `rotate(${angleRad}rad)`; - this.wrapper.style.width = getPx(visibleWidth); - this.wrapper.style.height = getPx(visibleHeight); + setWrapperSizeDimensions(wrapper, this.image, visibleWidth, visibleHeight); // Update the text-alignment to avoid the image to overflow if the parent element have align center or right // or if the direction is Right To Left - wrapper.style.textAlign = isRtl(wrapper.parentNode) ? 'right' : 'left'; + wrapper.style.textAlign = isRtl(this.shadowSpan.parentElement) ? 'right' : 'left'; // Update size of the image + this.clonedImage.style.width = getPx(originalWidth); this.clonedImage.style.height = getPx(originalHeight); @@ -501,13 +499,16 @@ export default class ImageEdit implements EditorPlugin { this.updateWrapper(); } - updateRotateHandlePosition( - this.editInfo, - this.editor.getVisibleViewport(), - marginVertical, - rotateCenter, - rotateHandle - ); + const viewport = this.editor.getVisibleViewport(); + if (rotateHandle && rotateCenter && viewport) { + updateRotateHandlePosition( + this.editInfo, + this.editor.getVisibleViewport(), + marginVertical, + rotateCenter, + rotateHandle + ); + } updateHandleCursor(resizeHandles, angleRad); } @@ -574,6 +575,23 @@ function setSize( element.style.height = getPx(height); } +function setWrapperSizeDimensions( + wrapper: HTMLElement, + image: HTMLImageElement, + width: number, + height: number +) { + const hasBorder = image.style.borderStyle; + if (hasBorder) { + const borderWidth = image.style.borderWidth ? 2 * parseInt(image.style.borderWidth) : 2; + wrapper.style.width = getPx(width + borderWidth); + wrapper.style.height = getPx(height + borderWidth); + return; + } + wrapper.style.width = getPx(width); + wrapper.style.height = getPx(height); +} + function getPx(value: number): string { return value === undefined ? null : value + 'px'; } diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts index 8a395da9bc8..7f862ccd35d 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/imageEditors/Rotator.ts @@ -51,19 +51,22 @@ export function updateRotateHandlePosition( rotateCenter: HTMLElement, rotateHandle: HTMLElement ) { - const top = rotateHandle.getBoundingClientRect()?.top - editorRect?.top; - const { angleRad, heightPx } = editInfo; - const cosAngle = Math.cos(angleRad); - const adjustedDistance = - cosAngle <= 0 - ? Number.MAX_SAFE_INTEGER - : (top + heightPx / 2 + marginVertical) / cosAngle - heightPx / 2; + const rotateHandleRect = rotateHandle.getBoundingClientRect(); + if (rotateHandleRect) { + const top = rotateHandleRect.top - editorRect?.top; + const { angleRad, heightPx } = editInfo; + const cosAngle = Math.cos(angleRad); + const adjustedDistance = + cosAngle <= 0 + ? Number.MAX_SAFE_INTEGER + : (top + heightPx / 2 + marginVertical) / cosAngle - heightPx / 2; - const rotateGap = Math.max(Math.min(ROTATE_GAP, adjustedDistance), 0); - const rotateTop = Math.max(Math.min(ROTATE_SIZE, adjustedDistance - rotateGap), 0); - rotateCenter.style.top = -rotateGap + 'px'; - rotateCenter.style.height = rotateGap + 'px'; - rotateHandle.style.top = -rotateTop + 'px'; + const rotateGap = Math.max(Math.min(ROTATE_GAP, adjustedDistance), 0); + const rotateTop = Math.max(Math.min(ROTATE_SIZE, adjustedDistance - rotateGap), 0); + rotateCenter.style.top = -rotateGap + 'px'; + rotateCenter.style.height = rotateGap + 'px'; + rotateHandle.style.top = -rotateTop + 'px'; + } } /** diff --git a/packages/roosterjs-editor-types/lib/interface/FormatState.ts b/packages/roosterjs-editor-types/lib/interface/FormatState.ts index b37945470a0..344f80d6013 100644 --- a/packages/roosterjs-editor-types/lib/interface/FormatState.ts +++ b/packages/roosterjs-editor-types/lib/interface/FormatState.ts @@ -138,6 +138,21 @@ export interface StyleBasedFormatState { * Mode independent background color for dark mode */ textColors?: ModeIndependentColor; + + /** + * Line height + */ + lineHeight?: string; + + /** + * Margin Top + */ + marginTop?: string; + + /** + * Margin Bottom + */ + marginBottom?: string; } /**