diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext.ts index f2e8e7aa50c..3724793dfb7 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/context/createDomToModelContext.ts @@ -2,23 +2,24 @@ import { defaultFormatParsers, getFormatParsers } from '../../formatHandlers/def import { defaultProcessorMap } from './defaultProcessors'; import { defaultStyleMap } from '../../formatHandlers/utils/defaultStyles'; import { DomToModelContext, DomToModelOption, EditorContext } from 'roosterjs-content-model-types'; -import { SelectionRangeTypes } from 'roosterjs-editor-types'; +import { SelectionRangeEx } from 'roosterjs-editor-types'; /** * Create context object form DOM to Content Model conversion * @param editorContext Context of editor * @param options Options for this context + * @param selection Selection that already exists in content */ export function createDomToModelContext( editorContext?: EditorContext, - options?: DomToModelOption + options?: DomToModelOption, + selection?: SelectionRangeEx ): DomToModelContext { const context: DomToModelContext = { ...editorContext, blockFormat: {}, segmentFormat: {}, - zoomScaleFormat: {}, isInSelection: false, listFormat: { @@ -57,46 +58,12 @@ export function createDomToModelContext( allowCacheElement: !options?.disableCacheElement, }; - const range = options?.selectionRange; - let selectionRoot: Node | undefined; - - switch (range?.type) { - case SelectionRangeTypes.Normal: - const regularRange = range.ranges[0]; - if (regularRange) { - selectionRoot = regularRange.commonAncestorContainer; - context.regularSelection = { - startContainer: regularRange.startContainer, - startOffset: regularRange.startOffset, - endContainer: regularRange.endContainer, - endOffset: regularRange.endOffset, - isSelectionCollapsed: regularRange.collapsed, - }; - } - break; - - case SelectionRangeTypes.TableSelection: - if (range.coordinates && range.table) { - selectionRoot = range.table; - context.tableSelection = { - table: range.table, - firstCell: { ...range.coordinates.firstCell }, - lastCell: { ...range.coordinates.lastCell }, - }; - } - - break; - - case SelectionRangeTypes.ImageSelection: - selectionRoot = range.image; - context.imageSelection = { - image: range.image, - }; - break; + if (editorContext?.isRootRtl) { + context.blockFormat.direction = 'rtl'; } - if (selectionRoot) { - context.selectionRootNode = selectionRoot; + if (selection) { + context.rangeEx = selection; } return context; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts index b347c6a21b6..874a82291dc 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/domToContentModel.ts @@ -1,11 +1,7 @@ import { createContentModelDocument } from '../modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from './context/createDomToModelContext'; -import { isNodeOfType } from '../domUtils/isNodeOfType'; -import { NodeType } from 'roosterjs-editor-types'; import { normalizeContentModel } from '../modelApi/common/normalizeContentModel'; -import { parseFormat } from './utils/parseFormat'; -import { rootDirectionFormatHandler } from '../formatHandlers/root/rootDirectionFormatHandler'; -import { zoomScaleFormatHandler } from '../formatHandlers/root/zoomScaleFormatHandler'; +import { SelectionRangeEx } from 'roosterjs-editor-types'; import { ContentModelDocument, DomToModelOption, @@ -15,25 +11,19 @@ import { /** * Create Content Model from DOM tree in this editor * @param root Root element of DOM tree to create Content Model from - * @param editorContext Context of content model editor * @param option The option to customize the behavior of DOM to Content Model conversion + * @param editorContext Context of content model editor + * @param selection Existing selection range in editor * @returns A ContentModelDocument object that contains all the models created from the give root element */ export function domToContentModel( root: HTMLElement | DocumentFragment, + option?: DomToModelOption, editorContext?: EditorContext, - option?: DomToModelOption + selection?: SelectionRangeEx ): ContentModelDocument { const model = createContentModelDocument(editorContext?.defaultFormat); - const context = createDomToModelContext(editorContext, option); - - if (isNodeOfType(root, NodeType.Element)) { - // Need to calculate direction (ltr or rtl), use it as initial value - parseFormat(root, [rootDirectionFormatHandler.parse], context.blockFormat, context); - - // Need to calculate zoom scale value from root element, use this value to calculate sizes for elements - parseFormat(root, [zoomScaleFormatHandler.parse], context.zoomScaleFormat, context); - } + const context = createDomToModelContext(editorContext, option, selection); context.elementProcessors.child(model, root, context); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/childProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/childProcessor.ts index ba6278d81bf..eb3f1add170 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/childProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/childProcessor.ts @@ -1,7 +1,7 @@ import { addSelectionMarker } from '../utils/addSelectionMarker'; import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets'; import { isNodeOfType } from '../../domUtils/isNodeOfType'; -import { NodeType } from 'roosterjs-editor-types'; +import { NodeType, SelectionRangeTypes } from 'roosterjs-editor-types'; import { ContentModelBlockGroup, DomToModelContext, @@ -73,8 +73,8 @@ export function handleRegularSelection( addSelectionMarker(group, context); } - if (index == nodeEndOffset) { - if (!context.regularSelection!.isSelectionCollapsed) { + if (index == nodeEndOffset && context.rangeEx?.type == SelectionRangeTypes.Normal) { + if (!context.rangeEx.areAllCollapsed) { addSelectionMarker(group, context); } context.isInSelection = false; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/headingProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/headingProcessor.ts index e37c7a0f2ca..b097a834cea 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/headingProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/headingProcessor.ts @@ -3,6 +3,7 @@ import { blockProcessor } from './blockProcessor'; import { ContentModelSegmentFormat, ElementProcessor } from 'roosterjs-content-model-types'; import { createParagraph } from '../../modelApi/creators/createParagraph'; import { createParagraphDecorator } from '../../modelApi/creators/createParagraphDecorator'; +import { getObjectKeys } from 'roosterjs-editor-dom'; import { parseFormat } from '../utils/parseFormat'; import { stackFormat } from '../utils/stackFormat'; @@ -18,6 +19,13 @@ export const headingProcessor: ElementProcessor = (group, el parseFormat(element, context.formatParsers.segmentOnBlock, segmentFormat, context); + // These formats are already declared on heading element, no need to keep them in context. + // And we should not duplicate them in context, either. Because when we want to turn off header, + // inner text should not keep those text format from header. + getObjectKeys(segmentFormat).forEach(key => { + delete context.segmentFormat[key]; + }); + context.blockDecorator = createParagraphDecorator(element.tagName, segmentFormat); blockProcessor(group, element, context); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/imageProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/imageProcessor.ts index 81047abb436..5d1aae1ee7f 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/imageProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/imageProcessor.ts @@ -3,6 +3,7 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { ContentModelImageFormat, ElementProcessor } from 'roosterjs-content-model-types'; import { createImage } from '../../modelApi/creators/createImage'; import { parseFormat } from '../utils/parseFormat'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { stackFormat } from '../utils/stackFormat'; /** @@ -32,7 +33,10 @@ export const imageProcessor: ElementProcessor = (group, elemen if (context.isInSelection) { image.isSelected = true; } - if (context.imageSelection?.image == element) { + if ( + context.rangeEx?.type == SelectionRangeTypes.ImageSelection && + context.rangeEx.image == element + ) { image.isSelectedAsImageSelection = true; image.isSelected = true; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/tableProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/tableProcessor.ts index 32fc3ccca5f..4000189240d 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/tableProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/tableProcessor.ts @@ -3,6 +3,7 @@ import { createTable } from '../../modelApi/creators/createTable'; import { createTableCell } from '../../modelApi/creators/createTableCell'; import { getBoundingClientRect } from '../utils/getBoundingClientRect'; import { parseFormat } from '../utils/parseFormat'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { stackFormat } from '../utils/stackFormat'; import { ContentModelTableCellFormat, @@ -39,8 +40,16 @@ export const tableProcessor: ElementProcessor = ( parseFormat(tableElement, context.formatParsers.block, context.blockFormat, context); const table = createTable(tableElement.rows.length, context.blockFormat); - const { table: selectedTable, firstCell, lastCell } = context.tableSelection || {}; - const hasTableSelection = selectedTable == tableElement && !!firstCell && !!lastCell; + const tableSelection = + context.rangeEx?.type == SelectionRangeTypes.TableSelection + ? context.rangeEx + : null; + const selectedTable = tableSelection?.table; + const coordinates = tableSelection?.coordinates; + const hasTableSelection = + selectedTable == tableElement && + !!coordinates?.firstCell && + !!coordinates?.lastCell; if (context.allowCacheElement) { table.cachedElement = tableElement; @@ -59,7 +68,7 @@ export const tableProcessor: ElementProcessor = ( const columnPositions: number[] = [0]; const rowPositions: number[] = [0]; - const zoomScale = context.zoomScaleFormat.zoomScale || 1; + const zoomScale = context.zoomScale || 1; for (let row = 0; row < tableElement.rows.length; row++) { const tr = tableElement.rows[row]; @@ -213,10 +222,10 @@ export const tableProcessor: ElementProcessor = ( if ( (hasSelectionBeforeCell && hasSelectionAfterCell) || (hasTableSelection && - row >= firstCell.y && - row <= lastCell.y && - targetCol >= firstCell.x && - targetCol <= lastCell.x) + row >= coordinates.firstCell.y && + row <= coordinates.lastCell.y && + targetCol >= coordinates.firstCell.x && + targetCol <= coordinates.lastCell.x) ) { cell.isSelected = true; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts index 3b7a6b7219b..649f625274f 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/textProcessor.ts @@ -2,14 +2,14 @@ import { addDecorators } from '../../modelApi/common/addDecorators'; import { addSegment } from '../../modelApi/common/addSegment'; import { addSelectionMarker } from '../utils/addSelectionMarker'; import { areSameFormats } from '../utils/areSameFormats'; +import { createText } from '../../modelApi/creators/createText'; +import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets'; +import { hasSpacesOnly } from '../../domUtils/stringUtil'; import { ContentModelBlockGroup, DomToModelContext, ElementProcessor, } from 'roosterjs-content-model-types'; -import { createText } from '../../modelApi/creators/createText'; -import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets'; -import { hasSpacesOnly } from '../../domUtils/stringUtil'; /** * @internal @@ -35,7 +35,7 @@ export const textProcessor: ElementProcessor = ( if (txtEndOffset >= 0) { addTextSegment(group, txt.substring(0, txtEndOffset), context); - if (!context.regularSelection!.isSelectionCollapsed) { + if (context.rangeEx && !context.rangeEx.areAllCollapsed) { addSelectionMarker(group, context); } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/utils/getRegularSelectionOffsets.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/utils/getRegularSelectionOffsets.ts index 6f360ef8b56..dfd7a125443 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/utils/getRegularSelectionOffsets.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/utils/getRegularSelectionOffsets.ts @@ -1,4 +1,5 @@ import { DomToModelContext } from 'roosterjs-content-model-types'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; /** * Get offset numbers of a regular (range based) selection. @@ -11,14 +12,11 @@ export function getRegularSelectionOffsets( context: DomToModelContext, currentContainer: Node ): [number, number] { - let startOffset = - context.regularSelection?.startContainer == currentContainer - ? context.regularSelection.startOffset! - : -1; - let endOffset = - context.regularSelection?.endContainer == currentContainer - ? context.regularSelection.endOffset! - : -1; + const range = + context.rangeEx?.type == SelectionRangeTypes.Normal ? context.rangeEx.ranges[0] : null; + + let startOffset = range?.startContainer == currentContainer ? range.startOffset : -1; + let endOffset = range?.endContainer == currentContainer ? range.endOffset! : -1; return [startOffset, endOffset]; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/root/rootDirectionFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/root/rootDirectionFormatHandler.ts deleted file mode 100644 index c30f5159a06..00000000000 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/root/rootDirectionFormatHandler.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { DirectionFormat } from 'roosterjs-content-model-types'; -import { FormatHandler } from '../FormatHandler'; - -/** - * @internal - */ -export const rootDirectionFormatHandler: FormatHandler = { - parse: (format, element) => { - const style = element.ownerDocument.defaultView?.getComputedStyle(element); - - if (style?.direction == 'rtl') { - format.direction = 'rtl'; - } - }, - apply: () => {}, -}; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/root/zoomScaleFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/root/zoomScaleFormatHandler.ts deleted file mode 100644 index 742480a1b42..00000000000 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/root/zoomScaleFormatHandler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { FormatHandler } from '../FormatHandler'; -import { ZoomScaleFormat } from 'roosterjs-content-model-types'; - -/** - * @internal - */ -export const zoomScaleFormatHandler: FormatHandler = { - parse: (format, element) => { - const originalWidth = element.getBoundingClientRect().width; - const visualWidth = element.offsetWidth; - - format.zoomScale = - visualWidth > 0 && originalWidth > 0 - ? Math.round((originalWidth / visualWidth) * 100) / 100 - : 1; - }, - apply: () => {}, -}; diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/context/createDomToModelContextTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/context/createDomToModelContextTest.ts index 2f488ee1570..5570f2ece6b 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/context/createDomToModelContextTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/context/createDomToModelContextTest.ts @@ -2,7 +2,6 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createD import { defaultProcessorMap } from '../../../lib/domToModel/context/defaultProcessors'; import { defaultStyleMap } from '../../../lib/formatHandlers/utils/defaultStyles'; import { DomToModelListFormat, EditorContext } from 'roosterjs-content-model-types'; -import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { defaultFormatParsers, getFormatParsers, @@ -28,7 +27,6 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, - zoomScaleFormat: {}, isInSelection: false, listFormat, link: { @@ -58,7 +56,6 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, - zoomScaleFormat: {}, isInSelection: false, listFormat, link: { @@ -88,7 +85,6 @@ describe('createDomToModelContext', () => { ...editorContext, segmentFormat: {}, blockFormat: {}, - zoomScaleFormat: {}, isInSelection: false, listFormat, link: { @@ -107,81 +103,14 @@ describe('createDomToModelContext', () => { }); }); - it('with normal selection', () => { - const mockNode = ('Node' as any) as Node; - const mockedRange = ({ - startContainer: 'DIV 1', - startOffset: 0, - endContainer: 'DIV 2', - endOffset: 1, - collapsed: false, - commonAncestorContainer: mockNode, - } as any) as Range; - const context = createDomToModelContext(undefined, { - selectionRange: { - type: SelectionRangeTypes.Normal, - ranges: [mockedRange], - areAllCollapsed: false, - }, - }); - - expect(context).toEqual({ - ...editorContext, - segmentFormat: {}, - blockFormat: {}, - zoomScaleFormat: {}, - isInSelection: false, - regularSelection: { - startContainer: 'DIV 1' as any, - startOffset: 0, - endContainer: 'DIV 2' as any, - endOffset: 1, - isSelectionCollapsed: false, - }, - listFormat, - link: { - format: {}, - dataset: {}, - }, - code: { - format: {}, - }, - blockDecorator: { - format: {}, - tagName: '', - }, - selectionRootNode: mockNode, - allowCacheElement: true, - ...contextOptions, - }); - }); - - it('with table selection', () => { - const mockTable = ('Table' as any) as HTMLTableElement; - const context = createDomToModelContext(undefined, { - selectionRange: { - type: SelectionRangeTypes.TableSelection, - ranges: [], - areAllCollapsed: false, - table: mockTable, - coordinates: { - firstCell: { x: 1, y: 2 }, - lastCell: { x: 3, y: 4 }, - }, - }, - }); + it('with selection context', () => { + const selectionContext = { name: 'SelectionContext' } as any; + const context = createDomToModelContext(undefined, undefined, selectionContext); expect(context).toEqual({ - ...editorContext, segmentFormat: {}, blockFormat: {}, - zoomScaleFormat: {}, isInSelection: false, - tableSelection: { - table: mockTable, - firstCell: { x: 1, y: 2 }, - lastCell: { x: 3, y: 4 }, - }, listFormat, link: { format: {}, @@ -194,123 +123,9 @@ describe('createDomToModelContext', () => { format: {}, tagName: '', }, - selectionRootNode: mockTable, - allowCacheElement: true, - ...contextOptions, - }); - }); - - it('with image selection', () => { - const mockImage = ('Image' as any) as HTMLImageElement; - const context = createDomToModelContext(undefined, { - selectionRange: { - type: SelectionRangeTypes.ImageSelection, - ranges: [], - areAllCollapsed: false, - image: mockImage, - }, - }); - - expect(context).toEqual({ - ...editorContext, - segmentFormat: {}, - blockFormat: {}, - zoomScaleFormat: {}, - link: { - format: {}, - dataset: {}, - }, - code: { - format: {}, - }, - blockDecorator: { - format: {}, - tagName: '', - }, - isInSelection: false, - imageSelection: { - image: mockImage, - }, - listFormat, - selectionRootNode: mockImage, - allowCacheElement: true, - ...contextOptions, - }); - }); - - it('with base parameters and wrong selection 1', () => { - const context = createDomToModelContext( - { - isDarkMode: true, - }, - { - selectionRange: { - type: SelectionRangeTypes.Normal, - ranges: [], - areAllCollapsed: true, - }, - } - ); - - expect(context).toEqual({ - isDarkMode: true, - isInSelection: false, - blockFormat: {}, - zoomScaleFormat: {}, - segmentFormat: {}, - listFormat, - link: { - format: {}, - dataset: {}, - }, - code: { - format: {}, - }, - blockDecorator: { - format: {}, - tagName: '', - }, - allowCacheElement: true, - ...contextOptions, - }); - }); - - it('with base parameters and wrong selection 2', () => { - const context = createDomToModelContext( - { - isDarkMode: true, - }, - { - selectionRange: { - type: SelectionRangeTypes.TableSelection, - ranges: [], - areAllCollapsed: false, - table: null!, - coordinates: null!, - }, - } - ); - - expect(context).toEqual({ - isDarkMode: true, - isInSelection: false, - blockFormat: {}, - zoomScaleFormat: {}, - segmentFormat: {}, - link: { - format: {}, - dataset: {}, - }, - code: { - format: {}, - }, - blockDecorator: { - format: {}, - tagName: '', - }, - listFormat, allowCacheElement: true, ...contextOptions, + rangeEx: selectionContext, }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/domToContentModelTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/domToContentModelTest.ts index 9c069447636..889ff65b742 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/domToContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/domToContentModelTest.ts @@ -17,7 +17,6 @@ describe('domToContentModel', () => { child: childProcessor, }, defaultStyles: {}, - zoomScaleFormat: {}, segmentFormat: {}, } as any) as DomToModelContext; @@ -32,7 +31,7 @@ describe('domToContentModel', () => { fontSize: '10pt', }, }; - const model = domToContentModel(rootElement, editorContext, options); + const model = domToContentModel(rootElement, options, editorContext); const result: ContentModelDocument = { blockGroupType: 'Document', blocks: [], @@ -45,7 +44,8 @@ describe('domToContentModel', () => { expect(createDomToModelContext.createDomToModelContext).toHaveBeenCalledTimes(1); expect(createDomToModelContext.createDomToModelContext).toHaveBeenCalledWith( editorContext, - options + options, + undefined ); expect(elementProcessor).not.toHaveBeenCalled(); expect(childProcessor).toHaveBeenCalledTimes(1); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts index 8e17006c08d..c4d5e096909 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/childProcessorTest.ts @@ -2,6 +2,7 @@ import { childProcessor } from '../../../lib/domToModel/processors/childProcesso import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { generalProcessor } from '../../../lib/domToModel/processors/generalProcessor'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { ContentModelDocument, DomToModelContext, @@ -120,12 +121,18 @@ describe('childProcessor', () => { it('Process a DIV with element selection', () => { const div = document.createElement('div'); div.innerHTML = 'test1test2test3'; - context.regularSelection = { - startContainer: div, - startOffset: 1, - endContainer: div, - endOffset: 2, - isSelectionCollapsed: false, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: div, + startOffset: 1, + endContainer: div, + endOffset: 2, + collapsed: false, + } as any, + ], + areAllCollapsed: false, }; childProcessor(doc, div, context); @@ -150,13 +157,20 @@ describe('childProcessor', () => { it('Process a DIV with element collapsed selection', () => { const div = document.createElement('div'); + div.innerHTML = 'test1test2test3'; - context.regularSelection = { - startContainer: div, - startOffset: 1, - endContainer: div, - endOffset: 1, - isSelectionCollapsed: true, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: div, + startOffset: 1, + endContainer: div, + endOffset: 1, + collapsed: true, + } as any, + ], + areAllCollapsed: true, }; childProcessor(doc, div, context); @@ -180,13 +194,20 @@ describe('childProcessor', () => { it('Process a DIV with SPAN and text selection', () => { const div = document.createElement('div'); + div.innerHTML = 'test1test2test3'; - context.regularSelection = { - startContainer: div.firstChild!, - startOffset: 5, - endContainer: div.firstChild!, - endOffset: 10, - isSelectionCollapsed: false, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: div.firstChild!, + startOffset: 5, + endContainer: div.firstChild!, + endOffset: 10, + collapsed: false, + } as any, + ], + areAllCollapsed: false, }; childProcessor(doc, div, context); @@ -211,13 +232,20 @@ describe('childProcessor', () => { it('Process a DIV with SPAN and collapsed text selection', () => { const div = document.createElement('div'); + div.innerHTML = 'test1test2test3'; - context.regularSelection = { - startContainer: div.firstChild!, - startOffset: 5, - endContainer: div.firstChild!, - endOffset: 5, - isSelectionCollapsed: true, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: div.firstChild!, + startOffset: 5, + endContainer: div.firstChild!, + endOffset: 5, + collapsed: true, + } as any, + ], + areAllCollapsed: true, }; childProcessor(doc, div, context); @@ -241,13 +269,20 @@ describe('childProcessor', () => { it('Process a DIV with mixed selection', () => { const div = document.createElement('div'); + div.innerHTML = 'test1test2test3'; - context.regularSelection = { - startContainer: div.firstChild!, - startOffset: 1, - endContainer: div.lastChild!, - endOffset: 5, - isSelectionCollapsed: false, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: div.firstChild!, + startOffset: 1, + endContainer: div.lastChild!, + endOffset: 5, + collapsed: false, + } as any, + ], + areAllCollapsed: false, }; childProcessor(doc, div, context); @@ -283,13 +318,20 @@ describe('childProcessor', () => { it('Process with segment format and selection marker', () => { const div = document.createElement('div'); + context.segmentFormat = { a: 'b' } as any; - context.regularSelection = { - startContainer: div, - startOffset: 0, - endContainer: div, - endOffset: 0, - isSelectionCollapsed: true, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: div, + startOffset: 0, + endContainer: div, + endOffset: 0, + collapsed: true, + } as any, + ], + areAllCollapsed: true, }; childProcessor(doc, div, context); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts index 5ea983879a3..904613857e0 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts @@ -4,6 +4,7 @@ import { childProcessor as originalChildProcessor } from '../../../lib/domToMode import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { generalProcessor } from '../../../lib/domToModel/processors/generalProcessor'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { ContentModelGeneralBlock, ContentModelGeneralSegment, @@ -149,12 +150,18 @@ describe('generalProcessor', () => { const text = document.createTextNode('test'); span.appendChild(text); - context.regularSelection = { - startContainer: text, - startOffset: 1, - endContainer: text, - endOffset: 3, - isSelectionCollapsed: false, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: text, + startOffset: 1, + endContainer: text, + endOffset: 3, + collapsed: false, + } as any, + ], + areAllCollapsed: false, }; childProcessor.and.callFake(originalChildProcessor); @@ -213,12 +220,18 @@ describe('generalProcessor', () => { const text = document.createTextNode('test'); span.appendChild(text); - context.regularSelection = { - startContainer: text, - startOffset: 1, - endContainer: text, - endOffset: 3, - isSelectionCollapsed: false, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: text, + startOffset: 1, + endContainer: text, + endOffset: 3, + collapsed: false, + } as any, + ], + areAllCollapsed: false, }; context.isInSelection = true; diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/headingProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/headingProcessorTest.ts index cd46e5a18c7..84666a1d7bb 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/headingProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/headingProcessorTest.ts @@ -86,4 +86,46 @@ describe('headingProcessor', () => { ], }); }); + + it('header with format from context', () => { + const group = createContentModelDocument(); + const h1 = document.createElement('h1'); + + h1.style.fontSize = '40px'; + h1.textContent = 'test'; + context.segmentFormat.fontSize = '20px'; + + headingProcessor(group, h1, context); + + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + decorator: { + tagName: 'h1', + format: { fontSize: '40px', fontWeight: 'bold' }, + }, + segments: [ + { + segmentType: 'Text', + format: {}, + text: 'test', + }, + ], + }, + { + blockType: 'Paragraph', + format: {}, + segments: [], + isImplicit: true, + }, + ], + }); + + expect(context.segmentFormat).toEqual({ + fontSize: '20px', + }); + }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts index 99dbec24e55..a6b4848e0ce 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts @@ -2,6 +2,7 @@ import { createContentModelDocument } from '../../../lib/modelApi/creators/creat import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { DomToModelContext } from 'roosterjs-content-model-types'; import { imageProcessor } from '../../../lib/domToModel/processors/imageProcessor'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('imageProcessor', () => { let context: DomToModelContext; @@ -99,7 +100,10 @@ describe('imageProcessor', () => { const doc = createContentModelDocument(); const img = document.createElement('img'); - context.imageSelection = { image: img }; + context.rangeEx = { + type: SelectionRangeTypes.ImageSelection, + image: img, + } as any; imageProcessor(doc, img, context); @@ -132,7 +136,10 @@ describe('imageProcessor', () => { img.id = 'id1'; img.style.display = 'block'; - context.imageSelection = { image: img }; + context.rangeEx = { + type: SelectionRangeTypes.ImageSelection, + image: img, + } as any; imageProcessor(doc, img, context); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts index e87d77f80d4..ce86a2d02f1 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts @@ -5,6 +5,7 @@ import { childProcessor as originalChildProcessor } from '../../../lib/domToMode import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { tableProcessor } from '../../../lib/domToModel/processors/tableProcessor'; import { ContentModelBlock, @@ -279,17 +280,20 @@ describe('tableProcessor', () => { const div = document.createElement('div'); div.innerHTML = tableHTML; - context.tableSelection = { + context.rangeEx = { + type: SelectionRangeTypes.TableSelection, table: div.firstChild as HTMLTableElement, - firstCell: { - x: 1, - y: 0, - }, - lastCell: { - x: 1, - y: 1, + coordinates: { + firstCell: { + x: 1, + y: 0, + }, + lastCell: { + x: 1, + y: 1, + }, }, - }; + } as any; tdModel2.isSelected = true; tdModel4.isSelected = true; @@ -452,7 +456,7 @@ describe('tableProcessor with format', () => { } as any) as HTMLTableElement; const doc = createContentModelDocument(); - context.zoomScaleFormat.zoomScale = 2; + context.zoomScale = 2; tableProcessor(doc, mockedTable, context); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts index d1037870bf4..5217d221f35 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts @@ -5,6 +5,7 @@ import { createDomToModelContext } from '../../../lib/domToModel/context/createD import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { createText } from '../../../lib/modelApi/creators/createText'; import { DomToModelContext } from 'roosterjs-content-model-types'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { textProcessor } from '../../../lib/domToModel/processors/textProcessor'; describe('textProcessor', () => { @@ -356,12 +357,18 @@ describe('textProcessor', () => { const text = document.createTextNode('test'); context.link = { format: { href: '/test' }, dataset: {} }; - context.regularSelection = { - startContainer: text, - startOffset: 2, - endContainer: text, - endOffset: 2, - isSelectionCollapsed: true, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + startContainer: text, + startOffset: 2, + endContainer: text, + endOffset: 2, + collapsed: true, + } as any, + ], + areAllCollapsed: true, }; textProcessor(doc, text, context); diff --git a/packages-content-model/roosterjs-content-model-dom/test/endToEndTest.ts b/packages-content-model/roosterjs-content-model-dom/test/endToEndTest.ts index c940916af11..2a2ccc6dffb 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/endToEndTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/endToEndTest.ts @@ -28,9 +28,13 @@ describe('End to end test for DOM => Model', () => { const div1 = document.createElement('div'); div1.innerHTML = html; - const model = domToContentModel(div1, context, { - disableCacheElement: true, - }); + const model = domToContentModel( + div1, + { + disableCacheElement: true, + }, + context + ); expect(model).toEqual(expectedModel); @@ -842,6 +846,33 @@ describe('End to end test for DOM => Model', () => { ); }); + it('Header with format from context', () => { + runTest( + '

test

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + }, + ], + decorator: { + tagName: 'h1', + format: { fontSize: '40px', fontWeight: 'bold' }, + }, + }, + ], + }, + '

test

' + ); + }); + it('PREs', () => { runTest( '
aaa\nbbb
aaa\nbb
', diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts deleted file mode 100644 index 160bedb678f..00000000000 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/root/rootDirectionFormatHandlerTest.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; -import { rootDirectionFormatHandler } from '../../../lib/formatHandlers/root/rootDirectionFormatHandler'; -import { - DirectionFormat, - DomToModelContext, - ModelToDomContext, -} from 'roosterjs-content-model-types'; - -describe('rootDirectionFormatHandler.parse', () => { - let div: HTMLElement; - let context: DomToModelContext; - let format: DirectionFormat; - - beforeEach(() => { - div = document.createElement('div'); - context = createDomToModelContext(); - format = {}; - }); - - it('No direction', () => { - rootDirectionFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({}); - }); - - it('LTR from CSS', () => { - div.style.direction = 'ltr'; - - rootDirectionFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({}); - }); - - it('LTR from attribute', () => { - div.dir = 'ltr'; - - rootDirectionFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({}); - }); - - it('RTL from CSS', () => { - div.style.direction = 'rtl'; - - rootDirectionFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({}); - }); - - it('RTL from attribute', () => { - div.dir = 'rtl'; - - rootDirectionFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({}); - }); -}); - -describe('rootDirectionFormatHandler.apply', () => { - let div: HTMLElement; - let format: DirectionFormat; - let context: ModelToDomContext; - - beforeEach(() => { - div = document.createElement('div'); - format = {}; - context = createModelToDomContext(); - }); - - it('ltr', () => { - format.direction = 'ltr'; - - rootDirectionFormatHandler.apply(format, div, context); - - expect(div.outerHTML).toEqual('
'); - }); - - it('rtl', () => { - format.direction = 'rtl'; - - rootDirectionFormatHandler.apply(format, div, context); - - expect(div.outerHTML).toEqual('
'); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts deleted file mode 100644 index e1692fe6533..00000000000 --- a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/root/zoomScaleFormatHandlerTest.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; -import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; -import { zoomScaleFormatHandler } from '../../../lib/formatHandlers/root/zoomScaleFormatHandler'; -import { - DomToModelContext, - ModelToDomContext, - ZoomScaleFormat, -} from 'roosterjs-content-model-types'; - -describe('zoomScaleFormatHandler.parse', () => { - let div: HTMLElement; - let context: DomToModelContext; - let format: ZoomScaleFormat; - - beforeEach(() => { - div = ({ - tagName: 'DIV', - ownerDocument: { - defaultView: { - getComputedStyle: () => ({}), - }, - }, - getBoundingClientRect: () => ({ width: 100 }), - } as any) as HTMLElement; - context = createDomToModelContext(); - format = {}; - }); - - it('No zoom scale', () => { - (div).offsetWidth = undefined; - - zoomScaleFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({ - zoomScale: 1, - }); - }); - - it('Zoom scale = 1', () => { - (div).offsetWidth = 100; - - zoomScaleFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({ - zoomScale: 1, - }); - }); - - it('Zoom scale = 2', () => { - (div).offsetWidth = 50; - - zoomScaleFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({ - zoomScale: 2, - }); - }); - - it('Zoom scale = 0.5', () => { - (div).offsetWidth = 200; - - zoomScaleFormatHandler.parse(format, div, context, {}); - - expect(format).toEqual({ - zoomScale: 0.5, - }); - }); -}); - -describe('zoomScaleFormatHandler.apply', () => { - let div: HTMLElement; - let format: ZoomScaleFormat; - let context: ModelToDomContext; - - beforeEach(() => { - div = document.createElement('div'); - format = {}; - context = createModelToDomContext(); - }); - - it('zoom 1', () => { - format.zoomScale = 1; - - zoomScaleFormatHandler.apply(format, div, context); - - expect(div.outerHTML).toEqual('
'); - }); - - it('zoom 2', () => { - format.zoomScale = 2; - - zoomScaleFormatHandler.apply(format, div, context); - - expect(div.outerHTML).toEqual('
'); - }); -}); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/context/createModelToDomContextTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/context/createModelToDomContextTest.ts similarity index 100% rename from packages-content-model/roosterjs-content-model-dom/test/domToModel/context/createModelToDomContextTest.ts rename to packages-content-model/roosterjs-content-model-dom/test/modelToDom/context/createModelToDomContextTest.ts diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/reducedModelChildProcessor.ts b/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/reducedModelChildProcessor.ts index 9b77d08a7dd..3eb7dd78cd9 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/reducedModelChildProcessor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/reducedModelChildProcessor.ts @@ -1,5 +1,6 @@ import { contains, getTagOfNode } from 'roosterjs-editor-dom'; import { ContentModelBlockGroup, DomToModelContext } from 'roosterjs-content-model-types'; +import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode'; import { getRegularSelectionOffsets, handleRegularSelection, @@ -29,9 +30,11 @@ export function reducedModelChildProcessor( parent: ParentNode, context: FormatStateContext ) { - if (context.selectionRootNode) { + const selectionRootNode = getSelectionRootNode(context.rangeEx); + + if (selectionRootNode) { if (!context.nodeStack) { - context.nodeStack = createNodeStack(parent, context.selectionRootNode); + context.nodeStack = createNodeStack(parent, selectionRootNode); } const stackChild = context.nodeStack.pop(); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/tablePreProcessor.ts b/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/tablePreProcessor.ts index e57733b62ce..8b3b539a8a6 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/tablePreProcessor.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/domToModel/processors/tablePreProcessor.ts @@ -1,6 +1,7 @@ import { contains } from 'roosterjs-editor-dom'; import { DomToModelContext, ElementProcessor } from 'roosterjs-content-model-types'; import { entityProcessor, hasMetadata, tableProcessor } from 'roosterjs-content-model-dom'; +import { getSelectionRootNode } from '../../modelApi/selection/getSelectionRootNode'; /** * @internal @@ -17,16 +18,9 @@ function shouldUseTableProcessor(element: HTMLTableElement, context: DomToModelC // 2. Table is in selection // 3. There is selection inside table (or whole table is selected) // Otherwise, we treat the table as entity so we will not change it when write back - return hasMetadata(element) || context.isInSelection || hasSelectionInTable(element, context); -} - -function hasSelectionInTable(element: HTMLTableElement, context: DomToModelContext) { - const selectedNodes = [ - context.imageSelection?.image, - context.tableSelection?.table, - context.regularSelection?.startContainer, - context.regularSelection?.endContainer, - ]; - - return selectedNodes.some(n => contains(element, n, true /*treatSameNodeAsContain*/)); + return ( + hasMetadata(element) || + context.isInSelection || + contains(element, getSelectionRootNode(context.rangeEx), true /*treatSameNodeAsContain*/) + ); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts index 6809ad92675..dfb23225ab9 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createContentModel.ts @@ -28,9 +28,8 @@ function internalCreateContentModel( option: DomToModelOption | undefined ) { const context: DomToModelOption = { - selectionRange: core.api.getSelectionRangeEx(core), ...core.defaultDomToModelOptions, - ...(option || {}), + ...option, }; context.processorOverride = { @@ -43,5 +42,10 @@ function internalCreateContentModel( context.disableCacheElement = true; } - return domToContentModel(core.contentDiv, core.api.createEditorContext(core), context); + return domToContentModel( + core.contentDiv, + context, + core.api.createEditorContext(core), + core.api.getSelectionRangeEx(core) + ); } diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts index fc4e195babf..33af8997f97 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/coreApi/createEditorContext.ts @@ -1,14 +1,39 @@ import { CreateEditorContext } from '../../publicTypes/ContentModelEditorCore'; +import { EditorContext } from 'roosterjs-content-model-types'; /** * @internal * Create a EditorContext object used by ContentModel API */ export const createEditorContext: CreateEditorContext = core => { - return { - isDarkMode: core.lifecycle.isDarkMode, - defaultFormat: core.defaultFormat, - darkColorHandler: core.darkColorHandler, - addDelimiterForEntity: core.addDelimiterForEntity, + const { lifecycle, defaultFormat, darkColorHandler, addDelimiterForEntity, contentDiv } = core; + + const context: EditorContext = { + isDarkMode: lifecycle.isDarkMode, + defaultFormat: defaultFormat, + darkColorHandler: darkColorHandler, + addDelimiterForEntity: addDelimiterForEntity, }; + + checkRootRtl(contentDiv, context); + checkZoomScale(contentDiv, context); + + return context; }; + +function checkZoomScale(element: HTMLElement, context: EditorContext) { + const originalWidth = element?.getBoundingClientRect()?.width || 0; + const visualWidth = element.offsetWidth; + + if (visualWidth > 0 && originalWidth > 0) { + context.zoomScale = Math.round((originalWidth / visualWidth) * 100) / 100; + } +} + +function checkRootRtl(element: HTMLElement, context: EditorContext) { + const style = element?.ownerDocument.defaultView?.getComputedStyle(element); + + if (style?.direction == 'rtl') { + context.isRootRtl = true; + } +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts index a42d570b64b..89e8ae33c4a 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts @@ -132,6 +132,10 @@ function mergeParagraph( if (newPara.decorator) { newParagraph.decorator = { ...newPara.decorator }; } + + if (!mergeToCurrentParagraph) { + newParagraph.format = newPara.format; + } } function mergeTable( diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/getSelectionRootNode.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/getSelectionRootNode.ts new file mode 100644 index 00000000000..b99b34a0108 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/getSelectionRootNode.ts @@ -0,0 +1,16 @@ +import { SelectionRangeEx, SelectionRangeTypes } from 'roosterjs-editor-types'; + +/** + * @internal + */ +export function getSelectionRootNode(rangeEx: SelectionRangeEx | undefined): Node | undefined { + return !rangeEx + ? undefined + : rangeEx.type == SelectionRangeTypes.Normal + ? rangeEx.ranges[0]?.commonAncestorContainer + : rangeEx.type == SelectionRangeTypes.TableSelection + ? rangeEx.table + : rangeEx.type == SelectionRangeTypes.ImageSelection + ? rangeEx.image + : undefined; +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts index 20db121b5da..2d4c257a4d4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts @@ -63,23 +63,21 @@ export default function paste( event ); - const pasteModel = domToContentModel( - fragment, - { - isDarkMode: editor.isDarkMode(), - darkColorHandler: editor.getDarkColorHandler(), - defaultFormat: editor.getDefaultFormat(), + const pasteModel = domToContentModel(fragment, { + ...event.domToModelOption, + disableCacheElement: true, + additionalFormatParsers: { + ...event.domToModelOption.additionalFormatParsers, + block: [ + ...(event.domToModelOption.additionalFormatParsers?.block || []), + ...(applyCurrentFormat ? [blockElementParser] : []), + ], + listLevel: [ + ...(event.domToModelOption.additionalFormatParsers?.listLevel || []), + ...(applyCurrentFormat ? [blockElementParser] : []), + ], }, - { - ...event.domToModelOption, - disableCacheElement: true, - additionalFormatParsers: { - ...event.domToModelOption, - block: [...(applyCurrentFormat ? [blockElementParser] : [])], - listLevel: [...(applyCurrentFormat ? [blockElementParser] : [])], - }, - } - ); + }); if (pasteModel) { formatWithContentModel( diff --git a/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/reducedModelChildProcessorTest.ts b/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/reducedModelChildProcessorTest.ts index 0f0f8ae2388..b51c67cb269 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/reducedModelChildProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/reducedModelChildProcessorTest.ts @@ -1,6 +1,7 @@ import { createContentModelDocument, createDomToModelContext } from 'roosterjs-content-model-dom'; import { DomToModelContext } from 'roosterjs-content-model-types'; import { reducedModelChildProcessor } from '../../../lib/domToModel/processors/reducedModelChildProcessor'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; describe('reducedModelChildProcessor', () => { let context: DomToModelContext; @@ -33,7 +34,15 @@ describe('reducedModelChildProcessor', () => { div.appendChild(span); span.textContent = 'test'; - context.selectionRootNode = span; + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: span, + } as any, + ], + areAllCollapsed: false, + }; reducedModelChildProcessor(doc, div, context); @@ -69,7 +78,15 @@ describe('reducedModelChildProcessor', () => { span1.textContent = 'test1'; span2.textContent = 'test2'; span3.textContent = 'test3'; - context.selectionRootNode = span2; + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: span2, + } as any, + ], + areAllCollapsed: false, + }; reducedModelChildProcessor(doc, div, context); @@ -105,7 +122,15 @@ describe('reducedModelChildProcessor', () => { span1.textContent = 'test1'; span2.innerHTML = '
line1
line2
'; span3.textContent = 'test3'; - context.selectionRootNode = span2; + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: span2, + } as any, + ], + areAllCollapsed: false, + }; reducedModelChildProcessor(doc, div, context); @@ -167,7 +192,16 @@ describe('reducedModelChildProcessor', () => { span1.textContent = 'test1'; span2.innerHTML = '
line1
line2
'; span3.textContent = 'test3'; - context.selectionRootNode = span2; + + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: span2, + } as any, + ], + areAllCollapsed: false, + }; reducedModelChildProcessor(doc, div1, context); @@ -208,7 +242,15 @@ describe('reducedModelChildProcessor', () => { const div = document.createElement('div'); div.innerHTML = 'aa
test1test2
bb'; - context.selectionRootNode = div.querySelector('#selection') as HTMLElement; + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: div.querySelector('#selection') as HTMLElement, + } as any, + ], + areAllCollapsed: false, + }; reducedModelChildProcessor(doc, div, context); diff --git a/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/tablePreProcessorTest.ts b/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/tablePreProcessorTest.ts index ce8a3c3b565..348a0688197 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/tablePreProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/domToModel/processors/tablePreProcessorTest.ts @@ -1,5 +1,6 @@ import * as tableProcessor from 'roosterjs-content-model-dom/lib/domToModel/processors/tableProcessor'; import { createContentModelDocument, createDomToModelContext } from 'roosterjs-content-model-dom'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { tablePreProcessor } from '../../../lib/domToModel/processors/tablePreProcessor'; describe('tablePreProcessor', () => { @@ -56,9 +57,15 @@ describe('tablePreProcessor', () => { tr.appendChild(td); td.appendChild(txt); - context.regularSelection = { - startContainer: txt, - } as any; + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: txt, + } as any, + ], + areAllCollapsed: false, + }; tablePreProcessor(group, table, context); @@ -82,8 +89,14 @@ describe('tablePreProcessor', () => { tr.appendChild(td); td.appendChild(txt); - context.regularSelection = { - endContainer: txt, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: txt, + } as any, + ], + areAllCollapsed: false, } as any; tablePreProcessor(group, table, context); @@ -108,8 +121,14 @@ describe('tablePreProcessor', () => { tr.appendChild(td); td.appendChild(txt); - context.regularSelection = { - startContainer: table, + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: table, + } as any, + ], + areAllCollapsed: false, } as any; tablePreProcessor(group, table, context); @@ -134,8 +153,13 @@ describe('tablePreProcessor', () => { tr.appendChild(td); td.appendChild(txt); - context.tableSelection = { + context.rangeEx = { + type: SelectionRangeTypes.TableSelection, table, + coordinates: { + firstCell: {}, + lastCell: {}, + }, } as any; tablePreProcessor(group, table, context); @@ -160,8 +184,9 @@ describe('tablePreProcessor', () => { tr.appendChild(td); td.appendChild(txt); - context.imageSelection = { - image: txt, + context.rangeEx = { + type: SelectionRangeTypes.ImageSelection, + image: txt as any, } as any; tablePreProcessor(group, table, context); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts index 7a31ca06c0c..e3e5b8a24cc 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/ContentModelEditorTest.ts @@ -29,17 +29,21 @@ describe('ContentModelEditor', () => { expect(model).toBe(mockedResult); expect(domToContentModel.domToContentModel).toHaveBeenCalledTimes(1); - expect(domToContentModel.domToContentModel).toHaveBeenCalledWith(div, editorContext, { - selectionRange: { + expect(domToContentModel.domToContentModel).toHaveBeenCalledWith( + div, + { + processorOverride: { + table: tablePreProcessor, + }, + disableCacheElement: true, + }, + editorContext, + { type: SelectionRangeTypes.Normal, - areAllCollapsed: true, ranges: [], - }, - processorOverride: { - table: tablePreProcessor, - }, - disableCacheElement: true, - }); + areAllCollapsed: true, + } + ); }); it('domToContentModel, with Reuse Content Model dont add disableCacheElement option', () => { @@ -57,16 +61,20 @@ describe('ContentModelEditor', () => { expect(model).toBe(mockedResult); expect(domToContentModel.domToContentModel).toHaveBeenCalledTimes(1); - expect(domToContentModel.domToContentModel).toHaveBeenCalledWith(div, editorContext, { - selectionRange: { + expect(domToContentModel.domToContentModel).toHaveBeenCalledWith( + div, + { + processorOverride: { + table: tablePreProcessor, + }, + }, + editorContext, + { type: SelectionRangeTypes.Normal, - areAllCollapsed: true, ranges: [], - }, - processorOverride: { - table: tablePreProcessor, - }, - }); + areAllCollapsed: true, + } + ); }); it('setContentModel with normal selection', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts index 80088ba436c..6697e67e044 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createContentModelTest.ts @@ -3,10 +3,10 @@ import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/d import { ContentModelEditorCore } from '../../../lib/publicTypes/ContentModelEditorCore'; import { createContentModel } from '../../../lib/editor/coreApi/createContentModel'; import { DomToModelOption } from 'roosterjs-content-model-types'; +import { SelectionRangeTypes } from 'roosterjs-editor-types'; import { tablePreProcessor } from '../../../lib/domToModel/processors/tablePreProcessor'; const mockedEditorContext = 'EDITORCONTEXT' as any; -const mockedRange = 'RANGE' as any; const mockedModel = 'MODEL' as any; const mockedDiv = 'DIV' as any; const mockedCachedMode = 'CACHEDMODEL' as any; @@ -23,7 +23,7 @@ describe('createContentModel', () => { createEditorContext = jasmine .createSpy('createEditorContext') .and.returnValue(mockedEditorContext); - getSelectionRangeEx = jasmine.createSpy('getSelectionRangeEx').and.returnValue(mockedRange); + getSelectionRangeEx = jasmine.createSpy('getSelectionRangeEx').and.returnValue(null); domToContentModelSpy = spyOn(domToContentModel, 'domToContentModel').and.returnValue( mockedModel @@ -48,13 +48,17 @@ describe('createContentModel', () => { expect(createEditorContext).toHaveBeenCalledWith(core); expect(getSelectionRangeEx).toHaveBeenCalledWith(core); - expect(domToContentModelSpy).toHaveBeenCalledWith(mockedDiv, mockedEditorContext, { - ...option, - selectionRange: mockedRange, - processorOverride: { - table: tablePreProcessor, + expect(domToContentModelSpy).toHaveBeenCalledWith( + mockedDiv, + { + ...option, + processorOverride: { + table: tablePreProcessor, + }, }, - }); + mockedEditorContext, + null + ); expect(model).toBe(mockedModel); }); @@ -68,13 +72,17 @@ describe('createContentModel', () => { expect(createEditorContext).toHaveBeenCalledWith(core); expect(getSelectionRangeEx).toHaveBeenCalledWith(core); - expect(domToContentModelSpy).toHaveBeenCalledWith(mockedDiv, mockedEditorContext, { - selectionRange: mockedRange, - processorOverride: { - table: tablePreProcessor, + expect(domToContentModelSpy).toHaveBeenCalledWith( + mockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + ...defaultOption, }, - ...defaultOption, - }); + mockedEditorContext, + null + ); expect(model).toBe(mockedModel); }); @@ -88,14 +96,18 @@ describe('createContentModel', () => { expect(createEditorContext).toHaveBeenCalledWith(core); expect(getSelectionRangeEx).toHaveBeenCalledWith(core); - expect(domToContentModelSpy).toHaveBeenCalledWith(mockedDiv, mockedEditorContext, { - selectionRange: mockedRange, - processorOverride: { - table: tablePreProcessor, + expect(domToContentModelSpy).toHaveBeenCalledWith( + mockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + ...defaultOption, + ...additionalOption, }, - ...defaultOption, - ...additionalOption, - }); + mockedEditorContext, + null + ); expect(model).toBe(mockedModel); }); @@ -109,13 +121,17 @@ describe('createContentModel', () => { expect(createEditorContext).toHaveBeenCalledWith(core); expect(getSelectionRangeEx).toHaveBeenCalledWith(core); - expect(domToContentModelSpy).toHaveBeenCalledWith(mockedDiv, mockedEditorContext, { - selectionRange: mockedRange, - disableCacheElement: false, - processorOverride: { - table: tablePreProcessor, + expect(domToContentModelSpy).toHaveBeenCalledWith( + mockedDiv, + { + disableCacheElement: false, + processorOverride: { + table: tablePreProcessor, + }, }, - }); + mockedEditorContext, + null + ); expect(model).toBe(mockedModel); }); @@ -147,3 +163,168 @@ describe('createContentModel', () => { expect(model).toBe(mockedClonedModel); }); }); + +describe('createContentModel with selection', () => { + let getSelectionRangeExSpy: jasmine.Spy; + let domToContentModelSpy: jasmine.Spy; + let createEditorContextSpy: jasmine.Spy; + let core: any; + const MockedDiv = 'CONTENT_DIV' as any; + + beforeEach(() => { + getSelectionRangeExSpy = jasmine.createSpy('getSelectionRangeEx'); + domToContentModelSpy = spyOn(domToContentModel, 'domToContentModel'); + createEditorContextSpy = jasmine.createSpy('createEditorContext'); + + core = { + contentDiv: MockedDiv, + api: { + getSelectionRangeEx: getSelectionRangeExSpy, + createEditorContext: createEditorContextSpy, + }, + }; + }); + + it('Regular selection', () => { + const MockedContainer = 'MockedContainer'; + const MockedRange = { + name: 'MockedRange', + commonAncestorContainer: MockedContainer, + } as any; + + getSelectionRangeExSpy.and.returnValue({ + type: SelectionRangeTypes.Normal, + ranges: [MockedRange], + }); + + createContentModel(core); + + expect(domToContentModelSpy).toHaveBeenCalledTimes(1); + expect(domToContentModelSpy).toHaveBeenCalledWith( + MockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + disableCacheElement: true, + }, + undefined, + { + type: SelectionRangeTypes.Normal, + ranges: [MockedRange], + } + ); + }); + + it('Table selection', () => { + const MockedContainer = 'MockedContainer'; + const MockedFirstCell = { name: 'FirstCell' }; + const MockedLastCell = { name: 'LastCell' }; + + getSelectionRangeExSpy.and.returnValue({ + type: SelectionRangeTypes.TableSelection, + table: MockedContainer, + coordinates: { + firstCell: MockedFirstCell, + lastCell: MockedLastCell, + }, + }); + + createContentModel(core); + + expect(domToContentModelSpy).toHaveBeenCalledTimes(1); + expect(domToContentModelSpy).toHaveBeenCalledWith( + MockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + disableCacheElement: true, + }, + undefined, + { + type: SelectionRangeTypes.TableSelection, + table: MockedContainer, + coordinates: { + firstCell: MockedFirstCell, + lastCell: MockedLastCell, + }, + } + ); + }); + + it('Image selection', () => { + const MockedContainer = 'MockedContainer'; + + getSelectionRangeExSpy.and.returnValue({ + type: SelectionRangeTypes.ImageSelection, + image: MockedContainer, + }); + + createContentModel(core); + + expect(domToContentModelSpy).toHaveBeenCalledTimes(1); + expect(domToContentModelSpy).toHaveBeenCalledWith( + MockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + disableCacheElement: true, + }, + undefined, + { + type: SelectionRangeTypes.ImageSelection, + image: MockedContainer, + } + ); + }); + + it('Incorrect regular selection', () => { + getSelectionRangeExSpy.and.returnValue({ + type: SelectionRangeTypes.Normal, + ranges: [], + }); + + createContentModel(core); + + expect(domToContentModelSpy).toHaveBeenCalledTimes(1); + expect(domToContentModelSpy).toHaveBeenCalledWith( + MockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + disableCacheElement: true, + }, + undefined, + { + type: SelectionRangeTypes.Normal, + ranges: [], + } + ); + }); + + it('Incorrect table selection', () => { + getSelectionRangeExSpy.and.returnValue({ + type: SelectionRangeTypes.TableSelection, + }); + + createContentModel(core); + + expect(domToContentModelSpy).toHaveBeenCalledTimes(1); + expect(domToContentModelSpy).toHaveBeenCalledWith( + MockedDiv, + { + processorOverride: { + table: tablePreProcessor, + }, + disableCacheElement: true, + }, + undefined, + { + type: SelectionRangeTypes.TableSelection, + } + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts index e4b676fdd67..659c83de790 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/coreApi/createEditorContextTest.ts @@ -7,8 +7,148 @@ describe('createEditorContext', () => { const defaultFormat = 'DEFAULTFORMAT' as any; const darkColorHandler = 'DARKHANDLER' as any; const addDelimiterForEntity = 'ADDDELIMITER' as any; + const getComputedStyleSpy = jasmine.createSpy('getComputedStyleSpy'); + const getBoundingClientRectSpy = jasmine.createSpy('getBoundingClientRect'); + + const div = { + ownerDocument: { + defaultView: { + getComputedStyle: getComputedStyleSpy, + }, + }, + getBoundingClientRect: getBoundingClientRectSpy, + }; const core = ({ + contentDiv: div, + lifecycle: { + isDarkMode, + }, + defaultFormat, + darkColorHandler, + addDelimiterForEntity, + } as any) as ContentModelEditorCore; + + const context = createEditorContext(core); + + expect(context).toEqual({ + isDarkMode, + darkColorHandler, + defaultFormat, + addDelimiterForEntity, + }); + }); +}); + +describe('createEditorContext - checkZoomScale', () => { + let core: ContentModelEditorCore; + let div: any; + let getComputedStyleSpy: jasmine.Spy; + let getBoundingClientRectSpy: jasmine.Spy; + const isDarkMode = 'DARKMODE' as any; + const defaultFormat = 'DEFAULTFORMAT' as any; + const darkColorHandler = 'DARKHANDLER' as any; + const addDelimiterForEntity = 'ADDDELIMITER' as any; + + beforeEach(() => { + getComputedStyleSpy = jasmine.createSpy('getComputedStyleSpy'); + getBoundingClientRectSpy = jasmine.createSpy('getBoundingClientRect'); + + div = { + ownerDocument: { + defaultView: { + getComputedStyle: getComputedStyleSpy, + }, + }, + getBoundingClientRect: getBoundingClientRectSpy, + }; + core = ({ + contentDiv: div, + lifecycle: { + isDarkMode, + }, + defaultFormat, + darkColorHandler, + addDelimiterForEntity, + } as any) as ContentModelEditorCore; + }); + + it('Zoom scale = 1', () => { + div.offsetWidth = 100; + getBoundingClientRectSpy.and.returnValue({ + width: 100, + }); + + const context = createEditorContext(core); + + expect(context).toEqual({ + isDarkMode, + defaultFormat, + darkColorHandler, + addDelimiterForEntity, + zoomScale: 1, + }); + }); + + it('Zoom scale = 2', () => { + div.offsetWidth = 50; + getBoundingClientRectSpy.and.returnValue({ + width: 100, + }); + + const context = createEditorContext(core); + + expect(context).toEqual({ + isDarkMode, + defaultFormat, + darkColorHandler, + addDelimiterForEntity, + zoomScale: 2, + }); + }); + + it('Zoom scale = 0.5', () => { + div.offsetWidth = 200; + getBoundingClientRectSpy.and.returnValue({ + width: 100, + }); + + const context = createEditorContext(core); + + expect(context).toEqual({ + isDarkMode, + defaultFormat, + darkColorHandler, + addDelimiterForEntity, + zoomScale: 0.5, + }); + }); +}); + +describe('createEditorContext - checkRootDir', () => { + let core: ContentModelEditorCore; + let div: any; + let getComputedStyleSpy: jasmine.Spy; + let getBoundingClientRectSpy: jasmine.Spy; + const isDarkMode = 'DARKMODE' as any; + const defaultFormat = 'DEFAULTFORMAT' as any; + const darkColorHandler = 'DARKHANDLER' as any; + const addDelimiterForEntity = 'ADDDELIMITER' as any; + + beforeEach(() => { + getComputedStyleSpy = jasmine.createSpy('getComputedStyleSpy'); + getBoundingClientRectSpy = jasmine.createSpy('getBoundingClientRect'); + + div = { + ownerDocument: { + defaultView: { + getComputedStyle: getComputedStyleSpy, + }, + }, + getBoundingClientRect: getBoundingClientRectSpy, + }; + core = ({ + contentDiv: div, lifecycle: { isDarkMode, }, @@ -16,14 +156,36 @@ describe('createEditorContext', () => { darkColorHandler, addDelimiterForEntity, } as any) as ContentModelEditorCore; + }); + + it('LTR CSS', () => { + getComputedStyleSpy.and.returnValue({ + direction: 'ltr', + }); const context = createEditorContext(core); expect(context).toEqual({ isDarkMode, + defaultFormat, darkColorHandler, + addDelimiterForEntity, + }); + }); + + it('RTL', () => { + getComputedStyleSpy.and.returnValue({ + direction: 'rtl', + }); + + const context = createEditorContext(core); + + expect(context).toEqual({ + isDarkMode, defaultFormat, + darkColorHandler, addDelimiterForEntity, + isRootRtl: true, }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts index 87d158942be..afc5e6f2e16 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/linkParserTest.ts @@ -22,13 +22,7 @@ describe('link parser test', () => { link: [parseLink], }; - const model = domToContentModel( - fragment, - { - isDarkMode: false, - }, - event.domToModelOption - ); + const model = domToContentModel(fragment, event.domToModelOption); if (expectedModel) { expect(model).toEqual(expectedModel); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts index e0bae2d44e2..e0cc430d002 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromExcelTest.ts @@ -22,16 +22,10 @@ describe('processPastedContentFromExcelTest', () => { event.clipboardData.html = source; processPastedContentFromExcel(event, (s: string) => s); - const model = domToContentModel( - fragment, - { - isDarkMode: false, - }, - { - ...event.domToModelOption, - disableCacheElement: true, - } - ); + const model = domToContentModel(fragment, { + ...event.domToModelOption, + disableCacheElement: true, + }); if (expectedModel) { expect(model).toEqual(expectedModel); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts index ef284a1fe00..c13ab8a4577 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWacTest.ts @@ -19,15 +19,9 @@ describe('processPastedContentFromWacTest', () => { const event = createBeforePasteEventMock(fragment); processPastedContentWacComponents(event); - const model = domToContentModel( - fragment, - { - isDarkMode: false, - }, - { - ...event.domToModelOption, - } - ); + const model = domToContentModel(fragment, { + ...event.domToModelOption, + }); if (expectedModel) { expect(model).toEqual(expectedModel); } @@ -129,16 +123,10 @@ describe('wordOnlineHandler', () => { const event = createBeforePasteEventMock(fragment); processPastedContentWacComponents(event); - const model = domToContentModel( - fragment, - { - isDarkMode: false, - }, - { - ...event.domToModelOption, - disableCacheElement: true, - } - ); + const model = domToContentModel(fragment, { + ...event.domToModelOption, + disableCacheElement: true, + }); if (expectedModel) { expect(model).toEqual(expectedModel); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts index f18688aeb97..8cbd3a33ff3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/paste/processPastedContentFromWordDesktopTest.ts @@ -20,16 +20,10 @@ describe('processPastedContentFromWordDesktopTest', () => { const event = createBeforePasteEventMock(fragment); processPastedContentFromWordDesktop(event); - const model = domToContentModel( - fragment, - { - isDarkMode: false, - }, - { - ...event.domToModelOption, - disableCacheElement: true, - } - ); + const model = domToContentModel(fragment, { + ...event.domToModelOption, + disableCacheElement: true, + }); if (expectedModel) { expect(model).toEqual(expectedModel); } @@ -102,7 +96,6 @@ describe('processPastedContentFromWordDesktopTest', () => { let source = 'TestTest'; runTest(source, 'TestTest', { blockGroupType: 'Document', - blocks: [ { blockType: 'Paragraph', @@ -950,6 +943,105 @@ describe('processPastedContentFromWordDesktopTest', () => { ); }); + it('Lists with margins', () => { + const source = + '

Test

Test

ยท      \nTEST

'; + runTest(source, undefined, { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '12pt', + }, + }, + ], + format: { + marginTop: '0in', + marginRight: '0in', + marginBottom: '0in', + marginLeft: '0in', + }, + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '12pt', + }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '12pt', + }, + }, + ], + format: { + marginTop: '0in', + marginRight: '0in', + marginBottom: '0in', + marginLeft: '0in', + }, + segmentFormat: { + fontFamily: 'Calibri, sans-serif', + fontSize: '12pt', + }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'TEST', + format: { + fontFamily: 'Calibri, sans-serif', + fontSize: '12pt', + }, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + marginTop: '0in', + marginRight: '0in', + marginBottom: undefined, + marginLeft: undefined, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + format: { + marginTop: '0in', + marginRight: undefined, + marginBottom: '0in', + marginLeft: undefined, + }, + }, + ], + }); + }); + /** * Test * 1. Test diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts index 480e5ba67b3..3fa542c5ecc 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts @@ -1968,4 +1968,280 @@ describe('mergeModel', () => { format: MockedFormat, }); }); + + it('Merge Table + Paragraph', () => { + const majorModel = createContentModelDocument(); + const sourceModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + width: '120px', + height: '22px', + useBorderBox: true, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + width: '120px', + height: '22px', + useBorderBox: true, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + width: '120px', + height: '22px', + useBorderBox: true, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + useBorderBox: true, + borderCollapse: true, + }, + widths: [120, 120, 120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0}', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'black', + fontWeight: 'bold', + }, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const para1 = createParagraph(); + const marker = createSelectionMarker(); + + para1.segments.push(marker); + majorModel.blocks.push(para1); + + mergeModel(majorModel, sourceModel, onDeleteEntityMock, { + mergeFormat: 'mergeAll', + }); + + expect(majorModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 22, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + width: '120px', + height: '22px', + useBorderBox: true, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + width: '120px', + height: '22px', + useBorderBox: true, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + format: { + borderTop: '1px solid rgb(171, 171, 171)', + borderRight: '1px solid rgb(171, 171, 171)', + borderBottom: '1px solid rgb(171, 171, 171)', + borderLeft: '1px solid rgb(171, 171, 171)', + width: '120px', + height: '22px', + useBorderBox: true, + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: { + useBorderBox: true, + borderCollapse: true, + }, + widths: [120, 120, 120], + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0}', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'black', + fontWeight: 'bold', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + }); + }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts index 64438a923aa..648e903b823 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getFormatStateTest.ts @@ -1,7 +1,7 @@ import * as getPendingFormat from '../../../lib/modelApi/format/pendingFormat'; import * as retrieveModelFormatState from '../../../lib/modelApi/common/retrieveModelFormatState'; import getFormatState from '../../../lib/publicApi/format/getFormatState'; -import { FormatState } from 'roosterjs-editor-types'; +import { FormatState, SelectionRangeTypes } from 'roosterjs-editor-types'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; import { createContentModelDocument, @@ -46,7 +46,15 @@ describe('getFormatState', () => { }); if (selectedNode) { - context.selectionRootNode = selectedNode; + context.rangeEx = { + type: SelectionRangeTypes.Normal, + ranges: [ + { + commonAncestorContainer: selectedNode, + } as any, + ], + areAllCollapsed: false, + }; } context.elementProcessors.child(model, editorDiv, context); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getSegmentFormatTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getSegmentFormatTest.ts index b7f100456dd..e0f923216c3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getSegmentFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/getSegmentFormatTest.ts @@ -34,27 +34,20 @@ describe('getSegmentFormat', () => { editorDiv.innerHTML = html; const selectedNode = editorDiv.querySelector('#' + selectedNodeId); - const context = createDomToModelContext(undefined, { - ...(options || {}), - selectionRange: selectedNode + const range = + selectedNode && + createRange(selectedNode, PositionType.Begin, selectedNode, PositionType.End); + const context = createDomToModelContext( + undefined, + options, + range ? { type: SelectionRangeTypes.Normal, - ranges: [ - createRange( - selectedNode, - PositionType.Begin, - selectedNode, - PositionType.End - ), - ], - areAllCollapsed: false, + ranges: [range], + areAllCollapsed: range.collapsed, } - : undefined, - }); - - if (selectedNode) { - context.selectionRootNode = selectedNode; - } + : undefined + ); context.elementProcessors.child(model, editorDiv, context); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts index e4ae664344c..25846d896db 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts @@ -343,18 +343,11 @@ describe('changeFontSize', () => { const editor = ({ createContentModel: (option: any) => - domToContentModel( - div, - { isDarkMode: false }, - { - selectionRange: { - type: SelectionRangeTypes.Normal, - areAllCollapsed: false, - ranges: [createRange(sub)], - }, - ...option, - } - ), + domToContentModel(div, option, undefined, { + type: SelectionRangeTypes.Normal, + ranges: [createRange(sub)], + areAllCollapsed: false, + }), addUndoSnapshot, focus: jasmine.createSpy(), setContentModel, diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts index c6a0326b48c..1919164c03b 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts @@ -20,9 +20,6 @@ describe('Paste ', () => { let getDocument: jasmine.Spy; let getTrustedHTMLHandler: jasmine.Spy; let triggerPluginEvent: jasmine.Spy; - let isDarkMode: jasmine.Spy; - let getDarkColorHandler: jasmine.Spy; - let getDefaultFormat: jasmine.Spy; let undoSnapshotResult: any; const mockedPos = 'POS' as any; @@ -54,13 +51,10 @@ describe('Paste ', () => { createContentModel = jasmine.createSpy('createContentModel').and.returnValue(mockedModel); setContentModel = jasmine.createSpy('setContentModel'); focus = jasmine.createSpy('focus'); - isDarkMode = jasmine.createSpy('isDarkMode'); getFocusedPosition = jasmine.createSpy('getFocusedPosition').and.returnValue(mockedPos); getContent = jasmine.createSpy('getContent'); triggerPluginEvent = jasmine.createSpy('triggerPluginEvent'); getSelectionRange = jasmine.createSpy('getSelectionRange'); - getDarkColorHandler = jasmine.createSpy('getDarkColorHandler'); - getDefaultFormat = jasmine.createSpy('getDefaultFormat'); getDocument = jasmine.createSpy('getDocument').and.returnValue(document); getTrustedHTMLHandler = jasmine .createSpy('getTrustedHTMLHandler') @@ -78,9 +72,6 @@ describe('Paste ', () => { getDocument, getTrustedHTMLHandler, triggerPluginEvent, - isDarkMode, - getDarkColorHandler, - getDefaultFormat, } as any) as IContentModelEditor; }); @@ -95,13 +86,10 @@ describe('Paste ', () => { expect(setContentModel).toHaveBeenCalled(); expect(focus).toHaveBeenCalled(); expect(addUndoSnapshot).toHaveBeenCalled(); - expect(isDarkMode).toHaveBeenCalled(); expect(getFocusedPosition).not.toHaveBeenCalled(); expect(getContent).toHaveBeenCalled(); expect(triggerPluginEvent).toHaveBeenCalled(); expect(getSelectionRange).toHaveBeenCalled(); - expect(getDarkColorHandler).toHaveBeenCalled(); - expect(getDefaultFormat).toHaveBeenCalled(); expect(getDocument).toHaveBeenCalled(); expect(getTrustedHTMLHandler).toHaveBeenCalled(); expect(mockedModel).toEqual(mockedMergeModel); diff --git a/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts b/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts index 5d575f7b769..8b264a26589 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelFormatContext.ts @@ -5,7 +5,6 @@ import { ContentModelLink } from '../decorator/ContentModelLink'; import { ContentModelListItemLevelFormat } from '../format/ContentModelListItemLevelFormat'; import { ContentModelParagraphDecorator } from '../decorator/ContentModelParagraphDecorator'; import { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; -import { ZoomScaleFormat } from '../format/formatParts/ZoomScaleFormat'; /** * Represents the context object used when do DOM to Content Model conversion and processing a List @@ -46,11 +45,6 @@ export interface DomToModelFormatContext { */ listFormat: DomToModelListFormat; - /** - * Zoom scale of the content - */ - zoomScaleFormat: ZoomScaleFormat; - /** * Whether put the source element into Content Model when possible. * When pass true, this cached element will be used to create DOM tree back when convert Content Model to DOM diff --git a/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelOption.ts b/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelOption.ts index 172427850da..9a40f7e7906 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelOption.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelOption.ts @@ -1,4 +1,3 @@ -import { SelectionRangeEx } from 'roosterjs-editor-types'; import { DefaultStyleMap, ElementProcessorMap, @@ -10,11 +9,6 @@ import { * Options for creating DomToModelContext */ export interface DomToModelOption { - /** - * Selection range to be included in Content Model - */ - selectionRange?: SelectionRangeEx; - /** * Overrides default element processors */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelSelectionContext.ts b/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelSelectionContext.ts index 8098816b2d1..dd30c8991ff 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelSelectionContext.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/context/DomToModelSelectionContext.ts @@ -1,64 +1,4 @@ -import { Coordinates } from 'roosterjs-editor-types'; - -/** - * Represents a regular selection for DOM to Content Model conversion - */ -export interface DomToModelRegularSelection { - /** - * Is the selection collapsed - */ - isSelectionCollapsed?: boolean; - - /** - * Start container of this selection - */ - startContainer?: Node; - - /** - * End container of this selection - */ - endContainer?: Node; - - /** - * Start offset of this selection - */ - startOffset?: number; - - /** - * End offset of this selection - */ - endOffset?: number; -} - -/** - * Represents a table for DOM to Content Model conversion - */ -export interface DomToModelTableSelection { - /** - * Table where selection is located - */ - table: HTMLTableElement; - - /** - * Coordinate of first selected cell - */ - firstCell: Coordinates; - - /** - * Coordinate of last selected cell - */ - lastCell: Coordinates; -} - -/** - * Represents an image for DOM to Content Model conversion - */ -export interface DomToModelImageSelection { - /** - * Selected image - */ - image: HTMLImageElement; -} +import { SelectionRangeEx } from 'roosterjs-editor-types'; /** * Represents the selection information of content used by DOM to Content Model conversion @@ -67,29 +7,10 @@ export interface DomToModelSelectionContext { /** * Is current context under a selection */ - isInSelection: boolean; - - /** - * Regular selection (selection with a highlight background provided by browser) - */ - regularSelection?: DomToModelRegularSelection; - - /** - * Table selection provided by editor - */ - tableSelection?: DomToModelTableSelection; - - /** - * Image selection provided by editor - */ - imageSelection?: DomToModelImageSelection; + isInSelection?: boolean; /** - * Root not that contains the selection. - * For regular selection, it is the common ancestor container of selection range. - * For table selection, it is the table node. - * For image selection, it is the image node. - * Otherwise, it is undefined. + * Current selection range */ - selectionRootNode?: Node; + rangeEx?: SelectionRangeEx; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/context/EditorContext.ts b/packages-content-model/roosterjs-content-model-types/lib/context/EditorContext.ts index 3d8ae58c568..b6411e231d2 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/context/EditorContext.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/context/EditorContext.ts @@ -24,4 +24,14 @@ export interface EditorContext { * Whether to handle delimiters in Content Model */ addDelimiterForEntity?: boolean; + + /** + * Zoom scale number + */ + zoomScale?: number; + + /** + * Whether the content is in Right-to-left from root level + */ + isRootRtl?: boolean; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/ZoomScaleFormat.ts b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/ZoomScaleFormat.ts deleted file mode 100644 index 2de9a3646a3..00000000000 --- a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/ZoomScaleFormat.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Format of scale - */ -export type ZoomScaleFormat = { - /** - * Zoom scale number - */ - zoomScale?: number; -}; diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index c2c1b35a7f5..c22158ca103 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -42,7 +42,6 @@ export { SpacingFormat } from './format/formatParts/SpacingFormat'; export { TableLayoutFormat } from './format/formatParts/TableLayoutFormat'; export { LinkFormat } from './format/formatParts/LinkFormat'; export { SizeFormat } from './format/formatParts/SizeFormat'; -export { ZoomScaleFormat } from './format/formatParts/ZoomScaleFormat'; export { BoxShadowFormat } from './format/formatParts/BoxShadowFormat'; export { ListThreadFormat } from './format/formatParts/ListThreadFormat'; export { ListStylePositionFormat } from './format/formatParts/ListStylePositionFormat'; @@ -114,12 +113,7 @@ export { } from './context/DomToModelSettings'; export { DomToModelContext } from './context/DomToModelContext'; export { ElementProcessor } from './context/ElementProcessor'; -export { - DomToModelSelectionContext, - DomToModelRegularSelection, - DomToModelTableSelection, - DomToModelImageSelection, -} from './context/DomToModelSelectionContext'; +export { DomToModelSelectionContext } from './context/DomToModelSelectionContext'; export { EditorContext } from './context/EditorContext'; export { DomToModelFormatContext, diff --git a/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts b/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts index ff467d5719b..17456e12881 100644 --- a/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts +++ b/packages/roosterjs-editor-core/lib/corePlugins/UndoPlugin.ts @@ -88,6 +88,7 @@ export default class UndoPlugin implements PluginWithState { return ( event.eventType == PluginEventType.KeyDown && event.rawEvent.which == Keys.BACKSPACE && + !event.rawEvent.ctrlKey && this.canUndoAutoComplete() ); } @@ -135,7 +136,7 @@ export default class UndoPlugin implements PluginWithState { // since we want the state prior to deletion restorable // Ignore if keycombo is ALT+BACKSPACE if ((evt.which == Keys.BACKSPACE && !evt.altKey) || evt.which == Keys.DELETE) { - if (evt.which == Keys.BACKSPACE && this.canUndoAutoComplete()) { + if (evt.which == Keys.BACKSPACE && !evt.ctrlKey && this.canUndoAutoComplete()) { evt.preventDefault(); this.editor?.undo(); this.state.autoCompletePosition = null; diff --git a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts index 8812e852de8..2ef508f1b39 100644 --- a/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts +++ b/packages/roosterjs-editor-dom/lib/htmlSanitizer/getAllowedValues.ts @@ -139,7 +139,7 @@ const ALLOWED_HTML_ATTRIBUTES = ( 'hreflang,ismap,kind,label,lang,list,low,max,maxlength,media,min,multiple,open,optimum,pattern,' + 'placeholder,readonly,rel,required,reversed,rows,rowspan,scope,selected,shape,size,sizes,span,' + 'spellcheck,src,srclang,srcset,start,step,style,tabindex,target,title,translate,type,usemap,valign,value,' + - 'width,wrap' + 'width,wrap,bgColor' ).split(','); const DEFAULT_STYLE_VALUES: { [name: string]: string } = { diff --git a/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts b/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts index e99a601e431..3c63cbc9c9b 100644 --- a/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts +++ b/packages/roosterjs-editor-dom/lib/table/isWholeTableSelected.ts @@ -13,7 +13,11 @@ export default function isWholeTableSelected(vTable: VTable, selection: TableSel } const { firstCell, lastCell } = selection; const rowsLength = vTable.cells.length - 1; - const colIndex = vTable.cells[rowsLength].length - 1; + const rowCells = vTable.cells[rowsLength]; + if (!rowCells) { + return false; + } + const colIndex = rowCells.length - 1; const firstX = firstCell.x; const firstY = firstCell.y; const lastX = lastCell.x; diff --git a/packages/roosterjs-editor-dom/test/table/isWholeTableSelectedTest.ts b/packages/roosterjs-editor-dom/test/table/isWholeTableSelectedTest.ts new file mode 100644 index 00000000000..6f47f2db446 --- /dev/null +++ b/packages/roosterjs-editor-dom/test/table/isWholeTableSelectedTest.ts @@ -0,0 +1,91 @@ +import * as isWholeTableSelectedFile from '../../lib/table/isWholeTableSelected'; +import VTable from '../../lib/table/VTable'; + +describe('isWholeTableSelectedTest', () => { + it('Table without rows', () => { + const table = document.createElement('table'); + spyOn(isWholeTableSelectedFile, 'default').and.callThrough(); + + const vTable = new VTable(table); + const result = isWholeTableSelectedFile.default(vTable, { + firstCell: { + x: 0, + y: 0, + }, + lastCell: { + x: 0, + y: 0, + }, + }); + + expect(result).toBeFalse(); + expect(isWholeTableSelectedFile.default).not.toThrow(); + }); + + it('Table with single row and no cells', () => { + const table = document.createElement('table'); + table.appendChild(document.createElement('tr')); + spyOn(isWholeTableSelectedFile, 'default').and.callThrough(); + + const vTable = new VTable(table); + const result = isWholeTableSelectedFile.default(vTable, { + firstCell: { + x: 0, + y: 0, + }, + lastCell: { + x: 0, + y: 0, + }, + }); + + expect(result).toBeFalse(); + expect(isWholeTableSelectedFile.default).not.toThrow(); + }); + + it('Table 1x2 is whole selected', () => { + const table = document.createElement('table'); + const tr = document.createElement('tr'); + tr.append(document.createElement('td'), document.createElement('td')); + table.appendChild(tr); + spyOn(isWholeTableSelectedFile, 'default').and.callThrough(); + + const vTable = new VTable(table); + const result = isWholeTableSelectedFile.default(vTable, { + firstCell: { + x: 0, + y: 0, + }, + lastCell: { + x: 1, + y: 0, + }, + }); + + expect(result).toBeTrue(); + expect(isWholeTableSelectedFile.default).not.toThrow(); + }); + + it('Table 1x2 is not whole selected', () => { + const table = document.createElement('table'); + const tr = document.createElement('tr'); + tr.append(document.createElement('td'), document.createElement('td')); + table.appendChild(tr); + spyOn(isWholeTableSelectedFile, 'default').and.callThrough(); + + const vTable = new VTable(table); + const result = isWholeTableSelectedFile.default(vTable, { + firstCell: { + x: 0, + y: 0, + }, + lastCell: { + x: 0, + y: 0, + }, + }); + + expect(result).toBeFalse(); + expect(isWholeTableSelectedFile.default).not.toThrow(); + }); +}); diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts index 06697fac4d0..3cf07da978a 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ContentEdit/features/tableFeatures.ts @@ -15,7 +15,6 @@ import { ExperimentalFeatures, } from 'roosterjs-editor-types'; import { - Browser, cacheGetEventData, contains, getTagOfNode, @@ -172,7 +171,6 @@ const UpDownInTable: BuildInEditFeature = { }); } }, - defaultDisabled: !Browser.isChrome && !Browser.isSafari, }; /** diff --git a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts index 8791f09204f..8ec267a8298 100644 --- a/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts +++ b/packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts @@ -480,6 +480,7 @@ export default class ImageEdit implements EditorPlugin { }); this.shadowSpan.style.verticalAlign = 'bottom'; + this.shadowSpan.style.fontSize = '24px'; shadowRoot.appendChild(wrapper); } diff --git a/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts b/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts index 1baef7da348..67bf780ec49 100644 --- a/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts +++ b/packages/roosterjs-editor-plugins/test/imageEdit/imageEditTest.ts @@ -220,7 +220,7 @@ describe('ImageEdit | rotate and flip', () => { editor.select(image); plugin.setEditingImage(image, ImageEditOperation.Resize); expect(editor.getContent()).toBe( - '' + '' ); }); }); diff --git a/packages/roosterjs-editor-plugins/test/paste/e2e/pasteFromWordTest.ts b/packages/roosterjs-editor-plugins/test/paste/e2e/pasteFromWordTest.ts index 0c536bf086d..200a1a41390 100644 --- a/packages/roosterjs-editor-plugins/test/paste/e2e/pasteFromWordTest.ts +++ b/packages/roosterjs-editor-plugins/test/paste/e2e/pasteFromWordTest.ts @@ -1,6 +1,5 @@ import * as convertPastedContentFromWord from '../../../lib/plugins/Paste/wordConverter/convertPastedContentFromWord'; import { Browser } from 'roosterjs-editor-dom'; -import { Browser } from 'roosterjs-editor-dom'; import { ClipboardData, IEditor } from 'roosterjs-editor-types'; import { initEditor } from '../../TestHelper'; import { Paste } from '../../../lib/index'; diff --git a/versions.json b/versions.json index da0b05dd0ac..8984d260fb8 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "packages": "8.51.0", + "packages": "8.51.1", "packages-ui": "8.50.1", - "packages-content-model": "0.10.0" + "packages-content-model": "0.11.0" } diff --git a/yarn.lock b/yarn.lock index 0c6f0a4118f..6f6cca71764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4618,25 +4618,15 @@ semver-regex@^2.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== - -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" - integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== - -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== send@0.18.0: version "0.18.0"