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, {