diff --git a/demo/scripts/controlsV2/mainPane/MainPane.tsx b/demo/scripts/controlsV2/mainPane/MainPane.tsx index 2cc9cd9a796..597e9e14d72 100644 --- a/demo/scripts/controlsV2/mainPane/MainPane.tsx +++ b/demo/scripts/controlsV2/mainPane/MainPane.tsx @@ -475,7 +475,13 @@ export class MainPane extends React.Component<{}, MainPaneState> { watermarkText, } = this.state.initState; return [ - pluginList.autoFormat && new AutoFormatPlugin(), + pluginList.autoFormat && + new AutoFormatPlugin({ + autoBullet: true, + autoNumbering: true, + autoUnlink: true, + autoLink: true, + }), pluginList.edit && new EditPlugin(), pluginList.paste && new PastePlugin(allowExcelNoBorderTable), pluginList.shortcut && new ShortcutPlugin(), diff --git a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts index 48e3531e3ea..439bb3466e4 100644 --- a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts +++ b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createDomToModelContextForSanitizing.ts @@ -30,7 +30,7 @@ export function createDomToModelContextForSanitizing( document: Document, defaultFormat?: ContentModelSegmentFormat, defaultOption?: DomToModelOption, - additionalSanitizingOption?: DomToModelOptionForSanitizing + additionalSanitizingOption?: Partial ): DomToModelContext { const sanitizingOption: DomToModelOptionForSanitizing = { ...DefaultSanitizingOption, diff --git a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts index 7164fa7cbfa..68b34cbc3a9 100644 --- a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts +++ b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/createModelFromHtml.ts @@ -4,7 +4,7 @@ import { createEmptyModel, domToContentModel, parseFormat } from 'roosterjs-cont import type { ContentModelDocument, ContentModelSegmentFormat, - DomToModelOption, + DomToModelOptionForSanitizing, TrustedHTMLHandler, } from 'roosterjs-content-model-types'; @@ -17,7 +17,7 @@ import type { */ export function createModelFromHtml( html: string, - options?: DomToModelOption, + options?: Partial, trustedHTMLHandler?: TrustedHTMLHandler, defaultSegmentFormat?: ContentModelSegmentFormat ): ContentModelDocument { @@ -26,7 +26,12 @@ export function createModelFromHtml( : null; if (doc?.body) { - const context = createDomToModelContextForSanitizing(doc, defaultSegmentFormat, options); + const context = createDomToModelContextForSanitizing( + doc, + defaultSegmentFormat, + undefined /*defaultOptions*/, + options + ); const cssRules = doc ? retrieveCssRules(doc) : []; convertInlineCss(doc, cssRules); diff --git a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts index 7f2e777e90d..4d1c1e5f574 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts @@ -76,8 +76,10 @@ export const formatContentModel: FormatContentModel = ( core.api.triggerEvent(core, eventData, true /*broadcast*/); if (canUndoByBackspace && selection?.type == 'range') { - core.undo.posContainer = selection.range.startContainer; - core.undo.posOffset = selection.range.startOffset; + core.undo.autoCompleteInsertPoint = { + node: selection.range.startContainer, + offset: selection.range.startOffset, + }; } if (shouldAddSnapshot) { @@ -128,8 +130,10 @@ function handlePendingFormat( if (pendingFormat && selection?.type == 'range' && selection.range.collapsed) { core.format.pendingFormat = { format: { ...pendingFormat }, - posContainer: selection.range.startContainer, - posOffset: selection.range.startOffset, + insertPoint: { + node: selection.range.startContainer, + offset: selection.range.startOffset, + }, }; } } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts index b4c0a279659..973bc685c52 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts @@ -163,9 +163,9 @@ class FormatPlugin implements PluginWithState { const selection = this.editor.getDOMSelection(); const range = selection?.type == 'range' && selection.range.collapsed ? selection.range : null; - const { posContainer, posOffset } = this.state.pendingFormat; + const { node, offset } = this.state.pendingFormat.insertPoint; - if (range && range.startContainer == posContainer && range.startOffset == posOffset) { + if (range && range.startContainer == node && range.startOffset == offset) { result = true; } } diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts index 9eccc9f6f5a..df553aa6f57 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/undo/UndoPlugin.ts @@ -30,8 +30,7 @@ class UndoPlugin implements PluginWithState { snapshotsManager: createSnapshotsManager(options.snapshots), isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }; } @@ -129,8 +128,7 @@ class UndoPlugin implements PluginWithState { if (evt.key == Backspace && !evt.ctrlKey && this.canUndoAutoComplete(editor)) { evt.preventDefault(); undo(editor); - this.state.posContainer = null; - this.state.posOffset = null; + this.state.autoCompleteInsertPoint = null; this.state.lastKeyPress = evt.key; } else if (!evt.defaultPrevented) { const selection = editor.getDOMSelection(); @@ -232,15 +230,14 @@ class UndoPlugin implements PluginWithState { this.state.snapshotsManager.canUndoAutoComplete() && selection?.type == 'range' && selection.range.collapsed && - selection.range.startContainer == this.state.posContainer && - selection.range.startOffset == this.state.posOffset + selection.range.startContainer == this.state.autoCompleteInsertPoint?.node && + selection.range.startOffset == this.state.autoCompleteInsertPoint.offset ); } private addUndoSnapshot() { this.editor?.takeSnapshot(); - this.state.posContainer = null; - this.state.posOffset = null; + this.state.autoCompleteInsertPoint = null; } private isCtrlOrMetaPressed(editor: IEditor, event: KeyboardEvent) { diff --git a/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createModelFromHtmlTest.ts b/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createModelFromHtmlTest.ts index d7874f315d6..b109ed62715 100644 --- a/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createModelFromHtmlTest.ts +++ b/packages/roosterjs-content-model-core/test/command/createModelFromHtml/createModelFromHtmlTest.ts @@ -2,8 +2,12 @@ import * as convertInlineCss from '../../../lib/command/createModelFromHtml/conv import * as createDomToModelContextForSanitizing from '../../../lib/command/createModelFromHtml/createDomToModelContextForSanitizing'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as parseFormat from 'roosterjs-content-model-dom/lib/domToModel/utils/parseFormat'; -import { ContentModelSegmentFormat } from 'roosterjs-content-model-types'; import { createModelFromHtml } from '../../../lib/command/createModelFromHtml/createModelFromHtml'; +import { + ContentModelGeneralBlock, + ContentModelSegmentFormat, + ElementProcessor, +} from 'roosterjs-content-model-types'; describe('createModelFromHtml', () => { it('Empty html, no options', () => { @@ -134,6 +138,7 @@ describe('createModelFromHtml', () => { expect(createContextSpy).toHaveBeenCalledWith( mockedDoc, mockedDefaultSegmentFormat, + undefined, mockedOptions ); expect(domToContentModelSpy).toHaveBeenCalledWith('BODY' as any, mockedContext); @@ -202,4 +207,51 @@ describe('createModelFromHtml', () => { expect(retrieveCssRulesSpy).not.toHaveBeenCalled(); expect(convertInlineCssSpy).not.toHaveBeenCalled(); }); + + it('Treat DIV with id as general block, and preserve id', () => { + const divProcessor: ElementProcessor = (group, element, context) => { + const processor = element.id + ? context.elementProcessors['*'] + : context.defaultElementProcessors.div; + + processor?.(group, element, context); + }; + const model = createModelFromHtml('
test
', { + processorOverride: { + div: divProcessor, + }, + attributeSanitizers: { + id: true, + }, + }); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything(), + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'test', + }, + ], + isImplicit: true, + }, + ], + format: {}, + }, + ], + }); + expect((model.blocks[0] as ContentModelGeneralBlock).element.outerHTML).toBe( + '
test
' + ); + }); }); diff --git a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts index 60385b723ed..17078241df9 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts @@ -615,8 +615,10 @@ describe('formatContentModel', () => { it('Has pending format, callback returns true, preserve pending format', () => { core.format.pendingFormat = { format: mockedFormat1, - posContainer: mockedStartContainer1, - posOffset: mockedStartOffset1, + insertPoint: { + node: mockedStartContainer1, + offset: mockedStartOffset1, + }, }; formatContentModel(core, (model, context) => { @@ -626,16 +628,20 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat1, - posContainer: mockedStartContainer2, - posOffset: mockedStartOffset2, + insertPoint: { + node: mockedStartContainer2, + offset: mockedStartOffset2, + }, } as any); }); it('Has pending format, callback returns false, preserve pending format', () => { core.format.pendingFormat = { format: mockedFormat1, - posContainer: mockedStartContainer1, - posOffset: mockedStartOffset1, + insertPoint: { + node: mockedStartContainer1, + offset: mockedStartOffset1, + }, }; formatContentModel(core, (model, context) => { @@ -645,8 +651,10 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat1, - posContainer: mockedStartContainer2, - posOffset: mockedStartOffset2, + insertPoint: { + node: mockedStartContainer2, + offset: mockedStartOffset2, + }, } as any); }); @@ -658,8 +666,10 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat2, - posContainer: mockedStartContainer2, - posOffset: mockedStartOffset2, + insertPoint: { + node: mockedStartContainer2, + offset: mockedStartOffset2, + }, }); }); @@ -671,16 +681,20 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat2, - posContainer: mockedStartContainer2, - posOffset: mockedStartOffset2, + insertPoint: { + node: mockedStartContainer2, + offset: mockedStartOffset2, + }, }); }); it('Has pending format, callback returns true, new format', () => { core.format.pendingFormat = { format: mockedFormat1, - posContainer: mockedStartContainer1, - posOffset: mockedStartOffset1, + insertPoint: { + node: mockedStartContainer1, + offset: mockedStartOffset1, + }, }; formatContentModel(core, (model, context) => { @@ -690,16 +704,20 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat2, - posContainer: mockedStartContainer2, - posOffset: mockedStartOffset2, + insertPoint: { + node: mockedStartContainer2, + offset: mockedStartOffset2, + }, }); }); it('Has pending format, callback returns false, new format', () => { core.format.pendingFormat = { format: mockedFormat1, - posContainer: mockedStartContainer1, - posOffset: mockedStartOffset1, + insertPoint: { + node: mockedStartContainer1, + offset: mockedStartOffset1, + }, }; formatContentModel(core, (model, context) => { @@ -709,16 +727,20 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat2, - posContainer: mockedStartContainer2, - posOffset: mockedStartOffset2, + insertPoint: { + node: mockedStartContainer2, + offset: mockedStartOffset2, + }, }); }); it('Has pending format, callback returns false, preserve format, selection is not collapsed', () => { core.format.pendingFormat = { format: mockedFormat1, - posContainer: mockedStartContainer1, - posOffset: mockedStartOffset1, + insertPoint: { + node: mockedStartContainer1, + offset: mockedStartOffset1, + }, }; core.api.getDOMSelection = () => @@ -738,8 +760,10 @@ describe('formatContentModel', () => { expect(core.format.pendingFormat).toEqual({ format: mockedFormat1, - posContainer: mockedStartContainer1, - posOffset: mockedStartOffset1, + insertPoint: { + node: mockedStartContainer1, + offset: mockedStartOffset1, + }, }); }); }); @@ -841,8 +865,10 @@ describe('formatContentModel', () => { expect(core.undo).toEqual({ isNested: false, snapshotsManager: {}, - posContainer: mockedContainer, - posOffset: mockedOffset, + autoCompleteInsertPoint: { + node: mockedContainer, + offset: mockedOffset, + }, } as any); }); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts index 1309523cdda..eb0cf93c732 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/undo/UndoPluginTest.ts @@ -58,8 +58,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(createSnapshotsManagerSpy).toHaveBeenCalledWith(undefined); @@ -81,8 +80,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(createSnapshotsManagerSpy).toHaveBeenCalledWith(mockedSnapshots); @@ -217,8 +215,7 @@ describe('UndoPlugin', () => { it('Not handled exclusively for KeyDown event, selection is not the same', () => { const state = plugin.getState(); - state.posContainer = 'P1' as any; - state.posOffset = 'O1' as any; + state.autoCompleteInsertPoint = { node: 'P1' as any, offset: 'O1' as any }; canUndoAutoCompleteSpy.and.returnValue(true); getDOMSelectionSpy.and.returnValue({ @@ -245,8 +242,7 @@ describe('UndoPlugin', () => { it('Handled exclusively for KeyDown event', () => { const state = plugin.getState(); - state.posContainer = 'P1' as any; - state.posOffset = 'O1' as any; + state.autoCompleteInsertPoint = { node: 'P1' as any, offset: 'O1' as any }; canUndoAutoCompleteSpy.and.returnValue(true); getDOMSelectionSpy.and.returnValue({ @@ -296,8 +292,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalse(); @@ -318,8 +313,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalse(); @@ -340,8 +334,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalse(); @@ -362,8 +355,7 @@ describe('UndoPlugin', () => { const state = plugin.getState(); - state.posContainer = 'C1' as any; - state.posOffset = 'O1' as any; + state.autoCompleteInsertPoint = { node: 'C1' as any, offset: 'O1' as any }; const preventDefaultSpy = jasmine.createSpy('preventDefault'); @@ -384,8 +376,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'Backspace', }); expect(mockedSnapshotsManager.hasNewContent).toBeFalse(); @@ -418,8 +409,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'Backspace', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -448,8 +438,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'Delete', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -478,8 +467,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -511,8 +499,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -545,8 +532,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'Backspace', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -576,8 +562,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -611,8 +596,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'Enter', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -646,8 +630,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'A', }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -677,8 +660,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: ' ', }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -708,8 +690,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: ' ', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -736,8 +717,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'Enter', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -764,8 +744,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'A', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -789,8 +768,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -815,8 +793,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: true, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -838,8 +815,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -861,8 +837,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -884,8 +859,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeFalsy(); @@ -907,8 +881,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: null, }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -935,8 +908,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'B', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); @@ -963,8 +935,7 @@ describe('UndoPlugin', () => { snapshotsManager: mockedSnapshotsManager, isRestoring: false, isNested: false, - posContainer: null, - posOffset: null, + autoCompleteInsertPoint: null, lastKeyPress: 'A', }); expect(mockedSnapshotsManager.hasNewContent).toBeTrue(); diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/utils/buildSelectionMarker.ts b/packages/roosterjs-content-model-dom/lib/domToModel/utils/buildSelectionMarker.ts index f455dc583e3..e71701eb2fd 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/utils/buildSelectionMarker.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/utils/buildSelectionMarker.ts @@ -39,8 +39,8 @@ export function buildSelectionMarker( const pendingFormat = context.pendingFormat && - context.pendingFormat.posContainer === container && - context.pendingFormat.posOffset === offset + context.pendingFormat.insertPoint.node === container && + context.pendingFormat.insertPoint.offset === offset ? context.pendingFormat.format : undefined; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts index b5725d1d870..dd4f8301fc7 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts @@ -127,8 +127,10 @@ describe('childProcessor', () => { format: { a: 'a', } as any, - posContainer: div, - posOffset: 0, + insertPoint: { + node: div, + offset: 0, + }, }; childProcessor(doc, div, context); @@ -173,8 +175,10 @@ describe('childProcessor', () => { format: { a: 'a', } as any, - posContainer: div, - posOffset: 1, + insertPoint: { + node: div, + offset: 1, + }, }; childProcessor(doc, div, context); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts index b84b8a14172..f264ce94157 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts @@ -754,8 +754,10 @@ describe('textProcessor', () => { format: { a: 'a', } as any, - posContainer: text, - posOffset: 2, + insertPoint: { + node: text, + offset: 2, + }, }; textProcessor(doc, text, context); @@ -813,8 +815,10 @@ describe('textProcessor', () => { format: { a: 'a', } as any, - posContainer: text, - posOffset: 3, + insertPoint: { + node: text, + offset: 3, + }, }; textProcessor(doc, text, context); @@ -874,8 +878,10 @@ describe('textProcessor', () => { format: { a: 'a', } as any, - posContainer: text, - posOffset: 3, + insertPoint: { + node: text, + offset: 3, + }, }; textProcessor(doc, text, context); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts index db265ad1937..104c19654a0 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textWithSelectionProcessorTest.ts @@ -576,8 +576,10 @@ describe('textWithSelectionProcessor', () => { format: { a: 'a', } as any, - posContainer: text, - posOffset: 2, + insertPoint: { + node: text, + offset: 2, + }, }; textWithSelectionProcessor(doc, text, context); @@ -635,8 +637,10 @@ describe('textWithSelectionProcessor', () => { format: { a: 'a', } as any, - posContainer: text, - posOffset: 3, + insertPoint: { + node: text, + offset: 3, + }, }; textWithSelectionProcessor(doc, text, context); @@ -696,8 +700,10 @@ describe('textWithSelectionProcessor', () => { format: { a: 'a', } as any, - posContainer: text, - posOffset: 3, + insertPoint: { + node: text, + offset: 3, + }, }; textWithSelectionProcessor(doc, text, context); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts index 9b83269e9ff..fb7f401f363 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/utils/addSelectionMarkerTest.ts @@ -218,8 +218,10 @@ describe('addSelectionMarker', () => { c: 'c3', e: 'e', } as any, - posContainer: mockedContainer, - posOffset: mockedOffset, + insertPoint: { + node: mockedContainer, + offset: mockedOffset, + }, }, }); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/utils/buildSelectionMarkerTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/utils/buildSelectionMarkerTest.ts index d1d7533f286..662abd0c56c 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/utils/buildSelectionMarkerTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/utils/buildSelectionMarkerTest.ts @@ -55,8 +55,10 @@ describe('buildSelectionMarker', () => { const mockedOffset = 'OFFSET' as any; context.pendingFormat = { - posContainer: mockedContainer, - posOffset: mockedOffset, + insertPoint: { + node: mockedContainer, + offset: mockedOffset, + }, format: { textColor: 'blue', backgroundColor: 'green', @@ -95,8 +97,10 @@ describe('buildSelectionMarker', () => { const mockedOffset2 = 'OFFSET2' as any; context.pendingFormat = { - posContainer: mockedContainer, - posOffset: mockedOffset1, + insertPoint: { + node: mockedContainer, + offset: mockedOffset1, + }, format: { textColor: 'blue', backgroundColor: 'green', @@ -193,8 +197,10 @@ describe('buildSelectionMarker', () => { const mockedOffset = 'OFFSET' as any; context.pendingFormat = { - posContainer: mockedContainer, - posOffset: mockedOffset, + insertPoint: { + node: mockedContainer, + offset: mockedOffset, + }, format: { textColor: 'blue', backgroundColor: 'green', diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index e531b6011e5..b10e68a94ce 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -40,10 +40,10 @@ export type AutoFormatOptions = { * @internal */ const DefaultOptions: Required = { - autoBullet: true, - autoNumbering: true, + autoBullet: false, + autoNumbering: false, autoUnlink: false, - autoLink: true, + autoLink: false, }; /** @@ -55,8 +55,10 @@ export class AutoFormatPlugin implements EditorPlugin { /** * @param options An optional parameter that takes in an object of type AutoFormatOptions, which includes the following properties: - * - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to true. - * - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to true. + * - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to false. + * - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to false. + * - autoLink: A boolean that enables or disables automatic hyperlink creation when pasting or typing content. Defaults to false. + * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. */ constructor(private options: AutoFormatOptions = DefaultOptions) {} @@ -113,8 +115,11 @@ export class AutoFormatPlugin implements EditorPlugin { if (rawEvent.inputType === 'insertText') { switch (rawEvent.data) { case ' ': - const { autoBullet, autoNumbering } = this.options; + const { autoBullet, autoNumbering, autoLink } = this.options; keyboardListTrigger(editor, autoBullet, autoNumbering); + if (autoLink) { + createLinkAfterSpace(editor); + } break; } } @@ -124,11 +129,6 @@ export class AutoFormatPlugin implements EditorPlugin { const rawEvent = event.rawEvent; if (!rawEvent.defaultPrevented && !event.handledByEditFeature) { switch (rawEvent.key) { - case ' ': - if (this.options.autoLink) { - createLinkAfterSpace(editor); - } - break; case 'Backspace': if (this.options.autoUnlink) { unlink(editor, rawEvent); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index 6c87cb68b01..d529474d025 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -1,37 +1,53 @@ -import { createText, getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; +import { getSelectedSegmentsAndParagraphs } from 'roosterjs-content-model-dom'; import { matchLink } from 'roosterjs-content-model-api'; -import type { IEditor } from 'roosterjs-content-model-types'; +import { splitTextSegment } from '../../pluginUtils/splitTextSegment'; +import type { IEditor, LinkData } from 'roosterjs-content-model-types'; /** * @internal */ export function createLinkAfterSpace(editor: IEditor) { - editor.formatContentModel(model => { + editor.formatContentModel((model, context) => { const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( model, false /* includingFormatHolder */ ); - if (selectedSegmentsAndParagraphs[0] && selectedSegmentsAndParagraphs[0][1]) { - const length = selectedSegmentsAndParagraphs[0][1].segments.length; - const marker = selectedSegmentsAndParagraphs[0][1].segments[length - 1]; - const textSegment = selectedSegmentsAndParagraphs[0][1].segments[length - 2]; - if ( - marker.segmentType == 'SelectionMarker' && - textSegment.segmentType == 'Text' && - !textSegment.link - ) { - const link = textSegment.text.split(' ').pop(); - if (link && matchLink(link)) { - textSegment.text = textSegment.text.replace(link, ''); - const linkSegment = createText(link, marker.format, { - format: { - href: link, - underline: true, - }, - dataset: {}, - }); - selectedSegmentsAndParagraphs[0][1].segments.splice(length - 1, 0, linkSegment); - return true; + if (selectedSegmentsAndParagraphs.length > 0 && selectedSegmentsAndParagraphs[0][1]) { + const markerIndex = selectedSegmentsAndParagraphs[0][1].segments.findIndex( + segment => segment.segmentType == 'SelectionMarker' + ); + const paragraph = selectedSegmentsAndParagraphs[0][1]; + if (markerIndex > 0) { + const textSegment = paragraph.segments[markerIndex - 1]; + const marker = paragraph.segments[markerIndex]; + if ( + marker.segmentType == 'SelectionMarker' && + textSegment && + textSegment.segmentType == 'Text' && + !textSegment.link + ) { + const link = textSegment.text.split(' ').pop(); + const url = link?.trim(); + let linkData: LinkData | null = null; + if (url && link && (linkData = matchLink(url))) { + const linkSegment = splitTextSegment( + textSegment, + paragraph, + textSegment.text.length - link.trimLeft().length, + textSegment.text.trimRight().length + ); + linkSegment.link = { + format: { + href: linkData.normalizedUrl, + underline: true, + }, + dataset: {}, + }; + + context.canUndoByBackspace = true; + + return true; + } } } } diff --git a/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts b/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts new file mode 100644 index 00000000000..d407a8d04e0 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/pluginUtils/splitTextSegment.ts @@ -0,0 +1,43 @@ +import { createText } from 'roosterjs-content-model-dom'; +import type { ContentModelParagraph, ContentModelText } from 'roosterjs-content-model-types'; + +/** + * @internal + */ +export function splitTextSegment( + textSegment: ContentModelText, + parent: ContentModelParagraph, + start: number, + end: number +): ContentModelText { + const text = textSegment.text; + const index = parent.segments.indexOf(textSegment); + const middleSegment = createText( + text.substring(start, end), + textSegment.format, + textSegment.link, + textSegment.code + ); + + const newSegments: ContentModelText[] = [middleSegment]; + if (start > 0) { + newSegments.unshift( + createText( + text.substring(0, start), + textSegment.format, + textSegment.link, + textSegment.code + ) + ); + } + if (end < text.length) { + newSegments.push( + createText(text.substring(end), textSegment.format, textSegment.link, textSegment.code) + ); + } + + newSegments.forEach(segment => (segment.isSelected = textSegment.isSelected)); + parent.segments.splice(index, 1, ...newSegments); + + return middleSegment; +} diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index ea9fc284209..23afee5ee30 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -60,7 +60,10 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: ' ', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, true); + runTest(event, true, { + autoBullet: true, + autoNumbering: true, + }); }); it('should not trigger keyboardListTrigger', () => { @@ -68,7 +71,10 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'input', rawEvent: { data: '*', defaultPrevented: false, inputType: 'insertText' } as any, }; - runTest(event, false); + runTest(event, false, { + autoBullet: true, + autoNumbering: true, + }); }); it('should not trigger keyboardListTrigger', () => { @@ -135,7 +141,9 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'contentChanged', source: 'Paste', }; - runTest(event, true); + runTest(event, true, { + autoLink: true, + }); }); it('should not call createLink - autolink disabled', () => { @@ -151,7 +159,9 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'contentChanged', source: 'Format', }; - runTest(event, false); + runTest(event, false, { + autoLink: true, + }); }); }); @@ -218,7 +228,7 @@ describe('Content Model Auto Format Plugin Test', () => { }); function runTest( - event: KeyDownEvent, + event: EditorInputEvent, shouldCallTrigger: boolean, options?: { autoLink: boolean; @@ -237,29 +247,37 @@ describe('Content Model Auto Format Plugin Test', () => { } it('should call createLinkAfterSpace', () => { - const event: KeyDownEvent = { - eventType: 'keyDown', - rawEvent: { key: ' ', preventDefault: () => {} } as any, + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, }; - runTest(event, true); + runTest(event, true, { + autoLink: true, + }); }); it('should not call createLinkAfterSpace - disable options', () => { - const event: KeyDownEvent = { - eventType: 'keyDown', - rawEvent: { key: ' ', preventDefault: () => {} } as any, + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, }; runTest(event, false, { autoLink: false, }); }); - it('should not call createLinkAfterSpace - not backspace', () => { - const event: KeyDownEvent = { - eventType: 'keyDown', - rawEvent: { key: 'Backspace', preventDefault: () => {} } as any, + it('should not call createLinkAfterSpace - not space', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { + data: 'Backspace', + preventDefault: () => {}, + inputType: 'insertText', + } as any, }; - runTest(event, false); + runTest(event, false, { + autoLink: true, + }); }); }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts index 7c01772f662..a93df5e9573 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts @@ -14,6 +14,7 @@ describe('createLinkAfterSpace', () => { newEntities: [], deletedEntities: [], newImages: [], + canUndoByBackspace: true, }); expect(result).toBe(expectedResult); }); @@ -105,18 +106,14 @@ describe('createLinkAfterSpace', () => { { blockType: 'Paragraph', segments: [ - { - segmentType: 'Text', - text: '', - format: {}, - }, { segmentType: 'Text', text: 'www.bing.com', format: {}, + isSelected: undefined, link: { format: { - href: 'www.bing.com', + href: 'http://www.bing.com', underline: true, }, dataset: {}, @@ -203,15 +200,17 @@ describe('createLinkAfterSpace', () => { segmentType: 'Text', text: 'this is the link ', format: {}, + isSelected: undefined, }, { segmentType: 'Text', text: 'www.bing.com', format: {}, + isSelected: undefined, link: { format: { underline: true, - href: 'www.bing.com', + href: 'http://www.bing.com', }, dataset: {}, }, @@ -256,4 +255,90 @@ describe('createLinkAfterSpace', () => { }; runTest(input, input, false); }); + + it('link after link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + link: { + format: { + href: 'www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: ' www.bing.com', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + link: { + format: { + href: 'www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: ' ', + format: {}, + isSelected: undefined, + }, + { + segmentType: 'Text', + text: 'www.bing.com', + format: {}, + isSelected: undefined, + link: { + format: { + href: 'http://www.bing.com', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/pluginUtils/splitTextSegmentTest.ts b/packages/roosterjs-content-model-plugins/test/pluginUtils/splitTextSegmentTest.ts new file mode 100644 index 00000000000..5d9363fc413 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/pluginUtils/splitTextSegmentTest.ts @@ -0,0 +1,96 @@ +import { ContentModelParagraph, ContentModelText } from 'roosterjs-content-model-types'; +import { splitTextSegment } from '../../lib/pluginUtils/splitTextSegment'; + +describe('splitTextSegment', () => { + function runTest( + textSegment: ContentModelText, + parent: ContentModelParagraph, + start: number, + end: number, + expectedResult: ContentModelText + ) { + const result = splitTextSegment(textSegment, parent, start, end); + expect(result).toEqual(expectedResult); + } + + it('splitTextSegment', () => { + const textSegment: ContentModelText = { + text: 'test test', + format: {}, + segmentType: 'Text', + }; + const parent: ContentModelParagraph = { + segments: [textSegment], + blockType: 'Paragraph', + format: {}, + }; + runTest(textSegment, parent, 0, 2, { + text: 'te', + format: {}, + segmentType: 'Text', + isSelected: undefined, + }); + }); + + it('splitTextSegment with selection', () => { + const textSegment: ContentModelText = { + text: 'test test', + format: {}, + segmentType: 'Text', + isSelected: true, + }; + const parent: ContentModelParagraph = { + segments: [textSegment], + blockType: 'Paragraph', + format: {}, + }; + runTest(textSegment, parent, 0, 2, { + text: 'te', + format: {}, + segmentType: 'Text', + isSelected: true, + }); + }); + + it('splitTextSegment with decorators', () => { + const textSegment: ContentModelText = { + text: 'test test', + format: {}, + segmentType: 'Text', + isSelected: true, + link: { + format: { + href: 'test', + }, + dataset: {}, + }, + code: { + format: { + fontFamily: 'Consolas', + }, + }, + }; + const parent: ContentModelParagraph = { + segments: [textSegment], + blockType: 'Paragraph', + format: {}, + }; + runTest(textSegment, parent, 0, 2, { + text: 'te', + format: {}, + segmentType: 'Text', + isSelected: true, + link: { + format: { + href: 'test', + }, + dataset: {}, + }, + code: { + format: { + fontFamily: 'Consolas', + }, + }, + }); + }); +}); diff --git a/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts b/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts index 05749a346f6..e0581094541 100644 --- a/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts +++ b/packages/roosterjs-content-model-types/lib/pluginState/FormatPluginState.ts @@ -1,3 +1,4 @@ +import type { DOMInsertPoint } from '../selection/DOMSelection'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; /** @@ -10,14 +11,9 @@ export interface PendingFormat { format: ContentModelSegmentFormat; /** - * Container node of pending format + * Insert point of pending format */ - posContainer: Node; - - /** - * Offset under container node of pending format - */ - posOffset: number; + insertPoint: DOMInsertPoint; } /** diff --git a/packages/roosterjs-content-model-types/lib/pluginState/UndoPluginState.ts b/packages/roosterjs-content-model-types/lib/pluginState/UndoPluginState.ts index 040e57d44f7..bf0ad3234ea 100644 --- a/packages/roosterjs-content-model-types/lib/pluginState/UndoPluginState.ts +++ b/packages/roosterjs-content-model-types/lib/pluginState/UndoPluginState.ts @@ -1,4 +1,5 @@ import type { SnapshotsManager } from '../parameter/SnapshotsManager'; +import type { DOMInsertPoint } from '../selection/DOMSelection'; /** * The state object for UndoPlugin @@ -20,14 +21,9 @@ export interface UndoPluginState { isNested: boolean; /** - * Container after last auto complete. Undo autoComplete only works if the current position matches this one + * Insert point after last auto complete. Undo autoComplete only works if the current position matches this one */ - posContainer: Node | null; - - /** - * Offset after last auto complete. Undo autoComplete only works if the current position matches this one - */ - posOffset: number | null; + autoCompleteInsertPoint: DOMInsertPoint | null; /** * Last key user pressed diff --git a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts index 8ee4773dca3..5ada4b80731 100644 --- a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts +++ b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts @@ -772,8 +772,10 @@ export class EditorAdapter extends Editor implements ILegacyEditor { if (selection?.type == 'range') { core.undo.snapshotsManager.hasNewContent = false; - core.undo.posContainer = selection.range.startContainer; - core.undo.posOffset = selection.range.startOffset; + core.undo.autoCompleteInsertPoint = { + node: selection.range.startContainer, + offset: selection.range.startOffset, + }; } } } diff --git a/yarn.lock b/yarn.lock index 375fab391ed..89044413ebf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1439,7 +1439,25 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -body-parser@1.20.1, body-parser@^1.19.0: +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +body-parser@^1.19.0: version "1.20.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== @@ -1862,6 +1880,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" @@ -1874,10 +1897,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== cookie@~0.4.1: version "0.4.2" @@ -2723,16 +2746,16 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: homedir-polyfill "^1.0.1" express@^4.17.1: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -5562,6 +5585,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"