From 84c8bd55bc07c64f037725a2ac29cc389c15211d Mon Sep 17 00:00:00 2001
From: Jiuqing Song <jisong@microsoft.com>
Date: Fri, 14 Apr 2023 10:24:07 -0700
Subject: [PATCH 1/7] Content Model: Fix #1702 Hyperlinking a text doesnt
 trigger ChangeSource.CreateLink event (#1703)

* Content Model: Fix #1702

* fix build

* fix test

* fix build
---
 .../context/createModelToDomContext.ts        |  11 +-
 .../lib/modelToDom/handlers/handleBr.ts       |   1 +
 .../lib/modelToDom/handlers/handleDivider.ts  |   6 +-
 .../lib/modelToDom/handlers/handleEntity.ts   |   2 +
 .../handlers/handleFormatContainer.ts         |  21 ++--
 .../modelToDom/handlers/handleGeneralModel.ts |   2 +
 .../lib/modelToDom/handlers/handleImage.ts    |   2 +
 .../lib/modelToDom/handlers/handleList.ts     |   2 +
 .../lib/modelToDom/handlers/handleListItem.ts |   2 +
 .../modelToDom/handlers/handleParagraph.ts    |  14 ++-
 .../handlers/handleSegmentDecorator.ts        |   4 +
 .../lib/modelToDom/handlers/handleTable.ts    |   4 +
 .../lib/modelToDom/handlers/handleText.ts     |   2 +
 .../lib/publicApi/link/insertLink.ts          |  84 ++++++++-----
 .../publicApi/utils/formatWithContentModel.ts |  47 ++++++--
 .../lib/publicTypes/IContentModelEditor.ts    |   8 ++
 .../publicTypes/context/ModelToDomSettings.ts |  24 ++++
 .../lib/publicTypes/index.ts                  |   1 +
 .../context/createModelToDomContextTest.ts    |   4 +
 .../plugins/ContentModelFormatPluginTest.ts   | 114 +++++++++---------
 .../test/modelToDom/handlers/handleBrTest.ts  |  15 +++
 .../modelToDom/handlers/handleDividerTest.ts  |  18 +++
 .../modelToDom/handlers/handleEntityTest.ts   |  26 ++++
 .../handlers/handleFormatContainerTest.ts     |  24 ++++
 .../handlers/handleGeneralModelTest.ts        |  17 +++
 .../modelToDom/handlers/handleImageTest.ts    |  21 ++++
 .../modelToDom/handlers/handleListItemTest.ts |  38 ++++++
 .../modelToDom/handlers/handleListTest.ts     |  42 +++++++
 .../handlers/handleParagraphTest.ts           |  25 ++++
 .../handlers/handleSegmentDecoratorTest.ts    |  37 ++++++
 .../modelToDom/handlers/handleTableTest.ts    |  28 +++++
 .../modelToDom/handlers/handleTextTest.ts     |  20 +++
 .../test/publicApi/block/setAlignmentTest.ts  |  22 ++--
 .../publicApi/link/adjustLinkSelectionTest.ts |   4 +-
 .../test/publicApi/link/insertLinkTest.ts     |  40 +++++-
 .../test/publicApi/link/removeLinkTest.ts     |   4 +-
 .../publicApi/segment/changeFontSizeTest.ts   |  41 ++++---
 .../publicApi/table/setTableCellShadeTest.ts  |  11 +-
 .../utils/formatWithContentModelTest.ts       |  44 ++++++-
 39 files changed, 686 insertions(+), 146 deletions(-)

diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts
index 98eed239c11..031aefefdef 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/context/createModelToDomContext.ts
@@ -17,6 +17,8 @@ export function createModelToDomContext(
     editorContext?: EditorContext,
     options?: ModelToDomOption
 ): ModelToDomContext {
+    options = options || {};
+
     return {
         ...(editorContext || {
             isDarkMode: false,
@@ -34,19 +36,20 @@ export function createModelToDomContext(
         },
         implicitFormat: {},
         formatAppliers: getFormatAppliers(
-            options?.formatApplierOverride,
-            options?.additionalFormatAppliers
+            options.formatApplierOverride,
+            options.additionalFormatAppliers
         ),
         modelHandlers: {
             ...defaultContentModelHandlers,
-            ...(options?.modelHandlerOverride || {}),
+            ...(options.modelHandlerOverride || {}),
         },
         defaultImplicitFormatMap: {
             ...defaultImplicitFormatMap,
-            ...(options?.defaultImplicitFormatOverride || {}),
+            ...(options.defaultImplicitFormatOverride || {}),
         },
 
         defaultModelHandlers: defaultContentModelHandlers,
         defaultFormatAppliers: defaultFormatAppliers,
+        onNodeCreated: options.onNodeCreated,
     };
 }
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts
index 32cec01a25f..26efaac8606 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts
@@ -21,4 +21,5 @@ export const handleBr: ContentModelHandler<ContentModelBr> = (
     applyFormat(element, context.formatAppliers.segment, segment.format, context);
 
     context.modelHandlers.segmentDecorator(doc, br, segment, context);
+    context.onNodeCreated?.(segment, br);
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleDivider.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleDivider.ts
index f8aee598469..d7cee83c40b 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleDivider.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleDivider.ts
@@ -14,12 +14,12 @@ export const handleDivider: ContentModelBlockHandler<ContentModelDivider> = (
     context: ModelToDomContext,
     refNode: Node | null
 ) => {
-    const element = divider.cachedElement;
+    let element = divider.cachedElement;
 
     if (element) {
         refNode = reuseCachedElement(parent, element, refNode);
     } else {
-        const element = doc.createElement(divider.tagName);
+        element = doc.createElement(divider.tagName);
 
         divider.cachedElement = element;
         parent.insertBefore(element, refNode);
@@ -27,5 +27,7 @@ export const handleDivider: ContentModelBlockHandler<ContentModelDivider> = (
         applyFormat(element, context.formatAppliers.divider, divider.format, context);
     }
 
+    context.onNodeCreated?.(divider, element);
+
     return refNode;
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts
index 1c7ad22c547..185a3bc7201 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleEntity.ts
@@ -53,5 +53,7 @@ export const handleEntity: ContentModelBlockHandler<ContentModelEntity> = (
         context.regularSelection.current.segment = after;
     }
 
+    context.onNodeCreated?.(entityModel, wrapper);
+
     return refNode;
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleFormatContainer.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleFormatContainer.ts
index e390780ed7f..60d9c5f76c3 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleFormatContainer.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleFormatContainer.ts
@@ -23,22 +23,21 @@ export const handleFormatContainer: ContentModelBlockHandler<ContentModelFormatC
 
         context.modelHandlers.blockGroupChildren(doc, element, container, context);
     } else if (!isBlockGroupEmpty(container)) {
-        const blockQuote = doc.createElement(container.tagName);
+        element = doc.createElement(container.tagName);
 
-        container.cachedElement = blockQuote;
-        parent.insertBefore(blockQuote, refNode);
+        container.cachedElement = element;
+        parent.insertBefore(element, refNode);
 
         stackFormat(context, container.tagName, () => {
-            applyFormat(blockQuote, context.formatAppliers.block, container.format, context);
-            applyFormat(
-                blockQuote,
-                context.formatAppliers.segmentOnBlock,
-                container.format,
-                context
-            );
+            applyFormat(element!, context.formatAppliers.block, container.format, context);
+            applyFormat(element!, context.formatAppliers.segmentOnBlock, container.format, context);
         });
 
-        context.modelHandlers.blockGroupChildren(doc, blockQuote, container, context);
+        context.modelHandlers.blockGroupChildren(doc, element, container, context);
+    }
+
+    if (element) {
+        context.onNodeCreated?.(container, element);
     }
 
     return refNode;
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts
index d30906493ef..db02c29c016 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts
@@ -40,5 +40,7 @@ export const handleGeneralModel: ContentModelBlockHandler<ContentModelGeneralBlo
 
     context.modelHandlers.blockGroupChildren(doc, element, group, context);
 
+    context.onNodeCreated?.(group, element);
+
     return refNode;
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts
index 58164df4111..d423c0ab25e 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleImage.ts
@@ -55,4 +55,6 @@ export const handleImage: ContentModelHandler<ContentModelImage> = (
             image: img,
         };
     }
+
+    context.onNodeCreated?.(imageModel, img);
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts
index 9a7983afdee..17bf25920d6 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleList.ts
@@ -56,6 +56,8 @@ export const handleList: ContentModelBlockHandler<ContentModelListItem> = (
         handleMetadata(level, newList, context);
 
         nodeStack.push({ node: newList, ...level });
+
+        context.onNodeCreated?.(level, newList);
     }
 
     return refNode;
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleListItem.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleListItem.ts
index 92feb3f5c1c..8b4c32dad12 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleListItem.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleListItem.ts
@@ -43,5 +43,7 @@ export const handleListItem: ContentModelBlockHandler<ContentModelListItem> = (
         unwrap(li);
     }
 
+    context.onNodeCreated?.(listItem, li);
+
     return refNode;
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts
index 67f4e142acc..e0aa5db3576 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleParagraph.ts
@@ -18,10 +18,10 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
     context: ModelToDomContext,
     refNode: Node | null
 ) => {
-    const element = paragraph.cachedElement;
+    let container = paragraph.cachedElement;
 
-    if (element) {
-        refNode = reuseCachedElement(parent, element, refNode);
+    if (container) {
+        refNode = reuseCachedElement(parent, container, refNode);
     } else {
         stackFormat(context, paragraph.decorator?.tagName || null, () => {
             const needParagraphWrapper =
@@ -30,7 +30,7 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
                 (getObjectKeys(paragraph.format).length > 0 &&
                     paragraph.segments.some(segment => segment.segmentType != 'SelectionMarker'));
 
-            let container = doc.createElement(paragraph.decorator?.tagName || DefaultParagraphTag);
+            container = doc.createElement(paragraph.decorator?.tagName || DefaultParagraphTag);
 
             parent.insertBefore(container, refNode);
 
@@ -53,7 +53,7 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
             };
 
             paragraph.segments.forEach(segment => {
-                context.modelHandlers.segment(doc, container, segment, context);
+                context.modelHandlers.segment(doc, container!, segment, context);
             });
 
             if (needParagraphWrapper) {
@@ -64,5 +64,9 @@ export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> =
         });
     }
 
+    if (container) {
+        context.onNodeCreated?.(paragraph, container);
+    }
+
     return refNode;
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegmentDecorator.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegmentDecorator.ts
index e3da58469ab..434a5c6b32d 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegmentDecorator.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegmentDecorator.ts
@@ -21,6 +21,8 @@ export const handleSegmentDecorator: ContentModelHandler<ContentModelSegment> =
 
             applyFormat(a, context.formatAppliers.link, link.format, context);
             applyFormat(a, context.formatAppliers.dataset, link.dataset, context);
+
+            context.onNodeCreated?.(link, a);
         });
     }
 
@@ -29,6 +31,8 @@ export const handleSegmentDecorator: ContentModelHandler<ContentModelSegment> =
             const codeNode = wrap(parent, 'code');
 
             applyFormat(codeNode, context.formatAppliers.code, code.format, context);
+
+            context.onNodeCreated?.(code, codeNode);
         });
     }
 };
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts
index 566b5916672..1c68a9cee94 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleTable.ts
@@ -39,6 +39,8 @@ export const handleTable: ContentModelBlockHandler<ContentModelTable> = (
 
     applyFormat(tableNode, context.formatAppliers.tableBorder, table.format, context);
 
+    context.onNodeCreated?.(table, tableNode);
+
     const tbody = doc.createElement('tbody');
     tableNode.appendChild(tbody);
 
@@ -106,6 +108,8 @@ export const handleTable: ContentModelBlockHandler<ContentModelTable> = (
                 }
 
                 context.modelHandlers.blockGroupChildren(doc, td, cell, context);
+
+                context.onNodeCreated?.(cell, td);
             }
         }
     }
diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts
index 6c037c22ec7..34ff608927b 100644
--- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts
+++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts
@@ -23,4 +23,6 @@ export const handleText: ContentModelHandler<ContentModelText> = (
     applyFormat(element, context.formatAppliers.segment, segment.format, context);
 
     context.modelHandlers.segmentDecorator(doc, txt, segment, context);
+
+    context.onNodeCreated?.(segment, txt);
 };
diff --git a/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts b/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts
index fe2f6aa8487..9cd89e10998 100644
--- a/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts
+++ b/packages/roosterjs-content-model/lib/publicApi/link/insertLink.ts
@@ -1,5 +1,6 @@
 import { addLink } from '../../modelApi/common/addDecorators';
 import { addSegment } from '../../modelApi/common/addSegment';
+import { ChangeSource } from 'roosterjs-editor-types';
 import { ContentModelLink } from '../../publicTypes/decorator/ContentModelLink';
 import { createContentModelDocument } from '../../modelApi/creators/createContentModelDocument';
 import { createText } from '../../modelApi/creators/createText';
@@ -50,35 +51,62 @@ export default function insertLink(
             },
         };
 
-        formatWithContentModel(editor, 'insertLink', model => {
-            const segments = getSelectedSegments(model, false /*includingFormatHolder*/);
-            const originalText = segments
-                .map(x => (x.segmentType == 'Text' ? x.text : ''))
-                .join('');
-            const text = displayText || originalText || '';
-
-            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 || (linkData ? linkData.originalUrl : url), {
-                    ...(segments[0]?.format || {}),
-                    ...(getPendingFormat(editor) || {}),
-                });
-                const doc = createContentModelDocument();
-
-                addLink(segment, link);
-                addSegment(doc, segment);
-
-                mergeModel(model, doc);
-            }
+        const links: ContentModelLink[] = [];
+        let anchorNode: Node | undefined;
+
+        formatWithContentModel(
+            editor,
+            'insertLink',
+            model => {
+                const segments = getSelectedSegments(model, false /*includingFormatHolder*/);
+                const originalText = segments
+                    .map(x => (x.segmentType == 'Text' ? x.text : ''))
+                    .join('');
+                const text = displayText || originalText || '';
+
+                if (
+                    segments.some(x => x.segmentType != 'SelectionMarker') &&
+                    originalText == text
+                ) {
+                    segments.forEach(x => {
+                        addLink(x, link);
+
+                        if (x.link) {
+                            links.push(x.link);
+                        }
+                    });
+                } else if (
+                    segments.every(x => x.segmentType == 'SelectionMarker') ||
+                    (!!text && text != originalText)
+                ) {
+                    const segment = createText(text || (linkData ? linkData.originalUrl : url), {
+                        ...(segments[0]?.format || {}),
+                        ...(getPendingFormat(editor) || {}),
+                    });
+                    const doc = createContentModelDocument();
+
+                    addLink(segment, link);
+                    addSegment(doc, segment);
 
-            return segments.length > 0;
-        });
+                    if (segment.link) {
+                        links.push(segment.link);
+                    }
+
+                    mergeModel(model, doc);
+                }
+
+                return segments.length > 0;
+            },
+            {
+                changeSource: ChangeSource.CreateLink,
+                onNodeCreated: (modelElement, node) => {
+                    if (!anchorNode && links.indexOf(modelElement as ContentModelLink) >= 0) {
+                        anchorNode = node;
+                    }
+                },
+                getChangeData: () => anchorNode,
+            }
+        );
     }
 }
 
diff --git a/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts b/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts
index 445ea89cb9a..fed4fa67da5 100644
--- a/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts
+++ b/packages/roosterjs-content-model/lib/publicApi/utils/formatWithContentModel.ts
@@ -2,6 +2,7 @@ import { ChangeSource } from 'roosterjs-editor-types';
 import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument';
 import { DomToModelOption, IContentModelEditor } from '../../publicTypes/IContentModelEditor';
 import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat';
+import { OnNodeCreated } from '../../publicTypes/context/ModelToDomSettings';
 import { reducedModelChildProcessor } from '../../domToModel/processors/reducedModelChildProcessor';
 
 /**
@@ -22,6 +23,23 @@ export interface FormatWithContentModelOptions {
      * When pass true, skip adding undo snapshot when write Content Model back to DOM
      */
     skipUndoSnapshot?: boolean;
+
+    /**
+     * Change source used for triggering a ContentChanged event. @default ChangeSource.Format.
+     */
+    changeSource?: string;
+
+    /**
+     * An optional callback that will be called when a DOM node is created
+     * @param modelElement The related Content Model element
+     * @param node The node created for this model element
+     */
+    onNodeCreated?: OnNodeCreated;
+
+    /**
+     * Optional callback to get an object used for change data in ContentChangedEvent
+     */
+    getChangeData?: () => any;
 }
 
 /**
@@ -33,7 +51,15 @@ export function formatWithContentModel(
     callback: (model: ContentModelDocument) => boolean,
     options?: FormatWithContentModelOptions
 ) {
-    const domToModelOption: DomToModelOption | undefined = options?.useReducedModel
+    const {
+        useReducedModel,
+        onNodeCreated,
+        preservePendingFormat,
+        getChangeData,
+        skipUndoSnapshot,
+        changeSource,
+    } = options || {};
+    const domToModelOption: DomToModelOption | undefined = useReducedModel
         ? {
               processorOverride: {
                   child: reducedModelChildProcessor,
@@ -46,10 +72,10 @@ export function formatWithContentModel(
         const callback = () => {
             editor.focus();
             if (model) {
-                editor.setContentModel(model);
+                editor.setContentModel(model, { onNodeCreated });
             }
 
-            if (options?.preservePendingFormat) {
+            if (preservePendingFormat) {
                 const pendingFormat = getPendingFormat(editor);
                 const pos = editor.getFocusedPosition();
 
@@ -57,14 +83,21 @@ export function formatWithContentModel(
                     setPendingFormat(editor, pendingFormat, pos);
                 }
             }
+
+            return getChangeData?.();
         };
 
-        if (options?.skipUndoSnapshot) {
+        if (skipUndoSnapshot) {
             callback();
         } else {
-            editor.addUndoSnapshot(callback, ChangeSource.Format, false /*canUndoByBackspace*/, {
-                formatApiName: apiName,
-            });
+            editor.addUndoSnapshot(
+                callback,
+                changeSource || ChangeSource.Format,
+                false /*canUndoByBackspace*/,
+                {
+                    formatApiName: apiName,
+                }
+            );
         }
 
         editor.cacheContentModel?.(model);
diff --git a/packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts b/packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts
index b2cc36089a3..0f7d37677a4 100644
--- a/packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts
+++ b/packages/roosterjs-content-model/lib/publicTypes/IContentModelEditor.ts
@@ -5,6 +5,7 @@ import {
     DefaultImplicitFormatMap,
     FormatAppliers,
     FormatAppliersPerCategory,
+    OnNodeCreated,
 } from './context/ModelToDomSettings';
 import {
     DefaultStyleMap,
@@ -78,6 +79,13 @@ export interface ModelToDomOption {
      * Overrides default element styles
      */
     defaultImplicitFormatOverride?: DefaultImplicitFormatMap;
+
+    /**
+     * An optional callback that will be called when a DOM node is created
+     * @param modelElement The related Content Model element
+     * @param node The node created for this model element
+     */
+    onNodeCreated?: OnNodeCreated;
 }
 
 /**
diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts
index 0d96b4f23fb..a7519867eb6 100644
--- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts
+++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts
@@ -3,6 +3,7 @@ import { ContentModelBlockFormat } from '../format/ContentModelBlockFormat';
 import { ContentModelBlockGroup } from '../group/ContentModelBlockGroup';
 import { ContentModelBlockHandler, ContentModelHandler } from './ContentModelHandler';
 import { ContentModelBr } from '../segment/ContentModelBr';
+import { ContentModelDecorator } from '../decorator/ContentModelDecorator';
 import { ContentModelDivider } from '../block/ContentModelDivider';
 import { ContentModelEntity } from '../entity/ContentModelEntity';
 import { ContentModelFormatBase } from '../format/ContentModelFormatBase';
@@ -11,6 +12,7 @@ import { ContentModelFormatMap } from '../format/ContentModelFormatMap';
 import { ContentModelGeneralBlock } from '../group/ContentModelGeneralBlock';
 import { ContentModelImage } from '../segment/ContentModelImage';
 import { ContentModelListItem } from '../group/ContentModelListItem';
+import { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat';
 import { ContentModelParagraph } from '../block/ContentModelParagraph';
 import { ContentModelSegment } from '../segment/ContentModelSegment';
 import { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat';
@@ -133,6 +135,21 @@ export type ContentModelHandlerMap = {
     text: ContentModelHandler<ContentModelText>;
 };
 
+/**
+ * An optional callback that will be called when a DOM node is created
+ * @param modelElement The related Content Model element
+ * @param node The node created for this model element
+ */
+export type OnNodeCreated = (
+    modelElement:
+        | ContentModelBlock
+        | ContentModelBlockGroup
+        | ContentModelSegment
+        | ContentModelDecorator
+        | ContentModelListItemLevelFormat,
+    node: Node
+) => void;
+
 /**
  * Represents settings to customize DOM to Content Model conversion
  */
@@ -163,4 +180,11 @@ export interface ModelToDomSettings {
      * This provides a way to call original format applier from an overridden applier function
      */
     defaultFormatAppliers: Readonly<FormatAppliers>;
+
+    /**
+     * An optional callback that will be called when a DOM node is created
+     * @param modelElement The related Content Model element
+     * @param node The node created for this model element
+     */
+    onNodeCreated?: OnNodeCreated;
 }
diff --git a/packages/roosterjs-content-model/lib/publicTypes/index.ts b/packages/roosterjs-content-model/lib/publicTypes/index.ts
index 99f16e2482a..de20ba96ab1 100644
--- a/packages/roosterjs-content-model/lib/publicTypes/index.ts
+++ b/packages/roosterjs-content-model/lib/publicTypes/index.ts
@@ -123,6 +123,7 @@ export {
     FormatAppliersPerCategory,
     ContentModelHandlerMap,
     DefaultImplicitFormatMap,
+    OnNodeCreated,
 } from './context/ModelToDomSettings';
 export { ElementProcessor } from './context/ElementProcessor';
 export { ContentModelHandler, ContentModelBlockHandler } from './context/ContentModelHandler';
diff --git a/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts b/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts
index ab0a9420a51..e72bc8315c6 100644
--- a/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts
+++ b/packages/roosterjs-content-model/test/domToModel/context/createModelToDomContextTest.ts
@@ -31,6 +31,7 @@ describe('createModelToDomContext', () => {
         defaultImplicitFormatMap: defaultImplicitFormatMap,
         defaultModelHandlers: defaultContentModelHandlers,
         defaultFormatAppliers: defaultFormatAppliers,
+        onNodeCreated: undefined,
     };
     it('no param', () => {
         const context = createModelToDomContext();
@@ -57,6 +58,7 @@ describe('createModelToDomContext', () => {
         const mockedBlockApplier = 'block' as any;
         const mockedBrHandler = 'br' as any;
         const mockedAStyle = 'a' as any;
+        const onNodeCreated = 'OnNodeCreated' as any;
         const context = createModelToDomContext(undefined, {
             formatApplierOverride: {
                 bold: mockedBoldApplier,
@@ -70,6 +72,7 @@ describe('createModelToDomContext', () => {
             defaultImplicitFormatOverride: {
                 a: mockedAStyle,
             },
+            onNodeCreated,
         });
 
         expect(context.regularSelection).toEqual({
@@ -91,5 +94,6 @@ describe('createModelToDomContext', () => {
         expect(context.defaultImplicitFormatMap.a).toEqual(mockedAStyle);
         expect(context.defaultModelHandlers).toEqual(defaultContentModelHandlers);
         expect(context.defaultFormatAppliers).toEqual(defaultFormatAppliers);
+        expect(context.onNodeCreated).toBe(onNodeCreated);
     });
 });
diff --git a/packages/roosterjs-content-model/test/editor/plugins/ContentModelFormatPluginTest.ts b/packages/roosterjs-content-model/test/editor/plugins/ContentModelFormatPluginTest.ts
index 08a2f85026f..c2207feeb62 100644
--- a/packages/roosterjs-content-model/test/editor/plugins/ContentModelFormatPluginTest.ts
+++ b/packages/roosterjs-content-model/test/editor/plugins/ContentModelFormatPluginTest.ts
@@ -156,33 +156,36 @@ describe('ContentModelFormatPlugin', () => {
         plugin.dispose();
 
         expect(setContentModel).toHaveBeenCalledTimes(1);
-        expect(setContentModel).toHaveBeenCalledWith({
-            blockGroupType: 'Document',
-            blocks: [
-                {
-                    blockType: 'Paragraph',
-                    format: {},
-                    isImplicit: true,
-                    segments: [
-                        {
-                            segmentType: 'Text',
-                            format: {},
-                            text: '',
-                        },
-                        {
-                            segmentType: 'Text',
-                            format: { fontSize: '10px' },
-                            text: 'a',
-                        },
-                        {
-                            segmentType: 'SelectionMarker',
-                            format: { fontSize: '10px' },
-                            isSelected: true,
-                        },
-                    ],
-                },
-            ],
-        });
+        expect(setContentModel).toHaveBeenCalledWith(
+            {
+                blockGroupType: 'Document',
+                blocks: [
+                    {
+                        blockType: 'Paragraph',
+                        format: {},
+                        isImplicit: true,
+                        segments: [
+                            {
+                                segmentType: 'Text',
+                                format: {},
+                                text: '',
+                            },
+                            {
+                                segmentType: 'Text',
+                                format: { fontSize: '10px' },
+                                text: 'a',
+                            },
+                            {
+                                segmentType: 'SelectionMarker',
+                                format: { fontSize: '10px' },
+                                isSelected: true,
+                            },
+                        ],
+                    },
+                ],
+            },
+            { onNodeCreated: undefined }
+        );
         expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1);
         expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor);
     });
@@ -220,33 +223,36 @@ describe('ContentModelFormatPlugin', () => {
         plugin.dispose();
 
         expect(setContentModel).toHaveBeenCalledTimes(1);
-        expect(setContentModel).toHaveBeenCalledWith({
-            blockGroupType: 'Document',
-            blocks: [
-                {
-                    blockType: 'Paragraph',
-                    format: {},
-                    isImplicit: true,
-                    segments: [
-                        {
-                            segmentType: 'Text',
-                            format: { fontFamily: 'Arial' },
-                            text: 'test a ',
-                        },
-                        {
-                            segmentType: 'Text',
-                            format: { fontSize: '10px', fontFamily: 'Arial' },
-                            text: 'test',
-                        },
-                        {
-                            segmentType: 'SelectionMarker',
-                            format: { fontSize: '10px' },
-                            isSelected: true,
-                        },
-                    ],
-                },
-            ],
-        });
+        expect(setContentModel).toHaveBeenCalledWith(
+            {
+                blockGroupType: 'Document',
+                blocks: [
+                    {
+                        blockType: 'Paragraph',
+                        format: {},
+                        isImplicit: true,
+                        segments: [
+                            {
+                                segmentType: 'Text',
+                                format: { fontFamily: 'Arial' },
+                                text: 'test a ',
+                            },
+                            {
+                                segmentType: 'Text',
+                                format: { fontSize: '10px', fontFamily: 'Arial' },
+                                text: 'test',
+                            },
+                            {
+                                segmentType: 'SelectionMarker',
+                                format: { fontSize: '10px' },
+                                isSelected: true,
+                            },
+                        ],
+                    },
+                ],
+            },
+            { onNodeCreated: undefined }
+        );
         expect(pendingFormat.clearPendingFormat).toHaveBeenCalledTimes(1);
         expect(pendingFormat.clearPendingFormat).toHaveBeenCalledWith(editor);
     });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts
index 722cdc4bdf5..56703f154ac 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts
@@ -33,4 +33,19 @@ describe('handleSegment', () => {
 
         expect(parent.innerHTML).toBe('<span style="color: red;"><br></span>');
     });
+
+    it('With onNodeCreated', () => {
+        const br: ContentModelBr = {
+            segmentType: 'Br',
+            format: { textColor: 'red' },
+        };
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+        handleBr(document, parent, br, context);
+
+        expect(parent.innerHTML).toBe('<span style="color: red;"><br></span>');
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(br);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('br'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleDividerTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleDividerTest.ts
index 29781c31df0..091ef35ebf9 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleDividerTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleDividerTest.ts
@@ -134,4 +134,22 @@ describe('handleDivider', () => {
         expect(parent.firstChild).toBe(hrNode);
         expect(result).toBe(br);
     });
+
+    it('With onNodeCreated', () => {
+        const hr: ContentModelDivider = {
+            blockType: 'Divider',
+            tagName: 'hr',
+            format: {},
+        };
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+        const parent = document.createElement('div');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleDivider(document, parent, hr, context, null);
+
+        expect(parent.innerHTML).toBe('<hr>');
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(hr);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('hr'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts
index fe872d564f4..f96c10c87a1 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts
@@ -172,4 +172,30 @@ describe('handleEntity', () => {
         expect(result).toBe(br);
         expect(context.regularSelection.current.segment).toBe(span.nextSibling);
     });
+
+    it('With onNodeCreated', () => {
+        const entityDiv = document.createElement('div');
+        const entityModel: ContentModelEntity = {
+            blockType: 'Entity',
+            segmentType: 'Entity',
+            format: {},
+            id: 'entity_1',
+            type: 'entity',
+            isReadonly: true,
+            wrapper: entityDiv,
+        };
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+        const parent = document.createElement('div');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleEntity(document, parent, entityModel, context, null);
+
+        expect(parent.innerHTML).toBe(
+            '<div class="_Entity _EType_entity _EId_entity_1 _EReadonly_1" contenteditable="false"></div>'
+        );
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(entityModel);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('div'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleFormatContainerTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleFormatContainerTest.ts
index 5051ca74120..b6b1a20c342 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleFormatContainerTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleFormatContainerTest.ts
@@ -84,4 +84,28 @@ describe('handleFormatContainer', () => {
         expect(quote.cachedElement).toBe(parent.firstChild as HTMLQuoteElement);
         expect(result).toBe(br);
     });
+
+    it('With onNodeCreated', () => {
+        const parent = document.createElement('div');
+        const quote = createQuote();
+        const paragraph = createParagraph();
+        const text = createText('test');
+        quote.blocks.push(paragraph);
+        paragraph.segments.push(text);
+
+        handleBlockGroupChildren.and.callFake(originalHandleBlockGroupChildren);
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleFormatContainer(document, parent, quote, context, null);
+
+        expect(parent.innerHTML).toBe(
+            '<blockquote style="margin: 0px;"><div><span>test</span></div></blockquote>'
+        );
+        expect(onNodeCreated).toHaveBeenCalledTimes(3);
+        expect(onNodeCreated.calls.argsFor(2)[0]).toBe(quote);
+        expect(onNodeCreated.calls.argsFor(2)[1]).toBe(parent.querySelector('blockquote'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts
index 651cba7a607..e20c583b1fc 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts
@@ -219,4 +219,21 @@ describe('handleBlockGroup', () => {
         expect(result).toBe(br);
         expect(group.element).toBe(node);
     });
+
+    it('With onNodeCreated', () => {
+        const parent = document.createElement('div');
+        const node = document.createElement('span');
+        const group = createGeneralBlock(node);
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleGeneralModel(document, parent, group, context, null);
+
+        expect(parent.innerHTML).toBe('<span></span>');
+        expect(onNodeCreated).toHaveBeenCalledTimes(1);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(group);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('span'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts
index e439dca2757..ecc84cab20e 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleImageTest.ts
@@ -136,4 +136,25 @@ describe('handleSegment', () => {
         expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1);
         expect((<jasmine.Spy>stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a');
     });
+
+    it('With onNodeCreated', () => {
+        const segment: ContentModelImage = {
+            segmentType: 'Image',
+            src: 'http://test.com/test',
+            format: {},
+            dataset: {},
+        };
+        const parent = document.createElement('div');
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleImage(document, parent, segment, context);
+
+        expect(parent.innerHTML).toBe('<span><img src="http://test.com/test"></span>');
+        expect(onNodeCreated).toHaveBeenCalledTimes(1);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(segment);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('img'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleListItemTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleListItemTest.ts
index 35e255f4268..2eeafc548a8 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleListItemTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleListItemTest.ts
@@ -1,6 +1,7 @@
 import * as applyFormat from '../../../lib/modelToDom/utils/applyFormat';
 import { ContentModelBlockGroup } from '../../../lib/publicTypes/group/ContentModelBlockGroup';
 import { ContentModelListItem } from '../../../lib/publicTypes/group/ContentModelListItem';
+import { ContentModelListItemLevelFormat } from '../../../lib/publicTypes/format/ContentModelListItemLevelFormat';
 import { createListItem } from '../../../lib/modelApi/creators/createListItem';
 import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext';
 import { createParagraph } from '../../../lib/modelApi/creators/createParagraph';
@@ -420,4 +421,41 @@ describe('handleListItem without format handler', () => {
         );
         expect(result).toBe(br);
     });
+
+    it('With onNodeCreated', () => {
+        const listLevel0: ContentModelListItemLevelFormat = {
+            listType: 'OL',
+        };
+        const listItem: ContentModelListItem = {
+            blockType: 'BlockGroup',
+            blockGroupType: 'ListItem',
+            blocks: [],
+            format: {},
+            formatHolder: {
+                segmentType: 'SelectionMarker',
+                format: {},
+                isSelected: true,
+            },
+            levels: [listLevel0],
+        };
+        const parent = document.createElement('div');
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleListItem(document, parent, listItem, context, null);
+
+        expect(
+            [
+                '<ol start="1" style="flex-direction: column; display: flex;"><li></li></ol>',
+                '<ol style="flex-direction: column; display: flex;" start="1"><li></li></ol>',
+            ].indexOf(parent.innerHTML) >= 0
+        ).toBeTrue();
+        expect(onNodeCreated).toHaveBeenCalledTimes(2);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(listLevel0);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('ol'));
+        expect(onNodeCreated.calls.argsFor(1)[0]).toBe(listItem);
+        expect(onNodeCreated.calls.argsFor(1)[1]).toBe(parent.querySelector('li'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleListTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleListTest.ts
index 712a9e002b1..c8f2d9ce5ee 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleListTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleListTest.ts
@@ -1,4 +1,6 @@
 import { BulletListType, NumberingListType } from 'roosterjs-editor-types';
+import { ContentModelListItem } from '../../../lib/publicTypes/group/ContentModelListItem';
+import { ContentModelListItemLevelFormat } from '../../../lib/publicTypes/format/ContentModelListItemLevelFormat';
 import { createListItem } from '../../../lib/modelApi/creators/createListItem';
 import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext';
 import { handleList } from '../../../lib/modelToDom/handlers/handleList';
@@ -855,4 +857,44 @@ describe('handleList handles metadata', () => {
         });
         expect(result).toBe(br);
     });
+
+    it('With onNodeCreated', () => {
+        const listLevel0: ContentModelListItemLevelFormat = {
+            listType: 'OL',
+        };
+        const listLevel1: ContentModelListItemLevelFormat = {
+            listType: 'UL',
+        };
+        const listItem: ContentModelListItem = {
+            blockType: 'BlockGroup',
+            blockGroupType: 'ListItem',
+            blocks: [],
+            format: {},
+            formatHolder: {
+                segmentType: 'SelectionMarker',
+                format: {},
+                isSelected: true,
+            },
+            levels: [listLevel0, listLevel1],
+        };
+        const parent = document.createElement('div');
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleList(document, parent, listItem, context, null);
+
+        expect(
+            [
+                '<ol start="1" style="flex-direction: column; display: flex;"><ul style="flex-direction: column; display: flex;"></ul></ol>',
+                '<ol style="flex-direction: column; display: flex;" start="1"><ul style="flex-direction: column; display: flex;"></ul></ol>',
+            ].indexOf(parent.innerHTML) >= 0
+        ).toBeTrue();
+        expect(onNodeCreated).toHaveBeenCalledTimes(2);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(listLevel0);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('ol'));
+        expect(onNodeCreated.calls.argsFor(1)[0]).toBe(listLevel1);
+        expect(onNodeCreated.calls.argsFor(1)[1]).toBe(parent.querySelector('ul'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts
index 5d3c5b23bf6..ffcd777b4a0 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleParagraphTest.ts
@@ -433,4 +433,29 @@ describe('handleParagraph', () => {
         expect(para2.cachedElement).toBe(parent.firstChild?.nextSibling as HTMLElement);
         expect(para2.cachedElement?.outerHTML).toBe('<div style="white-space: pre;">test2</div>');
     });
+
+    it('With onNodeCreated', () => {
+        const parent = document.createElement('div');
+        const segment: ContentModelSegment = {
+            segmentType: 'Text',
+            text: 'test',
+            format: {},
+        };
+        const paragraph: ContentModelParagraph = {
+            blockType: 'Paragraph',
+            segments: [segment],
+            format: {},
+        };
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleParagraph(document, parent, paragraph, context, null);
+
+        expect(parent.innerHTML).toBe('<div></div>');
+        expect(onNodeCreated).toHaveBeenCalledTimes(1);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(paragraph);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('div'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentDecoratorTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentDecoratorTest.ts
index bc8e0b4f6b5..850fce9c0b1 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentDecoratorTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentDecoratorTest.ts
@@ -1,6 +1,7 @@
 import { ContentModelCode } from '../../../lib/publicTypes/decorator/ContentModelCode';
 import { ContentModelLink } from '../../../lib/publicTypes/decorator/ContentModelLink';
 import { ContentModelSegment } from '../../../lib/publicTypes/segment/ContentModelSegment';
+import { ContentModelText } from '../../../lib/publicTypes/segment/ContentModelText';
 import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext';
 import { handleSegmentDecorator } from '../../../lib/modelToDom/handlers/handleSegmentDecorator';
 import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext';
@@ -124,4 +125,40 @@ describe('handleSegmentDecorator', () => {
 
         runTest(link, code, '<a href="http://test.com/test"><code>test</code></a>');
     });
+
+    it('Link with onNodeCreated', () => {
+        const parent = document.createElement('div');
+        const span = document.createElement('span');
+        const segment: ContentModelText = {
+            segmentType: 'Text',
+            format: {},
+            text: 'test',
+            link: {
+                format: {
+                    href: 'https://www.test.com',
+                },
+                dataset: {},
+            },
+            code: {
+                format: {},
+            },
+        };
+
+        parent.appendChild(span);
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleSegmentDecorator(document, span, segment, context);
+
+        expect(parent.innerHTML).toBe(
+            '<a href="https://www.test.com" style="text-decoration: none;"><code><span></span></code></a>'
+        );
+        expect(onNodeCreated).toHaveBeenCalledTimes(2);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(segment.link);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('a'));
+        expect(onNodeCreated.calls.argsFor(1)[0]).toBe(segment.code);
+        expect(onNodeCreated.calls.argsFor(1)[1]).toBe(parent.querySelector('code'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts
index 098932433b8..4bffa68bc73 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTableTest.ts
@@ -1,6 +1,7 @@
 import * as handleBlock from '../../../lib/modelToDom/handlers/handleBlock';
 import { ContentModelTable } from '../../../lib/publicTypes/block/ContentModelTable';
 import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext';
+import { createTable } from '../../../lib/modelApi/creators/createTable';
 import { createTableCell } from '../../../lib/modelApi/creators/createTableCell';
 import { handleTable } from '../../../lib/modelToDom/handlers/handleTable';
 import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext';
@@ -298,4 +299,31 @@ describe('handleTable', () => {
         );
         expect(result).toBe(br);
     });
+
+    it('With onNodeCreated', () => {
+        const parent = document.createElement('div');
+        const tableCell1 = createTableCell(false, false, true);
+        const tableCell2 = createTableCell();
+        const table = createTable(2);
+
+        table.cells[0].push(tableCell1);
+        table.cells[1].push(tableCell2);
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleTable(document, parent, table, context, null);
+
+        expect(parent.innerHTML).toBe(
+            '<table><tbody><tr><th></th></tr><tr><td></td></tr></tbody></table>'
+        );
+        expect(onNodeCreated).toHaveBeenCalledTimes(3);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(table);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('table'));
+        expect(onNodeCreated.calls.argsFor(1)[0]).toBe(tableCell1);
+        expect(onNodeCreated.calls.argsFor(1)[1]).toBe(parent.querySelector('th'));
+        expect(onNodeCreated.calls.argsFor(2)[0]).toBe(tableCell2);
+        expect(onNodeCreated.calls.argsFor(2)[1]).toBe(parent.querySelector('td'));
+    });
 });
diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts
index 3578e4da875..0cabb93fe6a 100644
--- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts
+++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts
@@ -83,4 +83,24 @@ describe('handleSegment', () => {
         expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1);
         expect((<jasmine.Spy>stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a');
     });
+
+    it('With onNodeCreated', () => {
+        const parent = document.createElement('div');
+        const text: ContentModelText = {
+            segmentType: 'Text',
+            text: 'test',
+            format: {},
+        };
+
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        context.onNodeCreated = onNodeCreated;
+
+        handleText(document, parent, text, context);
+
+        expect(parent.innerHTML).toBe('<span>test</span>');
+        expect(onNodeCreated).toHaveBeenCalledTimes(1);
+        expect(onNodeCreated.calls.argsFor(0)[0]).toBe(text);
+        expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('span')!.firstChild);
+    });
 });
diff --git a/packages/roosterjs-content-model/test/publicApi/block/setAlignmentTest.ts b/packages/roosterjs-content-model/test/publicApi/block/setAlignmentTest.ts
index 4a05e148c7c..5b5261ecf34 100644
--- a/packages/roosterjs-content-model/test/publicApi/block/setAlignmentTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/block/setAlignmentTest.ts
@@ -441,10 +441,13 @@ describe('setAlignment in table', () => {
 
         if (expectedTable) {
             expect(setContentModel).toHaveBeenCalledTimes(1);
-            expect(setContentModel).toHaveBeenCalledWith({
-                blockGroupType: 'Document',
-                blocks: [expectedTable],
-            });
+            expect(setContentModel).toHaveBeenCalledWith(
+                {
+                    blockGroupType: 'Document',
+                    blocks: [expectedTable],
+                },
+                { onNodeCreated: undefined }
+            );
         }
     }
 
@@ -805,10 +808,13 @@ describe('setAlignment in list', () => {
 
         if (expectedList) {
             expect(setContentModel).toHaveBeenCalledTimes(1);
-            expect(setContentModel).toHaveBeenCalledWith({
-                blockGroupType: 'Document',
-                blocks: [expectedList],
-            });
+            expect(setContentModel).toHaveBeenCalledWith(
+                {
+                    blockGroupType: 'Document',
+                    blocks: [expectedList],
+                },
+                { onNodeCreated: undefined }
+            );
         }
     }
 
diff --git a/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts b/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts
index d23468cfa5d..e0f628f2245 100644
--- a/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/link/adjustLinkSelectionTest.ts
@@ -39,7 +39,9 @@ describe('adjustLinkSelection', () => {
 
         if (expectedModel) {
             expect(setContentModel).toHaveBeenCalledTimes(1);
-            expect(setContentModel).toHaveBeenCalledWith(expectedModel);
+            expect(setContentModel).toHaveBeenCalledWith(expectedModel, {
+                onNodeCreated: undefined,
+            });
         } else {
             expect(setContentModel).not.toHaveBeenCalled();
         }
diff --git a/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts b/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts
index f20856dbce2..13d3b4d54bb 100644
--- a/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/link/insertLinkTest.ts
@@ -1,5 +1,7 @@
+import ContentModelEditor from '../../../lib/editor/ContentModelEditor';
 import insertLink from '../../../lib/publicApi/link/insertLink';
 import { addSegment } from '../../../lib/modelApi/common/addSegment';
+import { ChangeSource, PluginEventType } from 'roosterjs-editor-types';
 import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument';
 import { ContentModelLink } from '../../../lib/publicTypes/decorator/ContentModelLink';
 import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument';
@@ -41,7 +43,8 @@ describe('insertLink', () => {
 
         if (expectedModel) {
             expect(setContentModel).toHaveBeenCalledTimes(1);
-            expect(setContentModel).toHaveBeenCalledWith(expectedModel);
+            expect(setContentModel.calls.argsFor(0)[0]).toEqual(expectedModel);
+            expect(typeof setContentModel.calls.argsFor(0)[1]!.onNodeCreated).toEqual('function');
         } else {
             expect(setContentModel).not.toHaveBeenCalled();
         }
@@ -295,4 +298,39 @@ describe('insertLink', () => {
             'new text'
         );
     });
+
+    it('Valid url on existing text, trigger event with data', () => {
+        const div = document.createElement('div');
+        document.body.appendChild(div);
+
+        const onPluginEvent = jasmine.createSpy('onPluginEvent');
+        const mockedPlugin = {
+            initialize: () => {},
+            dispose: () => {},
+            getName: () => 'mock',
+            onPluginEvent: onPluginEvent,
+        };
+        const editor = new ContentModelEditor(div, { plugins: [mockedPlugin] });
+
+        editor.focus();
+
+        insertLink(editor, 'http://test.com', 'title');
+
+        editor.dispose();
+
+        const a = div.querySelector('a');
+
+        expect(a!.outerHTML).toBe('<a href="http://test.com" title="title">http://test.com</a>');
+        expect(onPluginEvent).toHaveBeenCalledTimes(4);
+        expect(onPluginEvent).toHaveBeenCalledWith({
+            eventType: PluginEventType.ContentChanged,
+            source: ChangeSource.CreateLink,
+            data: a,
+            additionalData: {
+                formatApiName: 'insertLink',
+            },
+        });
+
+        document.body.removeChild(div);
+    });
 });
diff --git a/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts b/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts
index e9a9f90fe22..bba5f7c743a 100644
--- a/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/link/removeLinkTest.ts
@@ -32,7 +32,9 @@ describe('removeLink', () => {
 
         if (expectedModel) {
             expect(setContentModel).toHaveBeenCalledTimes(1);
-            expect(setContentModel).toHaveBeenCalledWith(expectedModel);
+            expect(setContentModel).toHaveBeenCalledWith(expectedModel, {
+                onNodeCreated: undefined,
+            });
         } else {
             expect(setContentModel).not.toHaveBeenCalled();
         }
diff --git a/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts b/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts
index c116e61f0c9..e9e423f55c1 100644
--- a/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/segment/changeFontSizeTest.ts
@@ -363,25 +363,28 @@ describe('changeFontSize', () => {
 
         changeFontSize(editor, 'increase');
 
-        expect(setContentModel).toHaveBeenCalledWith({
-            blockGroupType: 'Document',
-            blocks: [
-                {
-                    blockType: 'Paragraph',
-                    format: {},
-                    segments: [
-                        {
-                            segmentType: 'Text',
-                            text: 'test',
-                            format: {
-                                fontSize: '22pt',
-                                superOrSubScriptSequence: 'sub',
+        expect(setContentModel).toHaveBeenCalledWith(
+            {
+                blockGroupType: 'Document',
+                blocks: [
+                    {
+                        blockType: 'Paragraph',
+                        format: {},
+                        segments: [
+                            {
+                                segmentType: 'Text',
+                                text: 'test',
+                                format: {
+                                    fontSize: '22pt',
+                                    superOrSubScriptSequence: 'sub',
+                                },
+                                isSelected: true,
                             },
-                            isSelected: true,
-                        },
-                    ],
-                },
-            ],
-        });
+                        ],
+                    },
+                ],
+            },
+            { onNodeCreated: undefined }
+        );
     });
 });
diff --git a/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts b/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts
index 3577472739d..961f763b29e 100644
--- a/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/table/setTableCellShadeTest.ts
@@ -37,10 +37,13 @@ describe('setTableCellShade', () => {
 
         if (expectedTable) {
             expect(setContentModel).toHaveBeenCalledTimes(1);
-            expect(setContentModel).toHaveBeenCalledWith({
-                blockGroupType: 'Document',
-                blocks: [expectedTable],
-            });
+            expect(setContentModel).toHaveBeenCalledWith(
+                {
+                    blockGroupType: 'Document',
+                    blocks: [expectedTable],
+                },
+                { onNodeCreated: undefined }
+            );
         } else {
             expect(setContentModel).not.toHaveBeenCalled();
         }
diff --git a/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts b/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts
index e5308f775a7..58770f74cce 100644
--- a/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts
+++ b/packages/roosterjs-content-model/test/publicApi/utils/formatWithContentModelTest.ts
@@ -63,7 +63,7 @@ describe('formatWithContentModel', () => {
             formatApiName: apiName,
         });
         expect(setContentModel).toHaveBeenCalledTimes(1);
-        expect(setContentModel).toHaveBeenCalledWith(mockedModel);
+        expect(setContentModel).toHaveBeenCalledWith(mockedModel, { onNodeCreated: undefined });
         expect(focus).toHaveBeenCalledTimes(1);
     });
 
@@ -109,4 +109,46 @@ describe('formatWithContentModel', () => {
         expect(createContentModel).toHaveBeenCalledTimes(1);
         expect(addUndoSnapshot).not.toHaveBeenCalled();
     });
+
+    it('Customize change source', () => {
+        const callback = jasmine.createSpy('callback').and.returnValue(true);
+
+        formatWithContentModel(editor, apiName, callback, { changeSource: 'TEST' });
+
+        expect(callback).toHaveBeenCalledWith(mockedModel);
+        expect(createContentModel).toHaveBeenCalledTimes(1);
+        expect(addUndoSnapshot).toHaveBeenCalled();
+        expect(addUndoSnapshot.calls.argsFor(0)[1]).toBe('TEST');
+    });
+
+    it('Has onNodeCreated', () => {
+        const callback = jasmine.createSpy('callback').and.returnValue(true);
+        const onNodeCreated = jasmine.createSpy('onNodeCreated');
+
+        formatWithContentModel(editor, apiName, callback, { onNodeCreated: onNodeCreated });
+
+        expect(callback).toHaveBeenCalledWith(mockedModel);
+        expect(createContentModel).toHaveBeenCalledTimes(1);
+        expect(addUndoSnapshot).toHaveBeenCalled();
+        expect(setContentModel).toHaveBeenCalledWith(mockedModel, { onNodeCreated });
+    });
+
+    it('Has getChangeData', () => {
+        const callback = jasmine.createSpy('callback').and.returnValue(true);
+        const mockedData = 'DATA' as any;
+        const getChangeData = jasmine.createSpy('getChangeData').and.returnValue(mockedData);
+
+        formatWithContentModel(editor, apiName, callback, { getChangeData });
+
+        expect(callback).toHaveBeenCalledWith(mockedModel);
+        expect(createContentModel).toHaveBeenCalledTimes(1);
+        expect(setContentModel).toHaveBeenCalledWith(mockedModel, { onNodeCreated: undefined });
+        expect(addUndoSnapshot).toHaveBeenCalled();
+
+        const wrappedCallback = addUndoSnapshot.calls.argsFor(0)[0] as any;
+        const result = wrappedCallback();
+
+        expect(getChangeData).toHaveBeenCalled();
+        expect(result).toBe(mockedData);
+    });
 });

From c18048fb4cdd0500267143473963fda288638be5 Mon Sep 17 00:00:00 2001
From: Jiuqing Song <jisong@microsoft.com>
Date: Fri, 14 Apr 2023 14:55:32 -0700
Subject: [PATCH 2/7] Fix #1713 (#1718)

---
 packages/roosterjs-editor-dom/lib/utils/createElement.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/roosterjs-editor-dom/lib/utils/createElement.ts b/packages/roosterjs-editor-dom/lib/utils/createElement.ts
index 826fb28380b..6623aee2f4f 100644
--- a/packages/roosterjs-editor-dom/lib/utils/createElement.ts
+++ b/packages/roosterjs-editor-dom/lib/utils/createElement.ts
@@ -22,7 +22,7 @@ export const KnownCreateElementData: Record<KnownCreateElementDataIndex, CreateE
     [KnownCreateElementDataIndex.CopyPasteTempDiv]: {
         tag: 'div',
         style:
-            'width: 1px; height: 1px; overflow: hidden; position: fixed; top: 0; left; 0; -webkit-user-select: text',
+            'width: 600px; height: 1px; overflow: hidden; position: fixed; top: 0; left; 0; -webkit-user-select: text',
         attributes: {
             contenteditable: 'true',
         },

From aafbb4fa6084c3ce43f36034bbdbcede26d16650 Mon Sep 17 00:00:00 2001
From: "SOUTHAMERICA\\bvalverde" <bvalverde@microsoft.com>
Date: Mon, 17 Apr 2023 08:15:32 -0600
Subject: [PATCH 3/7] Remove unneeded check

---
 .../lib/editor/coreApi/createPasteModel.ts                  | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts b/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts
index 559fdda5d7d..e6ddf1ce533 100644
--- a/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts
+++ b/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts
@@ -1,12 +1,12 @@
 import ContentModelBeforePasteEvent from '../../publicTypes/event/ContentModelBeforePasteEvent';
 import domToContentModel from '../../domToModel/domToContentModel';
+import { ClipboardData, EditorCore, NodePosition, PluginEventType } from 'roosterjs-editor-types';
 import { ContentModelEditorCore, CreatePasteModel } from '../../publicTypes/ContentModelEditorCore';
 import {
     createDefaultHtmlSanitizerOptions,
     createFragmentFromClipboardData,
     wrap,
 } from 'roosterjs-editor-dom';
-import { ClipboardData, EditorCore, PluginEventType, NodePosition } from 'roosterjs-editor-types';
 
 export const createPasteModel: CreatePasteModel = (
     core: ContentModelEditorCore,
@@ -16,10 +16,6 @@ export const createPasteModel: CreatePasteModel = (
     applyCurrentStyle: boolean,
     pasteAsImage: boolean = false
 ) => {
-    if (!clipboardData) {
-        return null;
-    }
-
     // Step 1: Prepare BeforePasteEvent object
     const event = createBeforePasteEvent(core, clipboardData);
 

From 15a13e49070b16d66fc2f50dd1fc609cd61ceafe Mon Sep 17 00:00:00 2001
From: "SOUTHAMERICA\\bvalverde" <bvalverde@microsoft.com>
Date: Mon, 17 Apr 2023 08:19:00 -0600
Subject: [PATCH 4/7] use formatWithContentModel

---
 .../lib/editor/ContentModelEditor.ts          | 23 ++++++++++++-------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts b/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts
index 906b1796577..820b4cbd302 100644
--- a/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts
+++ b/packages/roosterjs-content-model/lib/editor/ContentModelEditor.ts
@@ -1,8 +1,9 @@
-import { ClipboardData, GetContentMode } from 'roosterjs-editor-types';
+import { ChangeSource, ClipboardData, GetContentMode } from 'roosterjs-editor-types';
 import { ContentModelDocument } from '../publicTypes/group/ContentModelDocument';
 import { ContentModelEditorCore } from '../publicTypes/ContentModelEditorCore';
 import { createContentModelEditorCore } from './createContentModelEditorCore';
 import { EditorBase } from 'roosterjs-editor-core';
+import { formatWithContentModel } from '../publicApi/utils/formatWithContentModel';
 import { mergeModel } from '../modelApi/common/mergeModel';
 import { Position } from 'roosterjs-editor-dom';
 import {
@@ -90,7 +91,7 @@ export default class ContentModelEditor
 
         const range = this.getSelectionRange();
         const pos = range && Position.getStart(range);
-        const model = core.api.createPasteModel(
+        const pasteModel = core.api.createPasteModel(
             core,
             clipboardData,
             pos,
@@ -99,12 +100,18 @@ export default class ContentModelEditor
             pasteAsImage
         );
 
-        if (model) {
-            const currentModel = this.createContentModel();
-
-            mergeModel(currentModel, model);
-
-            this.setContentModel(currentModel);
+        if (pasteModel) {
+            formatWithContentModel(
+                this,
+                'Paste',
+                model => {
+                    mergeModel(model, pasteModel);
+                    return true;
+                },
+                {
+                    changeSource: ChangeSource.Paste,
+                }
+            );
         }
     }
 }

From fd5bce862cc581fcedf3b5d4285d19d5e5359c7a Mon Sep 17 00:00:00 2001
From: "SOUTHAMERICA\\bvalverde" <bvalverde@microsoft.com>
Date: Mon, 17 Apr 2023 08:20:14 -0600
Subject: [PATCH 5/7] Rearrange core apis

---
 .../lib/editor/createContentModelEditorCore.ts                 | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/roosterjs-content-model/lib/editor/createContentModelEditorCore.ts b/packages/roosterjs-content-model/lib/editor/createContentModelEditorCore.ts
index 3bf44e56171..7397f8aa87b 100644
--- a/packages/roosterjs-content-model/lib/editor/createContentModelEditorCore.ts
+++ b/packages/roosterjs-content-model/lib/editor/createContentModelEditorCore.ts
@@ -50,6 +50,7 @@ export function promoteToContentModelEditorCore(
         (cmCore.api.createEditorContext = createEditorContext);
     cmCore.api.createContentModel = createContentModel;
     cmCore.api.setContentModel = setContentModel;
+    cmCore.api.createPasteModel = createPasteModel;
 
     if (reuseModel) {
         // Only use Content Model shadow edit when reuse model is enabled because it relies on cached model for the original model
@@ -58,7 +59,7 @@ export function promoteToContentModelEditorCore(
     cmCore.originalApi.createEditorContext = createEditorContext;
     cmCore.originalApi.createContentModel = createContentModel;
     cmCore.originalApi.setContentModel = setContentModel;
-    cmCore.api.createPasteModel = createPasteModel;
+    cmCore.originalApi.createPasteModel = createPasteModel;
 }
 
 function getDefaultSegmentFormat(core: EditorCore): ContentModelSegmentFormat {

From c5a1def9b16b7f3d20faad5a92a8f0982b7561d2 Mon Sep 17 00:00:00 2001
From: "SOUTHAMERICA\\bvalverde" <bvalverde@microsoft.com>
Date: Mon, 17 Apr 2023 08:40:36 -0600
Subject: [PATCH 6/7] fix current unit tests

---
 .../test/editor/createContentModelEditorCoreTest.ts          | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/packages/roosterjs-content-model/test/editor/createContentModelEditorCoreTest.ts b/packages/roosterjs-content-model/test/editor/createContentModelEditorCoreTest.ts
index 460f1d9e0e7..44651b5e70b 100644
--- a/packages/roosterjs-content-model/test/editor/createContentModelEditorCoreTest.ts
+++ b/packages/roosterjs-content-model/test/editor/createContentModelEditorCoreTest.ts
@@ -53,6 +53,7 @@ describe('createContentModelEditorCore', () => {
                 createEditorContext,
                 createContentModel,
                 setContentModel,
+                createPasteModel,
             },
             defaultDomToModelOptions: {},
             defaultModelToDomOptions: {},
@@ -95,6 +96,7 @@ describe('createContentModelEditorCore', () => {
                 createEditorContext,
                 createContentModel,
                 setContentModel,
+                createPasteModel,
             },
             defaultDomToModelOptions,
             defaultModelToDomOptions,
@@ -152,6 +154,7 @@ describe('createContentModelEditorCore', () => {
                 createEditorContext,
                 createContentModel,
                 setContentModel,
+                createPasteModel,
             },
             defaultDomToModelOptions: {},
             defaultModelToDomOptions: {},
@@ -192,6 +195,7 @@ describe('createContentModelEditorCore', () => {
                 createEditorContext,
                 createContentModel,
                 setContentModel,
+                createPasteModel,
             },
             defaultDomToModelOptions: {},
             defaultModelToDomOptions: {},
@@ -234,6 +238,7 @@ describe('createContentModelEditorCore', () => {
                 createEditorContext,
                 createContentModel,
                 setContentModel,
+                createPasteModel,
             },
             defaultDomToModelOptions: {},
             defaultModelToDomOptions: {},

From db38f17e0c506138bc99576dbfb9447a61cf791c Mon Sep 17 00:00:00 2001
From: "SOUTHAMERICA\\bvalverde" <bvalverde@microsoft.com>
Date: Mon, 17 Apr 2023 15:55:16 -0600
Subject: [PATCH 7/7] Support Document Fragment in domToContentModel

---
 .../lib/domToModel/domToContentModel.ts       | 27 +++++++++++--------
 .../lib/editor/coreApi/createPasteModel.ts    |  3 +--
 .../lib/modelApi/common/insertContent.ts      |  8 ++----
 3 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts b/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts
index dcfbe62ddce..38020695ffc 100644
--- a/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts
+++ b/packages/roosterjs-content-model/lib/domToModel/domToContentModel.ts
@@ -7,6 +7,7 @@ import { EditorContext } from '../publicTypes/context/EditorContext';
 import { normalizeContentModel } from '../modelApi/common/normalizeContentModel';
 import { parseFormat } from './utils/parseFormat';
 import { rootDirectionFormatHandler } from '../formatHandlers/root/rootDirectionFormatHandler';
+import { safeInstanceOf } from 'roosterjs-editor-dom';
 import { zoomScaleFormatHandler } from '../formatHandlers/root/zoomScaleFormatHandler';
 
 /**
@@ -17,27 +18,31 @@ import { zoomScaleFormatHandler } from '../formatHandlers/root/zoomScaleFormatHa
  * @returns A ContentModelDocument object that contains all the models created from the give root element
  */
 export default function domToContentModel(
-    root: HTMLElement,
+    root: HTMLElement | DocumentFragment,
     editorContext: EditorContext,
     option: DomToModelOption
 ): ContentModelDocument {
     const model = createContentModelDocument(editorContext.defaultFormat);
     const context = createDomToModelContext(editorContext, option);
 
-    // For root element, use computed style as initial value of segment formats
-    parseFormat(root, [computedSegmentFormatHandler.parse], context.segmentFormat, context);
+    if (safeInstanceOf(root, 'DocumentFragment')) {
+        context.elementProcessors.child(model, root, context);
+    } else {
+        // 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 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);
+        // 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;
+        const processor = option.includeRoot
+            ? context.elementProcessors.element
+            : context.elementProcessors.child;
 
-    processor(model, root, context);
+        processor(model, root, context);
+    }
 
     normalizeContentModel(model);
 
diff --git a/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts b/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts
index e6ddf1ce533..9ab7210cf01 100644
--- a/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts
+++ b/packages/roosterjs-content-model/lib/editor/coreApi/createPasteModel.ts
@@ -5,7 +5,6 @@ import { ContentModelEditorCore, CreatePasteModel } from '../../publicTypes/Cont
 import {
     createDefaultHtmlSanitizerOptions,
     createFragmentFromClipboardData,
-    wrap,
 } from 'roosterjs-editor-dom';
 
 export const createPasteModel: CreatePasteModel = (
@@ -29,7 +28,7 @@ export const createPasteModel: CreatePasteModel = (
         event
     );
 
-    return domToContentModel(wrap(fragment, 'span'), core.api.createEditorContext(core), {
+    return domToContentModel(fragment, core.api.createEditorContext(core), {
         processorOverride: {
             element: (group, element, context) => {
                 const wasHandled =
diff --git a/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts b/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts
index 1e5a458dec6..86a4dcbb785 100644
--- a/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts
+++ b/packages/roosterjs-content-model/lib/modelApi/common/insertContent.ts
@@ -1,7 +1,7 @@
 import domToContentModel from '../../domToModel/domToContentModel';
 import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument';
 import { mergeModel } from '../../modelApi/common/mergeModel';
-import { safeInstanceOf, wrap } from 'roosterjs-editor-dom';
+import { safeInstanceOf } from 'roosterjs-editor-dom';
 import { setSelection } from '../../modelApi/selection/setSelection';
 
 /**
@@ -12,11 +12,7 @@ export function insertContent(
     htmlContent: DocumentFragment | HTMLElement | ContentModelDocument,
     isFromDarkMode?: boolean
 ) {
-    if (safeInstanceOf(htmlContent, 'DocumentFragment')) {
-        htmlContent = wrap(htmlContent, 'span');
-    }
-
-    if (safeInstanceOf(htmlContent, 'HTMLElement')) {
+    if (safeInstanceOf(htmlContent, 'Node')) {
         htmlContent = domToContentModel(
             htmlContent,
             {