From 1af0ee90bf1a3079a3f8f2e57e58e74e8a641f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 6 Jun 2024 19:28:29 -0300 Subject: [PATCH 1/2] image selection --- .../createSnapshotSelection.ts | 3 +- .../lib/coreApi/focus/focus.ts | 7 +- .../setDOMSelection/setDOMSelection.ts | 8 +- .../corePlugin/selection/SelectionPlugin.ts | 22 +++- .../lib/editor/Editor.ts | 4 +- .../lib/imageEdit/ImageEditPlugin.ts | 124 +++++++++++++++++- .../lib/imageEdit/utils/createImageWrapper.ts | 6 +- .../lib/editor/EditorCore.ts | 1 + .../lib/editor/IEditor.ts | 2 +- .../lib/event/SelectionChangedEvent.ts | 5 + 10 files changed, 167 insertions(+), 15 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/coreApi/addUndoSnapshot/createSnapshotSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/addUndoSnapshot/createSnapshotSelection.ts index 49f77588e6b..8e1dc7f808c 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/addUndoSnapshot/createSnapshotSelection.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/addUndoSnapshot/createSnapshotSelection.ts @@ -1,6 +1,6 @@ +import { getPath } from './getPath'; import { isElementOfType, isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom'; import type { EditorCore, SnapshotSelection } from 'roosterjs-content-model-types'; -import { getPath } from './getPath'; /** * @internal @@ -30,6 +30,7 @@ export function createSnapshotSelection(core: EditorCore): SnapshotSelection { range: newRange, isReverted: !!selection.isReverted, }, + selection, true /*skipSelectionChangedEvent*/ ); } diff --git a/packages/roosterjs-content-model-core/lib/coreApi/focus/focus.ts b/packages/roosterjs-content-model-core/lib/coreApi/focus/focus.ts index b02c5af9947..ba175d8145a 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/focus/focus.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/focus/focus.ts @@ -10,7 +10,12 @@ export const focus: Focus = core => { const { api, domHelper, selection } = core; if (!domHelper.hasFocus() && selection.selection?.type == 'range') { - api.setDOMSelection(core, selection.selection, true /*skipSelectionChangedEvent*/); + api.setDOMSelection( + core, + selection.selection, + undefined /* previousSelection */, + true /*skipSelectionChangedEvent*/ + ); } // fallback, in case editor still have no focus diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts index b547f8107f6..389540afa4b 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/setDOMSelection/setDOMSelection.ts @@ -23,7 +23,12 @@ const SELECTION_SELECTOR = '*::selection'; /** * @internal */ -export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionChangedEvent) => { +export const setDOMSelection: SetDOMSelection = ( + core, + selection, + previousSelection, + skipSelectionChangedEvent +) => { // We are applying a new selection, so we don't need to apply cached selection in DOMEventPlugin. // Set skipReselectOnFocus to skip this behavior const skipReselectOnFocus = core.selection.skipReselectOnFocus; @@ -150,6 +155,7 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC const eventData: SelectionChangedEvent = { eventType: 'selectionChanged', newSelection: selection, + previousSelection: previousSelection, }; core.api.triggerEvent(core, eventData, true /*broadcast*/); 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 395a0ef3684..6e31cb6529b 100644 --- a/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts +++ b/packages/roosterjs-content-model-core/lib/corePlugin/selection/SelectionPlugin.ts @@ -176,7 +176,12 @@ class SelectionPlugin implements PluginWithState { this.selectImageWithRange(image, rawEvent); return; } else if (selection?.type == 'image' && selection.image !== rawEvent.target) { - this.selectBeforeOrAfterElement(editor, selection.image); + this.selectBeforeOrAfterElement( + editor, + selection.image, + undefined /* after */, + selection + ); return; } @@ -523,7 +528,12 @@ class SelectionPlugin implements PluginWithState { } } - private selectBeforeOrAfterElement(editor: IEditor, element: HTMLElement, after?: boolean) { + private selectBeforeOrAfterElement( + editor: IEditor, + element: HTMLElement, + after?: boolean, + previousSelection?: DOMSelection + ) { const doc = editor.getDocument(); const parent = element.parentNode; const index = parent && toArray(parent.childNodes).indexOf(element); @@ -539,7 +549,8 @@ class SelectionPlugin implements PluginWithState { range: range, isReverted: false, }, - null /*tableSelection*/ + null /*tableSelection*/, + previousSelection ); } } @@ -686,9 +697,10 @@ class SelectionPlugin implements PluginWithState { private setDOMSelection( selection: DOMSelection | null, - tableSelection: TableSelectionInfo | null + tableSelection: TableSelectionInfo | null, + previousSelection?: DOMSelection ) { - this.editor?.setDOMSelection(selection); + this.editor?.setDOMSelection(selection, previousSelection); this.state.tableSelection = tableSelection; } diff --git a/packages/roosterjs-content-model-core/lib/editor/Editor.ts b/packages/roosterjs-content-model-core/lib/editor/Editor.ts index 3301ff252ee..61b96d9a726 100644 --- a/packages/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages/roosterjs-content-model-core/lib/editor/Editor.ts @@ -143,10 +143,10 @@ export class Editor implements IEditor { * Set DOMSelection into editor content. * @param selection The selection to set */ - setDOMSelection(selection: DOMSelection | null) { + setDOMSelection(selection: DOMSelection | null, previousSelection?: DOMSelection) { const core = this.getCore(); - core.api.setDOMSelection(core, selection); + core.api.setDOMSelection(core, selection, previousSelection); } /** diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts index 6cccb46a8e4..cb0bfb551d6 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -3,6 +3,7 @@ import { canRegenerateImage } from './utils/canRegenerateImage'; import { checkIfImageWasResized, isASmallImage } from './utils/imageEditUtils'; import { createImageWrapper } from './utils/createImageWrapper'; import { Cropper } from './Cropper/cropperContext'; +import { formatInsertPointWithContentModel } from 'roosterjs-content-model-api/lib'; import { getDropAndDragHelpers } from './utils/getDropAndDragHelpers'; import { getHTMLImageOptions } from './utils/getHTMLImageOptions'; import { getSelectedImageMetadata } from './utils/updateImageEditInfo'; @@ -23,11 +24,14 @@ import type { DragAndDropContext } from './types/DragAndDropContext'; import type { ImageHtmlOptions } from './types/ImageHtmlOptions'; import type { ImageEditOptions } from './types/ImageEditOptions'; import type { + DOMInsertPoint, + DOMSelection, EditorPlugin, IEditor, ImageEditOperation, ImageEditor, ImageMetadataFormat, + ImageSelection, PluginEvent, } from 'roosterjs-content-model-types'; @@ -118,7 +122,25 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { * exclusively by another plugin. * @param event The event to handle: */ - onPluginEvent(_event: PluginEvent) {} + onPluginEvent(event: PluginEvent) { + if (event.eventType == 'selectionChanged' && this.editor) { + const selection = event.newSelection; + const previousSelection = event.previousSelection; + console.log('selectionChanged', selection, previousSelection); + if (previousSelection?.type == 'image' && selection && this.shadowSpan) { + const previousImage = previousSelection.image; + if (previousImage) { + this.formatImageWhenSelectionChange(this.editor, selection, previousSelection); + } + } + if (selection && selection.type == 'image') { + const image = selection.image; + if (image) { + this.startRotateAndResize(this.editor, image); + } + } + } + } private startEditing( editor: IEditor, @@ -126,7 +148,12 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { apiOperation?: ImageEditOperation ) { const imageSpan = image.parentElement; - if (!imageSpan || (imageSpan && !isElementOfType(imageSpan, 'span'))) { + if ( + !imageSpan || + (imageSpan && !isElementOfType(imageSpan, 'span')) || + image.width <= 0 || + image.height <= 0 + ) { return; } this.imageEditInfo = getSelectedImageMetadata(editor, image); @@ -171,6 +198,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { if (this.wrapper && this.selectedImage && this.shadowSpan) { this.removeImageWrapper(); } + this.startEditing(editor, image, apiOperation); if (this.selectedImage && this.imageEditInfo && this.wrapper && this.clonedImage) { this.dndHelpers = [ @@ -454,6 +482,98 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { } } + private createInsertPoint(selection: DOMSelection): DOMInsertPoint | null { + if (selection.type === 'image' && selection.image) { + return { + node: selection.image, + offset: selection.image.offsetLeft, + }; + } else if ( + selection.type === 'range' && + isNodeOfType(selection.range.startContainer, 'ELEMENT_NODE') + ) { + return { + node: selection.range.startContainer, + offset: selection.range.startContainer.offsetLeft, + }; + } + return null; + } + + private formatImageWhenSelectionChange( + editor: IEditor, + newSelection: DOMSelection, + previousSelection: ImageSelection + ) { + const insertPoint = this.createInsertPoint(newSelection); + if ( + insertPoint && + this.lastSrc && + this.selectedImage && + this.imageEditInfo && + this.clonedImage && + this.shadowSpan + ) { + formatInsertPointWithContentModel( + editor, + insertPoint, + (model, _, insertPoint) => { + const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs( + model, + false + ); + if (!selectedSegmentsAndParagraphs[0]) { + return false; + } + + const segment = selectedSegmentsAndParagraphs[0][0]; + const paragraph = selectedSegmentsAndParagraphs[0][1]; + + if (paragraph && segment.segmentType == 'Image') { + mutateSegment(paragraph, segment, image => { + if ( + this.lastSrc && + this.selectedImage && + this.imageEditInfo && + this.clonedImage + ) { + applyChange( + editor, + this.selectedImage, + image, + this.imageEditInfo, + this.lastSrc, + this.wasImageResized || this.isCropMode, + this.clonedImage + ); + console.log('formatImageWhenSelectionChange', insertPoint); + if (insertPoint) { + insertPoint.marker.isSelected = true; + image.isSelected = false; + image.isSelectedAsImageSelection = false; + } + } + }); + + return true; + } + + return false; + }, + { + changeSource: IMAGE_EDIT_CHANGE_SOURCE, + onNodeCreated: () => { + this.cleanInfo(); + }, + selectionOverride: { + type: 'image', + image: previousSelection.image, + }, + } + ); + } + } + private removeImageWrapper() { let image: HTMLImageElement | null = null; if (this.shadowSpan && this.shadowSpan.parentElement) { diff --git a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/createImageWrapper.ts b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/createImageWrapper.ts index 9a11e44565f..01c3a3c275e 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/createImageWrapper.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/utils/createImageWrapper.ts @@ -141,8 +141,10 @@ const cloneImage = (image: HTMLImageElement, editInfo: ImageMetadataFormat) => { imageClone.removeAttribute('id'); imageClone.style.removeProperty('max-width'); imageClone.style.removeProperty('max-height'); - imageClone.style.width = editInfo.widthPx + 'px'; - imageClone.style.height = editInfo.heightPx + 'px'; + if (editInfo.widthPx && editInfo.heightPx) { + imageClone.style.width = editInfo.widthPx + 'px'; + imageClone.style.height = editInfo.heightPx + 'px'; + } } return imageClone; }; diff --git a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts index 46d00bd3733..191b2348bc9 100644 --- a/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts +++ b/packages/roosterjs-content-model-types/lib/editor/EditorCore.ts @@ -70,6 +70,7 @@ export type SetContentModel = ( export type SetDOMSelection = ( core: EditorCore, selection: DOMSelection | null, + previousSelection?: DOMSelection, skipSelectionChangedEvent?: boolean ) => void; diff --git a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts index 2ff5e28bad0..7e9c1691e00 100644 --- a/packages/roosterjs-content-model-types/lib/editor/IEditor.ts +++ b/packages/roosterjs-content-model-types/lib/editor/IEditor.ts @@ -55,7 +55,7 @@ export interface IEditor { * This is the replacement of IEditor.select. * @param selection The selection to set */ - setDOMSelection(selection: DOMSelection | null): void; + setDOMSelection(selection: DOMSelection | null, previousSelection?: DOMSelection): void; /** * Set a new logical root (most likely due to focus change) diff --git a/packages/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts b/packages/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts index d692e5a3873..b1e9d37ad05 100644 --- a/packages/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts +++ b/packages/roosterjs-content-model-types/lib/event/SelectionChangedEvent.ts @@ -9,4 +9,9 @@ export interface SelectionChangedEvent extends BasePluginEvent<'selectionChanged * The new selection after change */ newSelection: DOMSelection | null; + + /** + * Previous selection before change + */ + previousSelection?: DOMSelection; } From 377b27d2b137223f60aba390fec66c326d53c087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Roldi?= Date: Thu, 6 Jun 2024 19:32:35 -0300 Subject: [PATCH 2/2] remove consoles --- .../lib/imageEdit/ImageEditPlugin.ts | 3 +-- 1 file changed, 1 insertion(+), 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 cb0bfb551d6..69d5fcaa4df 100644 --- a/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/imageEdit/ImageEditPlugin.ts @@ -126,7 +126,7 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { if (event.eventType == 'selectionChanged' && this.editor) { const selection = event.newSelection; const previousSelection = event.previousSelection; - console.log('selectionChanged', selection, previousSelection); + if (previousSelection?.type == 'image' && selection && this.shadowSpan) { const previousImage = previousSelection.image; if (previousImage) { @@ -546,7 +546,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin { this.wasImageResized || this.isCropMode, this.clonedImage ); - console.log('formatImageWhenSelectionChange', insertPoint); if (insertPoint) { insertPoint.marker.isSelected = true; image.isSelected = false;