From 06d57be922701af49443b10980144e69f4a57fa0 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 16 Aug 2024 13:09:42 -0300 Subject: [PATCH 01/43] image-selection --- .../corePlugin/selection/SelectionPlugin.ts | 30 +++++----- .../selection/SelectionPluginTest.ts | 46 +++------------ .../lib/imageEdit/ImageEditPlugin.ts | 56 +++++-------------- 3 files changed, 37 insertions(+), 95 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index d2aaf74b2be..134b4ec908b 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -21,7 +21,6 @@ import type { ParsedTable, TableSelectionInfo, TableCellCoordinate, - RangeSelection, MouseUpEvent, } from 'roosterjs-content-model-types'; @@ -362,6 +361,19 @@ class SelectionPlugin implements PluginWithState { this.selectBeforeOrAfterElement(editor, selection.image); } } + + if ((isModifierKey(rawEvent) || rawEvent.shiftKey) && selection.image) { + const range = selection.image.ownerDocument.createRange(); + range.selectNode(selection.image); + this.setDOMSelection( + { + type: 'range', + range, + isReverted: false, + }, + null /* tableSelection */ + ); + } break; case 'range': @@ -674,7 +686,6 @@ class SelectionPlugin implements PluginWithState { if (this.isSafari) { this.state.selection = newSelection; } - this.trySelectSingleImage(newSelection); } } }; @@ -746,21 +757,6 @@ class SelectionPlugin implements PluginWithState { this.state.mouseDisposer = undefined; } } - - private trySelectSingleImage(selection: RangeSelection) { - if (!selection.range.collapsed) { - const image = isSingleImageInSelection(selection.range); - if (image) { - this.setDOMSelection( - { - type: 'image', - image: image, - }, - null /*tableSelection*/ - ); - } - } - } } /** diff --git a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts index e2584338b5d..f67a14db5c9 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts @@ -649,10 +649,18 @@ describe('SelectionPlugin handle image selection', () => { key: 'A', stopPropagation: stopPropagationSpy, ctrlKey: true, + shiftKey: false, } as any; const mockedImage = { parentNode: { childNodes: [] }, + ownerDocument: { + createRange: () => { + return { + selectNode: (node: any) => {}, + }; + }, + }, } as any; mockedImage.parentNode.childNodes.push(mockedImage); @@ -674,7 +682,7 @@ describe('SelectionPlugin handle image selection', () => { }); expect(stopPropagationSpy).not.toHaveBeenCalled(); - expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + expect(setDOMSelectionSpy).toHaveBeenCalled(); }); it('key down - other key, image has no parent', () => { @@ -2624,40 +2632,4 @@ describe('SelectionPlugin selectionChange on image selected', () => { expect(setDOMSelectionSpy).not.toHaveBeenCalled(); expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1); }); - - it('onSelectionChange on image | 4', () => { - const image = document.createElement('img'); - spyOn(isSingleImageInSelection, 'isSingleImageInSelection').and.returnValue(image); - - const plugin = createSelectionPlugin({}); - const state = plugin.getState(); - const mockedOldSelection = { - type: 'image', - image: {} as any, - } as DOMSelection; - - state.selection = mockedOldSelection; - - plugin.initialize(editor); - - const onSelectionChange = addEventListenerSpy.calls.argsFor(0)[1] as Function; - const mockedNewSelection = { - type: 'range', - range: {} as any, - } as any; - - hasFocusSpy.and.returnValue(true); - isInShadowEditSpy.and.returnValue(false); - getDOMSelectionSpy.and.returnValue(mockedNewSelection); - getRangeAtSpy.and.returnValue({ startContainer: {} }); - - onSelectionChange(); - - expect(setDOMSelectionSpy).toHaveBeenCalled(); - expect(setDOMSelectionSpy).toHaveBeenCalledWith({ - type: 'image', - image, - }); - expect(getDOMSelectionSpy).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 22d57145b1d..98a8af0809c 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -17,10 +17,8 @@ import { updateWrapper } from './utils/updateWrapper'; import { getSafeIdSelector, isElementOfType, - isModifierKey, isNodeOfType, mutateSegment, - toArray, unwrap, } from 'roosterjs-content-model-dom'; import type { DragAndDropHelper } from '../pluginUtils/DragAndDrop/DragAndDropHelper'; @@ -168,41 +166,16 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } } - //Sometimes the cursor can be inside the editing image and inside shadow dom, then the cursor need to moved out of shadow dom - private selectBeforeEditingImage(editor: IEditor, element: HTMLElement) { - let parent = element.parentNode; - if (parent && isNodeOfType(parent, 'ELEMENT_NODE') && parent.shadowRoot) { - element = parent; - parent = parent.parentNode; - } - const index = parent && toArray(parent.childNodes).indexOf(element); - if (index !== null && index >= 0 && parent) { - const doc = editor.getDocument(); - const range = doc.createRange(); - range.setStart(parent, index); - range.collapse(); - editor.setDOMSelection({ - type: 'range', - range, - isReverted: false, - }); - } - } - private keyDownHandler(editor: IEditor, event: KeyDownEvent) { if (this.isEditing) { if (event.rawEvent.key === 'Escape') { this.removeImageWrapper(); } else { - const selection = editor.getDOMSelection(); - const isImageSelection = selection?.type == 'image'; - if (isImageSelection) { - this.selectBeforeEditingImage(editor, selection.image); - } this.applyFormatWithContentModel( editor, this.isCropMode, - (isModifierKey(event.rawEvent) || event.rawEvent.shiftKey) && isImageSelection //if it's a modifier key over a image, the image should select the image + true /** should selectImage */, + false /* isApiOperation */ ); } } @@ -260,6 +233,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { if (shouldSelectImage) { normalizeImageSelection(previousSelectedImage); } + this.cleanInfo(); result = true; } @@ -289,18 +263,18 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { }, { onNodeCreated: (model, node) => { - if ( - !isApiOperation && - editingImageModel && - editingImageModel == model && - editingImageModel.dataset.isEditing && - isNodeOfType(node, 'ELEMENT_NODE') && - isElementOfType(node, 'img') - ) { - if (isCropMode) { - this.startCropMode(editor, node); - } else { - this.startRotateAndResize(editor, node); + if (isNodeOfType(node, 'ELEMENT_NODE') && isElementOfType(node, 'img')) { + if ( + !isApiOperation && + editingImageModel && + editingImageModel == model && + editingImageModel.dataset.isEditing + ) { + if (isCropMode) { + this.startCropMode(editor, node); + } else { + this.startRotateAndResize(editor, node); + } } } }, From aab10586def516b5547be12f5be8e08f045740de Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 16 Aug 2024 13:32:00 -0300 Subject: [PATCH 02/43] fixes --- .../lib/imageEdit/ImageEditPlugin.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 98a8af0809c..9f925fb8e12 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -263,18 +263,18 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { }, { onNodeCreated: (model, node) => { - if (isNodeOfType(node, 'ELEMENT_NODE') && isElementOfType(node, 'img')) { - if ( - !isApiOperation && - editingImageModel && - editingImageModel == model && - editingImageModel.dataset.isEditing - ) { - if (isCropMode) { - this.startCropMode(editor, node); - } else { - this.startRotateAndResize(editor, node); - } + if ( + !isApiOperation && + editingImageModel && + editingImageModel == model && + editingImageModel.dataset.isEditing && + isNodeOfType(node, 'ELEMENT_NODE') && + isElementOfType(node, 'img') + ) { + if (isCropMode) { + this.startCropMode(editor, node); + } else { + this.startRotateAndResize(editor, node); } } }, From 8b52988c8c0bdc74c9465e9baeb08936f07cca1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Fri, 16 Aug 2024 14:22:49 -0300 Subject: [PATCH 03/43] safari fix --- .../lib/corePlugin/selection/SelectionPlugin.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index 134b4ec908b..86ea966ec34 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -362,7 +362,11 @@ class SelectionPlugin implements PluginWithState { } } - if ((isModifierKey(rawEvent) || rawEvent.shiftKey) && selection.image) { + if ( + (isModifierKey(rawEvent) || rawEvent.shiftKey) && + selection.image && + !this.isSafari + ) { const range = selection.image.ownerDocument.createRange(); range.selectNode(selection.image); this.setDOMSelection( From 9c0db6e5d4e84f38328185b0802dd2ae15c3bfe3 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Wed, 21 Aug 2024 19:46:29 -0300 Subject: [PATCH 04/43] fix drag and drop --- .../lib/imageEdit/ImageEditPlugin.ts | 75 +++- .../lib/imageEdit/utils/findEditingImage.ts | 11 +- .../test/imageEdit/ImageEditPluginTest.ts | 415 +++++++++++------- .../imageEdit/utils/findEditingImageTest.ts | 75 ++++ 4 files changed, 395 insertions(+), 181 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 22d57145b1d..bf5c16fd466 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -14,11 +14,14 @@ import { Resizer } from './Resizer/resizerContext'; import { Rotator } from './Rotator/rotatorContext'; import { updateRotateHandle } from './Rotator/updateRotateHandle'; import { updateWrapper } from './utils/updateWrapper'; + import { + ChangeSource, getSafeIdSelector, isElementOfType, isModifierKey, isNodeOfType, + mutateBlock, mutateSegment, toArray, unwrap, @@ -35,6 +38,7 @@ import type { ImageEditor, ImageMetadataFormat, KeyDownEvent, + MouseDownEvent, MouseUpEvent, PluginEvent, } from 'roosterjs-content-model-types'; @@ -50,6 +54,7 @@ const DefaultOptions: Partial = { }; const MouseRightButton = 2; +const DRAG_ID = '_dragging'; /** * ImageEdit plugin handles the following image editing features: @@ -75,7 +80,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { private croppers: HTMLDivElement[] = []; private zoomScale: number = 1; private disposer: (() => void) | null = null; - //EXPOSED FOR TEST ONLY protected isEditing = false; constructor(protected options: ImageEditOptions = DefaultOptions) {} @@ -107,6 +111,16 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } }, }, + dragstart: { + beforeDispatch: ev => { + if (this.editor) { + const target = ev.target as Node; + if (this.isImageSelection(target)) { + target.id = target.id + DRAG_ID; + } + } + }, + }, }); } @@ -136,16 +150,24 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { return; } switch (event.eventType) { + case 'mouseDown': + this.mouseDownHandler(this.editor, event); + break; case 'mouseUp': this.mouseUpHandler(this.editor, event); break; case 'keyDown': this.keyDownHandler(this.editor, event); break; + case 'contentChanged': + if (event.source == ChangeSource.Drop) { + this.onDropHandler(this.editor); + } + break; } } - private isImageSelection(target: Node) { + private isImageSelection(target: Node): target is HTMLElement { return ( isNodeOfType(target, 'ELEMENT_NODE') && (isElementOfType(target, 'img') || @@ -168,6 +190,44 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } } + private mouseDownHandler(editor: IEditor, event: MouseDownEvent) { + if ( + this.isEditing && + this.isImageSelection(event.rawEvent.target as Node) && + event.rawEvent.button !== MouseRightButton + ) { + this.applyFormatWithContentModel(editor, this.isCropMode, true); + } + } + + private onDropHandler(editor: IEditor) { + const selection = editor.getDOMSelection(); + if (selection?.type == 'image') { + editor.formatContentModel(model => { + const imageDragged = findEditingImage(model, selection.image.id); + const imageDropped = findEditingImage( + model, + selection.image.id.replace(DRAG_ID, '') + ); + if (imageDragged && imageDropped) { + const draggedIndex = imageDragged.paragraph.segments.indexOf( + imageDragged.image + ); + mutateBlock(imageDragged.paragraph).segments.splice(draggedIndex, 1); + const segment = imageDropped.image; + const paragraph = imageDropped.paragraph; + mutateSegment(paragraph, segment, image => { + image.isSelected = true; + image.isSelectedAsImageSelection = true; + }); + + return true; + } + return false; + }); + } + } + //Sometimes the cursor can be inside the editing image and inside shadow dom, then the cursor need to moved out of shadow dom private selectBeforeEditingImage(editor: IEditor, element: HTMLElement) { let parent = element.parentNode; @@ -222,7 +282,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { const previousSelectedImage = isApiOperation ? editingImage : findEditingImage(model); - let result = false; if ( shouldSelectImage || @@ -252,21 +311,22 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.wasImageResized || this.isCropMode, clonedImage ); - delete image.dataset.isEditing; + image.isSelected = shouldSelectImage; image.isSelectedAsImageSelection = shouldSelectImage; } ); + if (shouldSelectImage) { normalizeImageSelection(previousSelectedImage); } + this.cleanInfo(); result = true; } this.isEditing = false; this.isCropMode = false; - if ( editingImage && selection?.type == 'image' && @@ -645,9 +705,4 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { }); } } - - //EXPOSED FOR TEST ONLY - public get isEditingImage() { - return this.isEditing; - } } diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index d4c2351dd75..8e027a80461 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -4,13 +4,16 @@ import type { ImageAndParagraph } from '../types/ImageAndParagraph'; /** * @internal */ -export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAndParagraph | null { +export function findEditingImage( + group: ReadonlyContentModelBlockGroup, + imageId?: string +): ImageAndParagraph | null { for (let i = 0; i < group.blocks.length; i++) { const block = group.blocks[i]; switch (block.blockType) { case 'BlockGroup': - const result = findEditingImage(block); + const result = findEditingImage(block, imageId); if (result) { return result; @@ -22,7 +25,7 @@ export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAn const segment = block.segments[j]; switch (segment.segmentType) { case 'Image': - if (segment.dataset.isEditing) { + if (segment.dataset.isEditing || segment.format.id == imageId) { return { paragraph: block, image: segment, @@ -31,7 +34,7 @@ export function findEditingImage(group: ReadonlyContentModelBlockGroup): ImageAn break; case 'General': - const result = findEditingImage(segment); + const result = findEditingImage(segment, imageId); if (result) { return result; diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts index fd16083deef..f0bbc6fd7da 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts @@ -1,35 +1,56 @@ -import { ContentModelDocument } from 'roosterjs-content-model-types'; +import * as findImage from '../../lib/imageEdit/utils/findEditingImage'; +import * as getSelectedImage from '../../lib/imageEdit/utils/getSelectedImage'; +import { ChangeSource, createImage, createParagraph } from 'roosterjs-content-model-dom'; import { getSelectedImageMetadata } from '../../lib/imageEdit/utils/updateImageEditInfo'; import { ImageEditPlugin } from '../../lib/imageEdit/ImageEditPlugin'; import { initEditor } from '../TestHelper'; +import { + ContentModelDocument, + ContentModelFormatter, + EditorEnvironment, + FormatContentModelOptions, + IEditor, +} from 'roosterjs-content-model-types'; const model: ContentModelDocument = { blockGroupType: 'Document', blocks: [ { - blockType: 'Paragraph', segments: [ { - segmentType: 'Image', - src: 'test', + isSelected: true, + segmentType: 'SelectionMarker', format: { fontFamily: 'Calibri', fontSize: '11pt', textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', }, - dataset: {}, + }, + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', isSelectedAsImageSelection: true, + segmentType: 'Image', isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1123px', + }, + dataset: { + isEditing: 'true', + }, }, ], - format: {}, segmentFormat: { fontFamily: 'Calibri', fontSize: '11pt', textColor: 'rgb(0, 0, 0)', }, + blockType: 'Paragraph', + format: {}, }, ], format: { @@ -40,209 +61,181 @@ const model: ContentModelDocument = { }; describe('ImageEditPlugin', () => { - it('keyDown', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: { - isEditing: 'true', - }, - isSelectedAsImageSelection: true, - isSelected: true, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', + let editor: IEditor; + let mockedEnvironment: EditorEnvironment; + let attachDomEventSpy: jasmine.Spy; + let getDOMSelectionSpy: jasmine.Spy; + let formatContentModelSpy: jasmine.Spy; + let focusSpy: jasmine.Spy; + let isDarkModeSpy: jasmine.Spy; + let setAttributeSpy: jasmine.Spy; + let appendChildSpy: jasmine.Spy; + let getDOMHelperSpy: jasmine.Spy; + let calculateZoomScaleSpy: jasmine.Spy; + let setEditorStyleSpy: jasmine.Spy; + let triggerEventSpy: jasmine.Spy; + let getAttributeSpy: jasmine.Spy; + beforeEach(() => { + attachDomEventSpy = jasmine.createSpy('attachDomEvent'); + getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); + mockedEnvironment = { + isSafari: false, + } as any; + getAttributeSpy = jasmine.createSpy('getAttribute'); + const image = createImage(''); + const editImage = createImage('test image'); + image.dataset = { + isEditing: 'true', + }; + const paragraph = createParagraph(); + paragraph.segments.push(image); + paragraph.segments.push(editImage); + spyOn(findImage, 'findEditingImage').and.returnValue({ + image, + paragraph, + }); + spyOn(getSelectedImage, 'getSelectedImage').and.returnValue({ + image: editImage, + paragraph, + }); + formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake( + (callback: ContentModelFormatter, _options: FormatContentModelOptions) => { + callback(model, { + newEntities: [], + deletedEntities: [], + newImages: [], + }); + } + ); + focusSpy = jasmine.createSpy('focus'); + isDarkModeSpy = jasmine.createSpy('isDarkMode'); + setAttributeSpy = jasmine.createSpy('setAttribute'); + appendChildSpy = jasmine.createSpy('appendChild'); + calculateZoomScaleSpy = jasmine.createSpy('calculateZoomScale'); + getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue({ + calculateZoomScale: calculateZoomScaleSpy, + }); + setEditorStyleSpy = jasmine.createSpy('setEditorStyle'); + triggerEventSpy = jasmine.createSpy('triggerEvent').and.returnValue({ + newSrc: '', + }); + editor = { + getEnvironment: () => mockedEnvironment, + attachDomEvent: attachDomEventSpy, + getDOMSelection: getDOMSelectionSpy, + formatContentModel: formatContentModelSpy, + focus: focusSpy, + isDarkMode: isDarkModeSpy, + getDOMHelper: getDOMHelperSpy, + getDocument: () => ({ + createElement: () => ({ + setAttribute: setAttributeSpy, + appendChild: appendChildSpy, + style: { + display: '', }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + + attachShadow: () => ({ + appendChild: appendChildSpy, + }), + }), + }), + setEditorStyle: setEditorStyleSpy, + triggerEvent: triggerEventSpy, + } as any; + }); + + it('keyDown', () => { + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + const image = createImage(''); + const paragraph = createParagraph(); + paragraph.segments.push(image); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, + target: mockedImage, } as any, }); plugin.onPluginEvent({ eventType: 'keyDown', rawEvent: { key: 'k', + target: mockedImage, } as any, }); - expect(plugin.isEditingImage).toBeFalsy(); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(2); plugin.dispose(); }); it('mouseUp', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: {}, - isSelectedAsImageSelection: true, - isSelected: true, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, + target: mockedImage, } as any, }); - - expect(plugin.isEditingImage).toBeTruthy(); + expect(formatContentModelSpy).toHaveBeenCalled(); plugin.dispose(); }); it('mouseUp - left click - remove selection', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: { - isEditing: 'true', - }, - isSelectedAsImageSelection: false, - isSelected: false, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, } as any, }); - expect(plugin.isEditingImage).toBeFalsy(); + expect(formatContentModelSpy).toHaveBeenCalled(); plugin.dispose(); }); it('mouseUp - right click - remove wrapper', () => { - const model: ContentModelDocument = { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Image', - src: 'test', - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - id: 'image_0', - maxWidth: '1800px', - }, - dataset: {}, - isSelectedAsImageSelection: true, - isSelected: true, - }, - ], - format: {}, - segmentFormat: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: 'rgb(0, 0, 0)', - }, - }, - ], - format: { - fontFamily: 'Calibri', - fontSize: '11pt', - textColor: '#000000', - }, + const mockedImage = { + getAttribute: getAttributeSpy, }; const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 0, target: { @@ -252,7 +245,7 @@ describe('ImageEditPlugin', () => { }); plugin.onPluginEvent({ eventType: 'mouseUp', - isClicking: true, + rawEvent: { button: 2, target: { @@ -261,27 +254,37 @@ describe('ImageEditPlugin', () => { } as any, } as any, }); - - expect(plugin.isEditingImage).toBeFalsy(); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(2); plugin.dispose(); }); it('cropImage', () => { const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); + const mockedImage = { + getAttribute: getAttributeSpy, + }; plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); plugin.cropImage(); - expect(plugin.isEditingImage).toBeTruthy(); + expect(formatContentModelSpy).toHaveBeenCalled(); plugin.dispose(); }); it('flip', () => { const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); const image = new Image(); image.src = 'test'; + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: image, + }); plugin.initialize(editor); plugin.flipImage('horizontal'); + expect(formatContentModelSpy).toHaveBeenCalled(); const dataset = getSelectedImageMetadata(editor, image); expect(dataset).toBeTruthy(); plugin.dispose(); @@ -289,12 +292,16 @@ describe('ImageEditPlugin', () => { it('rotate', () => { const plugin = new ImageEditPlugin(); - const editor = initEditor('image_edit', [plugin], model); const image = new Image(); image.src = 'test'; + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: image, + }); plugin.initialize(editor); plugin.rotateImage(Math.PI / 2); const dataset = getSelectedImageMetadata(editor, image); + expect(formatContentModelSpy).toHaveBeenCalled(); expect(dataset).toBeTruthy(); plugin.dispose(); }); @@ -349,4 +356,78 @@ describe('ImageEditPlugin', () => { ['span:has(>img[id="0"])'] ); }); + + it('mouseDown on edit image', () => { + const mockedImage = { + getAttribute: getAttributeSpy, + }; + const plugin = new ImageEditPlugin(); + plugin.initialize(editor); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + plugin.onPluginEvent({ + eventType: 'mouseUp', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: mockedImage, + }); + plugin.onPluginEvent({ + eventType: 'mouseDown', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(2); + plugin.dispose(); + }); + + it('dragImage', () => { + const mockedImage = { + id: 'image_0', + getAttribute: getAttributeSpy, + } as any; + const plugin = new ImageEditPlugin(); + plugin.initialize(editor); + const draggedImage = mockedImage as HTMLImageElement; + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: draggedImage, + }); + plugin.onPluginEvent({ + eventType: 'mouseUp', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + plugin.onPluginEvent({ + eventType: 'mouseDown', + rawEvent: { + button: 0, + target: new Image(), + } as any, + }); + getDOMSelectionSpy.and.returnValue({ + type: 'image', + image: { + id: 'image_0_dragging', + } as any, + }); + plugin.onPluginEvent({ + eventType: 'contentChanged', + source: ChangeSource.Drop, + }); + expect(formatContentModelSpy).toHaveBeenCalled(); + expect(formatContentModelSpy).toHaveBeenCalledTimes(3); + plugin.dispose(); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts index c85e55b326c..d68582dbe94 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts @@ -106,4 +106,79 @@ describe('findEditingImage', () => { }, }); }); + + it('editing image | by Id', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model, 'image_0'); + expect(image).toEqual({ + image: { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + }); + }); }); From 21efdc83484e64379ed8b49eee81d03998ade20f Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 22 Aug 2024 14:01:25 -0300 Subject: [PATCH 05/43] fixes --- .../lib/imageEdit/ImageEditPlugin.ts | 3 ++- .../lib/imageEdit/utils/findEditingImage.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 0f99d2b63e5..7c5537349aa 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -205,7 +205,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { const imageDragged = findEditingImage(model, selection.image.id); const imageDropped = findEditingImage( model, - selection.image.id.replace(DRAG_ID, '') + selection.image.id.replace(DRAG_ID, '').trim() ); if (imageDragged && imageDropped) { const draggedIndex = imageDragged.paragraph.segments.indexOf( @@ -215,6 +215,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { const segment = imageDropped.image; const paragraph = imageDropped.paragraph; mutateSegment(paragraph, segment, image => { + console.log(image.format.id); image.isSelected = true; image.isSelectedAsImageSelection = true; }); diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index 8e027a80461..65437ee66ff 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -25,7 +25,10 @@ export function findEditingImage( const segment = block.segments[j]; switch (segment.segmentType) { case 'Image': - if (segment.dataset.isEditing || segment.format.id == imageId) { + if ( + (segment.dataset.isEditing && !imageId) || + segment.format.id == imageId + ) { return { paragraph: block, image: segment, From 7b4b9c8c2bc4db87a2acf94c273d61c38d113796 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 22 Aug 2024 14:01:51 -0300 Subject: [PATCH 06/43] fixes --- .../lib/imageEdit/ImageEditPlugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 7c5537349aa..951931791a8 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -215,7 +215,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { const segment = imageDropped.image; const paragraph = imageDropped.paragraph; mutateSegment(paragraph, segment, image => { - console.log(image.format.id); image.isSelected = true; image.isSelectedAsImageSelection = true; }); From e97ff6eb340a2000daeef9ceadee872f585b7663 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 22 Aug 2024 13:40:05 -0600 Subject: [PATCH 07/43] Add `` elements to default processors and use knownElementProcessor for this type of element. (#2770) * init * Use alphabet order --- .../lib/domToModel/context/defaultProcessors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts b/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts index 152334499f5..0783be37e4f 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/context/defaultProcessors.ts @@ -29,6 +29,7 @@ export const defaultProcessorMap: ElementProcessorMap = { blockquote: knownElementProcessor, br: brProcessor, code: codeProcessor, + del: knownElementProcessor, div: knownElementProcessor, em: knownElementProcessor, font: fontProcessor, From b3111dfa63883393b61f1ba70ce8b346e7ccb441 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 28 Aug 2024 17:54:45 -0600 Subject: [PATCH 08/43] Dont remove the MarginTop/Bottom from lists when pasting from Word Online (#2778) * init * remove unneeded function * try fix build --- .../test/command/paste/pasteTest.ts | 2 +- .../lib/paste/WacComponents/constants.ts | 12 +- .../processPastedContentWacComponents.ts | 6 +- .../test/paste/ContentModelPastePluginTest.ts | 2 +- .../paste/processPastedContentFromWacTest.ts | 4058 +++++++++++------ 5 files changed, 2765 insertions(+), 1315 deletions(-) diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index 5fbed20a037..3e6168ebf72 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -178,7 +178,7 @@ describe('paste with content model & paste plugin', () => { paste(editor!, clipboardData); expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(2); - expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); + expect(addParserF.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7); expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1); }); diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts index 027909e7e7f..480e3a5a612 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/constants.ts @@ -52,9 +52,15 @@ export const TEMP_ELEMENTS_CLASSES: string[] = [ ...WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES, 'ListMarkerWrappingSpan', ]; + /** * @internal - **/ -export const WAC_IDENTIFY_SELECTOR: string = - `ul[class^="${BULLET_LIST_STYLE}"]>.${OUTLINE_ELEMENT},ol[class^="${NUMBER_LIST_STYLE}"]>.${OUTLINE_ELEMENT},span.${IMAGE_CONTAINER},span.${IMAGE_BORDER},.${COMMENT_HIGHLIGHT_CLASS},.${COMMENT_HIGHLIGHT_CLICKED_CLASS},` + + */ +export const REMOVE_MARGIN_ELEMENTS: string = + `span.${IMAGE_CONTAINER},span.${IMAGE_BORDER},.${COMMENT_HIGHLIGHT_CLASS},.${COMMENT_HIGHLIGHT_CLICKED_CLASS},` + WORD_ONLINE_TABLE_TEMP_ELEMENT_CLASSES.map(c => `table div[class^="${c}"]`).join(','); + +/** + * @internal + **/ +export const WAC_IDENTIFY_SELECTOR: string = `ul[class^="${BULLET_LIST_STYLE}"]>.${OUTLINE_ELEMENT},ol[class^="${NUMBER_LIST_STYLE}"]>.${OUTLINE_ELEMENT},${REMOVE_MARGIN_ELEMENTS}`; diff --git a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts index 9c51fb47e96..22851c36417 100644 --- a/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts +++ b/packages/roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents.ts @@ -5,8 +5,8 @@ import { COMMENT_HIGHLIGHT_CLASS, COMMENT_HIGHLIGHT_CLICKED_CLASS, LIST_CONTAINER_ELEMENT_CLASS_NAME, + REMOVE_MARGIN_ELEMENTS, TEMP_ELEMENTS_CLASSES, - WAC_IDENTIFY_SELECTOR, } from './constants'; import type { BeforePasteEvent, @@ -67,7 +67,7 @@ const wacElementProcessor: ElementProcessor = ( ): void => { const elementTag = element.tagName; - if (element.matches(WAC_IDENTIFY_SELECTOR)) { + if (element.matches(REMOVE_MARGIN_ELEMENTS)) { element.style.removeProperty('display'); element.style.removeProperty('margin'); } @@ -155,6 +155,7 @@ const wacListItemParser: FormatParser = ( } format.marginLeft = undefined; + format.marginRight = undefined; }; /** @@ -218,6 +219,7 @@ const wacCommentParser: FormatParser = ( export function processPastedContentWacComponents(ev: BeforePasteEvent) { addParser(ev.domToModelOption, 'segment', wacSubSuperParser); addParser(ev.domToModelOption, 'listItemThread', wacListItemParser); + addParser(ev.domToModelOption, 'listItemElement', wacListItemParser); addParser(ev.domToModelOption, 'listLevel', wacListLevelParser); addParser(ev.domToModelOption, 'container', wacContainerParser); addParser(ev.domToModelOption, 'table', wacContainerParser); diff --git a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts index c7a175d4093..e78f0beb1eb 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/ContentModelPastePluginTest.ts @@ -151,7 +151,7 @@ describe('Content Model Paste Plugin Test', () => { plugin.onPluginEvent(event); expect(WacFile.processPastedContentWacComponents).toHaveBeenCalledWith(event); - expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 6); + expect(addParser.addParser).toHaveBeenCalledTimes(DEFAULT_TIMES_ADD_PARSER_CALLED + 7); expect(setProcessor.setProcessor).toHaveBeenCalledTimes(2); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts index d45f6a5ea42..50ff7a50e80 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/processPastedContentFromWacTest.ts @@ -21,7 +21,12 @@ let div: HTMLElement; let fragment: DocumentFragment; describe('processPastedContentFromWacTest', () => { - function runTest(source?: string, expected?: string, expectedModel?: ContentModelDocument) { + function runTest( + source?: string, + expected?: string, + expectedModel?: ContentModelDocument, + removeUndefined?: boolean + ) { //Act if (source) { div = document.createElement('div'); @@ -37,7 +42,11 @@ describe('processPastedContentFromWacTest', () => { createDomToModelContext(undefined, event.domToModelOption) ); if (expectedModel) { - expect(model).toEqual(expectedModel); + if (removeUndefined) { + expectEqual(model, expectedModel); + } else { + expect(model).toEqual(expectedModel); + } } contentModelToDom( @@ -57,19 +66,37 @@ describe('processPastedContentFromWacTest', () => { ) ); + const innerHTML = div.innerHTML; //Assert if (expected) { - expect(div.innerHTML).toBe(expected); + expect(innerHTML).toBe(expected); } div.parentElement?.removeChild(div); + + return [innerHTML, model]; } it('Single text node', () => { - runTest('test', 'test'); + runTest( + 'test', + 'test', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + ], + }, + true + ); }); it('Empty DIV', () => { - runTest('
', ''); + runTest('
', '', { blockGroupType: 'Document', blocks: [] }, true); }); it('Single DIV', () => { @@ -101,34 +128,262 @@ describe('processPastedContentFromWacTest', () => { it('Single DIV with child LI', () => { runTest( '
  • 1
  • 2
', - '
  • 1
  • 2
' + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); it('Single DIV with deeper child LI', () => { runTest( '
  • 1
  • 2
', - '
  • 1
  • 2
' + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); it('Single DIV with text and LI', () => { runTest( '
test
  • 1
', - 'test
  • 1
' + 'test
  • 1
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); it('Single LI', () => { - runTest('
  • 1
', '
  • 1
'); + runTest( + '
  • 1
', + '
  • 1
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); }); it('Single LI and text', () => { - runTest('
  • 1
test', '
  • 1
test'); + runTest( + '
  • 1
test', + '
  • 1
test', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + format: {}, + isImplicit: true, + }, + ], + }, + true + ); }); it('Multiple LI', () => { - runTest('
  • 1
  • 2
', '
  • 1
  • 2
'); + runTest( + '
  • 1
  • 2
', + '
  • 1
  • 2
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: '2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); }); }); @@ -186,11 +441,14 @@ describe('wordOnlineHandler', () => { ) ); + const innerHTML = div.innerHTML; //Assert if (expected) { - expect(div.innerHTML).toBe(expected); + expect(innerHTML).toBe(expected); } div.parentElement?.removeChild(div); + + return [innerHTML, model]; } describe('HTML with fragment from Word Online', () => { describe('fragments only contain list items', () => { @@ -207,27 +465,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -241,27 +484,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -275,34 +503,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -312,7 +520,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); @@ -333,27 +542,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -367,34 +561,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -409,42 +583,15 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -454,7 +601,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); @@ -468,7 +616,7 @@ describe('wordOnlineHandler', () => { it('List items on different level but have different branch in each level', () => { runTest( '
  • A
  • B
  • C
  • D
  • E
', - '
  • A
    • B
      • C
    • D
      • E
', + '
  • A
    • B
      • C
    • D
      • E
', { blockGroupType: 'Document', blocks: [ @@ -478,27 +626,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -512,34 +645,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -554,43 +667,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C', format: {} }], format: {}, isImplicit: true, }, ], levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -599,12 +686,7 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, { blockType: 'BlockGroup', @@ -612,34 +694,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'D', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'D', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -654,43 +716,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'E', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'E', format: {} }], format: {}, isImplicit: true, }, ], levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -699,15 +735,11 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, ], - } + }, + true ); }); @@ -722,7 +754,7 @@ describe('wordOnlineHandler', () => { it('List items on different level with different branch with a combination of order and unordered list items', () => { runTest( '
  • A
  • B
  1. C1
  1. C2
  • D
', - '
  • A
    • B
      1. C1
      2. C2
    • D
', + '
  • A
    • B
      1. C1
      2. C2
    • D
', { blockGroupType: 'Document', blocks: [ @@ -732,27 +764,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -766,34 +783,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -808,43 +805,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C1', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C1', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'OL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -853,12 +824,7 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, { blockType: 'BlockGroup', @@ -866,43 +832,17 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C2', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'C2', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'OL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], @@ -911,12 +851,7 @@ describe('wordOnlineHandler', () => { isSelected: false, format: {}, }, - format: { - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '120px', - }, + format: { marginTop: '0px', marginBottom: '0px' }, }, { blockType: 'BlockGroup', @@ -924,34 +859,14 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'D', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'D', format: {} }], format: {}, isImplicit: true, }, ], levels: [ - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - paddingLeft: undefined, - marginLeft: undefined, - }, - dataset: {}, - }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, ], formatHolder: { segmentType: 'SelectionMarker', @@ -961,7 +876,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); }); @@ -978,7 +894,142 @@ describe('wordOnlineHandler', () => { it('only has text and list', () => { runTest( '

asdfasdf

  • A
  • B
  1. C1
  1. C2
  • D

asdfasdf

', - '

asdfasdf

  • A
    • B
      1. C1
      2. C2
    • D

asdfasdf

' + '

asdfasdf

  • A
    • B
      1. C1
      2. C2
    • D

asdfasdf

', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true ); }); @@ -1001,222 +1052,1071 @@ describe('wordOnlineHandler', () => { it('fragments contains text, list and table that consist of list 2', () => { runTest( '

asdfasdf

  • A
  • B
  1. C1
  1. C2
  • D

asdfasdf

asdfasdf

  • A
  • B
  • C
  • D

', - '

asdfasdf

  • A
    • B
      1. C1
      1. C2
    • D

asdfasdf

asdfasdf

  • A
  • B
  • C
  • D
' - ); - }); - // e.g. - // -------------- -------------- - //| text text | text text | - // -------------- -------------- - //| .a | .a | - // -------------- -------------- - it('fragments contains text, list and table that consist of list', () => { - runTest( - '

asdfasdf

asdfasdf222

  • A
  • A
', - '

asdfasdf

asdfasdf222

  • A
  • A
' - ); - }); - }); - - it('does not have list container', () => { - runTest( - '
  • A
  • B
  • C
  • D
  • E
', - '
  • A
    • B
      • C
    • D
      • E
' - ); - }); - - it('does not have BulletListStyle or NumberListStyle but has ListContainerWrapper', () => { - runTest( - '
  • A
  • B
  • C
', - '
  • A
    • B
      • C
' - ); - }); - - it('does not have BulletListStyle or NumberListStyle but has no ListContainerWrapper', () => { - runTest( - '
  • A
  • B
  • C
', - undefined, - { - blockGroupType: 'Document', - blocks: [ - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'A', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, + '

asdfasdf

  • A
    • B
      1. C1
      1. C2
    • D

asdfasdf

asdfasdf

  • A
  • B
  • C
  • D
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, }, - dataset: {}, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, format: {}, }, - format: {}, - }, - { - blockType: 'BlockGroup', - blockGroupType: 'ListItem', - blocks: [ - { - blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'B', - format: {}, - }, - ], - format: {}, - isImplicit: true, - }, - ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, - }, - dataset: {}, - }, - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, }, - dataset: {}, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, format: {}, }, - format: {}, - }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C1', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C2', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'OL', + format: { + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'asdfasdf', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdfasdf', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'B', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'C', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'D', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + }, + true + ); + }); + // e.g. + // -------------- -------------- + //| text text | text text | + // -------------- -------------- + //| .a | .a | + // -------------- -------------- + it('fragments contains text, list and table that consist of list', () => { + runTest( + '

asdfasdf

asdfasdf222

  • A
  • A
', + '

asdfasdf

asdfasdf222

  • A
  • A
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Table', + rows: [ + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdfasdf', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'asdfasdf222', + format: {}, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + height: 0, + format: {}, + cells: [ + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + { + blockGroupType: 'TableCell', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'A', + format: {}, + }, + ], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'UL', + format: {}, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + format: {}, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }, + ], + }, + true + ); + }); + }); + + it('does not have list container', () => { + runTest( + '
  • A
  • B
  • C
  • D
  • E
', + '
  • A
    • B
      • C
    • D
      • E
', + { + blockGroupType: 'Document', + blocks: [ { blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'C', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], format: {}, isImplicit: true, }, ], levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ { - listType: 'UL', - format: { marginLeft: undefined, paddingLeft: undefined }, - dataset: {}, + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, { listType: 'UL', - format: { marginLeft: undefined, paddingLeft: undefined }, + format: { marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, - { - listType: 'UL', - format: { marginLeft: undefined, paddingLeft: undefined }, - dataset: {}, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'E', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { + listType: 'UL', + format: { marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: { marginTop: '0px', marginBottom: '0px' }, + }, + ], + }, + true + ); + }); + + it('does not have BulletListStyle or NumberListStyle but has ListContainerWrapper', () => { + runTest( + '
  • A
  • B
  • C
', + '
  • A
    • B
      • C
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); + }); + + it('does not have BulletListStyle or NumberListStyle but has no ListContainerWrapper', () => { + runTest( + '
  • A
  • B
  • C
', + '
  • A
    • B
      • C
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); + }); + + describe('When html is not strictly formatted as word online, but can be identified as word online only contains one type of list', () => { + // html: + //
+ //
  • text
+ //
  • text
+ //
+ // result: + // .a + // .b + // .c + it('should process html properly, if ListContainerWrapper contains two UL', () => { + runTest( + '
  • A
  • B
  • C
', + '
  • A
  • B
  • C
', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true + ); + }); + + // html: + //
+ //
    + //
  • text
  • + //
  • text
  • + //
  • text
  • + //
    + // result: + // .test + // .test + // .test + it('shuold process html properly, when list items are not in side ul tag', () => { + runTest( + '
    • test

    • test

    • test

    • ', + '
    • test

    • test

    • test

    • ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + ], + }, + true + ); + }); + + // html: + //
      + //
        + //
      • text
      • + //
      • text
      • + //
          + //
        • text
        • + //
        • text
        • + //
        + //
      + //
      + // result: + // .text + // .text + // .text + // .text + // .text + it('should process html properly, if ListContainerWrapper contains list that is already well formatted', () => { + runTest( + '
      • A
        • B
          • C
        • D
          • E
      ', + '
      • A
        • B
          • C
        • D
          • E
      ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'E', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'UL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, }, - ], - formatHolder: { - segmentType: 'SelectionMarker', - isSelected: false, format: {}, }, - format: {}, - }, - ], - } - ); - }); - - describe('When html is not strictly formatted as word online, but can be identified as word online only contains one type of list', () => { - // html: - //
      - //
      • text
      - //
      • text
      - //
      - // result: - // .a - // .b - // .c - it('should process html properly, if ListContainerWrapper contains two UL', () => { - runTest( - '
      • A
      • B
      • C
      ', - '
      • A
      • B
      • C
      ' - ); - }); - - // html: - //
      - //
        - //
      • text
      • - //
      • text
      • - //
      • text
      • - //
        - // result: - // .test - // .test - // .test - it('shuold process html properly, when list items are not in side ul tag', () => { - runTest( - '
        • test

        • test

        • test

        • ', - '
        • test

        • test

        • test

        • ' - ); - }); - - // html: - //
          - //
            - //
          • text
          • - //
          • text
          • - //
              - //
            • text
            • - //
            • text
            • - //
            - //
          - //
          - // result: - // .text - // .text - // .text - // .text - // .text - it('should process html properly, if ListContainerWrapper contains list that is already well formatted', () => { - runTest( - '
          • A
            • B
              • C
            • D
              • E
          ', - '
          • A
            • B
              • C
            • D
              • E
          ' + ], + }, + true ); }); @@ -1234,7 +2134,76 @@ describe('wordOnlineHandler', () => { it('should process html properly, if there are multiple list item in ol (word online has one list item in each ol for ordered list)', () => { runTest( '
          1. A
          2. B
          1. C
          ', - '
          1. A
          2. B
          1. C
          ' + '
          1. A
          2. B
          1. C
          ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: { startNumberOverride: 1 }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1250,7 +2219,61 @@ describe('wordOnlineHandler', () => { it('shuold process html properly, if list item in a ListContainerWrapper are not inside ol ', () => { runTest( '
          1. test

          2. test

          3. test

          4. ', - '
          5. test

          6. test

          7. test

          8. ' + '
          9. test

          10. test

          11. test

          12. ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'General', + element: jasmine.anything() as any, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + format: {}, + }, + ], + }, + true ); }); }); @@ -1267,7 +2290,51 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains well formated UL and non formated ol', () => { runTest( '
            • A
            1. B
            ', - '
            • A
            1. B
            ' + '
            • A
            1. B
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1286,7 +2353,70 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains two OL', () => { runTest( '
            • A
            1. B
            1. C
            ', - '
            • A
            1. B
            2. C
            ' + '
            • A
            1. B
            2. C
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1303,7 +2433,76 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains two OL and one UL', () => { runTest( '
            • A
            1. B
            1. C
            ', - '
            • A
            1. B
            1. C
            ' + '
            • A
            1. B
            1. C
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [ + { + listType: 'OL', + format: { startNumberOverride: 1 }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1318,7 +2517,51 @@ describe('wordOnlineHandler', () => { it('should process html properly, if there are list not in the ListContainerWrapper', () => { runTest( '
            1. C
            • A
            ', - '
            1. C
            • A
            ' + '
            1. C
            • A
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1337,7 +2580,89 @@ describe('wordOnlineHandler', () => { it('should process html properly, if ListContainerWrapper contains two UL', () => { runTest( '
            1. C
            • A
            • A
            • A
            ', - '
            1. C
            • A
            • A
            • A
            ' + '
            1. C
            • A
            • A
            • A
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1349,7 +2674,38 @@ describe('wordOnlineHandler', () => { it('should retain all text, if ListContainerWrapper contains Elements before li and ul', () => { runTest( '

            paragraph

            1. C
            ', - '

            paragraph

            1. C
            ' + '

            paragraph

            1. C
            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'paragraph', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -1361,7 +2717,38 @@ describe('wordOnlineHandler', () => { it('should retain all text, if ListContainerWrapper contains Elements after li and ul', () => { runTest( '
            1. C

            paragraph

            ', - '
            1. C

            paragraph

            ' + '
            1. C

            paragraph

            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: {}, + isImplicit: true, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'paragraph', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true ); }); }); @@ -1419,7 +2806,38 @@ describe('wordOnlineHandler', () => { it('List directly under fragment', () => { runTest( '
            • A

            B

            ', - '
            • A

            B

            ' + '
            • A

            B

            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + true ); }); @@ -1432,7 +2850,92 @@ describe('wordOnlineHandler', () => { it('should remove the display and margin styles from the element', () => { runTest( '
            • A

            • B

            • C

              1. D

            ', - '
            • A

            • B

            • C

              1. D

            ' + '
            • A

            • B

            • C

              1. D

            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'A', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'B', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'C', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'D', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { listType: 'UL', format: {}, dataset: {} }, + { listType: 'OL', format: {}, dataset: {} }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); }); @@ -1537,27 +3040,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'List1', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'List1', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -1577,27 +3065,12 @@ describe('wordOnlineHandler', () => { blocks: [ { blockType: 'Paragraph', - segments: [ - { - segmentType: 'Text', - text: 'List2', - format: {}, - }, - ], + segments: [{ segmentType: 'Text', text: 'List2', format: {} }], format: {}, isImplicit: true, }, ], - levels: [ - { - listType: 'UL', - format: { - marginLeft: undefined, - paddingLeft: undefined, - }, - dataset: {}, - }, - ], + levels: [{ listType: 'UL', format: {}, dataset: {} }], formatHolder: { segmentType: 'SelectionMarker', isSelected: false, @@ -1606,7 +3079,8 @@ describe('wordOnlineHandler', () => { format: {}, }, ], - } + }, + true ); }); @@ -1623,7 +3097,81 @@ describe('wordOnlineHandler', () => { it('Remove temp marker from Word Online', () => { runTest( '

            it went:  

            1. Test

            1. Test. 


            ', - '

            it went:  

            1. Test

            2. Test. 


            ' + '

            it went:  

            1. Test

            2. Test. 


            ', + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'it went: ', format: {} }, + { segmentType: 'Text', text: ' ', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'Test', format: {} }], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'Test.', format: {} }, + { segmentType: 'Text', text: ' ', format: {} }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + }, + ], + levels: [{ listType: 'OL', format: {}, dataset: {} }], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + format: {}, + }, + ], + }, + true ); }); @@ -2856,39 +4404,20 @@ describe('wordOnlineHandler', () => { it('Test with multiple list items', () => { runTest( '
            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

            1. _ 

             

             

            _ 

             

            1. _ 

            _ 

            1. _ 

             

            _ 

             

            1. _ 

            1. _ 

             

            _ 

            1. _ 

            1. _ 

            ', - undefined, + '

             

             

             

             

             

             

            ', { blockGroupType: 'Document', blocks: [ { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2899,8 +4428,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2911,12 +4440,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -2926,42 +4449,41 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], - }, - { formatHolder: { - isSelected: false, segmentType: 'SelectionMarker', + isSelected: false, format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', }, }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2972,8 +4494,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -2984,12 +4506,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -2999,49 +4515,41 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], - }, - { formatHolder: { - isSelected: false, segmentType: 'SelectionMarker', + isSelected: false, format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', }, }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3052,8 +4560,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3064,12 +4572,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3079,49 +4581,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3132,8 +4636,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3144,12 +4648,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3159,56 +4657,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - { - listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3219,8 +4712,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3231,12 +4724,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3246,27 +4733,22 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, @@ -3274,28 +4756,38 @@ describe('wordOnlineHandler', () => { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3306,8 +4798,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3318,12 +4810,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3333,27 +4819,22 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, @@ -3361,21 +4842,38 @@ describe('wordOnlineHandler', () => { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3386,8 +4884,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3398,12 +4896,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3413,49 +4905,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, { listType: 'OL', - format: { - direction: 'ltr', - }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3466,8 +4960,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3478,12 +4972,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3493,42 +4981,51 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', }, dataset: {}, }, + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, + }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3539,8 +5036,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3551,12 +5048,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3566,42 +5057,41 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], - }, - { formatHolder: { - isSelected: false, segmentType: 'SelectionMarker', + isSelected: false, format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', }, }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - }, - dataset: {}, - }, - ], + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { blockType: 'BlockGroup', - format: { - direction: 'ltr', - }, blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3612,8 +5102,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3624,12 +5114,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3639,33 +5123,42 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3676,12 +5169,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -3692,16 +5179,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -3712,13 +5197,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3729,12 +5219,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -3745,16 +5229,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -3765,13 +5247,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3782,8 +5269,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3794,12 +5281,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -3810,16 +5291,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -3830,76 +5309,67 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', - italic: false, - textColor: 'rgb(0, 0, 0)', - fontWeight: 'normal', - lineHeight: '22.0875px', - }, - }, - ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - whiteSpace: 'pre-wrap', - marginTop: '0px', - marginRight: '0px', - marginBottom: '10.6667px', - marginLeft: '0px', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', + italic: false, + textColor: 'rgb(0, 0, 0)', + fontWeight: 'normal', + lineHeight: '22.0875px', + }, + }, + ], format: { direction: 'ltr', - startNumberOverride: 1, + textAlign: 'start', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginRight: '0px', + marginBottom: '10.6667px', + marginLeft: '0px', }, - dataset: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', + }, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3910,8 +5380,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3922,12 +5392,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -3937,33 +5401,47 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], + levels: [ + { + listType: 'OL', + format: { + direction: 'ltr', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, + }, + dataset: {}, + }, + ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3974,8 +5452,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -3986,12 +5464,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4002,42 +5474,35 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, - dataset: {}, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4048,8 +5513,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4060,12 +5525,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4075,33 +5534,42 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4112,12 +5580,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4128,16 +5590,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -4148,13 +5608,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4165,8 +5630,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4177,12 +5642,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4193,16 +5652,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -4213,76 +5670,67 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', italic: false, - textColor: 'rgb(0, 0, 0)', - fontWeight: 'normal', - lineHeight: '22.0875px', - }, - }, - ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - whiteSpace: 'pre-wrap', - marginTop: '0px', - marginRight: '0px', - marginBottom: '10.6667px', - marginLeft: '0px', - }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', + textColor: 'rgb(0, 0, 0)', + fontWeight: 'normal', + lineHeight: '22.0875px', + }, + }, + ], format: { direction: 'ltr', - startNumberOverride: 1, + textAlign: 'start', + textIndent: '0px', + whiteSpace: 'pre-wrap', + marginTop: '0px', + marginRight: '0px', + marginBottom: '10.6667px', + marginLeft: '0px', }, - dataset: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', + }, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4293,8 +5741,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4305,12 +5753,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4320,42 +5762,46 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4366,8 +5812,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4378,12 +5824,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4393,33 +5833,42 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, { - tagName: 'div', blockType: 'BlockGroup', - format: { - direction: 'ltr', - textAlign: 'start', - textIndent: '0px', - backgroundColor: 'rgb(255, 255, 255)', - marginTop: '0px', - marginRight: '0px', - marginBottom: '0px', - marginLeft: '0px', - }, blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4430,12 +5879,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4446,16 +5889,14 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - tagName: 'div', - blockType: 'BlockGroup', format: { direction: 'ltr', textAlign: 'start', @@ -4466,13 +5907,18 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'FormatContainer', + tagName: 'div', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4483,8 +5929,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4495,12 +5941,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { direction: 'ltr', textAlign: 'start', @@ -4511,43 +5951,35 @@ describe('wordOnlineHandler', () => { marginBottom: '10.6667px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, - }, - }, - ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, - levels: [ - { - listType: 'OL', - format: { - direction: 'ltr', - startNumberOverride: 1, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, - dataset: {}, + decorator: { tagName: 'p', format: {} }, }, ], - blockType: 'BlockGroup', format: { direction: 'ltr', + textAlign: 'start', + textIndent: '0px', + backgroundColor: 'rgb(255, 255, 255)', + marginTop: '0px', + marginRight: '0px', + marginBottom: '0px', + marginLeft: '0px', }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4558,8 +5990,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4570,12 +6002,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4585,42 +6011,46 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, }, ], - }, - { - formatHolder: { - isSelected: false, - segmentType: 'SelectionMarker', - format: { - fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', - fontSize: '12pt', - }, - }, levels: [ { listType: 'OL', format: { direction: 'ltr', + marginTop: '0px', + marginBottom: '0px', + startNumberOverride: 1, }, dataset: {}, }, ], - blockType: 'BlockGroup', - format: { - direction: 'ltr', + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + }, + { + blockType: 'BlockGroup', blockGroupType: 'ListItem', blocks: [ { + blockType: 'Paragraph', segments: [ { - text: '_', segmentType: 'Text', + text: '_', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4631,8 +6061,8 @@ describe('wordOnlineHandler', () => { }, }, { - text: ' ', segmentType: 'Text', + text: ' ', format: { fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', fontSize: '12pt', @@ -4643,12 +6073,6 @@ describe('wordOnlineHandler', () => { }, }, ], - segmentFormat: { - italic: false, - fontWeight: 'normal', - textColor: 'rgb(0, 0, 0)', - }, - blockType: 'Paragraph', format: { textAlign: 'start', textIndent: '0px', @@ -4658,12 +6082,30 @@ describe('wordOnlineHandler', () => { marginBottom: '0px', marginLeft: '0px', }, - decorator: { - tagName: 'p', - format: {}, + segmentFormat: { + italic: false, + fontWeight: 'normal', + textColor: 'rgb(0, 0, 0)', }, + decorator: { tagName: 'p', format: {} }, + }, + ], + levels: [ + { + listType: 'OL', + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, + dataset: {}, }, ], + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: { + fontFamily: 'Aptos, Aptos_MSFontService, sans-serif', + fontSize: '12pt', + }, + }, + format: { direction: 'ltr', marginTop: '0px', marginBottom: '0px' }, }, ], }, From 5b07b94b74ca448add162885951a5e3936d55da9 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 29 Aug 2024 12:03:42 -0700 Subject: [PATCH 09/43] Reconcile table and image selection for cache (#2714) * Improve cache * fix build * improve * add test * Cache and entity 2 * Add test * Reconcile table and image selection for cache * support reconcile entity delimiter * fix build * add test --------- Co-authored-by: Bryan Valverde U --- .../lib/corePlugin/cache/CachePlugin.ts | 24 +- .../lib/corePlugin/cache/MutationType.ts | 57 ++++ .../lib/corePlugin/cache/domIndexerImpl.ts | 200 +++++++++++- .../corePlugin/cache/textMutationObserver.ts | 90 +++--- .../lib/editor/core/DOMHelperImpl.ts | 4 +- .../test/corePlugin/cache/CachePluginTest.ts | 102 +++++- .../corePlugin/cache/domIndexerImplTest.ts | 299 +++++++++++++++++- .../cache/textMutationObserverTest.ts | 69 +++- .../test/editor/core/DOMHelperImplTest.ts | 31 ++ .../lib/modelApi/selection/setSelection.ts | 32 +- .../handlers/handleBlockGroupChildren.ts | 4 + .../lib/modelToDom/handlers/handleEntity.ts | 13 +- .../domToModel/processors/brProcessorTest.ts | 2 + .../processors/entityProcessorTest.ts | 2 + .../processors/generalProcessorTest.ts | 2 + .../processors/imageProcessorTest.ts | 2 + .../processors/tableProcessorTest.ts | 2 + .../processors/textProcessorTest.ts | 6 + .../modelApi/selection/setSelectionTest.ts | 67 ++++ .../handlers/handleBlockGroupChildrenTest.ts | 29 ++ .../modelToDom/handlers/handleEntityTest.ts | 4 +- .../handlers/handleParagraphTest.ts | 4 + .../modelToDom/handlers/handleTableTest.ts | 2 + .../lib/context/DomIndexer.ts | 16 + .../lib/parameter/DOMHelper.ts | 3 +- 25 files changed, 984 insertions(+), 82 deletions(-) create mode 100644 packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts index 4932076f5b2..7ad9cb7f3ec 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/CachePlugin.ts @@ -2,7 +2,7 @@ import { areSameSelections } from './areSameSelections'; import { createTextMutationObserver } from './textMutationObserver'; import { DomIndexerImpl } from './domIndexerImpl'; import { updateCache } from './updateCache'; -import type { Mutation } from './textMutationObserver'; +import type { Mutation } from './MutationType'; import type { CachePluginState, IEditor, @@ -90,6 +90,19 @@ class CachePlugin implements PluginWithState { } switch (event.eventType) { + case 'logicalRootChanged': + this.invalidateCache(); + + if (this.state.textMutationObserver) { + this.state.textMutationObserver.stopObserving(); + this.state.textMutationObserver = createTextMutationObserver( + event.logicalRoot, + this.onMutation + ); + this.state.textMutationObserver.startObserving(); + } + break; + case 'keyDown': case 'input': if (!this.state.textMutationObserver) { @@ -133,6 +146,15 @@ class CachePlugin implements PluginWithState { this.updateCachedModel(this.editor, true /*forceUpdate*/); break; + case 'elementId': + const element = mutation.element; + + if (!this.state.domIndexer?.reconcileElementId(element)) { + this.invalidateCache(); + } + + break; + case 'unknown': this.invalidateCache(); break; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts new file mode 100644 index 00000000000..03a8d513716 --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/MutationType.ts @@ -0,0 +1,57 @@ +/** + * @internal Type of mutations + */ +export type MutationType = + /** + * We found some change happened but we cannot handle it, so set mutation type as "unknown" + */ + | 'unknown' + /** + * Element id is changed + */ + | 'elementId' + /** + * Only text is changed + */ + | 'text' + /** + * Child list is changed + */ + | 'childList'; + +/** + * @internal + */ +export interface MutationBase { + type: T; +} + +/** + * @internal + */ +export interface UnknownMutation extends MutationBase<'unknown'> {} + +/** + * @internal + */ +export interface ElementIdMutation extends MutationBase<'elementId'> { + element: HTMLElement; +} + +/** + * @internal + */ +export interface TextMutation extends MutationBase<'text'> {} + +/** + * @internal + */ +export interface ChildListMutation extends MutationBase<'childList'> { + addedNodes: Node[]; + removedNodes: Node[]; +} + +/** + * @internal + */ +export type Mutation = UnknownMutation | ElementIdMutation | TextMutation | ChildListMutation; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts index 96bb781bb01..3887b42dbbb 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/domIndexerImpl.ts @@ -1,20 +1,24 @@ import { EmptySegmentFormat, + createParagraph, createSelectionMarker, createText, getObjectKeys, + isElementOfType, + isEntityDelimiter, isNodeOfType, setSelection, } from 'roosterjs-content-model-dom'; import type { CacheSelection, + ContentModelBlockGroup, ContentModelDocument, + ContentModelEntity, ContentModelParagraph, ContentModelSegment, ContentModelSegmentFormat, ContentModelSelectionMarker, ContentModelTable, - ContentModelTableRow, ContentModelText, DomIndexer, DOMSelection, @@ -34,7 +38,15 @@ export interface SegmentItem { * @internal Export for test only */ export interface TableItem { - tableRows: ContentModelTableRow[]; + table: ContentModelTable; +} + +/** + * @internal Export for test only + */ +export interface BlockEntityDelimiterItem { + entity: ContentModelEntity; + parent: ContentModelBlockGroup; } /** @@ -51,6 +63,13 @@ export interface IndexedTableElement extends HTMLTableElement { __roosterjsContentModel: TableItem; } +/** + * @internal Export for test only + */ +export interface IndexedEntityDelimiter extends Text { + __roosterjsContentModel: BlockEntityDelimiterItem; +} + /** * Context object used by DomIndexer when reconcile mutations with child list */ @@ -92,10 +111,38 @@ function isIndexedSegment(node: Node): node is IndexedSegmentNode { ); } +function isIndexedDelimiter(node: Node): node is IndexedEntityDelimiter { + const { entity, parent } = (node as IndexedEntityDelimiter).__roosterjsContentModel ?? {}; + + return ( + entity?.blockType == 'Entity' && + entity.wrapper && + parent?.blockGroupType && + Array.isArray(parent.blocks) + ); +} + function getIndexedSegmentItem(node: Node | null): SegmentItem | null { return node && isIndexedSegment(node) ? node.__roosterjsContentModel : null; } +function getIndexedTableItem(element: HTMLTableElement): TableItem | null { + const index = (element as IndexedTableElement).__roosterjsContentModel; + const table = index?.table; + + if ( + table?.blockType == 'Table' && + Array.isArray(table.rows) && + table.rows.every( + x => Array.isArray(x?.cells) && x.cells.every(y => y?.blockGroupType == 'TableCell') + ) + ) { + return index; + } else { + return null; + } +} + /** * @internal * Implementation of DomIndexer @@ -140,7 +187,12 @@ export class DomIndexerImpl implements DomIndexer { onTable(tableElement: HTMLTableElement, table: ContentModelTable) { const indexedTable = tableElement as IndexedTableElement; - indexedTable.__roosterjsContentModel = { tableRows: table.rows }; + indexedTable.__roosterjsContentModel = { table }; + } + + onBlockEntity(entity: ContentModelEntity, group: ContentModelBlockGroup) { + this.onBlockEntityDelimiter(entity.wrapper.previousSibling, entity, group); + this.onBlockEntityDelimiter(entity.wrapper.nextSibling, entity, group); } reconcileSelection( @@ -152,11 +204,10 @@ export class DomIndexerImpl implements DomIndexer { if ( oldSelection.type == 'range' && this.isCollapsed(oldSelection) && - isNodeOfType(oldSelection.start.node, 'TEXT_NODE') + isNodeOfType(oldSelection.start.node, 'TEXT_NODE') && + isIndexedSegment(oldSelection.start.node) ) { - if (isIndexedSegment(oldSelection.start.node)) { - this.reconcileTextSelection(oldSelection.start.node); - } + this.reconcileTextSelection(oldSelection.start.node); } else { setSelection(model); } @@ -164,8 +215,38 @@ export class DomIndexerImpl implements DomIndexer { switch (newSelection.type) { case 'image': + const indexedImage = getIndexedSegmentItem(newSelection.image); + const image = indexedImage?.segments[0]; + + if (image) { + image.isSelected = true; + setSelection(model, image); + + return true; + } else { + return false; + } + case 'table': - // For image and table selection, we just clear the cached model since during selecting the element id might be changed + const indexedTable = getIndexedTableItem(newSelection.table); + + if (indexedTable) { + const firstCell = + indexedTable.table.rows[newSelection.firstRow]?.cells[ + newSelection.firstColumn + ]; + const lastCell = + indexedTable.table.rows[newSelection.lastRow]?.cells[ + newSelection.lastColumn + ]; + + if (firstCell && lastCell) { + setSelection(model, firstCell, lastCell); + + return true; + } + } + return false; case 'range': @@ -182,7 +263,11 @@ export class DomIndexerImpl implements DomIndexer { delete model.hasRevertedRangeSelection; if (collapsed) { - return !!this.reconcileNodeSelection(startContainer, startOffset); + return !!this.reconcileNodeSelection( + startContainer, + startOffset, + model.format + ); } else if ( startContainer == endContainer && isNodeOfType(startContainer, 'TEXT_NODE') @@ -249,15 +334,63 @@ export class DomIndexerImpl implements DomIndexer { return canHandle && !context.pendingTextNode; } + reconcileElementId(element: HTMLElement) { + if (isElementOfType(element, 'img')) { + const indexedImg = getIndexedSegmentItem(element); + + if (indexedImg?.segments[0]?.segmentType == 'Image') { + indexedImg.segments[0].format.id = element.id; + + return true; + } else { + return false; + } + } else if (isElementOfType(element, 'table')) { + const indexedTable = getIndexedTableItem(element); + + if (indexedTable) { + indexedTable.table.format.id = element.id; + + return true; + } else { + return false; + } + } else { + return false; + } + } + + private onBlockEntityDelimiter( + node: Node | null, + entity: ContentModelEntity, + parent: ContentModelBlockGroup + ) { + if (isNodeOfType(node, 'ELEMENT_NODE') && isEntityDelimiter(node) && node.firstChild) { + const indexedDelimiter = node.firstChild as IndexedEntityDelimiter; + + indexedDelimiter.__roosterjsContentModel = { entity, parent }; + } + } + private isCollapsed(selection: RangeSelectionForCache): boolean { const { start, end } = selection; return start.node == end.node && start.offset == end.offset; } - private reconcileNodeSelection(node: Node, offset: number): Selectable | undefined { + private reconcileNodeSelection( + node: Node, + offset: number, + defaultFormat?: ContentModelSegmentFormat + ): Selectable | undefined { if (isNodeOfType(node, 'TEXT_NODE')) { - return isIndexedSegment(node) ? this.reconcileTextSelection(node, offset) : undefined; + if (isIndexedSegment(node)) { + return this.reconcileTextSelection(node, offset); + } else if (isIndexedDelimiter(node)) { + return this.reconcileDelimiterSelection(node, defaultFormat); + } else { + return undefined; + } } else if (offset >= node.childNodes.length) { return this.insertMarker(node.lastChild, true /*isAfter*/); } else { @@ -369,11 +502,56 @@ export class DomIndexerImpl implements DomIndexer { if (!this.persistCache) { delete paragraph.cachedElement; } + } else if (first?.segmentType == 'Entity' && first == last) { + const wrapper = first.wrapper; + const index = paragraph.segments.indexOf(first); + const delimiter = textNode.parentElement; + const isBefore = wrapper.previousSibling == delimiter; + const isAfter = wrapper.nextSibling == delimiter; + + if (index >= 0 && delimiter && isEntityDelimiter(delimiter) && (isBefore || isAfter)) { + const marker = createSelectionMarker( + (paragraph.segments[isAfter ? index + 1 : index - 1] ?? first).format + ); + + paragraph.segments.splice(isAfter ? index + 1 : index, 0, marker); + + selectable = marker; + } } return selectable; } + private reconcileDelimiterSelection( + node: IndexedEntityDelimiter, + defaultFormat?: ContentModelSegmentFormat + ) { + let marker: ContentModelSelectionMarker | undefined; + + const { entity, parent } = node.__roosterjsContentModel; + const index = parent.blocks.indexOf(entity); + const delimiter = node.parentElement; + const wrapper = entity.wrapper; + const isBefore = wrapper.previousSibling == delimiter; + const isAfter = wrapper.nextSibling == delimiter; + + if (index >= 0 && delimiter && isEntityDelimiter(delimiter) && (isBefore || isAfter)) { + marker = createSelectionMarker(defaultFormat); + + const para = createParagraph( + true /*isImplicit*/, + undefined /*blockFormat*/, + defaultFormat + ); + + para.segments.push(marker); + parent.blocks.splice(isBefore ? index : index + 1, 0, para); + } + + return marker; + } + private reconcileAddedNode(node: Text, context: ReconcileChildListContext): boolean { let segmentItem: SegmentItem | null = null; let index = -1; diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts index 4f833395646..d20aa100740 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/cache/textMutationObserver.ts @@ -1,13 +1,22 @@ -import type { TextMutationObserver } from 'roosterjs-content-model-types'; +import { createDOMHelper } from '../../editor/core/DOMHelperImpl'; +import { + findClosestBlockEntityContainer, + findClosestEntityWrapper, + isNodeOfType, +} from 'roosterjs-content-model-dom'; +import type { DOMHelper, TextMutationObserver } from 'roosterjs-content-model-types'; +import type { Mutation } from './MutationType'; class TextMutationObserverImpl implements TextMutationObserver { private observer: MutationObserver; + private domHelper: DOMHelper; constructor( private contentDiv: HTMLDivElement, private onMutation: (mutation: Mutation) => void ) { this.observer = new MutationObserver(this.onMutationInternal); + this.domHelper = createDOMHelper(contentDiv); } startObserving() { @@ -39,14 +48,40 @@ class TextMutationObserverImpl implements TextMutationObserver { let removedNodes: Node[] = []; let reconcileText = false; + const ignoredNodes = new Set(); + const includedNodes = new Set(); + for (let i = 0; i < mutations.length && canHandle; i++) { const mutation = mutations[i]; + const target = mutation.target; + + if (ignoredNodes.has(target)) { + continue; + } else if (!includedNodes.has(target)) { + if ( + findClosestEntityWrapper(target, this.domHelper) || + findClosestBlockEntityContainer(target, this.domHelper) + ) { + ignoredNodes.add(target); + + continue; + } else { + includedNodes.add(target); + } + } switch (mutation.type) { case 'attributes': - if (mutation.target != this.contentDiv) { - // We cannot handle attributes changes on editor content for now - canHandle = false; + if (this.domHelper.isNodeInEditor(target, true /*excludingSelf*/)) { + if ( + mutation.attributeName == 'id' && + isNodeOfType(target, 'ELEMENT_NODE') + ) { + this.onMutation({ type: 'elementId', element: target }); + } else { + // We cannot handle attributes changes on editor content for now + canHandle = false; + } } break; @@ -94,53 +129,6 @@ class TextMutationObserverImpl implements TextMutationObserver { }; } -/** - * @internal Type of mutations - */ -export type MutationType = - /** - * We found some change happened but we cannot handle it, so set mutation type as "unknown" - */ - | 'unknown' - /** - * Only text is changed - */ - | 'text' - /** - * Child list is changed - */ - | 'childList'; - -/** - * @internal - */ -export interface MutationBase { - type: T; -} - -/** - * @internal - */ -export interface UnknownMutation extends MutationBase<'unknown'> {} - -/** - * @internal - */ -export interface TextMutation extends MutationBase<'text'> {} - -/** - * @internal - */ -export interface ChildListMutation extends MutationBase<'childList'> { - addedNodes: Node[]; - removedNodes: Node[]; -} - -/** - * @internal - */ -export type Mutation = UnknownMutation | TextMutation | ChildListMutation; - /** * @internal */ diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index a638dc10027..a4b9a49474a 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -12,8 +12,8 @@ class DOMHelperImpl implements DOMHelper { return this.contentDiv.textContent || ''; } - isNodeInEditor(node: Node): boolean { - return this.contentDiv.contains(node); + isNodeInEditor(node: Node, excludeRoot?: boolean): boolean { + return excludeRoot && node == this.contentDiv ? false : this.contentDiv.contains(node); } calculateZoomScale(): number { diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts index c67de4cec50..dc15758c9cd 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/CachePluginTest.ts @@ -1,6 +1,7 @@ import * as textMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; import { createCachePlugin } from '../../../lib/corePlugin/cache/CachePlugin'; import { DomIndexerImpl } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { Mutation } from '../../../lib/corePlugin/cache/MutationType'; import { CachePluginState, DomIndexer, @@ -82,6 +83,52 @@ describe('CachePlugin', () => { }); }); + describe('logicalRootChanged event', () => { + it('Change to a new node', () => { + const startObservingSpy = jasmine.createSpy('startObserving'); + const stopObservingSpy = jasmine.createSpy('stopObserving'); + const mockedObserver = { + startObserving: startObservingSpy, + stopObserving: stopObservingSpy, + } as any; + const mockedNode = 'NODE' as any; + + const textMutationObserverSpy = spyOn( + textMutationObserver, + 'createTextMutationObserver' + ).and.returnValue(mockedObserver); + + init({}); + + const state = plugin.getState(); + + state.cachedModel = 'MODEL' as any; + state.cachedSelection = 'SELECTION' as any; + + expect(stopObservingSpy).toHaveBeenCalledTimes(0); + expect(startObservingSpy).toHaveBeenCalledTimes(1); + expect(textMutationObserverSpy).toHaveBeenCalledTimes(1); + + plugin.onPluginEvent({ + eventType: 'logicalRootChanged', + logicalRoot: mockedNode, + }); + + expect(state).toEqual({ + cachedModel: undefined, + cachedSelection: undefined, + domIndexer: new DomIndexerImpl(), + textMutationObserver: mockedObserver, + }); + expect(stopObservingSpy).toHaveBeenCalledTimes(1); + expect(startObservingSpy).toHaveBeenCalledTimes(2); + expect(textMutationObserverSpy).toHaveBeenCalledTimes(2); + expect(textMutationObserverSpy.calls.argsFor(1)[0]).toBe(mockedNode); + + plugin.dispose(); + }); + }); + describe('KeyDown event', () => { beforeEach(() => { init({ disableCache: true }); @@ -375,15 +422,17 @@ describe('CachePlugin', () => { }); describe('onMutation', () => { - let onMutation: (mutation: textMutationObserver.Mutation) => void; + let onMutation: (mutation: Mutation) => void; let startObservingSpy: jasmine.Spy; let stopObservingSpy: jasmine.Spy; let mockedObserver: any; let reconcileChildListSpy: jasmine.Spy; + let reconcileElementIdSpy: jasmine.Spy; let mockedIndexer: DomIndexer; beforeEach(() => { reconcileChildListSpy = jasmine.createSpy('reconcileChildList'); + reconcileElementIdSpy = jasmine.createSpy('reconcileElementId'); startObservingSpy = jasmine.createSpy('startObserving'); stopObservingSpy = jasmine.createSpy('stopObserving'); @@ -404,6 +453,7 @@ describe('CachePlugin', () => { mockedIndexer = { reconcileSelection: reconcileSelectionSpy, reconcileChildList: reconcileChildListSpy, + reconcileElementId: reconcileElementIdSpy, } as any; }); @@ -530,5 +580,55 @@ describe('CachePlugin', () => { cachedSelection: 'SELECTION' as any, }); }); + + it('elementId, can reconcile', () => { + const state = plugin.getState(); + state.cachedModel = 'MODEL' as any; + state.cachedSelection = 'SELECTION' as any; + state.domIndexer = mockedIndexer; + + const mockedElement = 'ELEMENT' as any; + + reconcileElementIdSpy.and.returnValue(true); + + onMutation({ + type: 'elementId', + element: mockedElement, + }); + + expect(reconcileElementIdSpy).toHaveBeenCalledTimes(1); + expect(reconcileElementIdSpy).toHaveBeenCalledWith(mockedElement); + expect(state).toEqual({ + domIndexer: mockedIndexer, + textMutationObserver: mockedObserver, + cachedModel: 'MODEL' as any, + cachedSelection: 'SELECTION' as any, + }); + }); + + it('elementId, cannot reconcile', () => { + const state = plugin.getState(); + state.cachedModel = 'MODEL' as any; + state.cachedSelection = 'SELECTION' as any; + state.domIndexer = mockedIndexer; + + const mockedElement = 'ELEMENT' as any; + + reconcileElementIdSpy.and.returnValue(false); + + onMutation({ + type: 'elementId', + element: mockedElement, + }); + + expect(reconcileElementIdSpy).toHaveBeenCalledTimes(1); + expect(reconcileElementIdSpy).toHaveBeenCalledWith(mockedElement); + expect(state).toEqual({ + domIndexer: mockedIndexer, + textMutationObserver: mockedObserver, + cachedModel: undefined, + cachedSelection: undefined, + }); + }); }); }); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts index 698df5f7afc..378a7addb88 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/domIndexerImplTest.ts @@ -1,6 +1,14 @@ import * as setSelection from 'roosterjs-content-model-dom/lib/modelApi/selection/setSelection'; import { createRange } from 'roosterjs-content-model-dom/test/testUtils'; -import { DomIndexerImpl, IndexedSegmentNode } from '../../../lib/corePlugin/cache/domIndexerImpl'; +import { + BlockEntityDelimiterItem, + DomIndexerImpl, + IndexedEntityDelimiter, + IndexedSegmentNode, + IndexedTableElement, + SegmentItem, + TableItem, +} from '../../../lib/corePlugin/cache/domIndexerImpl'; import { CacheSelection, ContentModelDocument, @@ -10,6 +18,7 @@ import { import { createBr, createContentModelDocument, + createEntity, createImage, createParagraph, createSelectionMarker, @@ -18,6 +27,8 @@ import { createText, } from 'roosterjs-content-model-dom'; +const ZERO_WIDTH_SPACE = '\u200B'; + describe('domIndexerImpl.onSegment', () => { it('onSegment', () => { const node = {} as any; @@ -185,7 +196,55 @@ describe('domIndexerImpl.onTable', () => { domIndexerImpl.onTable(node, table); expect(node).toEqual({ - __roosterjsContentModel: { tableRows: rows }, + __roosterjsContentModel: { table }, + }); + }); +}); + +describe('domIndexerImpl.onBlockEntity', () => { + it('no delimiter', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + + root.appendChild(wrapper); + + const group = createContentModelDocument(); + const entity = createEntity(wrapper); + + new DomIndexerImpl().onBlockEntity(entity, group); + + expect(root.innerHTML).toEqual(''); + }); + + it('has delimiters', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + const text1 = document.createTextNode(ZERO_WIDTH_SPACE); + const text2 = document.createTextNode(ZERO_WIDTH_SPACE); + + delimiter1.className = 'entityDelimiterBefore'; + delimiter1.appendChild(text1); + delimiter2.className = 'entityDelimiterAfter'; + delimiter2.appendChild(text2); + + root.appendChild(delimiter1); + root.appendChild(wrapper); + root.appendChild(delimiter2); + + const group = createContentModelDocument(); + const entity = createEntity(wrapper); + + new DomIndexerImpl().onBlockEntity(entity, group); + + expect((text1 as IndexedEntityDelimiter).__roosterjsContentModel).toEqual({ + entity, + parent: group, + }); + expect((text2 as IndexedEntityDelimiter).__roosterjsContentModel).toEqual({ + entity, + parent: group, }); }); }); @@ -517,7 +576,7 @@ describe('domIndexerImpl.reconcileSelection', () => { const result = domIndexerImpl.reconcileSelection(model, newRangeEx); - expect(result).toBeFalse(); + expect(result).toBeTrue(); expect(node1.__roosterjsContentModel).toEqual({ paragraph, segments: [oldSegment1], @@ -527,7 +586,14 @@ describe('domIndexerImpl.reconcileSelection', () => { format: {}, segments: [oldSegment1], }); - expect(setSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionSpy).toHaveBeenCalledWith(model, { + segmentType: 'Image', + src: 'test', + format: {}, + dataset: {}, + isSelected: true, + isSelectedAsImageSelection: true, + }); expect(model).toEqual({ blockGroupType: 'Document', blocks: [paragraph], @@ -537,6 +603,8 @@ describe('domIndexerImpl.reconcileSelection', () => { src: 'test', format: {}, dataset: {}, + isSelected: true, + isSelectedAsImageSelection: true, }); expect(model.hasRevertedRangeSelection).toBeFalsy(); }); @@ -574,11 +642,11 @@ describe('domIndexerImpl.reconcileSelection', () => { const result = domIndexerImpl.reconcileSelection(model, newRangeEx); - expect(result).toBeFalse(); + expect(result).toBeTrue(); expect(node1.__roosterjsContentModel).toEqual({ - tableRows: tableModel.rows, + table: tableModel, }); - expect(setSelectionSpy).not.toHaveBeenCalled(); + expect(setSelectionSpy).toHaveBeenCalledWith(model, cell10, cell21); expect(model).toEqual({ blockGroupType: 'Document', blocks: [tableModel], @@ -736,6 +804,136 @@ describe('domIndexerImpl.reconcileSelection', () => { expect(setSelectionSpy).toHaveBeenCalled(); expect(model.hasRevertedRangeSelection).toBeFalsy(); }); + + it('block entity: selection in first delimiter', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + const text1 = document.createTextNode(ZERO_WIDTH_SPACE); + const text2 = document.createTextNode(ZERO_WIDTH_SPACE); + + delimiter1.className = 'entityDelimiterBefore'; + delimiter2.className = 'entityDelimiterAfter'; + delimiter1.appendChild(text1); + delimiter2.appendChild(text2); + root.appendChild(delimiter1); + root.appendChild(wrapper); + root.appendChild(delimiter2); + + const entity = createEntity(wrapper); + const group = createContentModelDocument(); + + group.blocks.push(entity); + + const index1: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + const index2: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + + (text1 as IndexedEntityDelimiter).__roosterjsContentModel = index1; + (text2 as IndexedEntityDelimiter).__roosterjsContentModel = index2; + + const range = document.createRange(); + range.setStart(text1, 0); + + const indexer = new DomIndexerImpl(); + + const result = indexer.reconcileSelection(group, { + type: 'range', + range: range, + isReverted: false, + }); + + expect(result).toBeTrue(); + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'SelectionMarker', isSelected: true, format: {} }], + format: {}, + isImplicit: true, + }, + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { isReadonly: true, id: undefined, entityType: undefined }, + wrapper: wrapper, + }, + ], + }); + }); + + it('block entity: selection in last delimiter', () => { + const root = document.createElement('div'); + const wrapper = document.createElement('span'); + const delimiter1 = document.createElement('span'); + const delimiter2 = document.createElement('span'); + const text1 = document.createTextNode(ZERO_WIDTH_SPACE); + const text2 = document.createTextNode(ZERO_WIDTH_SPACE); + + delimiter1.className = 'entityDelimiterBefore'; + delimiter2.className = 'entityDelimiterAfter'; + delimiter1.appendChild(text1); + delimiter2.appendChild(text2); + root.appendChild(delimiter1); + root.appendChild(wrapper); + root.appendChild(delimiter2); + + const entity = createEntity(wrapper); + const group = createContentModelDocument(); + + group.blocks.push(entity); + + const index1: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + const index2: BlockEntityDelimiterItem = { + entity: entity, + parent: group, + }; + + (text1 as IndexedEntityDelimiter).__roosterjsContentModel = index1; + (text2 as IndexedEntityDelimiter).__roosterjsContentModel = index2; + + const range = document.createRange(); + range.setStart(text2, 1); + + const indexer = new DomIndexerImpl(); + + const result = indexer.reconcileSelection(group, { + type: 'range', + range: range, + isReverted: false, + }); + + expect(result).toBeTrue(); + expect(group).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { isReadonly: true, id: undefined, entityType: undefined }, + wrapper: wrapper, + }, + { + blockType: 'Paragraph', + segments: [{ segmentType: 'SelectionMarker', isSelected: true, format: {} }], + format: {}, + isImplicit: true, + }, + ], + }); + }); }); describe('domIndexerImpl.reconcileChildList', () => { @@ -915,3 +1113,90 @@ describe('domIndexerImpl.reconcileChildList', () => { }); }); }); + +describe('domIndexerImpl.reconcileElementId', () => { + it('unindexed image id', () => { + const img = document.createElement('img'); + const image = createImage('test'); + const para = createParagraph(); + + para.segments.push(image); + + img.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(img); + + expect(result).toBe(false); + expect(image).toEqual({ + segmentType: 'Image', + format: {}, + src: 'test', + dataset: {}, + }); + }); + + it('indexed image id', () => { + const img = document.createElement('img'); + const image = createImage('test'); + const para = createParagraph(); + const segIndex: SegmentItem = { + paragraph: para, + segments: [image], + }; + + para.segments.push(image); + + ((img as Node) as IndexedSegmentNode).__roosterjsContentModel = segIndex; + + img.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(img); + + expect(result).toBe(true); + expect(image).toEqual({ + segmentType: 'Image', + format: { id: 'testId' }, + src: 'test', + dataset: {}, + }); + }); + + it('unindexed table id', () => { + const tb = document.createElement('table'); + + tb.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(tb); + + expect(result).toBe(false); + }); + + it('indexed table id', () => { + const tb = document.createElement('table'); + const table = createTable(1); + const tbIndex: TableItem = { + table: table, + }; + + (tb as IndexedTableElement).__roosterjsContentModel = tbIndex; + + tb.id = 'testId'; + + const result = new DomIndexerImpl().reconcileElementId(tb); + + expect(result).toBe(true); + expect(table).toEqual({ + blockType: 'Table', + format: { id: 'testId' }, + widths: [], + dataset: {}, + rows: [ + { + height: 0, + format: {}, + cells: [], + }, + ], + }); + }); +}); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts index 16835b04b7a..2ca6cc15e2d 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/cache/textMutationObserverTest.ts @@ -1,3 +1,4 @@ +import * as entityUtils from 'roosterjs-content-model-dom/lib/domUtils/entityUtils'; import * as textMutationObserver from '../../../lib/corePlugin/cache/textMutationObserver'; import { TextMutationObserver } from 'roosterjs-content-model-types'; @@ -295,7 +296,7 @@ describe('TextMutationObserverImpl', () => { expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' }); }); - it('attribute change', async () => { + it('id change', async () => { const div = document.createElement('div'); const div1 = document.createElement('div'); @@ -314,6 +315,72 @@ describe('TextMutationObserverImpl', () => { window.setTimeout(resolve, 10); }); + expect(onMutation).toHaveBeenCalledTimes(1); + expect(onMutation).toHaveBeenCalledWith({ type: 'elementId', element: div1 }); + }); + + it('unknown attribute change', async () => { + const div = document.createElement('div'); + const div1 = document.createElement('div'); + + div.appendChild(div1); + + const onMutation = jasmine.createSpy('onMutation'); + observer = textMutationObserver.createTextMutationObserver(div, onMutation); + + observer.startObserving(); + + div1.setAttribute('attr', 'value'); + + observer.flushMutations(); + + await new Promise(resolve => { + window.setTimeout(resolve, 10); + }); + + expect(onMutation).toHaveBeenCalledTimes(1); + expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' }); + }); + + it('Ignore changes under entity', () => { + const div = document.createElement('div'); + const wrapper = document.createElement('div'); + const span1 = document.createElement('span'); + const span2 = document.createElement('span'); + + wrapper.appendChild(span1); + wrapper.className = '_Entity'; + + div.appendChild(wrapper); + div.appendChild(span2); + + const findClosestEntityWrapperSpy = spyOn( + entityUtils, + 'findClosestEntityWrapper' + ).and.callThrough(); + const findClosestBlockEntityContainer = spyOn( + entityUtils, + 'findClosestBlockEntityContainer' + ).and.callThrough(); + + const onMutation = jasmine.createSpy('onMutation'); + + observer = textMutationObserver.createTextMutationObserver(div, onMutation); + observer.startObserving(); + + span1.setAttribute('attr1', 'value1'); + span1.setAttribute('attr2', 'value2'); + span2.setAttribute('attr3', 'value3'); + span2.setAttribute('attr4', 'value4'); + + observer.flushMutations(); + + expect(findClosestEntityWrapperSpy).toHaveBeenCalledTimes(2); + expect(findClosestEntityWrapperSpy).toHaveBeenCalledWith(span1, jasmine.anything()); + expect(findClosestEntityWrapperSpy).toHaveBeenCalledWith(span2, jasmine.anything()); + expect(findClosestBlockEntityContainer).toHaveBeenCalledTimes(1); + expect(findClosestBlockEntityContainer).toHaveBeenCalledWith(span2, jasmine.anything()); + expect(onMutation).toHaveBeenCalledTimes(1); expect(onMutation).toHaveBeenCalledWith({ type: 'unknown' }); }); diff --git a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts index 2bc51a43872..fb490373ed3 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts @@ -17,6 +17,37 @@ describe('DOMHelperImpl', () => { expect(result).toBe(mockedResult); expect(containsSpy).toHaveBeenCalledWith(mockedNode); }); + + it('isNodeInEditor, check root node, excludeRoot=false', () => { + const div = document.createElement('div'); + const domHelper = createDOMHelper(div); + + const result = domHelper.isNodeInEditor(div); + + expect(result).toBeTrue(); + }); + + it('isNodeInEditor, check root node, excludeRoot=true', () => { + const div = document.createElement('div'); + const domHelper = createDOMHelper(div); + + const result = domHelper.isNodeInEditor(div, true); + + expect(result).toBeFalse(); + }); + + it('isNodeInEditor, check root node, excludeRoot=true, do not call contains', () => { + const containsSpy = jasmine.createSpy('contains'); + const mockedDiv = { + contains: containsSpy, + } as any; + const domHelper = createDOMHelper(mockedDiv); + + const result = domHelper.isNodeInEditor(mockedDiv, true); + + expect(result).toBeFalse(); + expect(containsSpy).not.toHaveBeenCalled(); + }); }); describe('queryElements', () => { diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts b/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts index 1d30893e6e6..16d71b6614e 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/selection/setSelection.ts @@ -37,10 +37,28 @@ function setSelectionToBlockGroup( setIsSelected(mutateBlock(group), isInSelection); } - group.blocks.forEach(block => { + const blocksToDelete: number[] = []; + + group.blocks.forEach((block, i) => { isInSelection = setSelectionToBlock(block, isInSelection, start, end); + + if (block.blockType == 'Paragraph' && block.segments.length == 0 && block.isImplicit) { + blocksToDelete.push(i); + } }); + let index: number | undefined; + + if (blocksToDelete.length > 0) { + const mutableGroup = mutateBlock(group); + + while ((index = blocksToDelete.pop()) !== undefined) { + if (index >= 0) { + mutableGroup.blocks.splice(index, 1); + } + } + } + return isInSelection; }); } @@ -97,11 +115,15 @@ function setSelectionToBlock( ); }); - let index: number | undefined; + if (segmentsToDelete.length > 0) { + const mutablePara = mutateBlock(block); - while ((index = segmentsToDelete.pop()) !== undefined) { - if (index >= 0) { - mutateBlock(block).segments.splice(index, 1); + let index: number | undefined; + + while ((index = segmentsToDelete.pop()) !== undefined) { + if (index >= 0) { + mutablePara.segments.splice(index, 1); + } } } diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts index 75cd3723db9..8c749f5a039 100644 --- a/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts +++ b/packages/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlockGroupChildren.ts @@ -32,6 +32,10 @@ export const handleBlockGroupChildren: ContentModelHandler if (context.addDelimiterForEntity && entityFormat.isReadonly) { const [after, before] = addDelimiters(doc, wrapper, getSegmentFormat(context), context); - newSegments?.push(after, before); + if (newSegments) { + newSegments.push(after, before); + + if (after.firstChild) { + newSegments.push(after.firstChild); + } + + if (before.firstChild) { + newSegments.push(before.firstChild); + } + } + context.regularSelection.current.segment = after; } else { context.regularSelection.current.segment = wrapper; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts index ef681426f59..f92da141de7 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts @@ -75,6 +75,8 @@ describe('brProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts index 0d6feb24ac9..8521782a205 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts @@ -260,6 +260,8 @@ describe('entityProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts index 872c1fde02a..a6d516f3e66 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/generalProcessorTest.ts @@ -395,6 +395,8 @@ describe('generalProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts index 3a2be47d866..15bd59f68a7 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/imageProcessorTest.ts @@ -324,6 +324,8 @@ describe('imageProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts index 08cceeab06a..27bcf4221be 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/tableProcessorTest.ts @@ -291,6 +291,8 @@ describe('tableProcessor', () => { onTable: onTableSpy, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts index 89ef5687a5d..621e29e17a8 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/textProcessorTest.ts @@ -578,6 +578,8 @@ describe('textProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; @@ -613,6 +615,8 @@ describe('textProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; @@ -659,6 +663,8 @@ describe('textProcessor', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts index b776c9a0c33..0762f0af7c6 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/selection/setSelectionTest.ts @@ -1,3 +1,4 @@ +import { ContentModelDocument } from 'roosterjs-content-model-types'; import { setSelection } from '../../../lib/modelApi/selection/setSelection'; import { createBr, @@ -937,4 +938,70 @@ describe('setSelection', () => { ], }); }); + + it('delete empty segment after setSelection', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + }, + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + }; + + setSelection(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Br', + format: {}, + }, + ], + }, + ], + }); + }); + + it('delete empty paragraph after setSelection', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'SelectionMarker', + format: {}, + }, + ], + isImplicit: true, + }, + ], + }; + + setSelection(model); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [], + }); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts index 489c5d9c097..2a5673a1af6 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts @@ -1,4 +1,5 @@ import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; +import { createEntity } from '../../../lib'; import { createListItem } from '../../../lib/modelApi/creators/createListItem'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; @@ -379,4 +380,32 @@ describe('handleBlockGroupChildren', () => { '

            ' ); }); + + it('child contains entity', () => { + const group = createContentModelDocument(); + const paragraph = createParagraph(); + const wrapper = document.createElement('div'); + const entity = createEntity(wrapper); + + group.blocks.push(paragraph, entity); + + const onBlockEntity = jasmine.createSpy('onBlockEntity'); + const onParagraph = jasmine.createSpy('onParagraph'); + + context.domIndexer = { + onBlockEntity, + onParagraph, + } as any; + + handleBlockGroupChildren(document, parent, group, context); + + expect(parent.outerHTML).toBe( + '
            ' + ); + expect(handleBlock).toHaveBeenCalledTimes(2); + expect(handleBlock).toHaveBeenCalledWith(document, parent, paragraph, context, null); + expect(handleBlock).toHaveBeenCalledWith(document, parent, entity, context, null); + expect(onBlockEntity).toHaveBeenCalledTimes(1); + expect(onBlockEntity).toHaveBeenCalledWith(entity, group); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts index cf98d8daffc..41dcf9f20c8 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts @@ -329,10 +329,12 @@ describe('handleEntity', () => { '' ); expect(entityUtils.addDelimiters).toHaveBeenCalledTimes(1); - expect(newSegments.length).toBe(3); + expect(newSegments.length).toBe(5); expect(newSegments[0]).toBe(span); expect(newSegments[1]).toBe(span.nextSibling!); expect(newSegments[2]).toBe(span.previousSibling!); + expect(newSegments[3]).toBe(span.nextSibling!.firstChild!); + expect(newSegments[4]).toBe(span.previousSibling!.firstChild!); }); it('Inline entity with newSegments but no delimiter', () => { diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts index 8fec57ab9aa..2c906304572 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts @@ -582,6 +582,8 @@ describe('handleParagraph', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; @@ -626,6 +628,8 @@ describe('handleParagraph', () => { onTable: null!, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts index a27afe8415e..5eac39a4727 100644 --- a/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelToDom/handlers/handleTableTest.ts @@ -602,6 +602,8 @@ describe('handleTable', () => { onTable: onTableSpy, reconcileSelection: null!, reconcileChildList: null!, + onBlockEntity: null!, + reconcileElementId: null!, }; context.domIndexer = domIndexer; diff --git a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts index f1d68f845a9..7c600dc0c8f 100644 --- a/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts +++ b/packages/roosterjs-content-model-types/lib/context/DomIndexer.ts @@ -1,9 +1,11 @@ +import type { ContentModelBlockGroup } from '../contentModel/blockGroup/ContentModelBlockGroup'; import type { CacheSelection } from '../pluginState/CachePluginState'; import type { ContentModelDocument } from '../contentModel/blockGroup/ContentModelDocument'; import type { ContentModelParagraph } from '../contentModel/block/ContentModelParagraph'; import type { ContentModelSegment } from '../contentModel/segment/ContentModelSegment'; import type { ContentModelTable } from '../contentModel/block/ContentModelTable'; import type { DOMSelection } from '../selection/DOMSelection'; +import type { ContentModelEntity } from '../contentModel/entity/ContentModelEntity'; /** * Represents an indexer object which provides methods to help build backward relationship @@ -34,6 +36,13 @@ export interface DomIndexer { */ onTable: (tableElement: HTMLTableElement, tableModel: ContentModelTable) => void; + /** + * Invoke when new block entity is created in DOM tree + * @param entity The related entity + * @param parent Parent of entity. For block element, this should be the parent block group. For inline entity, this should be the parent paragraph + */ + onBlockEntity: (entity: ContentModelEntity, group: ContentModelBlockGroup) => void; + /** * When document content or selection is changed by user, we need to use this function to update the content model * to reflect the latest document. This process can fail since the selected node may not have a related model data structure. @@ -48,6 +57,13 @@ export interface DomIndexer { oldSelection?: CacheSelection ) => boolean; + /** + * When id is changed from DOM element, update the new ID to related content model if possible + * @param element The element that has id changed + * @returns True if successfully updated, otherwise false + */ + reconcileElementId: (element: HTMLElement) => boolean; + /** * When child list of editor content is changed, we can use this method to do sync the change from editor into content model. * This is mostly used when user start to type in an empty line. In that case browser will remove the existing BR node in the empty line if any, diff --git a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts index 9bb886a6ac8..91bd2d976ad 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts @@ -5,8 +5,9 @@ export interface DOMHelper { /** * Check if the given DOM node is in editor * @param node The node to check + * @param excludeRoot When pass true, the function will return false if the passed in node is the root node itself */ - isNodeInEditor(node: Node): boolean; + isNodeInEditor(node: Node, excludeRoot?: boolean): boolean; /** * Query HTML elements in editor by tag name. From 1af04a38713d17ea6d1d5d2624fa07dc48c2d9f2 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 29 Aug 2024 16:12:26 -0600 Subject: [PATCH 10/43] Set segmentFormat text color to black when creating the model of the clipboard content and using Keep source formatting paste type (#2773) * init * add a link to tests and make sure it is handled correctly --- .../lib/command/paste/mergePasteContent.ts | 31 +- .../paste/htmlTemplates/ClipboardContent1.ts | 167 +++ .../command/paste/mergePasteContentTest.ts | 980 +++++++++++++++++- .../test/command/paste/pasteTest.ts | 12 +- 4 files changed, 1183 insertions(+), 7 deletions(-) create mode 100644 packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index 1985248838c..eda19869afd 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -13,11 +13,16 @@ import type { ClipboardData, CloneModelOptions, ContentModelDocument, + ContentModelSegmentFormat, IEditor, MergeModelOption, + PasteType, ReadonlyContentModelDocument, + ShallowMutableContentModelDocument, } from 'roosterjs-content-model-types'; +const BlackColor = 'rgb(0,0,0)'; + const CloneOption: CloneModelOptions = { includeCachedElement: (node, type) => (type == 'cache' ? undefined : node), }; @@ -46,7 +51,6 @@ export function mergePasteContent( model.blocks = clonedModel.blocks; } - const selectedSegment = getSelectedSegments(model, true /*includeFormatHolder*/)[0]; const domToModelContext = createDomToModelContextForSanitizing( editor.getDocument(), undefined /*defaultFormat*/, @@ -54,9 +58,7 @@ export function mergePasteContent( domToModelOption ); - domToModelContext.segmentFormat = selectedSegment - ? getSegmentTextFormat(selectedSegment) - : {}; + domToModelContext.segmentFormat = getSegmentFormatForPaste(model, pasteType); const pasteModel = domToContentModel(fragment, domToModelContext); const mergeOption: MergeModelOption = { @@ -87,6 +89,27 @@ export function mergePasteContent( ); } +function getSegmentFormatForPaste( + model: ShallowMutableContentModelDocument, + pasteType: PasteType +): ContentModelSegmentFormat { + const selectedSegment = getSelectedSegments(model, true /*includeFormatHolder*/)[0]; + + if (selectedSegment) { + const result = getSegmentTextFormat(selectedSegment); + if (pasteType == 'normal') { + // When using normal paste (Keep source formatting) set the default text color to black when creating the + // Model from the clipboard content, so the elements that do not contain any text color in their style + // Are set to black. Otherwise, These segments would get the selected segments format or the default text set in the content. + result.textColor = BlackColor; + } + + return result; + } + + return {}; +} + function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined { // If model contains a table and a paragraph element after the table with a single BR segment, remove the Paragraph after the table if ( diff --git a/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts b/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts new file mode 100644 index 00000000000..2d638bed89b --- /dev/null +++ b/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts @@ -0,0 +1,167 @@ +export const template: Readonly = ` + + + + + + + + + + + + + + +

            + Red bold +

            + +

            + Red italic +

            + +

            + Red underline +

            + +

            + Unformatted line +

            + +

            + Text underlink +

            + + + + +`; diff --git a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts index a7456ff4fcf..ac16a7284a1 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -1,8 +1,17 @@ import * as createDomToModelContextForSanitizing from '../../../lib/command/createModelFromHtml/createDomToModelContextForSanitizing'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; +import * as getSegmentTextFormatFile from 'roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat'; import * as mergeModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/mergeModel'; -import { createContentModelDocument } from 'roosterjs-content-model-dom'; +import { createPasteFragment } from '../../../lib/command/paste/createPasteFragment'; import { mergePasteContent } from '../../../lib/command/paste/mergePasteContent'; +import { template } from './htmlTemplates/ClipboardContent1'; +import { + addBlock, + createContentModelDocument, + createParagraph, + createSelectionMarker, + moveChildNodes, +} from 'roosterjs-content-model-dom'; import { ContentModelDocument, ContentModelFormatter, @@ -446,4 +455,973 @@ describe('mergePasteContent', () => { }, }); }); + + it('Merge paste content | Paste Type = normal | Make undefined text color equal to black', () => { + const html = new DOMParser().parseFromString(template, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: 'Calibri', + textColor: 'white', + }); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + marker.format = { + fontSize: 'Calibri', + textColor: 'white', + }; + para.segments.push(marker); + addBlock(sourceModel, para); + + mergePasteContent( + editor, + { + fragment, + domToModelOption: {}, + pasteType: 'normal', + }, + mockedClipboard + ); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + fontWeight: 'bold', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + italic: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + underline: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + underline: true, + lineHeight: '115%', + }, + link: { + format: { + underline: true, + href: 'https://github.com/microsoft/roosterjs', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { fontSize: 'Calibri', textColor: 'rgb(0,0,0)' }, + }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: 'Calibri', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'none', + mergeTable: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + fontWeight: 'bold', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + italic: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '28pt', + textColor: 'rgb(192, 0, 0)', + underline: true, + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + lineHeight: '115%', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: 'Calibri', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '28pt', + textColor: 'rgb(0,0,0)', + underline: true, + lineHeight: '115%', + }, + link: { + format: { + underline: true, + href: 'https://github.com/microsoft/roosterjs', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { fontSize: 'Calibri', textColor: 'rgb(0,0,0)' }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: 'Calibri', textColor: 'white' }, + }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = mergeFormat | Use current format', () => { + const html = new DOMParser().parseFromString(template, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(marker); + sourceModel.blocks.push(para); + + mergePasteContent( + editor, + { + fragment, + domToModelOption: {}, + pasteType: 'mergeFormat', + } as any, + mockedClipboard + ); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + fontWeight: 'bold', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + italic: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + link: { + format: { + href: 'https://github.com/microsoft/roosterjs', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'keepSourceEmphasisFormat', + mergeTable: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + fontWeight: 'bold', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + italic: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text underlink', + format: { + fontSize: '14px', + textColor: 'white', + underline: true, + }, + link: { + format: { + href: 'https://github.com/microsoft/roosterjs', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = asPlainText | Use current format', () => { + const fragment = createPasteFragment( + document, + { text: 'Red bold\r\nRed italic\r\nRed underline\r\nUnformatted line\r\n' } as any, + 'asPlainText', + document.body + ); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(marker); + sourceModel.blocks.push(para); + + mergePasteContent( + editor, + { + fragment, + domToModelOption: {}, + pasteType: 'asPlainText', + } as any, + mockedClipboard + ); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: '', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'none', + mergeTable: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red bold', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red italic', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Red underline', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Unformatted line', + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + segmentFormat: { + fontSize: '14px', + textColor: 'white', + }, + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index 3e6168ebf72..63777d1a211 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -398,7 +398,13 @@ describe('Paste with clipboardData', () => { blocks: [ { segments: [ - { text: 'Link', segmentType: 'Text', format: {} }, + { + text: 'Link', + segmentType: 'Text', + format: { + textColor: 'rgb(0, 0, 0)', + }, + }, { isSelected: true, segmentType: 'SelectionMarker', @@ -418,6 +424,7 @@ describe('Paste with clipboardData', () => { }, ], blockType: 'Paragraph', + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, format: {}, }, ], @@ -441,7 +448,7 @@ describe('Paste with clipboardData', () => { { text: 'Link', segmentType: 'Text', - format: {}, + format: { textColor: 'rgb(0, 0, 0)' }, link: { format: { underline: true, @@ -476,6 +483,7 @@ describe('Paste with clipboardData', () => { }, ], blockType: 'Paragraph', + segmentFormat: { textColor: 'rgb(0, 0, 0)' }, format: {}, }, ], From 63010b9dcda6885109a8d5f19589fe77069cfc73 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 30 Aug 2024 16:08:44 -0300 Subject: [PATCH 11/43] fixes image in tables --- .../lib/imageEdit/ImageEditPlugin.ts | 7 +- .../lib/imageEdit/utils/findEditingImage.ts | 23 +- .../imageEdit/utils/findEditingImageTest.ts | 268 ++++++++++++++++++ 3 files changed, 296 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 951931791a8..cea7b9d8b69 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -194,7 +194,11 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.isImageSelection(event.rawEvent.target as Node) && event.rawEvent.button !== MouseRightButton ) { - this.applyFormatWithContentModel(editor, this.isCropMode, true); + this.applyFormatWithContentModel( + editor, + this.isCropMode, + this.shadowSpan === event.rawEvent.target + ); } } @@ -287,6 +291,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { image.isSelected = shouldSelectImage; image.isSelectedAsImageSelection = shouldSelectImage; + delete image.dataset.isEditing; } ); diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index 65437ee66ff..689187d5b66 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -1,4 +1,7 @@ -import type { ReadonlyContentModelBlockGroup } from 'roosterjs-content-model-types'; +import type { + ReadonlyContentModelBlockGroup, + ReadonlyContentModelTable, +} from 'roosterjs-content-model-types'; import type { ImageAndParagraph } from '../types/ImageAndParagraph'; /** @@ -45,10 +48,28 @@ export function findEditingImage( break; } } + break; + case 'Table': + const imageInTable = findEditingImageOnTable(block, imageId); + if (imageInTable) { + return imageInTable; + } break; } } return null; } + +const findEditingImageOnTable = (table: ReadonlyContentModelTable, imageId?: string) => { + for (const row of table.rows) { + for (const cell of row.cells) { + const result = findEditingImage(cell, imageId); + if (result) { + return result; + } + } + } + return null; +}; diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts index d68582dbe94..3639e370da7 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts @@ -107,6 +107,274 @@ describe('findEditingImage', () => { }); }); + it('editing image in table', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + widths: [153, 120], + rows: [ + { + height: 157, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '773px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + 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)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + 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)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + ], + format: {}, + }, + { + height: 22, + cells: [ + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + 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)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + { + spanAbove: false, + spanLeft: false, + isHeader: false, + blockGroupType: 'TableCell', + blocks: [ + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + 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)', + verticalAlign: 'top', + width: '120px', + height: '22px', + useBorderBox: true, + }, + dataset: {}, + }, + ], + format: {}, + }, + ], + blockType: 'Table', + format: { + useBorderBox: true, + borderCollapse: true, + }, + dataset: { + editingInfo: + '{"topBorderColor":"#ABABAB","bottomBorderColor":"#ABABAB","verticalBorderColor":"#ABABAB","hasHeaderRow":false,"hasFirstColumn":false,"hasBandedRows":false,"hasBandedColumns":false,"bgColorEven":null,"bgColorOdd":"#ABABAB20","headerRowColor":"#ABABAB","tableBorderFormat":0,"verticalAlign":"top"}', + }, + }, + { + segments: [ + { + segmentType: 'Br', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model); + expect(image).toEqual({ + image: { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '773px', + }, + dataset: { + isEditing: 'true', + }, + }, + paragraph: { + segments: [ + { + src: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAsJCQcJCQcJCQkJCwkJCQkJCQsJCwsMCwsLDA0QDB...', + isSelectedAsImageSelection: true, + segmentType: 'Image', + isSelected: true, + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '773px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + blockType: 'Paragraph', + format: {}, + }, + }); + }); + it('editing image | by Id', () => { const model: ContentModelDocument = { blockGroupType: 'Document', From 8faa8e0b3bb28056f0d5bbeaebfb09b61a752a0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:50:45 -0700 Subject: [PATCH 12/43] Bump webpack from 5.84.1 to 5.94.0 (#2780) Bumps [webpack](https://github.com/webpack/webpack) from 5.84.1 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.84.1...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 410 ++++++++++++++++++++++++--------------------------- 2 files changed, 191 insertions(+), 221 deletions(-) diff --git a/package.json b/package.json index a600126896a..ab3e23aed45 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "typedoc-plugin-remove-references": "0.0.5", "typescript": "4.4.4", "url-loader": "4.1.0", - "webpack": "5.84.1", + "webpack": "5.94.0", "webpack-cli": "3.3.11", "webpack-dev-server": "3.10.3" }, diff --git a/yarn.lock b/yarn.lock index 536a974e869..939728d0c1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -471,7 +471,7 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": +"@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== @@ -480,23 +480,42 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/source-map@^0.3.2": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda" - integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" @@ -508,6 +527,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.18" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" @@ -516,6 +540,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jsdevtools/coverage-istanbul-loader@3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" @@ -614,26 +646,10 @@ dependencies: "@types/trusted-types" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.40.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.40.0.tgz#ae73dc9ec5237f2794c4f79efd6a4c73b13daf23" - integrity sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/events@*": version "3.0.0" @@ -654,11 +670,6 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.10.tgz#a1a41012012b5da9d4b205ba9eba58f6cce2ab7b" integrity sha512-3F8qpwBAiVc5+HPJeXJpbrl+XjawGmciN5LgiO7Gv1pl1RHtjoMNqZpqEksaPJW05ViKe8snYInRs6xB25Xdew== -"@types/json-schema@*", "@types/json-schema@^7.0.8": - version "7.0.12" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" - integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== - "@types/json-schema@^7.0.12": version "7.0.13" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" @@ -669,6 +680,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== +"@types/json-schema@^7.0.8": + version "7.0.12" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== + "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -903,10 +919,10 @@ "@typescript-eslint/types" "6.7.3" eslint-visitor-keys "^3.4.1" -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" @@ -921,10 +937,10 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" @@ -940,15 +956,15 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" @@ -969,59 +985,59 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== -"@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" - -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== - dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -1042,25 +1058,20 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.5.0, acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -acorn@^8.9.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== address@^1.0.1: version "1.1.2" @@ -1087,7 +1098,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: +ajv@^6.1.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1513,15 +1524,15 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5: - version "4.21.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.7.tgz#e2b420947e5fb0a58e8f4668ae6e23488127e551" - integrity sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA== +browserslist@^4.21.10: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== dependencies: - caniuse-lite "^1.0.30001489" - electron-to-chromium "^1.4.411" - node-releases "^2.0.12" - update-browserslist-db "^1.0.11" + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" buffer-from@^1.0.0: version "1.1.2" @@ -1581,10 +1592,10 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001489: - version "1.0.30001492" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz#4a06861788a52b4c81fd3344573b68cc87fe062b" - integrity sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw== +caniuse-lite@^1.0.30001646: + version "1.0.30001655" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" + integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== caseless@~0.12.0: version "0.12.0" @@ -2254,10 +2265,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.411: - version "1.4.414" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.414.tgz#f9eedb6fb01b50439d8228d8ee3a6fa5e0108437" - integrity sha512-RRuCvP6ekngVh2SAJaOKT/hxqc9JAsK+Pe0hP5tGQIfonU2Zy9gMGdJ+mBdyl/vNucMG6gkXYtuM4H/1giws5w== +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== emoji-regex@^7.0.1: version "7.0.3" @@ -2321,18 +2332,10 @@ enhanced-resolve@4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -enhanced-resolve@^5.0.0: - version "5.15.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" - integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enhanced-resolve@^5.14.1: - version "5.14.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz#de684b6803724477a4af5d74ccae5de52c25f6b3" - integrity sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow== +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -2461,6 +2464,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3324,12 +3332,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.6: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -graceful-fs@^4.2.10, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4681,36 +4684,17 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -mime-types@^2.1.26: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" +"mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== -mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -4869,16 +4853,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -neo-async@^2.6.0, neo-async@^2.6.2: +neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -neo-async@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -4889,10 +4868,10 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-releases@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.12.tgz#35627cc224a23bfb06fb3380f2b3afaaa7eb1039" - integrity sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ== +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== normalize-path@^2.1.1: version "2.1.1" @@ -5302,10 +5281,10 @@ picocolors@^0.2.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== picomatch@^2.0.4: version "2.2.1" @@ -5924,15 +5903,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: - version "2.6.6" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" - integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - -schema-utils@^2.7.0: +schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== @@ -5941,10 +5912,10 @@ schema-utils@^2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" -schema-utils@^3.1.1, schema-utils@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.2.tgz#36c10abca6f7577aeae136c804b0c741edeadc99" - integrity sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg== +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: "@types/json-schema" "^7.0.8" ajv "^6.12.5" @@ -6536,24 +6507,24 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.16.8: - version "5.17.6" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de" - integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ== +terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" @@ -6922,13 +6893,13 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== -update-browserslist-db@^1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" - integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: version "4.4.1" @@ -7016,10 +6987,10 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -7118,34 +7089,33 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@5.84.1: - version "5.84.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.84.1.tgz#d4493acdeca46b26ffc99d86d784cabfeb925a15" - integrity sha512-ZP4qaZ7vVn/K8WN/p990SGATmrL1qg4heP/MrVneczYtpDGJWlrgZv55vxaV2ul885Kz+25MP2kSXkPe3LZfmg== +webpack@5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^1.0.0" - "@webassemblyjs/ast" "^1.11.5" - "@webassemblyjs/wasm-edit" "^1.11.5" - "@webassemblyjs/wasm-parser" "^1.11.5" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" - browserslist "^4.14.5" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.14.1" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" + graceful-fs "^4.2.11" json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.2" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.3.7" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1: From 92de0a64e7e746834dc855c93b9a146f0e55db00 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 4 Sep 2024 12:42:58 -0600 Subject: [PATCH 13/43] Resolve null sheet in convertInlineCss (#2784) * init * update --- .../createModelFromHtml/convertInlineCss.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/convertInlineCss.ts b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/convertInlineCss.ts index 2e70912a611..e26cae1c002 100644 --- a/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/convertInlineCss.ts +++ b/packages/roosterjs-content-model-core/lib/command/createModelFromHtml/convertInlineCss.ts @@ -28,16 +28,18 @@ export function retrieveCssRules(doc: Document): CssRule[] { const result: CssRule[] = []; styles.forEach(styleNode => { - const sheet = styleNode.sheet as CSSStyleSheet; + const sheet = styleNode.sheet; - for (let ruleIndex = 0; ruleIndex < sheet.cssRules.length; ruleIndex++) { - const rule = sheet.cssRules[ruleIndex] as CSSStyleRule; + if (sheet) { + for (let ruleIndex = 0; ruleIndex < sheet.cssRules.length; ruleIndex++) { + const rule = sheet.cssRules[ruleIndex] as CSSStyleRule; - if (rule.type == CSSRule.STYLE_RULE && rule.selectorText) { - result.push({ - selectors: splitSelectors(rule.selectorText), - text: rule.style.cssText, - }); + if (rule.type == CSSRule.STYLE_RULE && rule.selectorText) { + result.push({ + selectors: splitSelectors(rule.selectorText), + text: rule.style.cssText, + }); + } } } From f4960422cc537a32160089bc38210409421b0cb3 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Wed, 4 Sep 2024 12:58:57 -0600 Subject: [PATCH 14/43] Update paste code to add a paragraph when the clipboard contains atleast a block element (#2777) * init * add a link to tests and make sure it is handled correctly * init * try fix build * fix build --- .../paste/generatePasteOptionFromPlugins.ts | 1 + .../lib/command/paste/mergePasteContent.ts | 37 +- .../lib/command/paste/paste.ts | 2 +- .../lib/command/paste/retrieveHtmlInfo.ts | 10 +- .../generatePasteOptionFromPluginsTest.ts | 9 + .../paste/htmlTemplates/ClipboardContent1.ts | 6 + .../command/paste/mergePasteContentTest.ts | 530 ++++++++++++++++-- .../test/command/paste/pasteTest.ts | 31 +- .../command/paste/retrieveHtmlInfoTest.ts | 9 + .../lib/modelApi/editing/mergeModel.ts | 7 + .../test/modelApi/editing/mergeModelTest.ts | 35 ++ .../test/paste/e2e/cmPasteFromExcelTest.ts | 10 + .../lib/event/BeforePasteEvent.ts | 5 + .../lib/parameter/MergeModelOption.ts | 5 + 14 files changed, 619 insertions(+), 78 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts b/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts index 3b8fd283fb8..3cd712671cc 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/generatePasteOptionFromPlugins.ts @@ -36,6 +36,7 @@ export function generatePasteOptionFromPlugins( htmlAttributes: htmlFromClipboard.metadata, pasteType: pasteType, domToModelOption, + containsBlockElements: !!htmlFromClipboard.containsBlockElements, }; return pasteType == 'asPlainText' diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index eda19869afd..fb0e61e966a 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -10,7 +10,6 @@ import { } from 'roosterjs-content-model-dom'; import type { BeforePasteEvent, - ClipboardData, CloneModelOptions, ContentModelDocument, ContentModelSegmentFormat, @@ -37,12 +36,15 @@ export function cloneModelForPaste(model: ReadonlyContentModelDocument) { /** * @internal */ -export function mergePasteContent( - editor: IEditor, - eventResult: BeforePasteEvent, - clipboardData: ClipboardData -) { - const { fragment, domToModelOption, customizedMerge, pasteType } = eventResult; +export function mergePasteContent(editor: IEditor, eventResult: BeforePasteEvent) { + const { + fragment, + domToModelOption, + customizedMerge, + pasteType, + clipboardData, + containsBlockElements, + } = eventResult; editor.formatContentModel( (model, context) => { @@ -64,6 +66,7 @@ export function mergePasteContent( const mergeOption: MergeModelOption = { mergeFormat: pasteType == 'mergeFormat' ? 'keepSourceEmphasisFormat' : 'none', mergeTable: shouldMergeTable(pasteModel), + addParagraphAfterMergedContent: containsBlockElements, }; const insertPoint = customizedMerge @@ -74,7 +77,9 @@ export function mergePasteContent( context.newPendingFormat = { ...EmptySegmentFormat, ...model.format, - ...insertPoint.marker.format, + ...(pasteType == 'normal' && !containsBlockElements + ? getLastSegmentFormat(pasteModel) + : insertPoint.marker.format), }; } @@ -124,3 +129,19 @@ function shouldMergeTable(pasteModel: ContentModelDocument): boolean | undefined // Only merge table when the document contain a single table. return pasteModel.blocks.length === 1 && pasteModel.blocks[0].blockType === 'Table'; } + +function getLastSegmentFormat(pasteModel: ContentModelDocument): ContentModelSegmentFormat { + if (pasteModel.blocks.length == 1) { + const [firstBlock] = pasteModel.blocks; + + if (firstBlock.blockType == 'Paragraph') { + const segment = firstBlock.segments[firstBlock.segments.length - 1]; + + return { + ...segment.format, + }; + } + } + + return {}; +} diff --git a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts index aca1944187a..69cd08340d5 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts @@ -67,7 +67,7 @@ export function paste( convertInlineCss(eventResult.fragment, htmlFromClipboard.globalCssRules); // 6. Merge pasted content into main Content Model - mergePasteContent(editor, eventResult, clipboardData); + mergePasteContent(editor, eventResult); } function createDOMFromHtml( diff --git a/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts b/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts index 95b86bee134..bf69907bd9f 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/retrieveHtmlInfo.ts @@ -1,4 +1,4 @@ -import { isNodeOfType, toArray } from 'roosterjs-content-model-dom'; +import { isBlockElement, isNodeOfType, toArray } from 'roosterjs-content-model-dom'; import { retrieveCssRules } from '../createModelFromHtml/convertInlineCss'; import type { ClipboardData } from 'roosterjs-content-model-types'; import type { CssRule } from '../createModelFromHtml/convertInlineCss'; @@ -14,6 +14,7 @@ export interface HtmlFromClipboard { globalCssRules: CssRule[]; htmlBefore?: string; htmlAfter?: string; + containsBlockElements?: boolean; } /** @@ -33,6 +34,7 @@ export function retrieveHtmlInfo( ...retrieveHtmlStrings(clipboardData), globalCssRules: retrieveCssRules(doc), metadata: retrieveMetadata(doc), + containsBlockElements: checkBlockElements(doc), }; clipboardData.htmlFirstLevelChildTags = retrieveTopLevelTags(doc); @@ -96,3 +98,9 @@ function retrieveHtmlStrings( return { htmlBefore, htmlAfter }; } + +function checkBlockElements(doc: Document): boolean { + const elements = toArray(doc.body.querySelectorAll('*')); + + return elements.some(el => isNodeOfType(el, 'ELEMENT_NODE') && isBlockElement(el)); +} diff --git a/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts b/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts index 185b30bf072..7c434d19259 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/generatePasteOptionFromPluginsTest.ts @@ -55,6 +55,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, + containsBlockElements: false, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(originalEvent).toEqual({ @@ -74,6 +75,7 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, + containsBlockElements: false, }); expect(triggerPluginEventSpy).toHaveBeenCalledWith( 'beforePaste', @@ -86,6 +88,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', + containsBlockElements: false, }, true ); @@ -126,6 +129,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, + containsBlockElements: false, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(triggerPluginEventSpy).toHaveBeenCalledWith( @@ -140,6 +144,7 @@ describe('generatePasteOptionFromPlugins', () => { pasteType: 'TypeResult', domToModelOption: 'OptionResult', customizedMerge: mockedCustomizedMerge, + containsBlockElements: false, }, true ); @@ -174,6 +179,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore: '', htmlAfter: '', htmlAttributes: mockedMetadata, + containsBlockElements: false, } as any); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); expect(triggerPluginEventSpy).toHaveBeenCalledWith( @@ -187,6 +193,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlAttributes: mockedMetadata, pasteType: 'TypeResult', domToModelOption: 'OptionResult', + containsBlockElements: false, }, true ); @@ -207,6 +214,7 @@ describe('generatePasteOptionFromPlugins', () => { styleSanitizers: {}, attributeSanitizers: {}, }, + containsBlockElements: false, }); }); @@ -244,6 +252,7 @@ describe('generatePasteOptionFromPlugins', () => { htmlBefore, htmlAfter, htmlAttributes: mockedMetadata, + containsBlockElements: false, }); expect(triggerPluginEventSpy).toHaveBeenCalledTimes(0); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts b/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts index 2d638bed89b..d943eb2fdbb 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/htmlTemplates/ClipboardContent1.ts @@ -165,3 +165,9 @@ export const template: Readonly = ` `; + +export const inlineTemplate: Readonly = ` + + + Inline text +`; diff --git a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts index ac16a7284a1..71efb5798c0 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -3,13 +3,15 @@ import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/d import * as getSegmentTextFormatFile from 'roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat'; import * as mergeModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/mergeModel'; import { createPasteFragment } from '../../../lib/command/paste/createPasteFragment'; +import { inlineTemplate, template } from './htmlTemplates/ClipboardContent1'; import { mergePasteContent } from '../../../lib/command/paste/mergePasteContent'; -import { template } from './htmlTemplates/ClipboardContent1'; + import { addBlock, createContentModelDocument, createParagraph, createSelectionMarker, + createText, moveChildNodes, } from 'roosterjs-content-model-dom'; import { @@ -162,15 +164,17 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, + clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith(sourceModel, pasteModel, context, { mergeFormat: 'none', mergeTable: true, + addParagraphAfterMergedContent: undefined, }); expect(context).toEqual({ newEntities: [], @@ -267,9 +271,10 @@ describe('mergePasteContent', () => { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, customizedMerge, + clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -288,9 +293,10 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: 'mergeFormat', domToModelOption: { additionalAllowedTags: [] }, + clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -301,6 +307,7 @@ describe('mergePasteContent', () => { { mergeFormat: 'keepSourceEmphasisFormat', mergeTable: false, + addParagraphAfterMergedContent: undefined, } ); }); @@ -370,14 +377,11 @@ describe('mergePasteContent', () => { }, }); - mergePasteContent( - editor, - { - fragment: mockedFragment, - domToModelOption: mockedDefaultDomToModelOptions, - } as any, - mockedClipboard - ); + mergePasteContent(editor, { + fragment: mockedFragment, + domToModelOption: mockedDefaultDomToModelOptions, + clipboardData: mockedClipboard, + } as any); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -403,6 +407,7 @@ describe('mergePasteContent', () => { expect(mergeModelSpy).toHaveBeenCalledWith(sourceModel, pasteModel, context, { mergeFormat: 'none', mergeTable: false, + addParagraphAfterMergedContent: undefined, }); expect(createDomToModelContextSpy).toHaveBeenCalledWith( document, @@ -430,9 +435,11 @@ describe('mergePasteContent', () => { const eventResult = { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, + clipboardData: mockedClipboard, + containsBlockElements: true, } as any; - mergePasteContent(editor, eventResult, mockedClipboard); + mergePasteContent(editor, eventResult); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -463,28 +470,26 @@ describe('mergePasteContent', () => { spyOn(mergeModelFile, 'mergeModel').and.callThrough(); spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ - fontSize: 'Calibri', + fontSize: '14px', textColor: 'white', }); sourceModel = createContentModelDocument(); const para = createParagraph(); const marker = createSelectionMarker(); marker.format = { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'white', }; para.segments.push(marker); addBlock(sourceModel, para); - mergePasteContent( - editor, - { - fragment, - domToModelOption: {}, - pasteType: 'normal', - }, - mockedClipboard - ); + mergePasteContent(editor, { + fragment, + containsBlockElements: true, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -508,7 +513,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -539,7 +544,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -570,7 +575,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -600,7 +605,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -637,12 +642,18 @@ describe('mergePasteContent', () => { { segmentType: 'Text', text: '\n', - format: { fontSize: 'Calibri', textColor: 'rgb(0,0,0)' }, + format: { fontSize: '14px', textColor: 'rgb(0,0,0)' }, }, ], format: { marginTop: '1em', marginBottom: '1em' }, decorator: { tagName: 'p', format: {} }, }, + { + blockType: 'Paragraph', + segments: [], + format: {}, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, ], }, { @@ -652,7 +663,7 @@ describe('mergePasteContent', () => { newPendingFormat: { backgroundColor: '', fontFamily: '', - fontSize: 'Calibri', + fontSize: '14px', fontWeight: '', italic: false, letterSpacing: '', @@ -666,6 +677,7 @@ describe('mergePasteContent', () => { { mergeFormat: 'none', mergeTable: false, + addParagraphAfterMergedContent: true, } ); @@ -689,7 +701,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -720,7 +732,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -751,7 +763,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -781,7 +793,7 @@ describe('mergePasteContent', () => { segmentType: 'Text', text: '\n', format: { - fontSize: 'Calibri', + fontSize: '14px', textColor: 'rgb(0,0,0)', }, }, @@ -818,16 +830,27 @@ describe('mergePasteContent', () => { { segmentType: 'Text', text: '\n', - format: { fontSize: 'Calibri', textColor: 'rgb(0,0,0)' }, + format: { fontSize: '14px', textColor: 'rgb(0,0,0)' }, }, + ], + format: { marginTop: '1em', marginBottom: '1em' }, + decorator: { tagName: 'p', format: {} }, + }, + { + blockType: 'Paragraph', + segments: [ { segmentType: 'SelectionMarker', isSelected: true, - format: { fontSize: 'Calibri', textColor: 'white' }, + format: { fontSize: '14px', textColor: 'white' }, + }, + { + segmentType: 'Br', + format: { fontSize: '14px', textColor: 'white' }, }, ], - format: { marginTop: '1em', marginBottom: '1em' }, - decorator: { tagName: 'p', format: {} }, + format: {}, + segmentFormat: { fontSize: '14px', textColor: 'white' }, }, ], }); @@ -853,15 +876,12 @@ describe('mergePasteContent', () => { para.segments.push(marker); sourceModel.blocks.push(para); - mergePasteContent( - editor, - { - fragment, - domToModelOption: {}, - pasteType: 'mergeFormat', - } as any, - mockedClipboard - ); + mergePasteContent(editor, { + fragment, + domToModelOption: {}, + pasteType: 'mergeFormat', + clipboardData: mockedClipboard, + } as any); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1027,6 +1047,7 @@ describe('mergePasteContent', () => { { mergeFormat: 'keepSourceEmphasisFormat', mergeTable: false, + addParagraphAfterMergedContent: undefined, } ); @@ -1224,15 +1245,12 @@ describe('mergePasteContent', () => { para.segments.push(marker); sourceModel.blocks.push(para); - mergePasteContent( - editor, - { - fragment, - domToModelOption: {}, - pasteType: 'asPlainText', - } as any, - mockedClipboard - ); + mergePasteContent(editor, { + fragment, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + } as any); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1335,6 +1353,7 @@ describe('mergePasteContent', () => { { mergeFormat: 'none', mergeTable: false, + addParagraphAfterMergedContent: undefined, } ); @@ -1424,4 +1443,403 @@ describe('mergePasteContent', () => { ], }); }); + + it('Merge paste content | Paste Type = normal | Paste content inline', () => { + const html = new DOMParser().parseFromString(inlineTemplate, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument({ + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const para = createParagraph(undefined, undefined, { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(createText('Text in source'), marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Aptos', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'rgb(0,0,0)', + underline: false, + }, + }, + { + mergeFormat: 'none', + mergeTable: false, + addParagraphAfterMergedContent: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text in source', + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }, + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontSize: '14px', + textColor: 'rgb(0,0,0)', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontSize: '14px', + textColor: 'white', + fontFamily: 'Aptos', + }, + }, + ], + format: {}, + }, + ], + }); + }); + + it('Merge paste content | Paste Type = mergeFormat | Paste content inline', () => { + const html = new DOMParser().parseFromString(inlineTemplate, 'text/html'); + const fragment = document.createDocumentFragment(); + moveChildNodes(fragment, html.body); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument({ + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const para = createParagraph(undefined, undefined, { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(createText('Text in source'), marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'mergeFormat', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Aptos', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { + mergeFormat: 'keepSourceEmphasisFormat', + mergeTable: false, + addParagraphAfterMergedContent: false, + } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'Text in source', format: {} }, + { + segmentType: 'Text', + text: 'Inline text', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Text', + text: '\n', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { fontSize: '14px', textColor: 'white' }, + }, + ], + format: {}, + segmentFormat: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }, + ], + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }); + }); + + it('Merge paste content | Paste Type = mergeFormat | Paste content inline', () => { + const fragment = createPasteFragment( + document, + { text: 'Inline text\r\n' } as any, + 'asPlainText', + document.body + ); + + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + spyOn(getSegmentTextFormatFile, 'getSegmentTextFormat').and.returnValue({ + fontSize: '14px', + textColor: 'white', + }); + sourceModel = createContentModelDocument({ + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const para = createParagraph(undefined, undefined, { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }); + const marker = createSelectionMarker(); + marker.format = { + fontSize: '14px', + textColor: 'white', + }; + para.segments.push(createText('Text in source'), marker); + addBlock(sourceModel, para); + + mergePasteContent(editor, { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + }); + + expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( + sourceModel, + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Inline text', + format: { fontSize: '14px', textColor: 'white' }, + }, + { segmentType: 'Br', format: { fontSize: '14px', textColor: 'white' } }, + ], + format: {}, + isImplicit: true, + segmentFormat: { fontSize: '14px', textColor: 'white' }, + }, + ], + }, + { + newEntities: [], + deletedEntities: [], + newImages: [], + newPendingFormat: { + backgroundColor: '', + fontFamily: 'Aptos', + fontSize: '14px', + fontWeight: '', + italic: false, + letterSpacing: '', + lineHeight: '', + strikethrough: false, + superOrSubScriptSequence: '', + textColor: 'white', + underline: false, + }, + }, + { mergeFormat: 'none', mergeTable: false, addParagraphAfterMergedContent: false } + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Text in source', + format: { + fontFamily: 'Aptos', + fontSize: '10pt', + textColor: 'blue', + }, + }, + { + segmentType: 'Text', + text: 'Inline text', + format: { fontSize: '14px', textColor: 'white' }, + }, + { segmentType: 'Br', format: { fontSize: '14px', textColor: 'white' } }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + { + segmentType: 'Br', + format: { + fontFamily: 'Aptos', + fontSize: '14px', + textColor: 'white', + }, + }, + ], + format: {}, + }, + ], + format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, + }); + }); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index 63777d1a211..a6aab524bb7 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -353,11 +353,22 @@ describe('Paste with clipboardData', () => { textColor: 'rgb(0, 0, 0)', }, }, + ], + format: { + marginTop: '1em', + marginBottom: '1em', + }, + decorator: { + tagName: 'p', + format: {}, + }, + }, + { + segments: [ { - segmentType: 'SelectionMarker', isSelected: true, + segmentType: 'SelectionMarker', format: { - textColor: '', backgroundColor: '', fontFamily: '', fontSize: '', @@ -367,18 +378,14 @@ describe('Paste with clipboardData', () => { lineHeight: '', strikethrough: false, superOrSubScriptSequence: '', + textColor: '', underline: false, }, }, + { segmentType: 'Br', format: {} }, ], - format: { - marginTop: '1em', - marginBottom: '1em', - }, - decorator: { - tagName: 'p', - format: {}, - }, + blockType: 'Paragraph', + format: {}, }, ], format: {}, @@ -418,7 +425,7 @@ describe('Paste with clipboardData', () => { lineHeight: '', strikethrough: false, superOrSubScriptSequence: '', - textColor: '', + textColor: 'rgb(0,0,0)', underline: false, }, }, @@ -470,7 +477,7 @@ describe('Paste with clipboardData', () => { lineHeight: '', strikethrough: false, superOrSubScriptSequence: '', - textColor: '', + textColor: 'rgb(0,0,0)', underline: false, }, link: { diff --git a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts index 542a9664180..2e6938eff6a 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts @@ -44,6 +44,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: false, }, { htmlFirstLevelChildTags: [], @@ -61,6 +62,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: false, }, { htmlFirstLevelChildTags: [''], @@ -78,6 +80,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -95,6 +98,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['', 'DIV', 'SPAN', ''], @@ -112,6 +116,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -129,6 +134,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -146,6 +152,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '
            ', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -163,6 +170,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: { a: 'b', 'c:d': 'e', f: 'g', h: 'i' }, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], @@ -181,6 +189,7 @@ describe('retrieveHtmlInfo', () => { htmlAfter: '', globalCssRules: [], metadata: {}, + containsBlockElements: true, }, { htmlFirstLevelChildTags: ['DIV'], diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index d0f148387eb..3ea33badf72 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -1,3 +1,4 @@ +import { addBlock } from '../common/addBlock'; import { addSegment } from '../common/addSegment'; import { applyTableFormat } from './applyTableFormat'; import { createListItem } from '../creators/createListItem'; @@ -49,6 +50,12 @@ export function mergeModel( const insertPosition = options?.insertPosition ?? deleteSelection(target, [], context).insertPoint; + if (options?.addParagraphAfterMergedContent) { + const { paragraph, marker } = insertPosition || {}; + const newPara = createParagraph(false /* isImplicit */, paragraph?.format, marker?.format); + addBlock(source, newPara); + } + if (insertPosition) { if (options?.mergeFormat && options.mergeFormat != 'none') { const newFormat: ContentModelSegmentFormat = { diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index 72c4023c39e..f6e6b58e40b 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -3835,4 +3835,39 @@ describe('mergeModel', () => { verticalAlign: 'top', }); }); + + it('Merge model with addParagraphAfterMergedContent', () => { + const source = createContentModelDocument(); + const para = createParagraph(); + para.segments.push(createText('Merge')); + source.blocks.push(para); + + const target = createContentModelDocument(); + const paraTarget = createParagraph(); + paraTarget.segments.push(createSelectionMarker()); + target.blocks.push(paraTarget); + + mergeModel(target, source, undefined, { + addParagraphAfterMergedContent: true, + }); + + expect(target).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Text', text: 'Merge', format: {} }], + format: {}, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts index 28a3b6c712a..5e4853c83c5 100644 --- a/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts +++ b/packages/roosterjs-content-model-plugins/test/paste/e2e/cmPasteFromExcelTest.ts @@ -65,6 +65,15 @@ describe(ID, () => { isSelectedAsImageSelection: undefined, isSelected: undefined, }, + ], + format: {}, + cachedElement: undefined, + isImplicit: undefined, + segmentFormat: undefined, + }, + { + blockType: 'Paragraph', + segments: [ { segmentType: 'SelectionMarker', isSelected: true, @@ -82,6 +91,7 @@ describe(ID, () => { underline: false, }, }, + { segmentType: 'Br', isSelected: undefined, format: {} }, ], format: {}, cachedElement: undefined, diff --git a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts index 2fbe371b9cc..870a81afcf7 100644 --- a/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts +++ b/packages/roosterjs-content-model-types/lib/event/BeforePasteEvent.ts @@ -62,4 +62,9 @@ export interface BeforePasteEvent extends BasePluginEvent<'beforePaste'> { * customizedMerge Customized merge function to use when merging the paste fragment into the editor */ customizedMerge?: MergePastedContentFunc; + + /** + * Whether the current clipboard contains at least a block element. + */ + readonly containsBlockElements?: boolean; } diff --git a/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts b/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts index 90f84d91f52..25a24267e52 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/MergeModelOption.ts @@ -27,4 +27,9 @@ export interface MergeModelOption { * @default undefined */ mergeFormat?: 'mergeAll' | 'keepSourceEmphasisFormat' | 'none'; + + /** + * Whether to add a paragraph after the merged content. + */ + addParagraphAfterMergedContent?: boolean; } From a4198326ed0ffcf1a912d2a83b7cbb3553229ff0 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Wed, 4 Sep 2024 12:23:54 -0700 Subject: [PATCH 15/43] Fix 300391: [Mail] The numbers will become a continuous sequence after typing enter at the last line of the numbering above (#2782) Co-authored-by: Bryan Valverde U --- .../lib/edit/inputSteps/handleEnterOnList.ts | 6 +- .../edit/inputSteps/handleEnterOnListTest.ts | 112 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts index 5a7205a2700..224510a39f6 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/inputSteps/handleEnterOnList.ts @@ -60,7 +60,11 @@ export const handleEnterOnList: DeleteSelectionStep = context => { nextListItem.levels[0] ) { nextListItem.levels.forEach(level => { - level.format.startNumberOverride = undefined; + // Remove startNumberOverride so that next list item can join current list, unless it is 1. + // List start with 1 means it should be an explicit new list and should never join another list before it + if (level.format.startNumberOverride !== 1) { + level.format.startNumberOverride = undefined; + } }); } } diff --git a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts index 81aeb2f74ca..c96f2291b05 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/inputSteps/handleEnterOnListTest.ts @@ -2439,6 +2439,118 @@ describe('handleEnterOnList - keyboardEnter', () => { runTest(input, true, expected, false, 1); }); + it('Two separate lists, Enter on first one', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [{ listType: 'OL', format: { startNumberOverride: 1 }, dataset: {} }], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'Text', text: 'test', format: {} }, + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { segmentType: 'SelectionMarker', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [{ listType: 'OL', format: { startNumberOverride: 1 }, dataset: {} }], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Br', format: {} }], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + format: {}, + }, + }, + ], + }; + + const expectedModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [{ listType: 'OL', format: { startNumberOverride: 1 }, dataset: {} }], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [{ segmentType: 'Text', text: 'test', format: {} }], + }, + ], + format: {}, + formatHolder: { segmentType: 'SelectionMarker', format: {} }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: undefined, + displayForDummyItem: undefined, + }, + dataset: {}, + }, + ], + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + { segmentType: 'Br', format: {} }, + ], + }, + ], + format: {}, + formatHolder: { segmentType: 'SelectionMarker', format: {}, isSelected: false }, + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + }, + ], + levels: [ + { + listType: 'OL', + format: { + startNumberOverride: 1, + }, + dataset: {}, + }, + ], + formatHolder: { segmentType: 'SelectionMarker', format: {} }, + format: {}, + }, + ], + }; + + runTest(model, false, expectedModel, false, 1); + }); + it('List item contains multiple blocks', () => { const model: ContentModelDocument = { blockGroupType: 'Document', From 865fdc7f09a6819027e6da407664fb29d8511ff3 Mon Sep 17 00:00:00 2001 From: Andres-CT98 <107568016+Andres-CT98@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:48:47 -0600 Subject: [PATCH 16/43] Pressing Tab inside a table should select all node contents of the next cell (#2764) * normalise position * revert * empty cell check * select using children * fix tests * fix --- .../corePlugin/selection/SelectionPlugin.ts | 12 ++++- .../selection/SelectionPluginTest.ts | 46 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts index 98ca2b61ba3..420514d1466 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -559,8 +559,16 @@ class SelectionPlugin implements PluginWithState { selectAll?: boolean ) { const range = editor.getDocument().createRange(); - if (selectAll) { - range.selectNodeContents(cell); + if (selectAll && cell.firstChild && cell.lastChild) { + const cellStart = cell.firstChild; + const cellEnd = cell.lastChild; + // Get first deepest editable position in the cell + const posStart = normalizePos(cellStart, 0); + // Get last deepest editable position in the cell + const posEnd = normalizePos(cellEnd, cellEnd.childNodes.length); + + range.setStart(posStart.node, posStart.offset); + range.setEnd(posEnd.node, posEnd.offset); } else { // Get deepest editable position in the cell const { node, offset } = normalizePos(cell, nodeOffset); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts index 62af29d2538..0d795d323ed 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/selection/SelectionPluginTest.ts @@ -1272,6 +1272,10 @@ describe('SelectionPlugin handle table selection', () => { let td4: HTMLTableCellElement; let tr1: HTMLElement; let tr2: HTMLElement; + let td1_text: Node; + let td2_text: Node; + let td3_text: Node; + let td4_text: Node; let table: HTMLTableElement; let div: HTMLElement; @@ -1291,6 +1295,18 @@ describe('SelectionPlugin handle table selection', () => { td3.id = 'td3'; td4.id = 'td4'; + // Craete text nodes + td1_text = document.createTextNode('1'); + td2_text = document.createTextNode('2'); + td3_text = document.createTextNode('3'); + td4_text = document.createTextNode('4'); + + // Add Text to each cell + td1.appendChild(td1_text); + td2.appendChild(td2_text); + td3.appendChild(td3_text); + td4.appendChild(td4_text); + tr1.appendChild(td1); tr1.appendChild(td2); tr2.appendChild(td3); @@ -1403,11 +1419,13 @@ describe('SelectionPlugin handle table selection', () => { }; }); - const selectNodeContentsSpy = jasmine.createSpy('selectNodeContents'); + const setStartSpy = jasmine.createSpy('setStart'); + const setEndSpy = jasmine.createSpy('setEnd'); const collapseSpy = jasmine.createSpy('collapse'); const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedRange = { - selectNodeContents: selectNodeContentsSpy, + setStart: setStartSpy, + setEnd: setEndSpy, collapse: collapseSpy, } as any; @@ -1436,7 +1454,8 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(selectNodeContentsSpy).toHaveBeenCalledWith(td2); + expect(setStartSpy).toHaveBeenCalledWith(td2_text, 0); + expect(setEndSpy).toHaveBeenCalledWith(td2_text, 0); expect(collapseSpy).not.toHaveBeenCalled(); expect(announceSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalledTimes(1); @@ -1474,11 +1493,13 @@ describe('SelectionPlugin handle table selection', () => { }; }); - const selectNodeContentsSpy = jasmine.createSpy('selectNodeContents'); + const setStartSpy = jasmine.createSpy('setStart'); + const setEndSpy = jasmine.createSpy('setEnd'); const collapseSpy = jasmine.createSpy('collapse'); const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedRange = { - selectNodeContents: selectNodeContentsSpy, + setStart: setStartSpy, + setEnd: setEndSpy, collapse: collapseSpy, } as any; @@ -1508,7 +1529,8 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(selectNodeContentsSpy).toHaveBeenCalledWith(td1); + expect(setStartSpy).toHaveBeenCalledWith(td1_text, 0); + expect(setEndSpy).toHaveBeenCalledWith(td1_text, 0); expect(collapseSpy).not.toHaveBeenCalled(); expect(announceSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalledTimes(1); @@ -1546,11 +1568,13 @@ describe('SelectionPlugin handle table selection', () => { }; }); - const selectNodeContentsSpy = jasmine.createSpy('selectNodeContents'); + const setStartSpy = jasmine.createSpy('setStart'); + const setEndSpy = jasmine.createSpy('setEnd'); const collapseSpy = jasmine.createSpy('collapse'); const preventDefaultSpy = jasmine.createSpy('preventDefault'); const mockedRange = { - selectNodeContents: selectNodeContentsSpy, + setStart: setStartSpy, + setEnd: setEndSpy, collapse: collapseSpy, } as any; @@ -1579,7 +1603,8 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(selectNodeContentsSpy).toHaveBeenCalledWith(td3); + expect(setStartSpy).toHaveBeenCalledWith(td3_text, 0); + expect(setEndSpy).toHaveBeenCalledWith(td3_text, 0); expect(collapseSpy).not.toHaveBeenCalled(); expect(announceSpy).not.toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalledTimes(1); @@ -1592,6 +1617,7 @@ describe('SelectionPlugin handle table selection', () => { getDOMSelectionSpy.and.callFake(() => { time++; + td1.appendChild(document.createTextNode('1')); return time == 1 ? { type: 'range', @@ -1790,7 +1816,7 @@ describe('SelectionPlugin handle table selection', () => { range: mockedRange, isReverted: false, }); - expect(setStartSpy).toHaveBeenCalledWith(td4, 0); + expect(setStartSpy).toHaveBeenCalledWith(td4_text, 0); expect(announceSpy).toHaveBeenCalledWith({ defaultStrings: 'announceOnFocusLastCell', }); From dda3102ed89a6b0c3f3e7017a36f202e2c6631ce Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Mon, 9 Sep 2024 18:56:32 -0300 Subject: [PATCH 17/43] fix list --- .../scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts b/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts index ab585097120..97617c0c5c4 100644 --- a/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts +++ b/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts @@ -10,7 +10,6 @@ const dropDownMenuItems = { [BulletListType.LongArrow]: 'LongArrow', [BulletListType.UnfilledArrow]: 'UnfilledArrow', [BulletListType.Hyphen]: 'Hyphen', - [BulletListType.DoubleLongArrow]: 'DoubleLongArrow', [BulletListType.Circle]: 'Circle', }; From 281a456ed9851617e87e8c54fbfa8e0e7dd19ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 10 Sep 2024 14:58:38 -0300 Subject: [PATCH 18/43] safari selection --- .../corePlugin/copyPaste/CopyPastePlugin.ts | 6 +- .../utils/adjustImageSelectionOnSafari.ts | 14 ++++ .../copyPaste/{ => utils}/deleteEmptyList.ts | 0 .../utils/adjustImageSelectionOnSafariTest.ts | 70 +++++++++++++++++++ .../{ => utils}/deleteEmptyListTest.ts | 2 +- 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts rename packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/{ => utils}/deleteEmptyList.ts (100%) create mode 100644 packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts rename packages/roosterjs-content-model-core/test/corePlugin/copyPaste/{ => utils}/deleteEmptyListTest.ts (99%) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts index 79965ccdcb2..b0fd0c791d9 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts @@ -1,8 +1,8 @@ import { addRangeToSelection } from '../../coreApi/setDOMSelection/addRangeToSelection'; -import { deleteEmptyList } from './deleteEmptyList'; +import { adjustImageSelectionOnSafari } from './utils/adjustImageSelectionOnSafari'; +import { deleteEmptyList } from './utils/deleteEmptyList'; import { onCreateCopyEntityNode } from '../../override/pasteCopyBlockEntityParser'; import { paste } from '../../command/paste/paste'; - import { ChangeSource, contentModelToDom, @@ -110,6 +110,8 @@ class CopyPastePlugin implements PluginWithState { const doc = this.editor.getDocument(); const selection = this.editor.getDOMSelection(); + adjustImageSelectionOnSafari(this.editor, selection); + if (selection && (selection.type != 'range' || !selection.range.collapsed)) { const pasteModel = this.editor.getContentModelCopy('disconnected'); diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts new file mode 100644 index 00000000000..0b8854f889e --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts @@ -0,0 +1,14 @@ +import type { IEditor, DOMSelection } from 'roosterjs-content-model-types'; + +export function adjustImageSelectionOnSafari(editor: IEditor, selection: DOMSelection | null) { + if (editor.getEnvironment().isSafari && selection?.type == 'image') { + const range = new Range(); + range.setStartBefore(selection.image); + range.setEndAfter(selection.image); + editor.setDOMSelection({ + range, + type: 'range', + isReverted: false, + }); + } +} diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/deleteEmptyList.ts similarity index 100% rename from packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts rename to packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/deleteEmptyList.ts diff --git a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts new file mode 100644 index 00000000000..6089bc4d604 --- /dev/null +++ b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts @@ -0,0 +1,70 @@ +import { adjustImageSelectionOnSafari } from '../../../../lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari'; +import type { DOMSelection, IEditor } from 'roosterjs-content-model-types'; + +describe('adjustImageSelectionOnSafari', () => { + let getEnvironmentSpy: jasmine.Spy; + let setDOMSelectionSpy: jasmine.Spy; + let editor: IEditor; + + beforeEach(() => { + getEnvironmentSpy = jasmine.createSpy('getEnvironment'); + setDOMSelectionSpy = jasmine.createSpy('setDOMSelection'); + editor = ({ + getEnvironment: getEnvironmentSpy, + setDOMSelection: setDOMSelectionSpy, + } as any) as IEditor; + }); + + it('should adjustSelection', () => { + getEnvironmentSpy.and.returnValue({ + isSafari: true, + }); + const image = document.createElement('img'); + document.body.appendChild(image); + const selection: DOMSelection = { + type: 'image', + image: image, + }; + const range = new Range(); + range.setStartBefore(image); + range.setEndAfter(image); + + adjustImageSelectionOnSafari(editor, selection); + expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); + expect(setDOMSelectionSpy).toHaveBeenCalledWith({ + range: range, + type: 'range', + isReverted: false, + }); + + document.body.removeChild(image); + }); + + it('should not adjustSelection - it is not safari', () => { + getEnvironmentSpy.and.returnValue({ + isSafari: false, + }); + const image = new Image(); + const selection: DOMSelection = { + type: 'image', + image: image, + }; + + adjustImageSelectionOnSafari(editor, selection); + expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + }); + + it('should not adjustSelection - it is not image', () => { + getEnvironmentSpy.and.returnValue({ + isSafari: true, + }); + const selection: DOMSelection = { + type: 'range', + range: new Range(), + isReverted: false, + }; + + adjustImageSelectionOnSafari(editor, selection); + expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/deleteEmptyListTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/deleteEmptyListTest.ts similarity index 99% rename from packages/roosterjs-content-model-core/test/corePlugin/copyPaste/deleteEmptyListTest.ts rename to packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/deleteEmptyListTest.ts index 5c13f319b09..51d3e7c7355 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/deleteEmptyListTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/deleteEmptyListTest.ts @@ -1,5 +1,5 @@ import { ContentModelBlockGroup, ContentModelSelectionMarker } from 'roosterjs-content-model-types'; -import { deleteEmptyList } from '../../../lib/corePlugin/copyPaste/deleteEmptyList'; +import { deleteEmptyList } from '../../../../lib/corePlugin/copyPaste/utils/deleteEmptyList'; import { deleteSelection } from 'roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection'; import { createContentModelDocument, From b4796e3ef3b1fed234b39e81bbb053aec6d8677e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Tue, 10 Sep 2024 15:10:20 -0300 Subject: [PATCH 19/43] comment --- .../copyPaste/utils/adjustImageSelectionOnSafari.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts index 0b8854f889e..e0c37d9e3da 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts @@ -1,5 +1,9 @@ import type { IEditor, DOMSelection } from 'roosterjs-content-model-types'; +/** + * @internal + * Adjust Image selection, so the copy by keyboard does not remove image selection. + */ export function adjustImageSelectionOnSafari(editor: IEditor, selection: DOMSelection | null) { if (editor.getEnvironment().isSafari && selection?.type == 'image') { const range = new Range(); From c9edd478c3170f4da624fe8f8b2107092a8f033c Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Wed, 11 Sep 2024 17:31:33 -0300 Subject: [PATCH 20/43] image pasted --- .../lib/imageEdit/utils/findEditingImage.ts | 4 +- .../imageEdit/utils/findEditingImageTest.ts | 277 ++++++++++++++++++ 2 files changed, 279 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts index 689187d5b66..4fff6e96286 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/findEditingImage.ts @@ -29,8 +29,8 @@ export function findEditingImage( switch (segment.segmentType) { case 'Image': if ( - (segment.dataset.isEditing && !imageId) || - segment.format.id == imageId + (imageId && segment.format.id == imageId) || + segment.dataset.isEditing ) { return { paragraph: block, diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts index 3639e370da7..44d5a0a1bb8 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/utils/findEditingImageTest.ts @@ -449,4 +449,281 @@ describe('findEditingImage', () => { }, }); }); + + it('editing image - no id - no editing image | by Id', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model); + expect(image).toEqual(null); + }); + + it('editing image - no id - editing image', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'second line', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'second-Image', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model); + expect(image).toEqual({ + image: { + segmentType: 'Image', + src: 'second-Image', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + }, + dataset: { + isEditing: 'true', + }, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'second-Image', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + }); + }); + + it('editing image - with id - editing image', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + id: 'testId', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'second line', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'second-Image', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + }, + dataset: { + isEditing: 'true', + }, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + + const image = findEditingImage(model, 'testId'); + expect(image).toEqual({ + image: { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + id: 'testId', + }, + dataset: {}, + }, + paragraph: { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + maxWidth: '1800px', + id: 'testId', + }, + dataset: {}, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + }); + }); }); From 1f1b92b874953dfe0b2110819ad571cdf2ce5cff Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 12 Sep 2024 18:35:16 -0300 Subject: [PATCH 21/43] WIP --- .../lib/autoFormat/AutoFormatPlugin.ts | 27 +- .../lib/autoFormat/link/createLink.ts | 21 +- .../autoFormat/link/createLinkAfterSpace.ts | 15 +- .../lib/autoFormat/link/getLinkUrl.ts | 31 ++ .../link/createLinkAfterSpaceTest.ts | 416 ++++++++++++++++++ 5 files changed, 492 insertions(+), 18 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index b0936bdf511..ecd39d3402e 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -37,7 +37,7 @@ export type AutoFormatOptions = { autoUnlink?: boolean; /** - * When paste content, create hyperlink for the pasted link + * When paste or type content with a link, create hyperlink for the link */ autoLink?: boolean; @@ -55,6 +55,16 @@ export type AutoFormatOptions = { * Transform ordinal numbers into superscript */ autoOrdinals?: boolean; + + /** + * When paste content or type content with telephone, create hyperlink for the telephone number + */ + autoTel?: boolean; + + /** + * When paste or type a content with mailto, create hyperlink for the content + */ + autoMailto?: boolean; }; /** @@ -161,6 +171,8 @@ export class AutoFormatPlugin implements EditorPlugin { autoHyphen, autoFraction, autoOrdinals, + autoMailto, + autoTel, } = this.options; let shouldHyphen = false; let shouldLink = false; @@ -178,11 +190,14 @@ export class AutoFormatPlugin implements EditorPlugin { ); } - if (autoLink) { + if (autoLink || autoTel || autoMailto) { shouldLink = createLinkAfterSpace( previousSegment, paragraph, - context + context, + autoLink, + autoTel, + autoMailto ); } @@ -243,9 +258,9 @@ export class AutoFormatPlugin implements EditorPlugin { } private handleContentChangedEvent(editor: IEditor, event: ContentChangedEvent) { - const { autoLink } = this.options; - if (event.source == 'Paste' && autoLink) { - createLink(editor); + const { autoLink, autoTel, autoMailto } = this.options; + if (event.source == 'Paste' && (autoLink || autoTel || autoMailto)) { + createLink(editor, autoLink, autoTel, autoMailto); } } } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts index 65993fffcc4..e8c2f5e9980 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts @@ -1,11 +1,17 @@ import { addLink, ChangeSource } from 'roosterjs-content-model-dom'; -import { formatTextSegmentBeforeSelectionMarker, matchLink } from 'roosterjs-content-model-api'; -import type { ContentModelLink, IEditor, LinkData } from 'roosterjs-content-model-types'; +import { formatTextSegmentBeforeSelectionMarker } from 'roosterjs-content-model-api'; +import { getLinkUrl } from './getLinkUrl'; +import type { ContentModelLink, IEditor } from 'roosterjs-content-model-types'; /** * @internal */ -export function createLink(editor: IEditor) { +export function createLink( + editor: IEditor, + autoLink?: boolean, + autoTel?: boolean, + autoMailto?: boolean +) { let anchorNode: Node | null = null; const links: ContentModelLink[] = []; formatTextSegmentBeforeSelectionMarker( @@ -15,11 +21,14 @@ export function createLink(editor: IEditor) { links.push(linkSegment.link); return true; } - let linkData: LinkData | null = null; - if (!linkSegment.link && (linkData = matchLink(linkSegment.text))) { + let linkUrl: string | undefined = undefined; + if ( + !linkSegment.link && + (linkUrl = getLinkUrl(linkSegment.text, autoLink, autoTel, autoMailto)) + ) { addLink(linkSegment, { format: { - href: linkData.normalizedUrl, + href: linkUrl, underline: true, }, dataset: {}, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index 95898e30d08..037afa901a1 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -1,8 +1,8 @@ -import { matchLink, splitTextSegment } from 'roosterjs-content-model-api'; +import { getLinkUrl } from './getLinkUrl'; +import { splitTextSegment } from 'roosterjs-content-model-api'; import type { ContentModelText, FormatContentModelContext, - LinkData, ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; @@ -12,12 +12,15 @@ import type { export function createLinkAfterSpace( previousSegment: ContentModelText, paragraph: ShallowMutableContentModelParagraph, - context: FormatContentModelContext + context: FormatContentModelContext, + autoLink?: boolean, + autoTel?: boolean, + autoMailto?: boolean ) { const link = previousSegment.text.split(' ').pop(); const url = link?.trim(); - let linkData: LinkData | null = null; - if (url && link && (linkData = matchLink(url))) { + let linkUrl: string | undefined = undefined; + if (url && link && (linkUrl = getLinkUrl(url, autoLink, autoTel, autoMailto))) { const linkSegment = splitTextSegment( previousSegment, paragraph, @@ -26,7 +29,7 @@ export function createLinkAfterSpace( ); linkSegment.link = { format: { - href: linkData.normalizedUrl, + href: linkUrl, underline: true, }, dataset: {}, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts new file mode 100644 index 00000000000..ce8fdce6ec3 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts @@ -0,0 +1,31 @@ +import { matchLink } from 'roosterjs-content-model-api'; + +const COMMON_REGEX = `[\s]*[a-zA-Z0-9][\s]*`; +const TELEPHONE_REGEX = `(T|t)el:${COMMON_REGEX}`; +const MAILTO_REGEX = `(M|m)ailto:${COMMON_REGEX}`; + +/** + * @internal + */ +export function getLinkUrl( + text: string, + shouldLink?: boolean, + shouldMatchTel?: boolean, + shouldMatchMailto?: boolean +): string | undefined { + return shouldLink + ? matchLink(text)?.normalizedUrl + : undefined || shouldMatchTel + ? matchTel(text) + : undefined || shouldMatchMailto + ? matchMailTo(text) + : undefined; +} + +function matchTel(text: string) { + return text.match(TELEPHONE_REGEX) ? text.toLocaleLowerCase() : undefined; +} + +function matchMailTo(text: string) { + return text.match(MAILTO_REGEX) ? text.toLocaleLowerCase() : undefined; +} diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts index 9ad724e4b9f..0a7c1ea1d2d 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts @@ -385,4 +385,420 @@ describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => }; runTest(input, expected, true); }); + + it('telephone link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with T', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link with M', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('telephone link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); }); From 5480a82415d011fd813d850243e9c43fe5772c62 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 13 Sep 2024 10:06:11 -0700 Subject: [PATCH 22/43] Fix #2755 (#2795) --- .../lib/modelApi/block/setModelIndentation.ts | 13 +- .../modelApi/block/setModelIndentationTest.ts | 299 +++++++++++++++++- 2 files changed, 309 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts index 217a0d5f641..6a1626f3c07 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -66,10 +66,19 @@ export function setModelIndentation( //if block has only one level, there is not need to check if it is multilevel selection } else if (block.levels.length == 1 || !isMultilevelSelection(model, block, parent)) { if (isIndent) { - const lastLevel = block.levels[block.levels.length - 1]; + const threadIdx = thread.indexOf(block); + const previousItem = thread[threadIdx - 1]; + const nextItem = thread[threadIdx + 1]; + const levelLength = block.levels.length; + const lastLevel = block.levels[levelLength - 1]; const newLevel: ContentModelListLevel = createListLevel( lastLevel?.listType || 'UL', - lastLevel?.format + lastLevel?.format, + previousItem && previousItem.levels.length > levelLength + ? previousItem.levels[levelLength].dataset + : nextItem && nextItem.levels.length > levelLength + ? nextItem.levels[levelLength].dataset + : undefined ); updateListMetadata(newLevel, metadata => { diff --git a/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts b/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts index 765ca3526e6..6bf1e233806 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts @@ -1,5 +1,5 @@ import * as getListAnnounceData from '../../../lib/modelApi/list/getListAnnounceData'; -import { FormatContentModelContext } from 'roosterjs-content-model-types'; +import { ContentModelDocument, FormatContentModelContext } from 'roosterjs-content-model-types'; import { setModelIndentation } from '../../../lib/modelApi/block/setModelIndentation'; import { createContentModelDocument, @@ -899,6 +899,303 @@ describe('indent', () => { }); expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); + + it('Indent and follow previous item style', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + ], + }; + + setModelIndentation(model, 'indent'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { + format: {}, + dataset: { a: 'c', editingInfo: '{"applyListStyleFromLevel":true}' }, + listType: 'OL', + }, + ], + }, + ], + }); + }); + + it('Indent and follow next item style', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + ], + }; + + setModelIndentation(model, 'indent'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { + format: {}, + dataset: { a: 'c', editingInfo: '{"applyListStyleFromLevel":true}' }, + listType: 'OL', + }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + ], + }); + }); + + it('Indent, no style to follow', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + ], + }; + + setModelIndentation(model, 'indent'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { + format: {}, + dataset: { editingInfo: '{"applyListStyleFromLevel":true}' }, + listType: 'OL', + }, + ], + }, + ], + }); + }); }); describe('outdent', () => { From 697e35a03370969f0be857d11f766d8706c2e22b Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 13 Sep 2024 15:06:23 -0300 Subject: [PATCH 23/43] auto-link enhacements --- .../editorOptions/EditorOptionsPlugin.ts | 2 + .../sidePane/editorOptions/Plugins.tsx | 14 + .../lib/autoFormat/AutoFormatPlugin.ts | 12 +- .../lib/autoFormat/link/createLink.ts | 6 +- .../autoFormat/link/createLinkAfterSpace.ts | 6 +- .../lib/autoFormat/link/getLinkUrl.ts | 20 +- .../test/autoFormat/AutoFormatPluginTest.ts | 44 +- .../link/createLinkAfterSpaceTest.ts | 60 ++- .../test/autoFormat/link/createLinkTest.ts | 480 +++++++++++++++++- 9 files changed, 609 insertions(+), 35 deletions(-) diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index a6d4c5af32a..6e216f2da1a 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -45,6 +45,8 @@ const initialState: OptionState = { autoHyphen: true, autoFraction: true, autoOrdinals: true, + autoMailto: true, + autoTel: true, }, markdownOptions: { bold: true, diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx index 86ebfe53fdf..d1ba83fee58 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx @@ -110,6 +110,8 @@ export class Plugins extends PluginsBase { private autoHyphen = React.createRef(); private autoFraction = React.createRef(); private autoOrdinals = React.createRef(); + private autoTel = React.createRef(); + private autoMailto = React.createRef(); private markdownBold = React.createRef(); private markdownItalic = React.createRef(); private markdownStrikethrough = React.createRef(); @@ -166,6 +168,18 @@ export class Plugins extends PluginsBase { this.props.state.autoFormatOptions.autoOrdinals, (state, value) => (state.autoFormatOptions.autoOrdinals = value) )} + {this.renderCheckBox( + 'Telephone', + this.autoTel, + this.props.state.autoFormatOptions.autoTel, + (state, value) => (state.autoFormatOptions.autoTel = value) + )} + {this.renderCheckBox( + 'Email', + this.autoMailto, + this.props.state.autoFormatOptions.autoMailto, + (state, value) => (state.autoFormatOptions.autoMailto = value) + )} )} {this.renderPluginItem( diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index ecd39d3402e..a85e60f57c4 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -90,11 +90,13 @@ export class AutoFormatPlugin implements EditorPlugin { * @param options An optional parameter that takes in an object of type AutoFormatOptions, which includes the following properties: * - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to false. * - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to false. - * - autoLink: A boolean that enables or disables automatic hyperlink creation when pasting or typing content. Defaults to false. + * - autoLink: A boolean that enables or disables automatic hyperlink url address creation when pasting or typing content. Defaults to false. * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. * - autoHyphen: A boolean that enables or disables automatic hyphen transformation. Defaults to false. * - autoFraction: A boolean that enables or disables automatic fraction transformation. Defaults to false. * - autoOrdinals: A boolean that enables or disables automatic ordinal number transformation. Defaults to false. + * - autoTel: A boolean that enables or disables automatic hyperlink telephone numbers transformation. Defaults to false. + * - autoMailto: A boolean that enables or disables automatic hyperlink email address transformation. Defaults to false. */ constructor(private options: AutoFormatOptions = DefaultOptions) {} @@ -195,9 +197,9 @@ export class AutoFormatPlugin implements EditorPlugin { previousSegment, paragraph, context, - autoLink, - autoTel, - autoMailto + !!autoLink, + !!autoTel, + !!autoMailto ); } @@ -260,7 +262,7 @@ export class AutoFormatPlugin implements EditorPlugin { private handleContentChangedEvent(editor: IEditor, event: ContentChangedEvent) { const { autoLink, autoTel, autoMailto } = this.options; if (event.source == 'Paste' && (autoLink || autoTel || autoMailto)) { - createLink(editor, autoLink, autoTel, autoMailto); + createLink(editor, !!autoLink, !!autoTel, !!autoMailto); } } } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts index e8c2f5e9980..b70a64a0380 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts @@ -8,9 +8,9 @@ import type { ContentModelLink, IEditor } from 'roosterjs-content-model-types'; */ export function createLink( editor: IEditor, - autoLink?: boolean, - autoTel?: boolean, - autoMailto?: boolean + autoLink: boolean, + autoTel: boolean, + autoMailto: boolean ) { let anchorNode: Node | null = null; const links: ContentModelLink[] = []; diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index 037afa901a1..e745df4e4a9 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -13,9 +13,9 @@ export function createLinkAfterSpace( previousSegment: ContentModelText, paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext, - autoLink?: boolean, - autoTel?: boolean, - autoMailto?: boolean + autoLink: boolean, + autoTel: boolean, + autoMailto: boolean ) { const link = previousSegment.text.split(' ').pop(); const url = link?.trim(); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts index ce8fdce6ec3..96c2ad7aa43 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts @@ -1,6 +1,6 @@ import { matchLink } from 'roosterjs-content-model-api'; -const COMMON_REGEX = `[\s]*[a-zA-Z0-9][\s]*`; +const COMMON_REGEX = `[\s]*[a-zA-Z0-9+][\s]*`; const TELEPHONE_REGEX = `(T|t)el:${COMMON_REGEX}`; const MAILTO_REGEX = `(M|m)ailto:${COMMON_REGEX}`; @@ -9,17 +9,15 @@ const MAILTO_REGEX = `(M|m)ailto:${COMMON_REGEX}`; */ export function getLinkUrl( text: string, - shouldLink?: boolean, - shouldMatchTel?: boolean, - shouldMatchMailto?: boolean + shouldLink: boolean, + shouldMatchTel: boolean, + shouldMatchMailto: boolean ): string | undefined { - return shouldLink - ? matchLink(text)?.normalizedUrl - : undefined || shouldMatchTel - ? matchTel(text) - : undefined || shouldMatchMailto - ? matchMailTo(text) - : undefined; + const linkMatch = shouldLink ? matchLink(text)?.normalizedUrl : undefined; + const telMatch = shouldMatchTel ? matchTel(text) : undefined; + const mailtoMatch = shouldMatchMailto ? matchMailTo(text) : undefined; + + return linkMatch || telMatch || mailtoMatch; } function matchTel(text: string) { diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index 9203fa27645..4aef157d717 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -176,17 +176,16 @@ describe('Content Model Auto Format Plugin Test', () => { function runTest( event: ContentChangedEvent, shouldCallTrigger: boolean, - options?: { - autoLink: boolean; - } + options: AutoFormatOptions ) { const plugin = new AutoFormatPlugin(options as AutoFormatOptions); plugin.initialize(editor); plugin.onPluginEvent(event); + const { autoLink, autoTel, autoMailto } = options; if (shouldCallTrigger) { - expect(createLinkSpy).toHaveBeenCalledWith(editor); + expect(createLinkSpy).toHaveBeenCalledWith(editor, autoLink, autoTel, autoMailto); } else { expect(createLinkSpy).not.toHaveBeenCalled(); } @@ -199,6 +198,8 @@ describe('Content Model Auto Format Plugin Test', () => { }; runTest(event, true, { autoLink: true, + autoMailto: true, + autoTel: true, }); }); @@ -207,7 +208,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'contentChanged', source: 'Paste', }; - runTest(event, false, { autoLink: false }); + runTest(event, false, { autoLink: false, autoMailto: false, autoTel: false }); }); it('should not call createLink - not paste', () => { @@ -217,6 +218,8 @@ describe('Content Model Auto Format Plugin Test', () => { }; runTest(event, false, { autoLink: true, + autoMailto: true, + autoTel: true, }); }); }); @@ -304,10 +307,17 @@ describe('Content Model Auto Format Plugin Test', () => { paragraph: ContentModelParagraph, context: FormatContentModelContext ) => { + const { autoLink, autoMailto, autoTel } = options; const result = options && - options.autoLink && - createLinkAfterSpace(segment, paragraph, context); + createLinkAfterSpace( + segment, + paragraph, + context, + !!autoLink, + !!autoTel, + !!autoMailto + ); expect(result).toBe(expectResult); @@ -328,6 +338,26 @@ describe('Content Model Auto Format Plugin Test', () => { }); }); + it('should call createLinkAfterSpace | autoTel', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, true, { + autoTel: true, + }); + }); + + it('should call createLinkAfterSpace | autoMailto', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, true, { + autoMailto: true, + }); + }); + it('should not call createLinkAfterSpace - disable options', () => { const event: EditorInputEvent = { eventType: 'input', diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts index 0a7c1ea1d2d..2ed75ec4cdb 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts @@ -14,7 +14,7 @@ describe('createLinkAfterSpace', () => { context: FormatContentModelContext, expectedResult: boolean ) { - const result = createLinkAfterSpace(previousSegment, paragraph, context); + const result = createLinkAfterSpace(previousSegment, paragraph, context, true, true, true); expect(result).toBe(expectedResult); } @@ -85,7 +85,7 @@ describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => formatContentModel: formatWithContentModelSpy, } as any, (_model, previousSegment, paragraph, _markerFormat, context) => { - return createLinkAfterSpace(previousSegment, paragraph, context); + return createLinkAfterSpace(previousSegment, paragraph, context, true, true, true); } ); @@ -442,6 +442,62 @@ describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => runTest(input, expected, true); }); + it('telephone link with +', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + link: { + format: { + href: 'tel:+9999-9999', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + it('telephone link with T', () => { const input: ContentModelDocument = { blockGroupType: 'Document', diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts index edd4e5e7e31..8ce869c69a4 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts @@ -20,10 +20,15 @@ describe('createLink', () => { expect(options.changeSource).toBe(ChangeSource.AutoLink); }); - createLink({ - focus: () => {}, - formatContentModel: formatWithContentModelSpy, - } as any); + createLink( + { + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any, + true, + true, + true + ); expect(formatWithContentModelSpy).toHaveBeenCalled(); expect(input).toEqual(expectedModel); @@ -165,4 +170,471 @@ describe('createLink', () => { runTest(input, input, true); }); + + it('telephone link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with +', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + link: { + format: { + href: 'tel:+9999-9999', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with T', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link with M', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('telephone link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); }); From 07640afb81682047cf239a5b7765bc40bd672b19 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 13 Sep 2024 17:52:09 -0300 Subject: [PATCH 24/43] links --- .../test/autoFormat/link/getLinkUrlTest.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts new file mode 100644 index 00000000000..dfa1ace55c1 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts @@ -0,0 +1,58 @@ +import { getLinkUrl } from '../../../lib/autoFormat/link/getLinkUrl'; + +describe('getLinkUrl', () => { + function runTest( + text: string, + shouldLink: boolean, + shouldMatchTel: boolean, + shouldMatchMailto: boolean, + expectedResult: string | undefined + ) { + const link = getLinkUrl(text, shouldLink, shouldMatchTel, shouldMatchMailto); + expect(link).toBe(expectedResult); + } + + it('link', () => { + runTest('http://www.bing.com', true, false, false, 'http://www.bing.com'); + }); + + it('do not return link', () => { + runTest('wwww.test.com', false, true, true, undefined); + }); + + it('invalid link', () => { + runTest('www3w.test.com', true, false, false, undefined); + }); + + it('telephone', () => { + runTest('tel:999999', false, true, false, 'tel:999999'); + }); + + it('telephone with T', () => { + runTest('Tel:999999', false, true, false, 'tel:999999'); + }); + + it('do not return telephone', () => { + runTest('tel:999999', true, false, true, undefined); + }); + + it('invalid telephone', () => { + runTest('tels:999999', false, true, false, undefined); + }); + + it('mailto', () => { + runTest('mailto:test', false, false, true, 'mailto:test'); + }); + + it('mailto with M', () => { + runTest('Mailto:test', false, false, true, 'mailto:test'); + }); + + it('do not return mailto', () => { + runTest('mailto:test', true, true, false, undefined); + }); + + it('invalid mailto', () => { + runTest('mailtos:test', false, false, true, undefined); + }); +}); From 7ef8730ab13273cb98976be796bd5f3b98ba46f4 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 16 Sep 2024 13:35:52 -0700 Subject: [PATCH 25/43] Use a different change source for keyboard formatting event (#2799) --- .../lib/edit/keyboardEnter.ts | 8 +++++++- .../lib/edit/keyboardInput.ts | 8 +++++++- .../lib/edit/keyboardTab.ts | 7 ++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index ef765e5e231..b8b2217563e 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -1,7 +1,12 @@ import { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote'; -import { deleteSelection, normalizeContentModel, runEditSteps } from 'roosterjs-content-model-dom'; import { handleEnterOnList } from './inputSteps/handleEnterOnList'; import { handleEnterOnParagraph } from './inputSteps/handleEnterOnParagraph'; +import { + ChangeSource, + deleteSelection, + normalizeContentModel, + runEditSteps, +} from 'roosterjs-content-model-dom'; import type { IEditor } from 'roosterjs-content-model-types'; /** @@ -49,6 +54,7 @@ export function keyboardEnter( { rawEvent, scrollCaretIntoView: true, + changeSource: ChangeSource.Keyboard, } ); } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index fe8dc9e7bfc..95ffb0d8667 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -1,4 +1,9 @@ -import { deleteSelection, isModifierKey, normalizeContentModel } from 'roosterjs-content-model-dom'; +import { + ChangeSource, + deleteSelection, + isModifierKey, + normalizeContentModel, +} from 'roosterjs-content-model-dom'; import type { DOMSelection, IEditor } from 'roosterjs-content-model-types'; /** @@ -32,6 +37,7 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) { { scrollCaretIntoView: true, rawEvent, + changeSource: ChangeSource.Keyboard, } ); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts index 7c274f2f0cc..495b79fdc80 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts @@ -1,9 +1,13 @@ -import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-dom'; import { handleTabOnList } from './tabUtils/handleTabOnList'; import { handleTabOnParagraph } from './tabUtils/handleTabOnParagraph'; import { handleTabOnTable } from './tabUtils/handleTabOnTable'; import { handleTabOnTableCell } from './tabUtils/handleTabOnTableCell'; import { setModelIndentation } from 'roosterjs-content-model-api'; +import { + ChangeSource, + getOperationalBlocks, + isBlockGroupOfType, +} from 'roosterjs-content-model-dom'; import type { ContentModelListItem, ContentModelTableCell, @@ -37,6 +41,7 @@ export function keyboardTab(editor: IEditor, rawEvent: KeyboardEvent) { }, { apiName: 'handleTabKey', + changeSource: ChangeSource.Keyboard, } ); return true; From de654a3db58acdedb04ed362f5f1e710d60640ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:19:08 -0700 Subject: [PATCH 26/43] Bump dompurify from 2.3.0 to 2.5.4 (#2800) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.0 to 2.5.4. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/2.3.0...2.5.4) --- updated-dependencies: - dependency-name: dompurify dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ab3e23aed45..9dab173dd0d 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "coverage-istanbul-loader": "3.0.5", "css-loader": "3.5.3", "detect-port": "^1.3.0", - "dompurify": "2.3.0", + "dompurify": "2.5.4", "eslint": "^8.50.0", "eslint-plugin-etc": "^2.0.3", "eslint-plugin-react": "^7.33.2", diff --git a/yarn.lock b/yarn.lock index 939728d0c1b..92cee2b7893 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2247,10 +2247,10 @@ dom-serialize@^2.2.1: extend "^3.0.0" void-elements "^2.0.0" -dompurify@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2" - integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw== +dompurify@2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.4.tgz#347e91070963b22db31c7c8d0ce9a0a2c3c08746" + integrity sha512-l5NNozANzaLPPe0XaAwvg3uZcHtDBnziX/HjsY1UcDj1MxTK8Dd0Kv096jyPK5HRzs/XM5IMj20dW8Fk+HnbUA== ecc-jsbn@~0.1.1: version "0.1.2" From 63747b9ca4430aa476fa31f195d8850a895f3da5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:47:58 -0700 Subject: [PATCH 27/43] Bump express from 4.19.2 to 4.21.0 (#2801) Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.21.0. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 179 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 140 insertions(+), 39 deletions(-) diff --git a/yarn.lock b/yarn.lock index 92cee2b7893..8c53e207369 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1445,10 +1445,10 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -1458,7 +1458,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -1582,6 +1582,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2096,6 +2107,15 @@ define-data-property@^1.0.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -2295,6 +2315,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -2404,6 +2429,18 @@ es-abstract@^1.22.1: unbox-primitive "^1.0.2" which-typed-array "^1.1.11" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-iterator-helpers@^1.0.12: version "1.0.15" resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" @@ -2754,36 +2791,36 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: homedir-polyfill "^1.0.1" express@^4.17.1: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -2940,13 +2977,13 @@ finalhandler@1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -3101,6 +3138,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" @@ -3150,6 +3192,17 @@ get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@ has-proto "^1.0.1" has-symbols "^1.0.3" +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -3394,6 +3447,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -3457,6 +3517,13 @@ hasha@^2.2.0: is-stream "^1.0.1" pinkie-promise "^2.0.0" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -4622,10 +4689,10 @@ memory-fs@^0.4.0, memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-source-map@^1.1.0: version "1.1.0" @@ -4928,6 +4995,11 @@ object-inspect@^1.12.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -5241,10 +5313,10 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-type@^4.0.0: version "4.0.0" @@ -5522,6 +5594,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -5960,10 +6039,10 @@ semver@^7.3.4, semver@^7.3.7, semver@^7.5.4: dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -5999,21 +6078,33 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + set-function-name@^2.0.0, set-function-name@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" @@ -6092,6 +6183,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From a13c33caa11c416e4ca826984ca15b0e577376fe Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Mon, 23 Sep 2024 11:21:11 -0300 Subject: [PATCH 28/43] fixes --- .../lib/autoFormat/AutoFormatPlugin.ts | 69 ++++--------------- .../autoFormat/interface/AutoFormatOptions.ts | 31 +++++++++ .../autoFormat/interface/AutoLinkOptions.ts | 24 +++++++ .../lib/autoFormat/link/createLink.ts | 13 +--- .../autoFormat/link/createLinkAfterSpace.ts | 7 +- .../lib/autoFormat/link/getLinkUrl.ts | 15 ++-- .../lib/index.ts | 4 +- .../test/autoFormat/AutoFormatPluginTest.ts | 16 ++--- .../link/createLinkAfterSpaceTest.ts | 12 +++- .../test/autoFormat/link/createLinkTest.ts | 4 +- .../test/autoFormat/link/getLinkUrlTest.ts | 33 ++++----- 11 files changed, 112 insertions(+), 116 deletions(-) create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index a85e60f57c4..81fb2695c0e 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -7,6 +7,7 @@ import { transformFraction } from './numbers/transformFraction'; import { transformHyphen } from './hyphen/transformHyphen'; import { transformOrdinals } from './numbers/transformOrdinals'; import { unlink } from './link/unlink'; +import type { AutoFormatOptions } from './interface/AutoFormatOptions'; import type { ContentChangedEvent, EditorInputEvent, @@ -17,56 +18,6 @@ import type { PluginEvent, } from 'roosterjs-content-model-types'; -/** - * Options to customize the Content Model Auto Format Plugin - */ -export type AutoFormatOptions = { - /** - * When true, after type *, ->, -, --, => , —, > and space key a type of bullet list will be triggered. @default true - */ - autoBullet?: boolean; - - /** - * When true, after type 1, A, a, i, I followed by ., ), - or between () and space key a type of numbering list will be triggered. @default true - */ - autoNumbering?: boolean; - - /** - * When press backspace before a link, remove the hyperlink - */ - autoUnlink?: boolean; - - /** - * When paste or type content with a link, create hyperlink for the link - */ - autoLink?: boolean; - - /** - * Transform -- into hyphen, if typed between two words - */ - autoHyphen?: boolean; - - /** - * Transform 1/2, 1/4, 3/4 into fraction character - */ - autoFraction?: boolean; - - /** - * Transform ordinal numbers into superscript - */ - autoOrdinals?: boolean; - - /** - * When paste content or type content with telephone, create hyperlink for the telephone number - */ - autoTel?: boolean; - - /** - * When paste or type a content with mailto, create hyperlink for the content - */ - autoMailto?: boolean; -}; - /** * @internal */ @@ -90,11 +41,11 @@ export class AutoFormatPlugin implements EditorPlugin { * @param options An optional parameter that takes in an object of type AutoFormatOptions, which includes the following properties: * - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to false. * - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to false. - * - autoLink: A boolean that enables or disables automatic hyperlink url address creation when pasting or typing content. Defaults to false. - * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. * - autoHyphen: A boolean that enables or disables automatic hyphen transformation. Defaults to false. * - autoFraction: A boolean that enables or disables automatic fraction transformation. Defaults to false. * - autoOrdinals: A boolean that enables or disables automatic ordinal number transformation. Defaults to false. + * - autoLink: A boolean that enables or disables automatic hyperlink url address creation when pasting or typing content. Defaults to false. + * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. * - autoTel: A boolean that enables or disables automatic hyperlink telephone numbers transformation. Defaults to false. * - autoMailto: A boolean that enables or disables automatic hyperlink email address transformation. Defaults to false. */ @@ -197,9 +148,11 @@ export class AutoFormatPlugin implements EditorPlugin { previousSegment, paragraph, context, - !!autoLink, - !!autoTel, - !!autoMailto + { + autoLink, + autoTel, + autoMailto, + } ); } @@ -262,7 +215,11 @@ export class AutoFormatPlugin implements EditorPlugin { private handleContentChangedEvent(editor: IEditor, event: ContentChangedEvent) { const { autoLink, autoTel, autoMailto } = this.options; if (event.source == 'Paste' && (autoLink || autoTel || autoMailto)) { - createLink(editor, !!autoLink, !!autoTel, !!autoMailto); + createLink(editor, { + autoLink, + autoTel, + autoMailto, + }); } } } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts new file mode 100644 index 00000000000..50682210fac --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts @@ -0,0 +1,31 @@ +import type { AutoLinkOptions } from './AutoLinkOptions'; + +/** + * Options to customize the Content Model Auto Format Plugin + */ +export interface AutoFormatOptions extends AutoLinkOptions { + /** + * When true, after type *, ->, -, --, => , —, > and space key a type of bullet list will be triggered. + */ + autoBullet?: boolean; + + /** + * When true, after type 1, A, a, i, I followed by ., ), - or between () and space key a type of numbering list will be triggered. + */ + autoNumbering?: boolean; + + /** + * Transform -- into hyphen, if typed between two words + */ + autoHyphen?: boolean; + + /** + * Transform 1/2, 1/4, 3/4 into fraction character + */ + autoFraction?: boolean; + + /** + * Transform ordinal numbers into superscript + */ + autoOrdinals?: boolean; +} diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts new file mode 100644 index 00000000000..a8f6113d5d4 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts @@ -0,0 +1,24 @@ +/** + * Options to customize the Auto link options in Auto Format Plugin + */ +export interface AutoLinkOptions { + /** + * When press backspace before a link, remove the hyperlink + */ + autoUnlink?: boolean; + + /** + * When paste or type content with a link, create hyperlink for the link + */ + autoLink?: boolean; + + /** + * When paste content or type content with telephone, create hyperlink for the telephone number + */ + autoTel?: boolean; + + /** + * When paste or type a content with mailto, create hyperlink for the content + */ + autoMailto?: boolean; +} diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts index b70a64a0380..fe29906835c 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts @@ -1,17 +1,13 @@ import { addLink, ChangeSource } from 'roosterjs-content-model-dom'; import { formatTextSegmentBeforeSelectionMarker } from 'roosterjs-content-model-api'; import { getLinkUrl } from './getLinkUrl'; +import type { AutoLinkOptions } from '../interface/AutoLinkOptions'; import type { ContentModelLink, IEditor } from 'roosterjs-content-model-types'; /** * @internal */ -export function createLink( - editor: IEditor, - autoLink: boolean, - autoTel: boolean, - autoMailto: boolean -) { +export function createLink(editor: IEditor, autoLinkOptions: AutoLinkOptions) { let anchorNode: Node | null = null; const links: ContentModelLink[] = []; formatTextSegmentBeforeSelectionMarker( @@ -22,10 +18,7 @@ export function createLink( return true; } let linkUrl: string | undefined = undefined; - if ( - !linkSegment.link && - (linkUrl = getLinkUrl(linkSegment.text, autoLink, autoTel, autoMailto)) - ) { + if (!linkSegment.link && (linkUrl = getLinkUrl(linkSegment.text, autoLinkOptions))) { addLink(linkSegment, { format: { href: linkUrl, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index e745df4e4a9..737917e25ab 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -1,5 +1,6 @@ import { getLinkUrl } from './getLinkUrl'; import { splitTextSegment } from 'roosterjs-content-model-api'; +import type { AutoLinkOptions } from '../interface/AutoLinkOptions'; import type { ContentModelText, FormatContentModelContext, @@ -13,14 +14,12 @@ export function createLinkAfterSpace( previousSegment: ContentModelText, paragraph: ShallowMutableContentModelParagraph, context: FormatContentModelContext, - autoLink: boolean, - autoTel: boolean, - autoMailto: boolean + autoLinkOptions: AutoLinkOptions ) { const link = previousSegment.text.split(' ').pop(); const url = link?.trim(); let linkUrl: string | undefined = undefined; - if (url && link && (linkUrl = getLinkUrl(url, autoLink, autoTel, autoMailto))) { + if (url && link && (linkUrl = getLinkUrl(url, autoLinkOptions))) { const linkSegment = splitTextSegment( previousSegment, paragraph, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts index 96c2ad7aa43..8c242f790ff 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts @@ -1,4 +1,5 @@ import { matchLink } from 'roosterjs-content-model-api'; +import type { AutoLinkOptions } from '../interface/AutoLinkOptions'; const COMMON_REGEX = `[\s]*[a-zA-Z0-9+][\s]*`; const TELEPHONE_REGEX = `(T|t)el:${COMMON_REGEX}`; @@ -7,15 +8,11 @@ const MAILTO_REGEX = `(M|m)ailto:${COMMON_REGEX}`; /** * @internal */ -export function getLinkUrl( - text: string, - shouldLink: boolean, - shouldMatchTel: boolean, - shouldMatchMailto: boolean -): string | undefined { - const linkMatch = shouldLink ? matchLink(text)?.normalizedUrl : undefined; - const telMatch = shouldMatchTel ? matchTel(text) : undefined; - const mailtoMatch = shouldMatchMailto ? matchMailTo(text) : undefined; +export function getLinkUrl(text: string, autoLinkOptions: AutoLinkOptions): string | undefined { + const { autoLink, autoMailto, autoTel } = autoLinkOptions; + const linkMatch = autoLink ? matchLink(text)?.normalizedUrl : undefined; + const telMatch = autoTel ? matchTel(text) : undefined; + const mailtoMatch = autoMailto ? matchMailTo(text) : undefined; return linkMatch || telMatch || mailtoMatch; } diff --git a/packages/roosterjs-content-model-plugins/lib/index.ts b/packages/roosterjs-content-model-plugins/lib/index.ts index 4ff598eb9ce..89af142dedc 100644 --- a/packages/roosterjs-content-model-plugins/lib/index.ts +++ b/packages/roosterjs-content-model-plugins/lib/index.ts @@ -4,7 +4,9 @@ export { TableEditFeatureName } from './tableEdit/editors/features/TableEditFeat export { PastePlugin } from './paste/PastePlugin'; export { DefaultSanitizers } from './paste/DefaultSanitizers'; export { EditPlugin, EditOptions } from './edit/EditPlugin'; -export { AutoFormatPlugin, AutoFormatOptions } from './autoFormat/AutoFormatPlugin'; +export { AutoFormatPlugin } from './autoFormat/AutoFormatPlugin'; +export { AutoFormatOptions } from './autoFormat/interface/AutoFormatOptions'; +export { AutoLinkOptions } from './autoFormat/interface/AutoLinkOptions'; export { ShortcutBold, diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index 4aef157d717..c33d6f1dbf9 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -1,7 +1,8 @@ import * as createLink from '../../lib/autoFormat/link/createLink'; import * as formatTextSegmentBeforeSelectionMarker from 'roosterjs-content-model-api/lib/publicApi/utils/formatTextSegmentBeforeSelectionMarker'; import * as unlink from '../../lib/autoFormat/link/unlink'; -import { AutoFormatOptions, AutoFormatPlugin } from '../../lib/autoFormat/AutoFormatPlugin'; +import { AutoFormatOptions } from '../../lib/autoFormat/interface/AutoFormatOptions'; +import { AutoFormatPlugin } from '../../lib/autoFormat/AutoFormatPlugin'; import { ChangeSource } from '../../../roosterjs-content-model-dom/lib/constants/ChangeSource'; import { createLinkAfterSpace } from '../../lib/autoFormat/link/createLinkAfterSpace'; import { keyboardListTrigger } from '../../lib/autoFormat/list/keyboardListTrigger'; @@ -182,10 +183,9 @@ describe('Content Model Auto Format Plugin Test', () => { plugin.initialize(editor); plugin.onPluginEvent(event); - const { autoLink, autoTel, autoMailto } = options; if (shouldCallTrigger) { - expect(createLinkSpy).toHaveBeenCalledWith(editor, autoLink, autoTel, autoMailto); + expect(createLinkSpy).toHaveBeenCalledWith(editor, options); } else { expect(createLinkSpy).not.toHaveBeenCalled(); } @@ -309,15 +309,7 @@ describe('Content Model Auto Format Plugin Test', () => { ) => { const { autoLink, autoMailto, autoTel } = options; const result = - options && - createLinkAfterSpace( - segment, - paragraph, - context, - !!autoLink, - !!autoTel, - !!autoMailto - ); + options && createLinkAfterSpace(segment, paragraph, context, options); expect(result).toBe(expectResult); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts index 2ed75ec4cdb..0bf95c157d4 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts @@ -14,7 +14,11 @@ describe('createLinkAfterSpace', () => { context: FormatContentModelContext, expectedResult: boolean ) { - const result = createLinkAfterSpace(previousSegment, paragraph, context, true, true, true); + const result = createLinkAfterSpace(previousSegment, paragraph, context, { + autoLink: true, + autoMailto: true, + autoTel: true, + }); expect(result).toBe(expectedResult); } @@ -85,7 +89,11 @@ describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => formatContentModel: formatWithContentModelSpy, } as any, (_model, previousSegment, paragraph, _markerFormat, context) => { - return createLinkAfterSpace(previousSegment, paragraph, context, true, true, true); + return createLinkAfterSpace(previousSegment, paragraph, context, { + autoLink: true, + autoMailto: true, + autoTel: true, + }); } ); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts index 8ce869c69a4..4b709ab5a60 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts @@ -25,9 +25,7 @@ describe('createLink', () => { focus: () => {}, formatContentModel: formatWithContentModelSpy, } as any, - true, - true, - true + { autoLink: true, autoMailto: true, autoTel: true } ); expect(formatWithContentModelSpy).toHaveBeenCalled(); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts index dfa1ace55c1..66d117a4d32 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts @@ -1,58 +1,53 @@ +import { AutoLinkOptions } from '../../../lib/autoFormat/interface/AutoLinkOptions'; import { getLinkUrl } from '../../../lib/autoFormat/link/getLinkUrl'; describe('getLinkUrl', () => { - function runTest( - text: string, - shouldLink: boolean, - shouldMatchTel: boolean, - shouldMatchMailto: boolean, - expectedResult: string | undefined - ) { - const link = getLinkUrl(text, shouldLink, shouldMatchTel, shouldMatchMailto); + function runTest(text: string, options: AutoLinkOptions, expectedResult: string | undefined) { + const link = getLinkUrl(text, options); expect(link).toBe(expectedResult); } it('link', () => { - runTest('http://www.bing.com', true, false, false, 'http://www.bing.com'); + runTest('http://www.bing.com', { autoLink: true }, 'http://www.bing.com'); }); it('do not return link', () => { - runTest('wwww.test.com', false, true, true, undefined); + runTest('wwww.test.com', { autoLink: false }, undefined); }); it('invalid link', () => { - runTest('www3w.test.com', true, false, false, undefined); + runTest('www3w.test.com', { autoLink: true }, undefined); }); it('telephone', () => { - runTest('tel:999999', false, true, false, 'tel:999999'); + runTest('tel:999999', { autoTel: true }, 'tel:999999'); }); it('telephone with T', () => { - runTest('Tel:999999', false, true, false, 'tel:999999'); + runTest('Tel:999999', { autoTel: true }, 'tel:999999'); }); it('do not return telephone', () => { - runTest('tel:999999', true, false, true, undefined); + runTest('tel:999999', { autoTel: false }, undefined); }); it('invalid telephone', () => { - runTest('tels:999999', false, true, false, undefined); + runTest('tels:999999', { autoTel: true }, undefined); }); it('mailto', () => { - runTest('mailto:test', false, false, true, 'mailto:test'); + runTest('mailto:test', { autoMailto: true }, 'mailto:test'); }); it('mailto with M', () => { - runTest('Mailto:test', false, false, true, 'mailto:test'); + runTest('Mailto:test', { autoMailto: true }, 'mailto:test'); }); it('do not return mailto', () => { - runTest('mailto:test', true, true, false, undefined); + runTest('mailto:test', { autoMailto: false }, undefined); }); it('invalid mailto', () => { - runTest('mailtos:test', false, false, true, undefined); + runTest('mailtos:test', { autoMailto: true }, undefined); }); }); From cd35c3f5fff25c1117db717be0c9e8711c8a2605 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Mon, 23 Sep 2024 12:51:23 -0300 Subject: [PATCH 29/43] fix build --- .../test/autoFormat/AutoFormatPluginTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index c33d6f1dbf9..e799ed4441b 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -307,7 +307,6 @@ describe('Content Model Auto Format Plugin Test', () => { paragraph: ContentModelParagraph, context: FormatContentModelContext ) => { - const { autoLink, autoMailto, autoTel } = options; const result = options && createLinkAfterSpace(segment, paragraph, context, options); From f2ffdabd8e14e53a2aeb1ad516013b72378d1812 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Mon, 23 Sep 2024 09:58:40 -0700 Subject: [PATCH 30/43] Fix #2699 (#2797) Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> --- .../lib/modelApi/common/clearModelFormat.ts | 14 +++- .../lib/publicApi/format/clearFormat.ts | 28 +++++--- .../modelApi/common/clearModelFormatTest.ts | 42 +++++++---- .../test/publicApi/format/clearFormatTest.ts | 72 +++++++++++++++++++ 4 files changed, 131 insertions(+), 25 deletions(-) diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts index 04d8cff9d3e..5e1a551f6d0 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts @@ -33,7 +33,9 @@ export function clearModelFormat( blocksToClear: [ReadonlyContentModelBlockGroup[], ShallowMutableContentModelBlock][], segmentsToClear: ShallowMutableContentModelSegment[], tablesToClear: [ContentModelTable, boolean][] -) { +): boolean { + let pendingStructureChange = false; + iterateSelections( model, (path, tableContext, block, segments) => { @@ -75,14 +77,14 @@ export function clearModelFormat( blocksToClear.length == 1 ) { segmentsToClear.splice(0, segmentsToClear.length, ...adjustWordSelection(model, marker)); - clearListFormat(blocksToClear[0][0]); + pendingStructureChange = clearListFormat(blocksToClear[0][0]) || pendingStructureChange; } else if (blocksToClear.length > 1 || blocksToClear.some(x => isWholeBlockSelected(x[1]))) { // 2. If a full block or multiple blocks are selected, clear block format for (let i = blocksToClear.length - 1; i >= 0; i--) { const [path, block] = blocksToClear[i]; clearBlockFormat(path, block); - clearListFormat(path); + pendingStructureChange = clearListFormat(path) || pendingStructureChange; clearContainerFormat(path, block); } } @@ -92,6 +94,8 @@ export function clearModelFormat( // 4. Clear format for table if any createTablesFormat(tablesToClear); + + return pendingStructureChange; } function createTablesFormat(tablesToClear: [ContentModelTable, boolean][]) { @@ -191,6 +195,10 @@ function clearListFormat(path: ReadonlyContentModelBlockGroup[]) { if (listItem) { mutateBlock(listItem).levels = []; + + return true; + } else { + return false; } } diff --git a/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts b/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts index 5e8fa8a0a1a..05998818f3b 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts @@ -8,6 +8,8 @@ import type { ContentModelTable, } from 'roosterjs-content-model-types'; +const MAX_TRY = 3; + /** * Clear format of selection * @param editor The editor to clear format from @@ -17,17 +19,27 @@ export function clearFormat(editor: IEditor) { editor.formatContentModel( model => { - const blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][] = []; - const segmentsToClear: ContentModelSegment[] = []; - const tablesToClear: [ContentModelTable, boolean][] = []; + let changed = false; + let needtoRun = true; + let triedTimes = 0; + + while (needtoRun && triedTimes++ < MAX_TRY) { + const blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][] = []; + const segmentsToClear: ContentModelSegment[] = []; + const tablesToClear: [ContentModelTable, boolean][] = []; + + needtoRun = clearModelFormat(model, blocksToClear, segmentsToClear, tablesToClear); - clearModelFormat(model, blocksToClear, segmentsToClear, tablesToClear); + normalizeContentModel(model); - normalizeContentModel(model); + changed = + changed || + blocksToClear.length > 0 || + segmentsToClear.length > 0 || + tablesToClear.length > 0; + } - return ( - blocksToClear.length > 0 || segmentsToClear.length > 0 || tablesToClear.length > 0 - ); + return changed; }, { apiName: 'clearFormat', diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts index 4eb36438c3e..932cb19fe05 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts @@ -102,12 +102,13 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, [], [], []); + const result = clearModelFormat(model, [], [], []); expect(model).toEqual({ blockGroupType: 'Document', blocks: [] }); expect(blocks).toEqual([]); expect(segments).toEqual([]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model without selection', () => { @@ -122,7 +123,7 @@ describe('clearModelFormat', () => { para.segments.push(text); model.blocks.push(para); - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -144,6 +145,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([]); expect(segments).toEqual([]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with text selection', () => { @@ -161,7 +163,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -188,6 +190,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text2]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with link', () => { @@ -211,7 +214,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -239,6 +242,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text1]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with code', () => { @@ -260,7 +264,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -282,6 +286,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text1]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with text selection in whole paragraph', () => { @@ -300,7 +305,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -328,6 +333,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text1, text2]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with collapsed selection', () => { @@ -344,7 +350,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -375,6 +381,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([marker]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with collapsed selection inside word', () => { @@ -392,7 +399,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -447,6 +454,7 @@ describe('clearModelFormat', () => { text3, ]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with collapsed selection under list', () => { @@ -463,7 +471,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -499,6 +507,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[list, model], para]]); expect(segments).toEqual([marker]); expect(tables).toEqual([]); + expect(result).toBeTrue(); }); it('Model with divider selection', () => { @@ -516,7 +525,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -540,6 +549,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], divider]]); expect(segments).toEqual([]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with selection under list', () => { @@ -558,7 +568,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -600,6 +610,7 @@ describe('clearModelFormat', () => { }, ]); expect(tables).toEqual([]); + expect(result).toBeTrue(); }); it('Model with selection under list, has defaultSegmentFormat', () => { @@ -620,7 +631,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -662,6 +673,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[list, model], para]]); expect(segments).toEqual([text]); expect(tables).toEqual([]); + expect(result).toBeTrue(); }); it('Model with selection under quote', () => { @@ -688,7 +700,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -774,6 +786,7 @@ describe('clearModelFormat', () => { ]); expect(segments).toEqual([text3, text4]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with selection under table', () => { @@ -796,7 +809,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -855,5 +868,6 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([]); expect(segments).toEqual([]); expect(tables).toEqual([[table, true]]); + expect(result).toBeFalse(); }); }); diff --git a/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts b/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts index 80958b94380..489b81fa813 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts @@ -33,4 +33,76 @@ describe('clearFormat', () => { expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1); expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(model); }); + + it('Clear format with list under quote', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + tagName: 'blockquote', + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'FormatContainer', + blocks: [ + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [{ listType: 'OL', format: {}, dataset: {} }], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + isSelected: true, + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ], + }, + ], + }; + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter, options: FormatContentModelOptions) => { + expect(options.apiName).toEqual('clearFormat'); + callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); + }); + const editor = ({ + focus: () => {}, + formatContentModel: formatContentModelSpy, + } as any) as IEditor; + + clearFormat(editor); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + isSelected: true, + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }); + }); }); From 5d08bab26062da40c63915d4c10705d497bb2d53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:33:09 -0700 Subject: [PATCH 31/43] Bump body-parser from 1.20.1 to 1.20.3 (#2802) Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.1 to 1.20.3. - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.1...1.20.3) --- updated-dependencies: - dependency-name: body-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jiuqing Song --- yarn.lock | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8c53e207369..b003e590332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1445,7 +1445,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -body-parser@1.20.3: +body-parser@1.20.3, body-parser@^1.19.0: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== @@ -1463,24 +1463,6 @@ body-parser@1.20.3: type-is "~1.6.18" unpipe "1.0.0" -body-parser@^1.19.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - bonjour@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" @@ -5587,13 +5569,6 @@ qjobs@^1.2.0: resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - qs@6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -5633,16 +5608,6 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" From 84957c8873347263cac39fb38f0dd0464dea986d Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 26 Sep 2024 13:30:31 -0300 Subject: [PATCH 32/43] protected --- .../lib/imageEdit/ImageEditPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index cea7b9d8b69..fa2761b460c 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -66,7 +66,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { private shadowSpan: HTMLSpanElement | null = null; private selectedImage: HTMLImageElement | null = null; protected wrapper: HTMLSpanElement | null = null; - private imageEditInfo: ImageMetadataFormat | null = null; + protected imageEditInfo: ImageMetadataFormat | null = null; private imageHTMLOptions: ImageHtmlOptions | null = null; private dndHelpers: DragAndDropHelper[] = []; private clonedImage: HTMLImageElement | null = null; From fd0098912394a5a1bbe7bfe36a998def37c311ed Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 26 Sep 2024 09:55:37 -0700 Subject: [PATCH 33/43] Let DOM Helper return a cloned root (#2805) --- .../lib/editor/core/DOMHelperImpl.ts | 7 +++++++ .../test/editor/core/DOMHelperImplTest.ts | 16 ++++++++++++++++ .../lib/parameter/DOMHelper.ts | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index a4b9a49474a..67eb876eae8 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -81,6 +81,13 @@ class DOMHelperImpl implements DOMHelper { const paddingRight = parseValueWithUnit(style?.paddingRight); return this.contentDiv.clientWidth - (paddingLeft + paddingRight); } + + /** + * Get a deep cloned root element + */ + getClonedRoot(): HTMLElement { + return this.contentDiv.cloneNode(true /*deep*/) as HTMLElement; + } } /** diff --git a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts index fb490373ed3..2481f57a73a 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts @@ -350,4 +350,20 @@ describe('DOMHelperImpl', () => { expect(domHelper.getClientWidth()).toBe(1000); }); }); + + describe('getClonedRoot', () => { + it('getClonedRoot', () => { + const mockedClone = 'CLONE' as any; + const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone); + const mockedDiv: HTMLElement = { + cloneNode: cloneSpy, + } as any; + const domHelper = createDOMHelper(mockedDiv); + + const result = domHelper.getClonedRoot(); + + expect(result).toBe(mockedClone); + expect(cloneSpy).toHaveBeenCalledWith(true); + }); + }); }); diff --git a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts index 91bd2d976ad..27169dd2681 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts @@ -92,4 +92,9 @@ export interface DOMHelper { * Get the width of the editable area of the editor content div */ getClientWidth(): number; + + /** + * Get a deep cloned root element + */ + getClonedRoot(): HTMLElement; } From 9d3deb2d1c9c2725491c21d4c02aa68f6cf913b8 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 26 Sep 2024 13:16:46 -0600 Subject: [PATCH 34/43] Fix mergeTable when pasting #2810 --- .../lib/modelApi/editing/mergeModel.ts | 14 +- .../test/modelApi/editing/mergeModelTest.ts | 182 ++++++++++++++++++ 2 files changed, 190 insertions(+), 6 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index 3ea33badf72..08497fea2a2 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -50,20 +50,22 @@ export function mergeModel( const insertPosition = options?.insertPosition ?? deleteSelection(target, [], context).insertPoint; - if (options?.addParagraphAfterMergedContent) { + const { addParagraphAfterMergedContent, mergeFormat, mergeTable } = options || {}; + + if (addParagraphAfterMergedContent && !mergeTable) { const { paragraph, marker } = insertPosition || {}; const newPara = createParagraph(false /* isImplicit */, paragraph?.format, marker?.format); addBlock(source, newPara); } if (insertPosition) { - if (options?.mergeFormat && options.mergeFormat != 'none') { + if (mergeFormat && mergeFormat != 'none') { const newFormat: ContentModelSegmentFormat = { ...(target.format || {}), ...insertPosition.marker.format, }; - applyDefaultFormat(source, newFormat, options?.mergeFormat); + applyDefaultFormat(source, newFormat, mergeFormat); } for (let i = 0; i < source.blocks.length; i++) { @@ -84,8 +86,8 @@ export function mergeModel( break; case 'Table': - if (source.blocks.length == 1 && options?.mergeTable) { - mergeTable(insertPosition, block, source); + if (source.blocks.length == 1 && mergeTable) { + mergeTables(insertPosition, block, source); } else { insertBlock(insertPosition, block); } @@ -176,7 +178,7 @@ function mergeParagraph( } } -function mergeTable( +function mergeTables( markerPosition: InsertPoint, newTable: ContentModelTable, source: ContentModelDocument diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index f6e6b58e40b..6d55c59b8d7 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -1507,6 +1507,157 @@ describe('mergeModel', () => { }); }); + it('table to table, merge table 4, mergeTable and addParagraphAfterMergedContent should be noop', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(false, false, false, { backgroundColor: '12' }); + const cell21 = createTableCell(false, false, false, { backgroundColor: '21' }); + const cell22 = createTableCell(false, false, false, { backgroundColor: '22' }); + const cell31 = createTableCell(false, false, false, { backgroundColor: '31' }); + const cell32 = createTableCell(false, false, false, { backgroundColor: '32' }); + const table1 = createTable(4); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.rows = [ + { format: {}, height: 0, cells: [cell01, cell02] }, + { format: {}, height: 0, cells: [cell11, cell12] }, + { format: {}, height: 0, cells: [cell21, cell22] }, + { format: {}, height: 0, cells: [cell31, cell32] }, + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell12.blocks.push(newPara1); + newTable1.rows = [ + { format: {}, height: 0, cells: [newCell11, newCell12] }, + { format: {}, height: 0, cells: [newCell21, newCell22] }, + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + const result = mergeModel( + majorModel, + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeTable: true, + addParagraphAfterMergedContent: true, + } + ); + + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: true, + }; + const tableCell: ContentModelTableCell = { + blockGroupType: 'TableCell', + blocks: [paragraph], + format: { + backgroundColor: 'n11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }; + const table: ContentModelTable = { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + cell01, + cell02, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + backgroundColor: '02', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + format: {}, + height: 0, + cells: [cell11, tableCell, newCell12], + }, + { format: {}, height: 0, cells: [cell21, newCell21, newCell22] }, + { + format: {}, + height: 0, + cells: [ + cell31, + cell32, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + backgroundColor: '32', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }; + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(majorModel).toEqual({ + blockGroupType: 'Document', + blocks: [table], + }); + expect(result).toEqual({ + marker, + paragraph, + path: [tableCell, majorModel], + tableContext: { + table, + rowIndex: 1, + colIndex: 1, + isWholeTableSelected: false, + }, + }); + }); + it('Use customized insert position', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); @@ -3870,4 +4021,35 @@ describe('mergeModel', () => { ], }); }); + + it('Merge model with addParagraphAfterMergedContent and mergeTable, addParagraphAfterMergedContent should be noop', () => { + const source = createContentModelDocument(); + const para = createParagraph(); + para.segments.push(createText('Merge')); + source.blocks.push(para); + + const target = createContentModelDocument(); + const paraTarget = createParagraph(); + paraTarget.segments.push(createSelectionMarker()); + target.blocks.push(paraTarget); + + mergeModel(target, source, undefined, { + addParagraphAfterMergedContent: true, + mergeTable: true, + }); + + expect(target).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'Merge', format: {} }, + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + ], + format: {}, + }, + ], + }); + }); }); From 33531e19ea64cec3810573b649a595db0f42bfef Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 26 Sep 2024 12:57:12 -0700 Subject: [PATCH 35/43] Fix #2807 (#2809) Co-authored-by: Bryan Valverde U --- .../lib/domToModel/processors/brProcessor.ts | 11 +++ .../domToModel/processors/brProcessorTest.ts | 70 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts b/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts index f5677434c77..eb060bdd3b9 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts @@ -1,5 +1,6 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { createBr } from '../../modelApi/creators/createBr'; +import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets'; import type { ElementProcessor } from 'roosterjs-content-model-types'; /** @@ -7,11 +8,21 @@ import type { ElementProcessor } from 'roosterjs-content-model-types'; */ export const brProcessor: ElementProcessor = (group, element, context) => { const br = createBr(context.segmentFormat); + const [start, end] = getRegularSelectionOffsets(context, element); + + if (start >= 0) { + context.isInSelection = true; + } if (context.isInSelection) { br.isSelected = true; } const paragraph = addSegment(group, br, context.blockFormat); + + if (end >= 0) { + context.isInSelection = false; + } + context.domIndexer?.onSegment(element, paragraph, [br]); }; diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts index f92da141de7..9ba29b9601b 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts @@ -100,4 +100,74 @@ describe('brProcessor', () => { }); expect(onSegmentSpy).toHaveBeenCalledWith(br, paragraphModel, [brModel]); }); + + it('Selection starts in BR', () => { + const doc = createContentModelDocument(); + const div = document.createElement('div'); + const br = document.createElement('br'); + const range = document.createRange(); + + div.appendChild(br); + range.setStart(br, 0); + range.setEnd(div, 1); + context.selection = { + type: 'range', + range: range, + isReverted: false, + }; + + brProcessor(doc, br, context); + + const brModel: ContentModelBr = { + segmentType: 'Br', + format: {}, + isSelected: true, + }; + const paragraphModel: ContentModelParagraph = { + blockType: 'Paragraph', + isImplicit: true, + segments: [brModel], + format: {}, + }; + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [paragraphModel], + }); + expect(context.isInSelection).toBeTrue(); + }); + + it('Selection ends in BR', () => { + const doc = createContentModelDocument(); + const br = document.createElement('br'); + const range = document.createRange(); + + range.setEnd(br, 0); + context.selection = { + type: 'range', + range: range, + isReverted: false, + }; + context.isInSelection = true; + + brProcessor(doc, br, context); + + const brModel: ContentModelBr = { + segmentType: 'Br', + format: {}, + isSelected: true, + }; + const paragraphModel: ContentModelParagraph = { + blockType: 'Paragraph', + isImplicit: true, + segments: [brModel], + format: {}, + }; + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [paragraphModel], + }); + expect(context.isInSelection).toBeFalse(); + }); }); From f74c18f729b97794cd6379c91b1b731bbf6deb69 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 26 Sep 2024 17:34:36 -0300 Subject: [PATCH 36/43] caret color --- .../lib/imageEdit/ImageEditPlugin.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index cea7b9d8b69..3aacd5888c5 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -53,6 +53,8 @@ const DefaultOptions: Partial = { const MouseRightButton = 2; const DRAG_ID = '_dragging'; +const IMAGE_EDIT_CLASS = 'imageEdit'; +const IMAGE_EDIT_CLASS_CARET = 'imageEditCaretColor'; /** * ImageEdit plugin handles the following image editing features: @@ -384,9 +386,13 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.croppers = croppers; this.zoomScale = editor.getDOMHelper().calculateZoomScale(); - editor.setEditorStyle('imageEdit', `outline-style:none!important;`, [ - `span:has(>img${getSafeIdSelector(this.selectedImage.id)})`, - ]); + editor.setEditorStyle( + IMAGE_EDIT_CLASS, + `outline-style:none!important;caret-color: transparent;`, + [`span:has(>img${getSafeIdSelector(this.selectedImage.id)})`] + ); + + editor.setEditorStyle(IMAGE_EDIT_CLASS_CARET, `caret-color: transparent;`); } public startRotateAndResize(editor: IEditor, image: HTMLImageElement) { @@ -607,7 +613,8 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } private cleanInfo() { - this.editor?.setEditorStyle('imageEdit', null); + this.editor?.setEditorStyle(IMAGE_EDIT_CLASS, null); + this.editor?.setEditorStyle(IMAGE_EDIT_CLASS_CARET, null); this.selectedImage = null; this.shadowSpan = null; this.wrapper = null; From 33def51077a31f40666daf5dc2d0d183053adab4 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Thu, 26 Sep 2024 17:55:00 -0300 Subject: [PATCH 37/43] caret color --- .../lib/imageEdit/ImageEditPlugin.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 3aacd5888c5..2474d4f2193 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -386,11 +386,9 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.croppers = croppers; this.zoomScale = editor.getDOMHelper().calculateZoomScale(); - editor.setEditorStyle( - IMAGE_EDIT_CLASS, - `outline-style:none!important;caret-color: transparent;`, - [`span:has(>img${getSafeIdSelector(this.selectedImage.id)})`] - ); + editor.setEditorStyle(IMAGE_EDIT_CLASS, `outline-style:none!important;`, [ + `span:has(>img${getSafeIdSelector(this.selectedImage.id)})`, + ]); editor.setEditorStyle(IMAGE_EDIT_CLASS_CARET, `caret-color: transparent;`); } From a20c7bc1d2c1339ce6471010c2745624ea53618c Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Thu, 26 Sep 2024 21:50:16 -0600 Subject: [PATCH 38/43] bump --- .../demoButtons/setBulletedListStyleButton.ts | 1 - .../editorOptions/EditorOptionsPlugin.ts | 2 + .../sidePane/editorOptions/Plugins.tsx | 14 + package.json | 2 +- .../lib/modelApi/block/setModelIndentation.ts | 13 +- .../lib/modelApi/common/clearModelFormat.ts | 14 +- .../lib/publicApi/format/clearFormat.ts | 28 +- .../modelApi/block/setModelIndentationTest.ts | 299 ++++++++++- .../modelApi/common/clearModelFormatTest.ts | 42 +- .../test/publicApi/format/clearFormatTest.ts | 72 +++ .../corePlugin/copyPaste/CopyPastePlugin.ts | 6 +- .../utils/adjustImageSelectionOnSafari.ts | 18 + .../copyPaste/{ => utils}/deleteEmptyList.ts | 0 .../lib/editor/core/DOMHelperImpl.ts | 7 + .../utils/adjustImageSelectionOnSafariTest.ts | 70 +++ .../{ => utils}/deleteEmptyListTest.ts | 2 +- .../test/editor/core/DOMHelperImplTest.ts | 16 + .../lib/domToModel/processors/brProcessor.ts | 11 + .../lib/modelApi/editing/mergeModel.ts | 14 +- .../domToModel/processors/brProcessorTest.ts | 70 +++ .../test/modelApi/editing/mergeModelTest.ts | 182 +++++++ .../lib/autoFormat/AutoFormatPlugin.ts | 68 +-- .../autoFormat/interface/AutoFormatOptions.ts | 31 ++ .../autoFormat/interface/AutoLinkOptions.ts | 24 + .../lib/autoFormat/link/createLink.ts | 14 +- .../autoFormat/link/createLinkAfterSpace.ts | 14 +- .../lib/autoFormat/link/getLinkUrl.ts | 26 + .../lib/edit/keyboardEnter.ts | 8 +- .../lib/edit/keyboardInput.ts | 8 +- .../lib/edit/keyboardTab.ts | 7 +- .../lib/index.ts | 4 +- .../test/autoFormat/AutoFormatPluginTest.ts | 39 +- .../link/createLinkAfterSpaceTest.ts | 484 +++++++++++++++++- .../test/autoFormat/link/createLinkTest.ts | 478 ++++++++++++++++- .../test/autoFormat/link/getLinkUrlTest.ts | 53 ++ .../lib/parameter/DOMHelper.ts | 5 + yarn.lock | 218 +++++--- 37 files changed, 2171 insertions(+), 193 deletions(-) create mode 100644 packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts rename packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/{ => utils}/deleteEmptyList.ts (100%) create mode 100644 packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts rename packages/roosterjs-content-model-core/test/corePlugin/copyPaste/{ => utils}/deleteEmptyListTest.ts (99%) create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts create mode 100644 packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts create mode 100644 packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts diff --git a/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts b/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts index ab585097120..97617c0c5c4 100644 --- a/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts +++ b/demo/scripts/controlsV2/demoButtons/setBulletedListStyleButton.ts @@ -10,7 +10,6 @@ const dropDownMenuItems = { [BulletListType.LongArrow]: 'LongArrow', [BulletListType.UnfilledArrow]: 'UnfilledArrow', [BulletListType.Hyphen]: 'Hyphen', - [BulletListType.DoubleLongArrow]: 'DoubleLongArrow', [BulletListType.Circle]: 'Circle', }; diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index a6d4c5af32a..6e216f2da1a 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -45,6 +45,8 @@ const initialState: OptionState = { autoHyphen: true, autoFraction: true, autoOrdinals: true, + autoMailto: true, + autoTel: true, }, markdownOptions: { bold: true, diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx index 86ebfe53fdf..d1ba83fee58 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/Plugins.tsx @@ -110,6 +110,8 @@ export class Plugins extends PluginsBase { private autoHyphen = React.createRef(); private autoFraction = React.createRef(); private autoOrdinals = React.createRef(); + private autoTel = React.createRef(); + private autoMailto = React.createRef(); private markdownBold = React.createRef(); private markdownItalic = React.createRef(); private markdownStrikethrough = React.createRef(); @@ -166,6 +168,18 @@ export class Plugins extends PluginsBase { this.props.state.autoFormatOptions.autoOrdinals, (state, value) => (state.autoFormatOptions.autoOrdinals = value) )} + {this.renderCheckBox( + 'Telephone', + this.autoTel, + this.props.state.autoFormatOptions.autoTel, + (state, value) => (state.autoFormatOptions.autoTel = value) + )} + {this.renderCheckBox( + 'Email', + this.autoMailto, + this.props.state.autoFormatOptions.autoMailto, + (state, value) => (state.autoFormatOptions.autoMailto = value) + )} )} {this.renderPluginItem( diff --git a/package.json b/package.json index ab3e23aed45..9dab173dd0d 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "coverage-istanbul-loader": "3.0.5", "css-loader": "3.5.3", "detect-port": "^1.3.0", - "dompurify": "2.3.0", + "dompurify": "2.5.4", "eslint": "^8.50.0", "eslint-plugin-etc": "^2.0.3", "eslint-plugin-react": "^7.33.2", diff --git a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts index 217a0d5f641..6a1626f3c07 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/block/setModelIndentation.ts @@ -66,10 +66,19 @@ export function setModelIndentation( //if block has only one level, there is not need to check if it is multilevel selection } else if (block.levels.length == 1 || !isMultilevelSelection(model, block, parent)) { if (isIndent) { - const lastLevel = block.levels[block.levels.length - 1]; + const threadIdx = thread.indexOf(block); + const previousItem = thread[threadIdx - 1]; + const nextItem = thread[threadIdx + 1]; + const levelLength = block.levels.length; + const lastLevel = block.levels[levelLength - 1]; const newLevel: ContentModelListLevel = createListLevel( lastLevel?.listType || 'UL', - lastLevel?.format + lastLevel?.format, + previousItem && previousItem.levels.length > levelLength + ? previousItem.levels[levelLength].dataset + : nextItem && nextItem.levels.length > levelLength + ? nextItem.levels[levelLength].dataset + : undefined ); updateListMetadata(newLevel, metadata => { diff --git a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts index 04d8cff9d3e..5e1a551f6d0 100644 --- a/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts +++ b/packages/roosterjs-content-model-api/lib/modelApi/common/clearModelFormat.ts @@ -33,7 +33,9 @@ export function clearModelFormat( blocksToClear: [ReadonlyContentModelBlockGroup[], ShallowMutableContentModelBlock][], segmentsToClear: ShallowMutableContentModelSegment[], tablesToClear: [ContentModelTable, boolean][] -) { +): boolean { + let pendingStructureChange = false; + iterateSelections( model, (path, tableContext, block, segments) => { @@ -75,14 +77,14 @@ export function clearModelFormat( blocksToClear.length == 1 ) { segmentsToClear.splice(0, segmentsToClear.length, ...adjustWordSelection(model, marker)); - clearListFormat(blocksToClear[0][0]); + pendingStructureChange = clearListFormat(blocksToClear[0][0]) || pendingStructureChange; } else if (blocksToClear.length > 1 || blocksToClear.some(x => isWholeBlockSelected(x[1]))) { // 2. If a full block or multiple blocks are selected, clear block format for (let i = blocksToClear.length - 1; i >= 0; i--) { const [path, block] = blocksToClear[i]; clearBlockFormat(path, block); - clearListFormat(path); + pendingStructureChange = clearListFormat(path) || pendingStructureChange; clearContainerFormat(path, block); } } @@ -92,6 +94,8 @@ export function clearModelFormat( // 4. Clear format for table if any createTablesFormat(tablesToClear); + + return pendingStructureChange; } function createTablesFormat(tablesToClear: [ContentModelTable, boolean][]) { @@ -191,6 +195,10 @@ function clearListFormat(path: ReadonlyContentModelBlockGroup[]) { if (listItem) { mutateBlock(listItem).levels = []; + + return true; + } else { + return false; } } diff --git a/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts b/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts index 5e8fa8a0a1a..05998818f3b 100644 --- a/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts +++ b/packages/roosterjs-content-model-api/lib/publicApi/format/clearFormat.ts @@ -8,6 +8,8 @@ import type { ContentModelTable, } from 'roosterjs-content-model-types'; +const MAX_TRY = 3; + /** * Clear format of selection * @param editor The editor to clear format from @@ -17,17 +19,27 @@ export function clearFormat(editor: IEditor) { editor.formatContentModel( model => { - const blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][] = []; - const segmentsToClear: ContentModelSegment[] = []; - const tablesToClear: [ContentModelTable, boolean][] = []; + let changed = false; + let needtoRun = true; + let triedTimes = 0; + + while (needtoRun && triedTimes++ < MAX_TRY) { + const blocksToClear: [ContentModelBlockGroup[], ContentModelBlock][] = []; + const segmentsToClear: ContentModelSegment[] = []; + const tablesToClear: [ContentModelTable, boolean][] = []; + + needtoRun = clearModelFormat(model, blocksToClear, segmentsToClear, tablesToClear); - clearModelFormat(model, blocksToClear, segmentsToClear, tablesToClear); + normalizeContentModel(model); - normalizeContentModel(model); + changed = + changed || + blocksToClear.length > 0 || + segmentsToClear.length > 0 || + tablesToClear.length > 0; + } - return ( - blocksToClear.length > 0 || segmentsToClear.length > 0 || tablesToClear.length > 0 - ); + return changed; }, { apiName: 'clearFormat', diff --git a/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts b/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts index 765ca3526e6..6bf1e233806 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/block/setModelIndentationTest.ts @@ -1,5 +1,5 @@ import * as getListAnnounceData from '../../../lib/modelApi/list/getListAnnounceData'; -import { FormatContentModelContext } from 'roosterjs-content-model-types'; +import { ContentModelDocument, FormatContentModelContext } from 'roosterjs-content-model-types'; import { setModelIndentation } from '../../../lib/modelApi/block/setModelIndentation'; import { createContentModelDocument, @@ -899,6 +899,303 @@ describe('indent', () => { }); expect(getListAnnounceDataSpy).not.toHaveBeenCalled(); }); + + it('Indent and follow previous item style', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + ], + }; + + setModelIndentation(model, 'indent'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { + format: {}, + dataset: { a: 'c', editingInfo: '{"applyListStyleFromLevel":true}' }, + listType: 'OL', + }, + ], + }, + ], + }); + }); + + it('Indent and follow next item style', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + ], + }; + + setModelIndentation(model, 'indent'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { + format: {}, + dataset: { a: 'c', editingInfo: '{"applyListStyleFromLevel":true}' }, + listType: 'OL', + }, + ], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { format: {}, dataset: { a: 'c' }, listType: 'OL' }, + ], + }, + ], + }); + }); + + it('Indent, no style to follow', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + ], + }; + + setModelIndentation(model, 'indent'); + + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [{ format: {}, dataset: { a: 'b' }, listType: 'OL' }], + }, + { + blockType: 'BlockGroup', + blockGroupType: 'ListItem', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { segmentType: 'SelectionMarker', format: {}, isSelected: true }, + ], + }, + ], + format: {}, + formatHolder: { + segmentType: 'SelectionMarker', + isSelected: false, + format: {}, + }, + levels: [ + { format: {}, dataset: { a: 'b' }, listType: 'OL' }, + { + format: {}, + dataset: { editingInfo: '{"applyListStyleFromLevel":true}' }, + listType: 'OL', + }, + ], + }, + ], + }); + }); }); describe('outdent', () => { diff --git a/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts b/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts index 4eb36438c3e..932cb19fe05 100644 --- a/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts +++ b/packages/roosterjs-content-model-api/test/modelApi/common/clearModelFormatTest.ts @@ -102,12 +102,13 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, [], [], []); + const result = clearModelFormat(model, [], [], []); expect(model).toEqual({ blockGroupType: 'Document', blocks: [] }); expect(blocks).toEqual([]); expect(segments).toEqual([]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model without selection', () => { @@ -122,7 +123,7 @@ describe('clearModelFormat', () => { para.segments.push(text); model.blocks.push(para); - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -144,6 +145,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([]); expect(segments).toEqual([]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with text selection', () => { @@ -161,7 +163,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -188,6 +190,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text2]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with link', () => { @@ -211,7 +214,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -239,6 +242,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text1]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with code', () => { @@ -260,7 +264,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -282,6 +286,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text1]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with text selection in whole paragraph', () => { @@ -300,7 +305,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -328,6 +333,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([text1, text2]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with collapsed selection', () => { @@ -344,7 +350,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -375,6 +381,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], para]]); expect(segments).toEqual([marker]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with collapsed selection inside word', () => { @@ -392,7 +399,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -447,6 +454,7 @@ describe('clearModelFormat', () => { text3, ]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with collapsed selection under list', () => { @@ -463,7 +471,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -499,6 +507,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[list, model], para]]); expect(segments).toEqual([marker]); expect(tables).toEqual([]); + expect(result).toBeTrue(); }); it('Model with divider selection', () => { @@ -516,7 +525,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -540,6 +549,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[model], divider]]); expect(segments).toEqual([]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with selection under list', () => { @@ -558,7 +568,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -600,6 +610,7 @@ describe('clearModelFormat', () => { }, ]); expect(tables).toEqual([]); + expect(result).toBeTrue(); }); it('Model with selection under list, has defaultSegmentFormat', () => { @@ -620,7 +631,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -662,6 +673,7 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([[[list, model], para]]); expect(segments).toEqual([text]); expect(tables).toEqual([]); + expect(result).toBeTrue(); }); it('Model with selection under quote', () => { @@ -688,7 +700,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -774,6 +786,7 @@ describe('clearModelFormat', () => { ]); expect(segments).toEqual([text3, text4]); expect(tables).toEqual([]); + expect(result).toBeFalse(); }); it('Model with selection under table', () => { @@ -796,7 +809,7 @@ describe('clearModelFormat', () => { const segments: any[] = []; const tables: any[] = []; - clearModelFormat(model, blocks, segments, tables); + const result = clearModelFormat(model, blocks, segments, tables); expect(model).toEqual({ blockGroupType: 'Document', @@ -855,5 +868,6 @@ describe('clearModelFormat', () => { expect(blocks).toEqual([]); expect(segments).toEqual([]); expect(tables).toEqual([[table, true]]); + expect(result).toBeFalse(); }); }); diff --git a/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts b/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts index 80958b94380..489b81fa813 100644 --- a/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts +++ b/packages/roosterjs-content-model-api/test/publicApi/format/clearFormatTest.ts @@ -33,4 +33,76 @@ describe('clearFormat', () => { expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledTimes(1); expect(normalizeContentModel.normalizeContentModel).toHaveBeenCalledWith(model); }); + + it('Clear format with list under quote', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + tagName: 'blockquote', + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'FormatContainer', + blocks: [ + { + formatHolder: { + isSelected: false, + segmentType: 'SelectionMarker', + format: {}, + }, + levels: [{ listType: 'OL', format: {}, dataset: {} }], + blockType: 'BlockGroup', + format: {}, + blockGroupType: 'ListItem', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + isSelected: true, + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }, + ], + }, + ], + }; + const formatContentModelSpy = jasmine + .createSpy('formatContentModel') + .and.callFake((callback: ContentModelFormatter, options: FormatContentModelOptions) => { + expect(options.apiName).toEqual('clearFormat'); + callback(model, { newEntities: [], deletedEntities: [], newImages: [] }); + }); + const editor = ({ + focus: () => {}, + formatContentModel: formatContentModelSpy, + } as any) as IEditor; + + clearFormat(editor); + + expect(formatContentModelSpy).toHaveBeenCalledTimes(1); + expect(model).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + text: 'test', + segmentType: 'Text', + isSelected: true, + format: {}, + }, + ], + blockType: 'Paragraph', + format: {}, + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts index 79965ccdcb2..b0fd0c791d9 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/CopyPastePlugin.ts @@ -1,8 +1,8 @@ import { addRangeToSelection } from '../../coreApi/setDOMSelection/addRangeToSelection'; -import { deleteEmptyList } from './deleteEmptyList'; +import { adjustImageSelectionOnSafari } from './utils/adjustImageSelectionOnSafari'; +import { deleteEmptyList } from './utils/deleteEmptyList'; import { onCreateCopyEntityNode } from '../../override/pasteCopyBlockEntityParser'; import { paste } from '../../command/paste/paste'; - import { ChangeSource, contentModelToDom, @@ -110,6 +110,8 @@ class CopyPastePlugin implements PluginWithState { const doc = this.editor.getDocument(); const selection = this.editor.getDOMSelection(); + adjustImageSelectionOnSafari(this.editor, selection); + if (selection && (selection.type != 'range' || !selection.range.collapsed)) { const pasteModel = this.editor.getContentModelCopy('disconnected'); diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts new file mode 100644 index 00000000000..e0c37d9e3da --- /dev/null +++ b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari.ts @@ -0,0 +1,18 @@ +import type { IEditor, DOMSelection } from 'roosterjs-content-model-types'; + +/** + * @internal + * Adjust Image selection, so the copy by keyboard does not remove image selection. + */ +export function adjustImageSelectionOnSafari(editor: IEditor, selection: DOMSelection | null) { + if (editor.getEnvironment().isSafari && selection?.type == 'image') { + const range = new Range(); + range.setStartBefore(selection.image); + range.setEndAfter(selection.image); + editor.setDOMSelection({ + range, + type: 'range', + isReverted: false, + }); + } +} diff --git a/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts b/packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/deleteEmptyList.ts similarity index 100% rename from packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/deleteEmptyList.ts rename to packages/roosterjs-content-model-core/lib/corePlugin/copyPaste/utils/deleteEmptyList.ts diff --git a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts index a4b9a49474a..67eb876eae8 100644 --- a/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts +++ b/packages/roosterjs-content-model-core/lib/editor/core/DOMHelperImpl.ts @@ -81,6 +81,13 @@ class DOMHelperImpl implements DOMHelper { const paddingRight = parseValueWithUnit(style?.paddingRight); return this.contentDiv.clientWidth - (paddingLeft + paddingRight); } + + /** + * Get a deep cloned root element + */ + getClonedRoot(): HTMLElement { + return this.contentDiv.cloneNode(true /*deep*/) as HTMLElement; + } } /** diff --git a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts new file mode 100644 index 00000000000..6089bc4d604 --- /dev/null +++ b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/adjustImageSelectionOnSafariTest.ts @@ -0,0 +1,70 @@ +import { adjustImageSelectionOnSafari } from '../../../../lib/corePlugin/copyPaste/utils/adjustImageSelectionOnSafari'; +import type { DOMSelection, IEditor } from 'roosterjs-content-model-types'; + +describe('adjustImageSelectionOnSafari', () => { + let getEnvironmentSpy: jasmine.Spy; + let setDOMSelectionSpy: jasmine.Spy; + let editor: IEditor; + + beforeEach(() => { + getEnvironmentSpy = jasmine.createSpy('getEnvironment'); + setDOMSelectionSpy = jasmine.createSpy('setDOMSelection'); + editor = ({ + getEnvironment: getEnvironmentSpy, + setDOMSelection: setDOMSelectionSpy, + } as any) as IEditor; + }); + + it('should adjustSelection', () => { + getEnvironmentSpy.and.returnValue({ + isSafari: true, + }); + const image = document.createElement('img'); + document.body.appendChild(image); + const selection: DOMSelection = { + type: 'image', + image: image, + }; + const range = new Range(); + range.setStartBefore(image); + range.setEndAfter(image); + + adjustImageSelectionOnSafari(editor, selection); + expect(setDOMSelectionSpy).toHaveBeenCalledTimes(1); + expect(setDOMSelectionSpy).toHaveBeenCalledWith({ + range: range, + type: 'range', + isReverted: false, + }); + + document.body.removeChild(image); + }); + + it('should not adjustSelection - it is not safari', () => { + getEnvironmentSpy.and.returnValue({ + isSafari: false, + }); + const image = new Image(); + const selection: DOMSelection = { + type: 'image', + image: image, + }; + + adjustImageSelectionOnSafari(editor, selection); + expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + }); + + it('should not adjustSelection - it is not image', () => { + getEnvironmentSpy.and.returnValue({ + isSafari: true, + }); + const selection: DOMSelection = { + type: 'range', + range: new Range(), + isReverted: false, + }; + + adjustImageSelectionOnSafari(editor, selection); + expect(setDOMSelectionSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/deleteEmptyListTest.ts b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/deleteEmptyListTest.ts similarity index 99% rename from packages/roosterjs-content-model-core/test/corePlugin/copyPaste/deleteEmptyListTest.ts rename to packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/deleteEmptyListTest.ts index 5c13f319b09..51d3e7c7355 100644 --- a/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/deleteEmptyListTest.ts +++ b/packages/roosterjs-content-model-core/test/corePlugin/copyPaste/utils/deleteEmptyListTest.ts @@ -1,5 +1,5 @@ import { ContentModelBlockGroup, ContentModelSelectionMarker } from 'roosterjs-content-model-types'; -import { deleteEmptyList } from '../../../lib/corePlugin/copyPaste/deleteEmptyList'; +import { deleteEmptyList } from '../../../../lib/corePlugin/copyPaste/utils/deleteEmptyList'; import { deleteSelection } from 'roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection'; import { createContentModelDocument, diff --git a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts index fb490373ed3..2481f57a73a 100644 --- a/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts +++ b/packages/roosterjs-content-model-core/test/editor/core/DOMHelperImplTest.ts @@ -350,4 +350,20 @@ describe('DOMHelperImpl', () => { expect(domHelper.getClientWidth()).toBe(1000); }); }); + + describe('getClonedRoot', () => { + it('getClonedRoot', () => { + const mockedClone = 'CLONE' as any; + const cloneSpy = jasmine.createSpy('cloneSpy').and.returnValue(mockedClone); + const mockedDiv: HTMLElement = { + cloneNode: cloneSpy, + } as any; + const domHelper = createDOMHelper(mockedDiv); + + const result = domHelper.getClonedRoot(); + + expect(result).toBe(mockedClone); + expect(cloneSpy).toHaveBeenCalledWith(true); + }); + }); }); diff --git a/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts b/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts index f5677434c77..eb060bdd3b9 100644 --- a/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts +++ b/packages/roosterjs-content-model-dom/lib/domToModel/processors/brProcessor.ts @@ -1,5 +1,6 @@ import { addSegment } from '../../modelApi/common/addSegment'; import { createBr } from '../../modelApi/creators/createBr'; +import { getRegularSelectionOffsets } from '../utils/getRegularSelectionOffsets'; import type { ElementProcessor } from 'roosterjs-content-model-types'; /** @@ -7,11 +8,21 @@ import type { ElementProcessor } from 'roosterjs-content-model-types'; */ export const brProcessor: ElementProcessor = (group, element, context) => { const br = createBr(context.segmentFormat); + const [start, end] = getRegularSelectionOffsets(context, element); + + if (start >= 0) { + context.isInSelection = true; + } if (context.isInSelection) { br.isSelected = true; } const paragraph = addSegment(group, br, context.blockFormat); + + if (end >= 0) { + context.isInSelection = false; + } + context.domIndexer?.onSegment(element, paragraph, [br]); }; diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index 3ea33badf72..08497fea2a2 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -50,20 +50,22 @@ export function mergeModel( const insertPosition = options?.insertPosition ?? deleteSelection(target, [], context).insertPoint; - if (options?.addParagraphAfterMergedContent) { + const { addParagraphAfterMergedContent, mergeFormat, mergeTable } = options || {}; + + if (addParagraphAfterMergedContent && !mergeTable) { const { paragraph, marker } = insertPosition || {}; const newPara = createParagraph(false /* isImplicit */, paragraph?.format, marker?.format); addBlock(source, newPara); } if (insertPosition) { - if (options?.mergeFormat && options.mergeFormat != 'none') { + if (mergeFormat && mergeFormat != 'none') { const newFormat: ContentModelSegmentFormat = { ...(target.format || {}), ...insertPosition.marker.format, }; - applyDefaultFormat(source, newFormat, options?.mergeFormat); + applyDefaultFormat(source, newFormat, mergeFormat); } for (let i = 0; i < source.blocks.length; i++) { @@ -84,8 +86,8 @@ export function mergeModel( break; case 'Table': - if (source.blocks.length == 1 && options?.mergeTable) { - mergeTable(insertPosition, block, source); + if (source.blocks.length == 1 && mergeTable) { + mergeTables(insertPosition, block, source); } else { insertBlock(insertPosition, block); } @@ -176,7 +178,7 @@ function mergeParagraph( } } -function mergeTable( +function mergeTables( markerPosition: InsertPoint, newTable: ContentModelTable, source: ContentModelDocument diff --git a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts index f92da141de7..9ba29b9601b 100644 --- a/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts +++ b/packages/roosterjs-content-model-dom/test/domToModel/processors/brProcessorTest.ts @@ -100,4 +100,74 @@ describe('brProcessor', () => { }); expect(onSegmentSpy).toHaveBeenCalledWith(br, paragraphModel, [brModel]); }); + + it('Selection starts in BR', () => { + const doc = createContentModelDocument(); + const div = document.createElement('div'); + const br = document.createElement('br'); + const range = document.createRange(); + + div.appendChild(br); + range.setStart(br, 0); + range.setEnd(div, 1); + context.selection = { + type: 'range', + range: range, + isReverted: false, + }; + + brProcessor(doc, br, context); + + const brModel: ContentModelBr = { + segmentType: 'Br', + format: {}, + isSelected: true, + }; + const paragraphModel: ContentModelParagraph = { + blockType: 'Paragraph', + isImplicit: true, + segments: [brModel], + format: {}, + }; + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [paragraphModel], + }); + expect(context.isInSelection).toBeTrue(); + }); + + it('Selection ends in BR', () => { + const doc = createContentModelDocument(); + const br = document.createElement('br'); + const range = document.createRange(); + + range.setEnd(br, 0); + context.selection = { + type: 'range', + range: range, + isReverted: false, + }; + context.isInSelection = true; + + brProcessor(doc, br, context); + + const brModel: ContentModelBr = { + segmentType: 'Br', + format: {}, + isSelected: true, + }; + const paragraphModel: ContentModelParagraph = { + blockType: 'Paragraph', + isImplicit: true, + segments: [brModel], + format: {}, + }; + + expect(doc).toEqual({ + blockGroupType: 'Document', + blocks: [paragraphModel], + }); + expect(context.isInSelection).toBeFalse(); + }); }); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index f6e6b58e40b..6d55c59b8d7 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -1507,6 +1507,157 @@ describe('mergeModel', () => { }); }); + it('table to table, merge table 4, mergeTable and addParagraphAfterMergedContent should be noop', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(false, false, false, { backgroundColor: '12' }); + const cell21 = createTableCell(false, false, false, { backgroundColor: '21' }); + const cell22 = createTableCell(false, false, false, { backgroundColor: '22' }); + const cell31 = createTableCell(false, false, false, { backgroundColor: '31' }); + const cell32 = createTableCell(false, false, false, { backgroundColor: '32' }); + const table1 = createTable(4); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.rows = [ + { format: {}, height: 0, cells: [cell01, cell02] }, + { format: {}, height: 0, cells: [cell11, cell12] }, + { format: {}, height: 0, cells: [cell21, cell22] }, + { format: {}, height: 0, cells: [cell31, cell32] }, + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell12.blocks.push(newPara1); + newTable1.rows = [ + { format: {}, height: 0, cells: [newCell11, newCell12] }, + { format: {}, height: 0, cells: [newCell21, newCell22] }, + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + const result = mergeModel( + majorModel, + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeTable: true, + addParagraphAfterMergedContent: true, + } + ); + + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: true, + }; + const tableCell: ContentModelTableCell = { + blockGroupType: 'TableCell', + blocks: [paragraph], + format: { + backgroundColor: 'n11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }; + const table: ContentModelTable = { + blockType: 'Table', + rows: [ + { + format: {}, + height: 0, + cells: [ + cell01, + cell02, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + backgroundColor: '02', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + { + format: {}, + height: 0, + cells: [cell11, tableCell, newCell12], + }, + { format: {}, height: 0, cells: [cell21, newCell21, newCell22] }, + { + format: {}, + height: 0, + cells: [ + cell31, + cell32, + { + blockGroupType: 'TableCell', + blocks: [], + format: { + backgroundColor: '32', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }, + ], + }, + ], + format: {}, + widths: [], + dataset: {}, + }; + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(majorModel).toEqual({ + blockGroupType: 'Document', + blocks: [table], + }); + expect(result).toEqual({ + marker, + paragraph, + path: [tableCell, majorModel], + tableContext: { + table, + rowIndex: 1, + colIndex: 1, + isWholeTableSelected: false, + }, + }); + }); + it('Use customized insert position', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); @@ -3870,4 +4021,35 @@ describe('mergeModel', () => { ], }); }); + + it('Merge model with addParagraphAfterMergedContent and mergeTable, addParagraphAfterMergedContent should be noop', () => { + const source = createContentModelDocument(); + const para = createParagraph(); + para.segments.push(createText('Merge')); + source.blocks.push(para); + + const target = createContentModelDocument(); + const paraTarget = createParagraph(); + paraTarget.segments.push(createSelectionMarker()); + target.blocks.push(paraTarget); + + mergeModel(target, source, undefined, { + addParagraphAfterMergedContent: true, + mergeTable: true, + }); + + expect(target).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'Merge', format: {} }, + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + ], + format: {}, + }, + ], + }); + }); }); diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts index b0936bdf511..81fb2695c0e 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/AutoFormatPlugin.ts @@ -7,6 +7,7 @@ import { transformFraction } from './numbers/transformFraction'; import { transformHyphen } from './hyphen/transformHyphen'; import { transformOrdinals } from './numbers/transformOrdinals'; import { unlink } from './link/unlink'; +import type { AutoFormatOptions } from './interface/AutoFormatOptions'; import type { ContentChangedEvent, EditorInputEvent, @@ -17,46 +18,6 @@ import type { PluginEvent, } from 'roosterjs-content-model-types'; -/** - * Options to customize the Content Model Auto Format Plugin - */ -export type AutoFormatOptions = { - /** - * When true, after type *, ->, -, --, => , —, > and space key a type of bullet list will be triggered. @default true - */ - autoBullet?: boolean; - - /** - * When true, after type 1, A, a, i, I followed by ., ), - or between () and space key a type of numbering list will be triggered. @default true - */ - autoNumbering?: boolean; - - /** - * When press backspace before a link, remove the hyperlink - */ - autoUnlink?: boolean; - - /** - * When paste content, create hyperlink for the pasted link - */ - autoLink?: boolean; - - /** - * Transform -- into hyphen, if typed between two words - */ - autoHyphen?: boolean; - - /** - * Transform 1/2, 1/4, 3/4 into fraction character - */ - autoFraction?: boolean; - - /** - * Transform ordinal numbers into superscript - */ - autoOrdinals?: boolean; -}; - /** * @internal */ @@ -80,11 +41,13 @@ export class AutoFormatPlugin implements EditorPlugin { * @param options An optional parameter that takes in an object of type AutoFormatOptions, which includes the following properties: * - autoBullet: A boolean that enables or disables automatic bullet list formatting. Defaults to false. * - autoNumbering: A boolean that enables or disables automatic numbering formatting. Defaults to false. - * - autoLink: A boolean that enables or disables automatic hyperlink creation when pasting or typing content. Defaults to false. - * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. * - autoHyphen: A boolean that enables or disables automatic hyphen transformation. Defaults to false. * - autoFraction: A boolean that enables or disables automatic fraction transformation. Defaults to false. * - autoOrdinals: A boolean that enables or disables automatic ordinal number transformation. Defaults to false. + * - autoLink: A boolean that enables or disables automatic hyperlink url address creation when pasting or typing content. Defaults to false. + * - autoUnlink: A boolean that enables or disables automatic hyperlink removal when pressing backspace. Defaults to false. + * - autoTel: A boolean that enables or disables automatic hyperlink telephone numbers transformation. Defaults to false. + * - autoMailto: A boolean that enables or disables automatic hyperlink email address transformation. Defaults to false. */ constructor(private options: AutoFormatOptions = DefaultOptions) {} @@ -161,6 +124,8 @@ export class AutoFormatPlugin implements EditorPlugin { autoHyphen, autoFraction, autoOrdinals, + autoMailto, + autoTel, } = this.options; let shouldHyphen = false; let shouldLink = false; @@ -178,11 +143,16 @@ export class AutoFormatPlugin implements EditorPlugin { ); } - if (autoLink) { + if (autoLink || autoTel || autoMailto) { shouldLink = createLinkAfterSpace( previousSegment, paragraph, - context + context, + { + autoLink, + autoTel, + autoMailto, + } ); } @@ -243,9 +213,13 @@ export class AutoFormatPlugin implements EditorPlugin { } private handleContentChangedEvent(editor: IEditor, event: ContentChangedEvent) { - const { autoLink } = this.options; - if (event.source == 'Paste' && autoLink) { - createLink(editor); + const { autoLink, autoTel, autoMailto } = this.options; + if (event.source == 'Paste' && (autoLink || autoTel || autoMailto)) { + createLink(editor, { + autoLink, + autoTel, + autoMailto, + }); } } } diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts new file mode 100644 index 00000000000..50682210fac --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoFormatOptions.ts @@ -0,0 +1,31 @@ +import type { AutoLinkOptions } from './AutoLinkOptions'; + +/** + * Options to customize the Content Model Auto Format Plugin + */ +export interface AutoFormatOptions extends AutoLinkOptions { + /** + * When true, after type *, ->, -, --, => , —, > and space key a type of bullet list will be triggered. + */ + autoBullet?: boolean; + + /** + * When true, after type 1, A, a, i, I followed by ., ), - or between () and space key a type of numbering list will be triggered. + */ + autoNumbering?: boolean; + + /** + * Transform -- into hyphen, if typed between two words + */ + autoHyphen?: boolean; + + /** + * Transform 1/2, 1/4, 3/4 into fraction character + */ + autoFraction?: boolean; + + /** + * Transform ordinal numbers into superscript + */ + autoOrdinals?: boolean; +} diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts new file mode 100644 index 00000000000..a8f6113d5d4 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/interface/AutoLinkOptions.ts @@ -0,0 +1,24 @@ +/** + * Options to customize the Auto link options in Auto Format Plugin + */ +export interface AutoLinkOptions { + /** + * When press backspace before a link, remove the hyperlink + */ + autoUnlink?: boolean; + + /** + * When paste or type content with a link, create hyperlink for the link + */ + autoLink?: boolean; + + /** + * When paste content or type content with telephone, create hyperlink for the telephone number + */ + autoTel?: boolean; + + /** + * When paste or type a content with mailto, create hyperlink for the content + */ + autoMailto?: boolean; +} diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts index 65993fffcc4..fe29906835c 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLink.ts @@ -1,11 +1,13 @@ import { addLink, ChangeSource } from 'roosterjs-content-model-dom'; -import { formatTextSegmentBeforeSelectionMarker, matchLink } from 'roosterjs-content-model-api'; -import type { ContentModelLink, IEditor, LinkData } from 'roosterjs-content-model-types'; +import { formatTextSegmentBeforeSelectionMarker } from 'roosterjs-content-model-api'; +import { getLinkUrl } from './getLinkUrl'; +import type { AutoLinkOptions } from '../interface/AutoLinkOptions'; +import type { ContentModelLink, IEditor } from 'roosterjs-content-model-types'; /** * @internal */ -export function createLink(editor: IEditor) { +export function createLink(editor: IEditor, autoLinkOptions: AutoLinkOptions) { let anchorNode: Node | null = null; const links: ContentModelLink[] = []; formatTextSegmentBeforeSelectionMarker( @@ -15,11 +17,11 @@ export function createLink(editor: IEditor) { links.push(linkSegment.link); return true; } - let linkData: LinkData | null = null; - if (!linkSegment.link && (linkData = matchLink(linkSegment.text))) { + let linkUrl: string | undefined = undefined; + if (!linkSegment.link && (linkUrl = getLinkUrl(linkSegment.text, autoLinkOptions))) { addLink(linkSegment, { format: { - href: linkData.normalizedUrl, + href: linkUrl, underline: true, }, dataset: {}, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts index 95898e30d08..737917e25ab 100644 --- a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/createLinkAfterSpace.ts @@ -1,8 +1,9 @@ -import { matchLink, splitTextSegment } from 'roosterjs-content-model-api'; +import { getLinkUrl } from './getLinkUrl'; +import { splitTextSegment } from 'roosterjs-content-model-api'; +import type { AutoLinkOptions } from '../interface/AutoLinkOptions'; import type { ContentModelText, FormatContentModelContext, - LinkData, ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; @@ -12,12 +13,13 @@ import type { export function createLinkAfterSpace( previousSegment: ContentModelText, paragraph: ShallowMutableContentModelParagraph, - context: FormatContentModelContext + context: FormatContentModelContext, + autoLinkOptions: AutoLinkOptions ) { const link = previousSegment.text.split(' ').pop(); const url = link?.trim(); - let linkData: LinkData | null = null; - if (url && link && (linkData = matchLink(url))) { + let linkUrl: string | undefined = undefined; + if (url && link && (linkUrl = getLinkUrl(url, autoLinkOptions))) { const linkSegment = splitTextSegment( previousSegment, paragraph, @@ -26,7 +28,7 @@ export function createLinkAfterSpace( ); linkSegment.link = { format: { - href: linkData.normalizedUrl, + href: linkUrl, underline: true, }, dataset: {}, diff --git a/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts new file mode 100644 index 00000000000..8c242f790ff --- /dev/null +++ b/packages/roosterjs-content-model-plugins/lib/autoFormat/link/getLinkUrl.ts @@ -0,0 +1,26 @@ +import { matchLink } from 'roosterjs-content-model-api'; +import type { AutoLinkOptions } from '../interface/AutoLinkOptions'; + +const COMMON_REGEX = `[\s]*[a-zA-Z0-9+][\s]*`; +const TELEPHONE_REGEX = `(T|t)el:${COMMON_REGEX}`; +const MAILTO_REGEX = `(M|m)ailto:${COMMON_REGEX}`; + +/** + * @internal + */ +export function getLinkUrl(text: string, autoLinkOptions: AutoLinkOptions): string | undefined { + const { autoLink, autoMailto, autoTel } = autoLinkOptions; + const linkMatch = autoLink ? matchLink(text)?.normalizedUrl : undefined; + const telMatch = autoTel ? matchTel(text) : undefined; + const mailtoMatch = autoMailto ? matchMailTo(text) : undefined; + + return linkMatch || telMatch || mailtoMatch; +} + +function matchTel(text: string) { + return text.match(TELEPHONE_REGEX) ? text.toLocaleLowerCase() : undefined; +} + +function matchMailTo(text: string) { + return text.match(MAILTO_REGEX) ? text.toLocaleLowerCase() : undefined; +} diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts index ef765e5e231..b8b2217563e 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts @@ -1,7 +1,12 @@ import { deleteEmptyQuote } from './deleteSteps/deleteEmptyQuote'; -import { deleteSelection, normalizeContentModel, runEditSteps } from 'roosterjs-content-model-dom'; import { handleEnterOnList } from './inputSteps/handleEnterOnList'; import { handleEnterOnParagraph } from './inputSteps/handleEnterOnParagraph'; +import { + ChangeSource, + deleteSelection, + normalizeContentModel, + runEditSteps, +} from 'roosterjs-content-model-dom'; import type { IEditor } from 'roosterjs-content-model-types'; /** @@ -49,6 +54,7 @@ export function keyboardEnter( { rawEvent, scrollCaretIntoView: true, + changeSource: ChangeSource.Keyboard, } ); } diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts index fe8dc9e7bfc..95ffb0d8667 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts @@ -1,4 +1,9 @@ -import { deleteSelection, isModifierKey, normalizeContentModel } from 'roosterjs-content-model-dom'; +import { + ChangeSource, + deleteSelection, + isModifierKey, + normalizeContentModel, +} from 'roosterjs-content-model-dom'; import type { DOMSelection, IEditor } from 'roosterjs-content-model-types'; /** @@ -32,6 +37,7 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) { { scrollCaretIntoView: true, rawEvent, + changeSource: ChangeSource.Keyboard, } ); diff --git a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts index 7c274f2f0cc..495b79fdc80 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/keyboardTab.ts @@ -1,9 +1,13 @@ -import { getOperationalBlocks, isBlockGroupOfType } from 'roosterjs-content-model-dom'; import { handleTabOnList } from './tabUtils/handleTabOnList'; import { handleTabOnParagraph } from './tabUtils/handleTabOnParagraph'; import { handleTabOnTable } from './tabUtils/handleTabOnTable'; import { handleTabOnTableCell } from './tabUtils/handleTabOnTableCell'; import { setModelIndentation } from 'roosterjs-content-model-api'; +import { + ChangeSource, + getOperationalBlocks, + isBlockGroupOfType, +} from 'roosterjs-content-model-dom'; import type { ContentModelListItem, ContentModelTableCell, @@ -37,6 +41,7 @@ export function keyboardTab(editor: IEditor, rawEvent: KeyboardEvent) { }, { apiName: 'handleTabKey', + changeSource: ChangeSource.Keyboard, } ); return true; diff --git a/packages/roosterjs-content-model-plugins/lib/index.ts b/packages/roosterjs-content-model-plugins/lib/index.ts index 4ff598eb9ce..89af142dedc 100644 --- a/packages/roosterjs-content-model-plugins/lib/index.ts +++ b/packages/roosterjs-content-model-plugins/lib/index.ts @@ -4,7 +4,9 @@ export { TableEditFeatureName } from './tableEdit/editors/features/TableEditFeat export { PastePlugin } from './paste/PastePlugin'; export { DefaultSanitizers } from './paste/DefaultSanitizers'; export { EditPlugin, EditOptions } from './edit/EditPlugin'; -export { AutoFormatPlugin, AutoFormatOptions } from './autoFormat/AutoFormatPlugin'; +export { AutoFormatPlugin } from './autoFormat/AutoFormatPlugin'; +export { AutoFormatOptions } from './autoFormat/interface/AutoFormatOptions'; +export { AutoLinkOptions } from './autoFormat/interface/AutoLinkOptions'; export { ShortcutBold, diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts index 9203fa27645..e799ed4441b 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/AutoFormatPluginTest.ts @@ -1,7 +1,8 @@ import * as createLink from '../../lib/autoFormat/link/createLink'; import * as formatTextSegmentBeforeSelectionMarker from 'roosterjs-content-model-api/lib/publicApi/utils/formatTextSegmentBeforeSelectionMarker'; import * as unlink from '../../lib/autoFormat/link/unlink'; -import { AutoFormatOptions, AutoFormatPlugin } from '../../lib/autoFormat/AutoFormatPlugin'; +import { AutoFormatOptions } from '../../lib/autoFormat/interface/AutoFormatOptions'; +import { AutoFormatPlugin } from '../../lib/autoFormat/AutoFormatPlugin'; import { ChangeSource } from '../../../roosterjs-content-model-dom/lib/constants/ChangeSource'; import { createLinkAfterSpace } from '../../lib/autoFormat/link/createLinkAfterSpace'; import { keyboardListTrigger } from '../../lib/autoFormat/list/keyboardListTrigger'; @@ -176,9 +177,7 @@ describe('Content Model Auto Format Plugin Test', () => { function runTest( event: ContentChangedEvent, shouldCallTrigger: boolean, - options?: { - autoLink: boolean; - } + options: AutoFormatOptions ) { const plugin = new AutoFormatPlugin(options as AutoFormatOptions); plugin.initialize(editor); @@ -186,7 +185,7 @@ describe('Content Model Auto Format Plugin Test', () => { plugin.onPluginEvent(event); if (shouldCallTrigger) { - expect(createLinkSpy).toHaveBeenCalledWith(editor); + expect(createLinkSpy).toHaveBeenCalledWith(editor, options); } else { expect(createLinkSpy).not.toHaveBeenCalled(); } @@ -199,6 +198,8 @@ describe('Content Model Auto Format Plugin Test', () => { }; runTest(event, true, { autoLink: true, + autoMailto: true, + autoTel: true, }); }); @@ -207,7 +208,7 @@ describe('Content Model Auto Format Plugin Test', () => { eventType: 'contentChanged', source: 'Paste', }; - runTest(event, false, { autoLink: false }); + runTest(event, false, { autoLink: false, autoMailto: false, autoTel: false }); }); it('should not call createLink - not paste', () => { @@ -217,6 +218,8 @@ describe('Content Model Auto Format Plugin Test', () => { }; runTest(event, false, { autoLink: true, + autoMailto: true, + autoTel: true, }); }); }); @@ -305,9 +308,7 @@ describe('Content Model Auto Format Plugin Test', () => { context: FormatContentModelContext ) => { const result = - options && - options.autoLink && - createLinkAfterSpace(segment, paragraph, context); + options && createLinkAfterSpace(segment, paragraph, context, options); expect(result).toBe(expectResult); @@ -328,6 +329,26 @@ describe('Content Model Auto Format Plugin Test', () => { }); }); + it('should call createLinkAfterSpace | autoTel', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, true, { + autoTel: true, + }); + }); + + it('should call createLinkAfterSpace | autoMailto', () => { + const event: EditorInputEvent = { + eventType: 'input', + rawEvent: { data: ' ', preventDefault: () => {}, inputType: 'insertText' } as any, + }; + runTest(event, true, { + autoMailto: true, + }); + }); + it('should not call createLinkAfterSpace - disable options', () => { const event: EditorInputEvent = { eventType: 'input', diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts index 9ad724e4b9f..0bf95c157d4 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkAfterSpaceTest.ts @@ -14,7 +14,11 @@ describe('createLinkAfterSpace', () => { context: FormatContentModelContext, expectedResult: boolean ) { - const result = createLinkAfterSpace(previousSegment, paragraph, context); + const result = createLinkAfterSpace(previousSegment, paragraph, context, { + autoLink: true, + autoMailto: true, + autoTel: true, + }); expect(result).toBe(expectedResult); } @@ -85,7 +89,11 @@ describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => formatContentModel: formatWithContentModelSpy, } as any, (_model, previousSegment, paragraph, _markerFormat, context) => { - return createLinkAfterSpace(previousSegment, paragraph, context); + return createLinkAfterSpace(previousSegment, paragraph, context, { + autoLink: true, + autoMailto: true, + autoTel: true, + }); } ); @@ -385,4 +393,476 @@ describe('formatTextSegmentBeforeSelectionMarker - createLinkAfterSpace', () => }; runTest(input, expected, true); }); + + it('telephone link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with +', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + link: { + format: { + href: 'tel:+9999-9999', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with T', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link with M', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + isSelected: undefined, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('telephone link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts index edd4e5e7e31..4b709ab5a60 100644 --- a/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/createLinkTest.ts @@ -20,10 +20,13 @@ describe('createLink', () => { expect(options.changeSource).toBe(ChangeSource.AutoLink); }); - createLink({ - focus: () => {}, - formatContentModel: formatWithContentModelSpy, - } as any); + createLink( + { + focus: () => {}, + formatContentModel: formatWithContentModelSpy, + } as any, + { autoLink: true, autoMailto: true, autoTel: true } + ); expect(formatWithContentModelSpy).toHaveBeenCalled(); expect(input).toEqual(expectedModel); @@ -165,4 +168,471 @@ describe('createLink', () => { runTest(input, input, true); }); + + it('telephone link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with +', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel:+9999-9999', + format: {}, + link: { + format: { + href: 'tel:+9999-9999', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with T', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Tel:9999-9999', + format: {}, + link: { + format: { + href: 'tel:9999-9999', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('mailto link with M', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Mailto:test', + format: {}, + link: { + format: { + href: 'mailto:test', + underline: true, + }, + dataset: {}, + }, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, true); + }); + + it('telephone link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tel: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link with space', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailto: 9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('telephone link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'tels:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); + + it('mailto link spelled wrong', () => { + const input: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + const expected: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'mailTo:9999-9999', + format: {}, + }, + { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }, + ], + format: {}, + }, + ], + format: {}, + }; + runTest(input, expected, false); + }); }); diff --git a/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts new file mode 100644 index 00000000000..66d117a4d32 --- /dev/null +++ b/packages/roosterjs-content-model-plugins/test/autoFormat/link/getLinkUrlTest.ts @@ -0,0 +1,53 @@ +import { AutoLinkOptions } from '../../../lib/autoFormat/interface/AutoLinkOptions'; +import { getLinkUrl } from '../../../lib/autoFormat/link/getLinkUrl'; + +describe('getLinkUrl', () => { + function runTest(text: string, options: AutoLinkOptions, expectedResult: string | undefined) { + const link = getLinkUrl(text, options); + expect(link).toBe(expectedResult); + } + + it('link', () => { + runTest('http://www.bing.com', { autoLink: true }, 'http://www.bing.com'); + }); + + it('do not return link', () => { + runTest('wwww.test.com', { autoLink: false }, undefined); + }); + + it('invalid link', () => { + runTest('www3w.test.com', { autoLink: true }, undefined); + }); + + it('telephone', () => { + runTest('tel:999999', { autoTel: true }, 'tel:999999'); + }); + + it('telephone with T', () => { + runTest('Tel:999999', { autoTel: true }, 'tel:999999'); + }); + + it('do not return telephone', () => { + runTest('tel:999999', { autoTel: false }, undefined); + }); + + it('invalid telephone', () => { + runTest('tels:999999', { autoTel: true }, undefined); + }); + + it('mailto', () => { + runTest('mailto:test', { autoMailto: true }, 'mailto:test'); + }); + + it('mailto with M', () => { + runTest('Mailto:test', { autoMailto: true }, 'mailto:test'); + }); + + it('do not return mailto', () => { + runTest('mailto:test', { autoMailto: false }, undefined); + }); + + it('invalid mailto', () => { + runTest('mailtos:test', { autoMailto: true }, undefined); + }); +}); diff --git a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts index 91bd2d976ad..27169dd2681 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/DOMHelper.ts @@ -92,4 +92,9 @@ export interface DOMHelper { * Get the width of the editable area of the editor content div */ getClientWidth(): number; + + /** + * Get a deep cloned root element + */ + getClonedRoot(): HTMLElement; } diff --git a/yarn.lock b/yarn.lock index 939728d0c1b..b003e590332 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1445,10 +1445,10 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3, body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -1458,29 +1458,11 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" -body-parser@^1.19.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - bonjour@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" @@ -1582,6 +1564,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2096,6 +2089,15 @@ define-data-property@^1.0.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -2247,10 +2249,10 @@ dom-serialize@^2.2.1: extend "^3.0.0" void-elements "^2.0.0" -dompurify@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2" - integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw== +dompurify@2.5.4: + version "2.5.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.4.tgz#347e91070963b22db31c7c8d0ce9a0a2c3c08746" + integrity sha512-l5NNozANzaLPPe0XaAwvg3uZcHtDBnziX/HjsY1UcDj1MxTK8Dd0Kv096jyPK5HRzs/XM5IMj20dW8Fk+HnbUA== ecc-jsbn@~0.1.1: version "0.1.2" @@ -2295,6 +2297,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -2404,6 +2411,18 @@ es-abstract@^1.22.1: unbox-primitive "^1.0.2" which-typed-array "^1.1.11" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-iterator-helpers@^1.0.12: version "1.0.15" resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" @@ -2754,36 +2773,36 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: homedir-polyfill "^1.0.1" express@^4.17.1: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.21.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" + integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -2940,13 +2959,13 @@ finalhandler@1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -3101,6 +3120,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" @@ -3150,6 +3174,17 @@ get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@ has-proto "^1.0.1" has-symbols "^1.0.3" +get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -3394,6 +3429,13 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + has-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" @@ -3457,6 +3499,13 @@ hasha@^2.2.0: is-stream "^1.0.1" pinkie-promise "^2.0.0" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -4622,10 +4671,10 @@ memory-fs@^0.4.0, memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-source-map@^1.1.0: version "1.1.0" @@ -4928,6 +4977,11 @@ object-inspect@^1.12.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -5241,10 +5295,10 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-type@^4.0.0: version "4.0.0" @@ -5515,12 +5569,12 @@ qjobs@^1.2.0: resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" qs@~6.5.2: version "6.5.3" @@ -5554,16 +5608,6 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" @@ -5960,10 +6004,10 @@ semver@^7.3.4, semver@^7.3.7, semver@^7.5.4: dependencies: lru-cache "^6.0.0" -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -5999,21 +6043,33 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + set-function-name@^2.0.0, set-function-name@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" @@ -6092,6 +6148,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From f56fd8e6f9c4d6f3c4372f65f9a426cd1fbd1320 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Thu, 26 Sep 2024 21:52:37 -0600 Subject: [PATCH 39/43] update version.json --- versions.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/versions.json b/versions.json index c6bdf62c320..613b99d3f83 100644 --- a/versions.json +++ b/versions.json @@ -1,8 +1,5 @@ { "react": "9.0.0", - "main": "9.10.0", - "legacyAdapter": "8.62.1", - "overrides": { - "roosterjs-content-model-plugins": "9.10.1" - } + "main": "9.11.0", + "legacyAdapter": "8.62.1" } From da9c80fed9ce742284f13a07734eee3fd2b97f33 Mon Sep 17 00:00:00 2001 From: "SOUTHAMERICA\\bvalverde" Date: Thu, 26 Sep 2024 21:54:26 -0600 Subject: [PATCH 40/43] update --- versions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 613b99d3f83..f1699a478dd 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,6 @@ { "react": "9.0.0", "main": "9.11.0", - "legacyAdapter": "8.62.1" + "legacyAdapter": "8.62.1", + "overrides": {} } From 12d27dff41a75883bed143efcac0ee8bbc9efff1 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 27 Sep 2024 08:41:30 -0700 Subject: [PATCH 41/43] Use a separate experimental feature for Enter key (#2811) Co-authored-by: Bryan Valverde U --- .../sidePane/editorOptions/EditorOptionsPlugin.ts | 2 +- .../sidePane/editorOptions/ExperimentalFeatures.tsx | 8 +++++++- .../lib/edit/EditPlugin.ts | 2 +- .../test/edit/EditPluginTest.ts | 2 +- .../lib/editor/ExperimentalFeature.ts | 6 +++++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts index 6e216f2da1a..1e9085c302b 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts +++ b/demo/scripts/controlsV2/sidePane/editorOptions/EditorOptionsPlugin.ts @@ -58,7 +58,7 @@ const initialState: OptionState = { handleTabKey: true, }, customReplacements: emojiReplacements, - experimentalFeatures: new Set(['PersistCache']), + experimentalFeatures: new Set(['PersistCache', 'HandleEnterKey']), }; export class EditorOptionsPlugin extends SidePanePluginImpl { diff --git a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx index da543da70c9..2bd26858972 100644 --- a/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx +++ b/demo/scripts/controlsV2/sidePane/editorOptions/ExperimentalFeatures.tsx @@ -9,7 +9,13 @@ export interface DefaultFormatProps { export class ExperimentalFeatures extends React.Component { render() { - return this.renderFeature('PersistCache'); + return ( + <> + {this.renderFeature('PersistCache')} + {this.renderFeature('HandleEnterKey')} + {this.renderFeature('LegacyImageSelection')} + + ); } private renderFeature(featureName: ExperimentalFeature): JSX.Element { diff --git a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts index 82bf6f54b52..0e3af9c35c3 100644 --- a/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/edit/EditPlugin.ts @@ -63,7 +63,7 @@ export class EditPlugin implements EditorPlugin { */ initialize(editor: IEditor) { this.editor = editor; - this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('PersistCache'); + this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('HandleEnterKey'); if (editor.getEnvironment().isAndroid) { this.disposer = this.editor.attachDomEvent({ diff --git a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts index bbe1a0acb9b..6ad3df27402 100644 --- a/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/edit/EditPluginTest.ts @@ -162,7 +162,7 @@ describe('EditPlugin', () => { it('Enter, normal enter enabled', () => { isExperimentalFeatureEnabledSpy.and.callFake( - (featureName: string) => featureName == 'PersistCache' + (featureName: string) => featureName == 'HandleEnterKey' ); plugin = new EditPlugin(); const rawEvent = { which: 13, key: 'Enter' } as any; diff --git a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts index 15159013fd9..46e58d639df 100644 --- a/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts +++ b/packages/roosterjs-content-model-types/lib/editor/ExperimentalFeature.ts @@ -12,4 +12,8 @@ export type ExperimentalFeature = /** * Workaround for the Legacy Image Edit */ - | 'LegacyImageSelection'; + | 'LegacyImageSelection' + /** + * Use Content Model handle ENTER key + */ + | 'HandleEnterKey'; From c3b29486c68984039a74610ee35bbba87f784f18 Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 27 Sep 2024 16:36:12 -0300 Subject: [PATCH 42/43] test --- .../test/imageEdit/ImageEditPluginTest.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts index f0bbc6fd7da..1d8108ae389 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts @@ -430,4 +430,61 @@ describe('ImageEditPlugin', () => { expect(formatContentModelSpy).toHaveBeenCalledTimes(3); plugin.dispose(); }); + + it('flip setEditorStyle', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + isSelectedAsImageSelection: true, + isSelected: true, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + const plugin = new ImageEditPlugin(); + const editor = initEditor('image_edit', [plugin], model); + spyOn(editor, 'setEditorStyle').and.callThrough(); + + plugin.initialize(editor); + plugin.flipImage('horizontal'); + plugin.dispose(); + + expect(editor.setEditorStyle).toHaveBeenCalledWith( + 'imageEdit', + 'outline-style:none!important;', + ['span:has(>img#image_0)'] + ); + expect(editor.setEditorStyle).toHaveBeenCalledWith( + 'imageEditCaretColor', + 'caret-color: transparent;' + ); + expect(editor.setEditorStyle).toHaveBeenCalledWith('imageEdit', null); + expect(editor.setEditorStyle).toHaveBeenCalledWith('imageEditCaretColor', null); + }); }); From da47dfe6fdbe8e59b515b30154f244e33cfe4a1f Mon Sep 17 00:00:00 2001 From: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:54:59 -0300 Subject: [PATCH 43/43] Merge pull request #2815 from microsoft/u/juliaroldi/image-text-ip [Image Edit] When the image is in edit mode, hide the text caret --- .../lib/imageEdit/ImageEditPlugin.ts | 9 ++- .../test/imageEdit/ImageEditPluginTest.ts | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index cea7b9d8b69..2474d4f2193 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -53,6 +53,8 @@ const DefaultOptions: Partial = { const MouseRightButton = 2; const DRAG_ID = '_dragging'; +const IMAGE_EDIT_CLASS = 'imageEdit'; +const IMAGE_EDIT_CLASS_CARET = 'imageEditCaretColor'; /** * ImageEdit plugin handles the following image editing features: @@ -384,9 +386,11 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.croppers = croppers; this.zoomScale = editor.getDOMHelper().calculateZoomScale(); - editor.setEditorStyle('imageEdit', `outline-style:none!important;`, [ + editor.setEditorStyle(IMAGE_EDIT_CLASS, `outline-style:none!important;`, [ `span:has(>img${getSafeIdSelector(this.selectedImage.id)})`, ]); + + editor.setEditorStyle(IMAGE_EDIT_CLASS_CARET, `caret-color: transparent;`); } public startRotateAndResize(editor: IEditor, image: HTMLImageElement) { @@ -607,7 +611,8 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } private cleanInfo() { - this.editor?.setEditorStyle('imageEdit', null); + this.editor?.setEditorStyle(IMAGE_EDIT_CLASS, null); + this.editor?.setEditorStyle(IMAGE_EDIT_CLASS_CARET, null); this.selectedImage = null; this.shadowSpan = null; this.wrapper = null; diff --git a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts index f0bbc6fd7da..1d8108ae389 100644 --- a/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/imageEdit/ImageEditPluginTest.ts @@ -430,4 +430,61 @@ describe('ImageEditPlugin', () => { expect(formatContentModelSpy).toHaveBeenCalledTimes(3); plugin.dispose(); }); + + it('flip setEditorStyle', () => { + const model: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Image', + src: 'test', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + id: 'image_0', + maxWidth: '1800px', + }, + dataset: {}, + isSelectedAsImageSelection: true, + isSelected: true, + }, + ], + format: {}, + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: 'rgb(0, 0, 0)', + }, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + const plugin = new ImageEditPlugin(); + const editor = initEditor('image_edit', [plugin], model); + spyOn(editor, 'setEditorStyle').and.callThrough(); + + plugin.initialize(editor); + plugin.flipImage('horizontal'); + plugin.dispose(); + + expect(editor.setEditorStyle).toHaveBeenCalledWith( + 'imageEdit', + 'outline-style:none!important;', + ['span:has(>img#image_0)'] + ); + expect(editor.setEditorStyle).toHaveBeenCalledWith( + 'imageEditCaretColor', + 'caret-color: transparent;' + ); + expect(editor.setEditorStyle).toHaveBeenCalledWith('imageEdit', null); + expect(editor.setEditorStyle).toHaveBeenCalledWith('imageEditCaretColor', null); + }); });