diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index 319412e4e6c2..325b199093da 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -70,12 +70,7 @@ const initialState: OptionState = { }, customReplacements: emojiReplacements, disableSideResize: false, - experimentalFeatures: new Set([ - 'HandleEnterKey', - 'CloneIndependentRoot', - 'CacheList', - 'TransformTableBorderColors', - ]), + experimentalFeatures: new Set(['TransformTableBorderColors']), }; export class EditorOptionsPlugin extends SidePanePluginImpl { diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx index dc9aa65739af..7de49ba7833f 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx @@ -11,10 +11,7 @@ export class ExperimentalFeatures extends React.Component - {this.renderFeature('HandleEnterKey')} {this.renderFeature('KeepSelectionMarkerWhenEnteringTextNode')} - {this.renderFeature('CloneIndependentRoot')} - {this.renderFeature('CacheList')} {this.renderFeature('TransformTableBorderColors')} ); diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx index 44022a59ccb0..50d9698b3de9 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx @@ -99,7 +99,6 @@ abstract class PluginsBase extends Re export class Plugins extends PluginsBase { private allowExcelNoBorderTable = React.createRef(); private handleTabKey = React.createRef(); - private handleEnterKey = React.createRef(); private listMenu = React.createRef(); private tableMenu = React.createRef(); private imageMenu = React.createRef(); @@ -240,13 +239,6 @@ export class Plugins extends PluginsBase { (state, value) => (state.editPluginOptions.handleTabKey.indentParagraph = value) )} - {this.renderCheckBox( - 'Handle Enter Key', - this.handleEnterKey, - this.props.state.editPluginOptions.shouldHandleEnterKey as boolean, - (state, value) => - (state.editPluginOptions.shouldHandleEnterKey = value) - )} )} {this.renderPluginItem( diff --git a/package.json b/package.json index a35fd48100d7..bf5a6a272f28 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "coverage-istanbul-loader": "3.0.5", "css-loader": "3.5.3", "detect-port": "^1.3.0", - "dompurify": "2.5.4", + "dompurify": "3.4.0", "eslint": "^8.50.0", "eslint-plugin-etc": "^2.0.3", "eslint-plugin-react": "^7.33.2", diff --git a/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts b/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts index 47f07595c8a8..2296099c2df2 100644 --- a/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/exportContent/exportContent.ts @@ -15,7 +15,7 @@ import type { /** * Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity * @param editor The editor to get content from - * @param mode Specify HTML to get HTML. This is the default option + * @param mode Specify HTML to get HTML. * @param options @optional Options for Model to DOM conversion */ export function exportContent(editor: IEditor, mode?: 'HTML', options?: ModelToDomOption): string; @@ -24,9 +24,9 @@ export function exportContent(editor: IEditor, mode?: 'HTML', options?: ModelToD * Export HTML content. If there are entities, this will cause EntityOperation event with option = 'replaceTemporaryContent' to get a dehydrated entity. * This is a fast version, it retrieve HTML content directly from editor without going through content model conversion. * @param editor The editor to get content from - * @param mode Specify HTMLFast to get HTML result. + * @param mode Specify HTMLFast to get HTML result. This is the default option */ -export function exportContent(editor: IEditor, mode: 'HTMLFast'): string; +export function exportContent(editor: IEditor, mode?: 'HTMLFast'): string; /** * Export plain text content @@ -52,7 +52,7 @@ export function exportContent(editor: IEditor, mode: 'PlainTextFast'): string; // Once we are confident that 'HTMLFast' is stable, we can fully switch 'HTML' to use the 'HTMLFast' approach export function exportContent( editor: IEditor, - mode: ExportContentMode | 'HTMLFast' = 'HTML', + mode: ExportContentMode | 'HTMLFast' = 'HTMLFast', optionsOrCallbacks?: ModelToDomOption | ModelToTextCallbacks ): string { let model: ContentModelDocument; @@ -70,6 +70,7 @@ export function exportContent( ); case 'HTMLFast': + default: const clonedRoot = editor.getDOMHelper().getClonedRoot(); if (editor.isDarkMode()) { @@ -89,7 +90,6 @@ export function exportContent( return getHTMLFromDOM(editor, clonedRoot); case 'HTML': - default: model = editor.getContentModelCopy('clean'); const doc = editor.getDocument(); diff --git a/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts b/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts index 9a2983ae6601..20f38f2c89cc 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/createEditorContext/createEditorContext.ts @@ -17,7 +17,6 @@ export const createEditorContext: CreateEditorContext = (core, saveIndex) => { darkColorHandler: darkColorHandler, addDelimiterForEntity: true, allowCacheElement: true, - allowCacheListItem: !!core.experimentalFeatures?.includes('CacheList'), domIndexer: saveIndex ? cache.domIndexer : undefined, zoomScale: domHelper.calculateZoomScale(), experimentalFeatures: core.experimentalFeatures ?? [], diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index def7bec62747..016d5a8240b4 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -16,11 +16,14 @@ import type { * @internal */ export interface DOMHelperImplOption { + /** + * @deprecated This is always treated as true now + */ cloneIndependentRoot?: boolean; } class DOMHelperImpl implements DOMHelper { - constructor(private contentDiv: HTMLElement, private options: DOMHelperImplOption) {} + constructor(private contentDiv: HTMLElement, options?: DOMHelperImplOption) {} queryElements(selector: string): HTMLElement[] { return toArray(this.contentDiv.querySelectorAll(selector)) as HTMLElement[]; @@ -123,14 +126,10 @@ class DOMHelperImpl implements DOMHelper { * Get a deep cloned root element */ getClonedRoot(): HTMLElement { - if (this.options.cloneIndependentRoot) { - const doc = this.contentDiv.ownerDocument.implementation.createHTMLDocument(); - const clone = doc.importNode(this.contentDiv, true /*deep*/); + const doc = this.contentDiv.ownerDocument.implementation.createHTMLDocument(); + const clone = doc.importNode(this.contentDiv, true /*deep*/); - return clone; - } else { - return this.contentDiv.cloneNode(true /*deep*/) as HTMLElement; - } + return clone; } /** diff --git a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts index a15e5fbf9efb..96398a35276d 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/createEditorCore.ts @@ -50,9 +50,7 @@ export function createEditorCore(contentDiv: HTMLDivElement, options: EditorOpti ? options.trustedHTMLHandler : createTrustedHTMLHandler(domCreator), domCreator: domCreator, - domHelper: createDOMHelper(contentDiv, { - cloneIndependentRoot: options.experimentalFeatures?.includes('CloneIndependentRoot'), - }), + domHelper: createDOMHelper(contentDiv), ...getPluginState(corePlugins), disposeErrorHandler: options.disposeErrorHandler, onFixUpModel: options.onFixUpModel, diff --git a/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts b/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts index 9e97fcc987c8..855e5b8e2364 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/createEditorContext/createEditorContextTest.ts @@ -51,7 +51,6 @@ describe('createEditorContext', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); @@ -104,7 +103,6 @@ describe('createEditorContext', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); @@ -154,7 +152,6 @@ describe('createEditorContext', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); @@ -207,7 +204,6 @@ describe('createEditorContext', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); }); @@ -266,7 +262,6 @@ describe('createEditorContext - checkZoomScale', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); }); @@ -325,7 +320,6 @@ describe('createEditorContext - checkRootDir', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); @@ -347,7 +341,6 @@ describe('createEditorContext - checkRootDir', () => { experimentalFeatures: [], paragraphMap: mockedParagraphMap, editorViewWidth: 800, - allowCacheListItem: false, }); }); }); diff --git a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts index 8fa6f84eb29e..86da6ac65ad5 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts @@ -354,20 +354,6 @@ describe('DOMHelperImpl', () => { describe('getClonedRoot', () => { it('getClonedRoot', () => { - const mockedClone = 'CLONE' as any; - const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone); - const mockedDiv: HTMLElement = { - cloneNode: cloneSpy, - } as any; - const domHelper = createDOMHelper(mockedDiv); - - const result = domHelper.getClonedRoot(); - - expect(result).toBe(mockedClone); - expect(cloneSpy).toHaveBeenCalledWith(true); - }); - - it('getClonedRoot, with CloneIndependentRoot on', () => { const mockedClone = 'CLONE' as any; const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone); const importNodeSpy = jasmine.createSpy('importNodeSpy').and.returnValue(mockedClone); @@ -381,9 +367,7 @@ describe('DOMHelperImpl', () => { }, }, } as any; - const domHelper = createDOMHelper(mockedDiv, { - cloneIndependentRoot: true, - }); + const domHelper = createDOMHelper(mockedDiv); const result = domHelper.getClonedRoot(); diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts index 07d3f1c178a0..61ccf92bd380 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listItemProcessor.ts @@ -1,4 +1,5 @@ import { createListItem } from '../../modelApi/creators/createListItem'; +import { createListLevel } from '../../modelApi/creators/createListLevel'; import { parseFormat } from '../utils/parseFormat'; import { stackFormat } from '../utils/stackFormat'; import type { ElementProcessor } from 'roosterjs-content-model-types'; @@ -8,8 +9,21 @@ import type { ElementProcessor } from 'roosterjs-content-model-types'; */ export const listItemProcessor: ElementProcessor = (group, element, context) => { const { listFormat } = context; + const originalListParent = listFormat.listParent; + let shouldPopListLevel = false; + + try { + listFormat.listParent = listFormat.listParent ?? group; + + const listParent = listFormat.listParent; + + if (listFormat.levels.length == 0) { + listFormat.levels.push( + createListLevel(listFormat.potentialListType || 'UL', context.blockFormat) + ); + shouldPopListLevel = true; + } - if (listFormat.listParent && listFormat.levels.length > 0) { stackFormat( context, { @@ -31,7 +45,7 @@ export const listItemProcessor: ElementProcessor = (group, elemen context ); - listFormat.listParent!.blocks.push(listItem); + listParent.blocks.push(listItem); parseFormat( element, @@ -54,14 +68,11 @@ export const listItemProcessor: ElementProcessor = (group, elemen } } ); - } else { - const currentBlocks = listFormat.listParent?.blocks; - const lastItem = currentBlocks?.[currentBlocks?.length - 1]; + } finally { + if (shouldPopListLevel) { + listFormat.levels.pop(); + } - context.elementProcessors['*']( - lastItem?.blockType == 'BlockGroup' ? lastItem : group, - element, - context - ); + listFormat.listParent = originalListParent; } }; diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts index 92517b7e51bc..e6334346ae51 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/processors/listProcessor.ts @@ -18,10 +18,8 @@ export const listProcessor: ElementProcessor { - const level: ContentModelListLevel = createListLevel( - element.tagName as 'OL' | 'UL', - context.blockFormat - ); + const tagName = element.tagName as 'OL' | 'UL'; + const level: ContentModelListLevel = createListLevel(tagName, context.blockFormat); const { listFormat } = context; parseFormat(element, context.formatParsers.dataset, level.dataset, context); @@ -31,6 +29,7 @@ export const listProcessor: ElementProcessor 0) { + if (nodeStack.length > 0) { // Clear list stack, only run to nodeStack[1] because nodeStack[0] is the parent node for (let i = nodeStack.length - 1; i > 0; i--) { const node = nodeStack.pop()?.refNode ?? null; diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts index 6ed248c629f6..ae6f190ac7b8 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleList.ts @@ -50,24 +50,18 @@ export const handleList: ContentModelBlockHandler = ( applyMetadata(itemLevel, context.metadataAppliers.listLevel, itemLevel.format, context); } - if ( - context.allowCacheListItem && - parentLevel.refNode && - itemLevel.cachedElement == parentLevel.refNode - ) { + if (parentLevel.refNode && itemLevel.cachedElement == parentLevel.refNode) { // Move refNode to next node since we are reusing this cached element parentLevel.refNode = parentLevel.refNode.nextSibling; } } // Cut off remained list levels that we can't reuse - if (context.allowCacheListItem) { - // Clean up all rest nodes in the reused list levels - for (let i = layer + 1; i < nodeStack.length; i++) { - const stackLevel = nodeStack[i]; + // Clean up all rest nodes in the reused list levels + for (let i = layer + 1; i < nodeStack.length; i++) { + const stackLevel = nodeStack[i]; - cleanUpRestNodes(stackLevel.refNode, context.rewriteFromModel); - } + cleanUpRestNodes(stackLevel.refNode, context.rewriteFromModel); } nodeStack.splice(layer + 1); @@ -83,7 +77,7 @@ export const handleList: ContentModelBlockHandler = ( context.listFormat.currentLevel = layer; - if (context.allowCacheListItem && level.cachedElement) { + if (level.cachedElement) { newList = level.cachedElement; nodeStack[layer].refNode = reuseCachedElement( @@ -112,9 +106,7 @@ export const handleList: ContentModelBlockHandler = ( dataset: { ...level.dataset }, }); - if (context.allowCacheListItem) { - level.cachedElement = newList; - } + level.cachedElement = newList; } applyFormat(newList, context.formatAppliers.listLevelThread, level.format, context); diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts index 5f6ca79ce67e..1d6d51e42f43 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleListItem.ts @@ -36,7 +36,7 @@ export const handleListItem: ContentModelBlockHandler = ( let li: HTMLLIElement; let isNewlyCreated = false; - if (context.allowCacheListItem && listItem.cachedElement) { + if (listItem.cachedElement) { li = listItem.cachedElement; // Check if the cached LI is used as refNode under another list level, @@ -62,10 +62,7 @@ export const handleListItem: ContentModelBlockHandler = ( // This happens when outdent a list item to cause it has no list level listParent.insertBefore(li, itemRefNode?.parentNode == listParent ? itemRefNode : null); context.rewriteFromModel.addedBlockElements.push(li); - - if (context.allowCacheListItem) { - listItem.cachedElement = li; - } + listItem.cachedElement = li; } if (level) { 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 5696b6a0ebb7..00b1eba1501a 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts @@ -536,6 +536,7 @@ describe('childProcessor', () => { levels: [], listParent: undefined, threadItemCounts: [1], + potentialListType: 'OL', }); }); diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts index a39fc7173e7e..2811d3c7859a 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/listItemProcessorTest.ts @@ -26,10 +26,21 @@ describe('listItemProcessor', () => { blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: li, + blockGroupType: 'ListItem', blocks: [], format: {}, + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, }, ], }); @@ -381,10 +392,21 @@ describe('listItemProcessor without format handlers', () => { blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: li, + blockGroupType: 'ListItem', blocks: [], format: {}, + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, }, ], }); diff --git a/packages/roosterjs-content-model-dom/test/endToEndTest.ts b/packages/roosterjs-content-model-dom/test/endToEndTest.ts index 27fc73fa576f..8d55177d6929 100644 --- a/packages/roosterjs-content-model-dom/test/endToEndTest.ts +++ b/packages/roosterjs-content-model-dom/test/endToEndTest.ts @@ -3027,4 +3027,41 @@ describe('End to end test for DOM => Model => DOM/TEXT', () => { 'www.bing.com' ); }); + + it('LI without UL followed by other blocks', () => { + runTest( + '
  • test
  • other
    ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'other', format: {} }], + format: {}, + }, + ], + }, + 'test\r\nother', + '
    • test
    other
    ' + ); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts index 9081f216f454..ed79cbfaa9a5 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts @@ -489,7 +489,6 @@ describe('handleBlockGroupChildren', () => { group.blocks.push(listItem, paragraph); context.listFormat.nodeStack = nodeStack; - context.allowCacheListItem = true; expect(parent.outerHTML).toBe('

    '); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts index a473c59606fb..1d31b5764bf2 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListItemTest.ts @@ -465,7 +465,6 @@ describe('handleListItem with cache', () => { listItem: { applierFunction: listItemMetadataApplier }, }, }); - context.allowCacheListItem = true; context.onNodeCreated = onNodeCreatedSpy; spyOn(applyFormat, 'applyFormat').and.callThrough(); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts index 4cf5be44d043..1e82b4a4fcf0 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleListTest.ts @@ -740,7 +740,6 @@ describe('handleList with cache', () => { ]); parent.appendChild(cachedUL); - context.allowCacheListItem = true; const newRefNode = handleList(document, parent, listItem, context, cachedUL); @@ -783,7 +782,6 @@ describe('handleList with cache', () => { listItem.levels[0].cachedElement = cachedUL; parent.appendChild(cachedUL); - context.allowCacheListItem = true; const newRefNode = handleList(document, parent, listItem, context, cachedUL); @@ -831,7 +829,6 @@ describe('handleList with cache', () => { listItem.levels[0].cachedElement = cachedUL; parent.appendChild(refNode); - context.allowCacheListItem = true; const newRefNode = handleList(document, parent, listItem, context, refNode); @@ -878,7 +875,6 @@ describe('handleList with cache', () => { listItem.levels[0].cachedElement = cachedUL; parent.appendChild(cachedUL); - context.allowCacheListItem = true; const newRefNode = handleList(document, parent, listItem, context, cachedUL); @@ -929,7 +925,7 @@ describe('handleList with cache', () => { listItem.levels[1].cachedElement = cachedUL; parent.appendChild(cachedOL); cachedOL.appendChild(cachedUL); - context.allowCacheListItem = true; + const newRefNode = handleList(document, parent, listItem, context, cachedUL); expect(parent.outerHTML).toBe('
      '); @@ -984,7 +980,7 @@ describe('handleList with cache', () => { parent.appendChild(refNode); listItem.levels[1].cachedElement = cachedUL; - context.allowCacheListItem = true; + const newRefNode = handleList(document, parent, listItem, context, refNode); expect(parent.outerHTML).toBe('

        '); @@ -1036,7 +1032,6 @@ describe('handleList with cache', () => { parent.appendChild(existingOL1); existingOL1.appendChild(existingLI); existingOL1.appendChild(existingOL2); - context.allowCacheListItem = true; context.listFormat.nodeStack = [ { node: parent, refNode: existingOL1 }, @@ -1090,7 +1085,6 @@ describe('handleList with cache', () => { ]); listItem.levels[0].cachedElement = existingOL1; - context.allowCacheListItem = true; context.listFormat.nodeStack = [ { node: parent, refNode: null }, diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditOptions.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditOptions.ts index 86f814472a79..e5f85d5ae098 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditOptions.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditOptions.ts @@ -55,6 +55,7 @@ export type EditOptions = { handleExpandedSelectionOnDelete?: boolean; /** + * @deprecated This is always treated as true now * Callback function to determine whether the Rooster should handle the Enter key press. * If the function returns true, the Rooster will handle the Enter key press instead of the browser. * @param editor - The editor instance. diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts index f2b1c8409eb8..6954ed6027c7 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts @@ -55,7 +55,6 @@ export class EditPlugin implements EditorPlugin { private disposer: (() => void) | null = null; private shouldHandleNextInputEvent = false; private selectionAfterDelete: DOMSelection | null = null; - private handleNormalEnter: (editor: IEditor) => boolean = () => false; private options: EditOptions & { handleTabKey: Required }; /** @@ -72,23 +71,6 @@ export class EditPlugin implements EditorPlugin { this.options = { ...DefaultOptions, ...options, handleTabKey: tabOptions }; } - private createNormalEnterChecker(result: boolean) { - return result ? () => true : () => false; - } - - private getHandleNormalEnter(editor: IEditor) { - switch (typeof this.options.shouldHandleEnterKey) { - case 'function': - return this.options.shouldHandleEnterKey; - case 'boolean': - return this.createNormalEnterChecker(this.options.shouldHandleEnterKey); - default: - return this.createNormalEnterChecker( - editor.isExperimentalFeatureEnabled('HandleEnterKey') - ); - } - } - /** * Get name of this plugin */ @@ -104,7 +86,6 @@ export class EditPlugin implements EditorPlugin { */ initialize(editor: IEditor) { this.editor = editor; - this.handleNormalEnter = this.getHandleNormalEnter(editor); if (editor.getEnvironment().isAndroid) { this.disposer = this.editor.attachDomEvent({ @@ -224,12 +205,7 @@ export class EditPlugin implements EditorPlugin { !event.rawEvent.isComposing && event.rawEvent.keyCode !== DEAD_KEY ) { - keyboardEnter( - editor, - rawEvent, - this.handleNormalEnter(editor), - this.options.formatsToPreserveOnMerge - ); + keyboardEnter(editor, rawEvent, this.options.formatsToPreserveOnMerge); } break; diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index 08b5c74d5cc0..2ee5581efb6e 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -8,7 +8,7 @@ import { normalizeContentModel, runEditSteps, } from 'roosterjs-content-model-dom'; -import type { IEditor, ReadonlyContentModelParagraph } from 'roosterjs-content-model-types'; +import type { IEditor } from 'roosterjs-content-model-types'; /** * @internal @@ -16,7 +16,6 @@ import type { IEditor, ReadonlyContentModelParagraph } from 'roosterjs-content-m export function keyboardEnter( editor: IEditor, rawEvent: KeyboardEvent, - handleNormalEnter: boolean, formatsToPreserveOnMerge: string[] = [] ) { const selection = editor.getDOMSelection(); @@ -36,9 +35,7 @@ export function keyboardEnter( ? [] : [handleAutoLink, handleEnterOnList, deleteEmptyQuote]; - if (handleNormalEnter || handleEnterForEntity(result.insertPoint?.paragraph)) { - steps.push(handleEnterOnParagraph(formatsToPreserveOnMerge)); - } + steps.push(handleEnterOnParagraph(formatsToPreserveOnMerge)); runEditSteps(steps, result); } @@ -64,10 +61,3 @@ export function keyboardEnter( } ); } - -function handleEnterForEntity(paragraph: ReadonlyContentModelParagraph | undefined) { - return ( - paragraph && - (paragraph.isImplicit || paragraph.segments.some(x => x.segmentType == 'Entity')) - ); -} diff --git a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts index b687e5969f80..bf88bde843d8 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts @@ -20,7 +20,6 @@ describe('EditPlugin', () => { let eventMap: Record; let attachDOMEventSpy: jasmine.Spy; let getEnvironmentSpy: jasmine.Spy; - let isExperimentalFeatureEnabledSpy: jasmine.Spy; beforeEach(() => { attachDOMEventSpy = jasmine @@ -32,9 +31,6 @@ describe('EditPlugin', () => { getEnvironmentSpy = jasmine.createSpy('getEnvironment').and.returnValue({ isAndroid: true, }); - isExperimentalFeatureEnabledSpy = jasmine - .createSpy('isExperimentalFeatureEnabled') - .and.returnValue(false); editor = ({ attachDomEvent: attachDOMEventSpy, @@ -43,7 +39,6 @@ describe('EditPlugin', () => { ({ type: -1, } as any), // Force return invalid range to go through content model code - isExperimentalFeatureEnabled: isExperimentalFeatureEnabledSpy, } as any) as IEditor; }); @@ -122,7 +117,7 @@ describe('EditPlugin', () => { }); it('handleExpandedSelectionOnDelete with options', () => { - plugin = new EditPlugin({ shouldHandleEnterKey: true }); + plugin = new EditPlugin(); const rawEvent = { key: 'Delete' } as any; plugin.initialize(editor); @@ -135,7 +130,6 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent, { handleTabKey: DefaultHandleTabOptions, handleExpandedSelectionOnDelete: true, - shouldHandleEnterKey: true, }); }); @@ -191,9 +185,7 @@ describe('EditPlugin', () => { }); it('Tab - custom options with options', () => { - plugin = new EditPlugin({ - shouldHandleEnterKey: true, - }); + plugin = new EditPlugin(); const rawEvent = { key: 'Tab' } as any; plugin.initialize(editor); @@ -248,14 +240,11 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, false, undefined); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, undefined); expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Enter, normal enter enabled with experimental feature', () => { - isExperimentalFeatureEnabledSpy.and.callFake( - (featureName: string) => featureName == 'HandleEnterKey' - ); plugin = new EditPlugin(); const rawEvent = { keyCode: 13, which: 13, key: 'Enter' } as any; const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); @@ -271,14 +260,12 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, undefined); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, undefined); expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Enter, normal enter enabled', () => { - plugin = new EditPlugin({ - shouldHandleEnterKey: true, - }); + plugin = new EditPlugin(); const rawEvent = { keyCode: 13, which: 13, key: 'Enter' } as any; const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); @@ -293,16 +280,12 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, undefined); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, undefined); expect(keyboardTabSpy).not.toHaveBeenCalled(); }); it('Enter, normal enter enabled with callback', () => { - plugin = new EditPlugin({ - shouldHandleEnterKey: _editor => { - return true; - }, - }); + plugin = new EditPlugin(); const rawEvent = { keyCode: 13, which: 13, key: 'Enter' } as any; const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot'); @@ -317,7 +300,7 @@ describe('EditPlugin', () => { expect(keyboardDeleteSpy).not.toHaveBeenCalled(); expect(keyboardInputSpy).not.toHaveBeenCalled(); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, undefined); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, undefined); expect(keyboardTabSpy).not.toHaveBeenCalled(); }); @@ -495,10 +478,6 @@ describe('EditPlugin', () => { beforeEach(() => { keyboardEnterSpy = spyOn(keyboardEnter, 'keyboardEnter'); - // Configure editor to handle Enter key (needed for handleNormalEnter to return true) - isExperimentalFeatureEnabledSpy.and.callFake((feature: string) => { - return feature === 'HandleEnterKey'; - }); }); it('should pass formatsToPreserveOnMerge to keyboardEnter', () => { @@ -523,7 +502,7 @@ describe('EditPlugin', () => { }); expect(keyboardEnterSpy).toHaveBeenCalledTimes(1); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, [ + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, [ 'className', 'fontFamily', ]); @@ -550,7 +529,7 @@ describe('EditPlugin', () => { }); expect(keyboardEnterSpy).toHaveBeenCalledTimes(1); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, undefined); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, undefined); }); it('should pass empty formatsToPreserveOnMerge array', () => { @@ -575,7 +554,7 @@ describe('EditPlugin', () => { }); expect(keyboardEnterSpy).toHaveBeenCalledTimes(1); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, []); + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, []); }); it('should work with multiple custom format properties', () => { @@ -600,7 +579,7 @@ describe('EditPlugin', () => { }); expect(keyboardEnterSpy).toHaveBeenCalledTimes(1); - expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, true, [ + expect(keyboardEnterSpy).toHaveBeenCalledWith(editor, rawEvent, [ 'className', 'customProp', 'data-testid', diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index c902d166c354..be234488ffd2 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -1929,7 +1929,7 @@ describe('handleEnterOnList - keyboardEnter', () => { }, }); - keyboardEnter(editor, mockedEvent, true); + keyboardEnter(editor, mockedEvent); }, input, expectedResult, diff --git a/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts b/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts index 02ced3f372c4..d8a5c9cf8892 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/keyboardEnterTest.ts @@ -61,7 +61,7 @@ describe('keyboardEnter', () => { expect(); }); - keyboardEnter(editor, rawEvent, true); + keyboardEnter(editor, rawEvent); expect(formatContentModelSpy).toHaveBeenCalledTimes(1); expect(input).toEqual(output); @@ -1396,7 +1396,7 @@ describe('keyboardEnter', () => { const runEditStepsSpy = spyOn(runEditSteps, 'runEditSteps'); - keyboardEnter(editor, {} as any, false, []); + keyboardEnter(editor, {} as any, []); expect(runEditStepsSpy).toHaveBeenCalledTimes(2); // Check that the second call to runEditSteps includes steps for handling entity @@ -1434,7 +1434,7 @@ describe('keyboardEnter', () => { const runEditStepsSpy = spyOn(runEditSteps, 'runEditSteps'); - keyboardEnter(editor, {} as any, false, []); + keyboardEnter(editor, {} as any, []); expect(runEditStepsSpy).toHaveBeenCalledTimes(2); expect( @@ -1479,7 +1479,6 @@ describe('keyboardEnter', () => { keyboardEnter( editor, { preventDefault: () => {}, shiftKey: false } as any, - true, formatsToPreserve ); @@ -1515,7 +1514,7 @@ describe('keyboardEnter', () => { callback(model, context); }); - keyboardEnter(editor, { preventDefault: () => {}, shiftKey: false } as any, true, []); + keyboardEnter(editor, { preventDefault: () => {}, shiftKey: false } as any, []); expect(formatContentModelSpy).toHaveBeenCalledTimes(1); }); @@ -1549,7 +1548,7 @@ describe('keyboardEnter', () => { callback(model, context); }); - keyboardEnter(editor, { preventDefault: () => {}, shiftKey: false } as any, true); + keyboardEnter(editor, { preventDefault: () => {}, shiftKey: false } as any); expect(formatContentModelSpy).toHaveBeenCalledTimes(1); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts index 2a046e3a208a..d254b7198d6a 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/word/processPastedContentFromWacTest.ts @@ -2043,17 +2043,16 @@ describe('wordOnlineHandler', () => { // .test // .test // .test - it('shuold process html properly, when list items are not in side ul tag', () => { + it('should process html properly, when list items are not inside ul tag', () => { runTest( '
        • test

        • test

        • test

        • ', - '
        • test

        • test

        • test

        • ', + '
          • test

          • test

          • test

          ', { blockGroupType: 'Document', blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2064,12 +2063,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2080,12 +2084,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2096,6 +2105,12 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, ], @@ -2345,17 +2360,16 @@ describe('wordOnlineHandler', () => { // result: // 1. text // 2. text - it('shuold process html properly, if list item in a ListContainerWrapper are not inside ol ', () => { + it('should process html properly, if list item in a ListContainerWrapper are not inside ol ', () => { runTest( '
          1. test

          2. test

          3. test

          4. ', - '
          5. test

          6. test

          7. test

          8. ', + '
            1. test

            2. test

            3. test

            ', { blockGroupType: 'Document', blocks: [ { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2366,12 +2380,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2382,12 +2401,17 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, { blockType: 'BlockGroup', - blockGroupType: 'General', - element: jasmine.anything() as any, + blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', @@ -2398,6 +2422,12 @@ describe('wordOnlineHandler', () => { decorator: { tagName: 'p', format: {} }, }, ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, format: {}, }, ], diff --git a/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts b/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts index dbe1884ab1ef..df71a8e58dca 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts @@ -24,6 +24,12 @@ export interface DomToModelListFormat { * Current list type stack */ levels: ContentModelListLevel[]; + + /** + * This is used for handling an abnormal case where list items are not inside a ul or ol tag + * It is not common and against the HTML specification, but we need to handle it for robustness + */ + potentialListType?: 'OL' | 'UL'; } /** diff --git a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts index 4f7752faa4bb..ba11488e89bc 100644 --- a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts +++ b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts @@ -50,6 +50,7 @@ export interface EditorContext { allowCacheElement?: boolean; /** + * @deprecated This is now always be treated as true * Whether to allow caching list item elements separately. */ allowCacheListItem?: boolean; diff --git a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts index 3120182d7ba9..4b4e7f3be7a2 100644 --- a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts +++ b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts @@ -18,15 +18,13 @@ export type GraduatedExperimentalFeature = * Prevent default browser behavior for copy/cut event, * and set the clipboard data with custom implementation. */ - | 'CustomCopyCut'; + | 'CustomCopyCut' -/** - * Predefined experiment features - * By default these features are not enabled. To enable them, pass the feature name into EditorOptions.experimentalFeatures - * when create editor - */ -export type ExperimentalFeature = - | GraduatedExperimentalFeature + /** + * @deprecated + * Export editor content as HTML using HTMLFast option + */ + | 'ExportHTMLFast' /** * @deprecated Please use the shouldHandleEnterKey option of the EditPlugin Options @@ -35,28 +33,33 @@ export type ExperimentalFeature = | 'HandleEnterKey' /** - * For CJK keyboard input on mobile, if the user toggles bold/italic/underline on an empty div, - * the pending format will be applied on the selection marker. When typing text, the selection moves to the text node and the - * selection marker may be recreated during reconciliation, potentially losing its original formatting. This feature ensures - * the original formatting of the selection marker is kept to match the pending format. - */ - | 'KeepSelectionMarkerWhenEnteringTextNode' - - /** - * Export editor content as HTML using HTMLFast option - */ - | 'ExportHTMLFast' - - /** + * @deprecated * Get cloned root element from an independent HTML document instead of current document. * So any operation to the cloned root won't trigger network request for resources like images */ | 'CloneIndependentRoot' /** + * @deprecated * Allow caching list item elements. */ - | 'CacheList' + | 'CacheList'; + +/** + * Predefined experiment features + * By default these features are not enabled. To enable them, pass the feature name into EditorOptions.experimentalFeatures + * when create editor + */ +export type ExperimentalFeature = + | GraduatedExperimentalFeature + + /** + * For CJK keyboard input on mobile, if the user toggles bold/italic/underline on an empty div, + * the pending format will be applied on the selection marker. When typing text, the selection moves to the text node and the + * selection marker may be recreated during reconciliation, potentially losing its original formatting. This feature ensures + * the original formatting of the selection marker is kept to match the pending format. + */ + | 'KeepSelectionMarkerWhenEnteringTextNode' /** * Transform the table border colors when switching from light to dark mode diff --git a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts index 91b64831958a..45b3f2618371 100644 --- a/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts +++ b/packages/roosterjs-editor-adapter/lib/editor/EditorAdapter.ts @@ -354,13 +354,7 @@ export class EditorAdapter extends Editor implements ILegacyEditor { switch (exportMode) { case 'HTML': - return this.isExperimentalFeatureEnabled('ExportHTMLFast') - ? exportContent(this, 'HTMLFast') - : exportContent( - this, - 'HTML', - this.getCore().environment.modelToDomSettings.customized - ); + return exportContent(this, 'HTMLFast'); case 'PlainText': return exportContent(this, 'PlainText'); diff --git a/versions.json b/versions.json index e170021a53bd..1e20c30ddb70 100644 --- a/versions.json +++ b/versions.json @@ -1,6 +1,6 @@ { "react": "9.0.4", - "main": "9.50.1", - "legacyAdapter": "8.65.3", + "main": "9.51.0", + "legacyAdapter": "8.66.0", "overrides": {} } diff --git a/yarn.lock b/yarn.lock index b7075d7ea2fc..652d7587dfbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -773,7 +773,7 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.3.tgz#9a726e116beb26c24f1ccd6850201e1246122e04" integrity sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw== -"@types/trusted-types@*": +"@types/trusted-types@*", "@types/trusted-types@^2.0.7": version "2.0.7" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== @@ -2292,10 +2292,12 @@ dom-serialize@^2.2.1: extend "^3.0.0" void-elements "^2.0.0" -dompurify@2.5.4: - version "2.5.4" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.5.4.tgz#347e91070963b22db31c7c8d0ce9a0a2c3c08746" - integrity sha512-l5NNozANzaLPPe0XaAwvg3uZcHtDBnziX/HjsY1UcDj1MxTK8Dd0Kv096jyPK5HRzs/XM5IMj20dW8Fk+HnbUA== +dompurify@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.0.tgz#b1fc33ebdadb373241621e0a30e4ad81573dfd0b" + integrity sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg== + optionalDependencies: + "@types/trusted-types" "^2.0.7" ecc-jsbn@~0.1.1: version "0.1.2" @@ -3075,9 +3077,9 @@ flatted@^3.2.4, flatted@^3.2.7: integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== follow-redirects@^1.0.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== for-each@^0.3.3: version "0.3.3" @@ -4612,9 +4614,9 @@ lodash.merge@^4.6.2: integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: - version "4.17.23" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" - integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== log4js@^6.4.1: version "6.4.1"