Skip to content

Commit

Permalink
Image edit 2
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong committed Jun 7, 2024
1 parent a0f32c6 commit 2f70bf8
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { addRangeToSelection } from './addRangeToSelection';
import { areSameSelection } from '../../corePlugin/cache/areSameSelection';
import { ensureImageHasSpanParent } from './ensureImageHasSpanParent';
import { ensureUniqueId } from '../setEditorStyle/ensureUniqueId';
import { findLastedCoInMergedCell } from './findLastedCoInMergedCell';
Expand All @@ -24,6 +25,12 @@ const SELECTION_SELECTOR = '*::selection';
* @internal
*/
export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionChangedEvent) => {
const existingSelection = core.api.getDOMSelection(core);

if (existingSelection && selection && areSameSelection(existingSelection, selection)) {
return;
}

// 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;
Expand Down Expand Up @@ -150,6 +157,7 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
const eventData: SelectionChangedEvent = {
eventType: 'selectionChanged',
newSelection: selection,
previousSelection: existingSelection,
};

core.api.triggerEvent(core, eventData, true /*broadcast*/);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { CacheSelection, DOMSelection } from 'roosterjs-content-model-types';
import type {
CacheSelection,
DOMSelection,
RangeSelection,
RangeSelectionForCache,
} from 'roosterjs-content-model-types';

/**
* @internal
* Check if the given selections are the same
*/
export function areSameSelection(sel1: DOMSelection, sel2: CacheSelection): boolean {
export function areSameSelection(sel1: DOMSelection, sel2: CacheSelection | DOMSelection): boolean {
if (sel1 == sel2) {
return true;
}
Expand All @@ -25,12 +30,36 @@ export function areSameSelection(sel1: DOMSelection, sel2: CacheSelection): bool

case 'range':
default:
return (
sel2.type == 'range' &&
sel1.range.startContainer == sel2.start.node &&
sel1.range.endContainer == sel2.end.node &&
sel1.range.startOffset == sel2.start.offset &&
sel1.range.endOffset == sel2.end.offset
);
if (sel2.type == 'range') {
const { startContainer, startOffset, endContainer, endOffset } = sel1.range;

if (isCacheSelection(sel2)) {
const { start, end } = sel2;

return (
startContainer == start.node &&
endContainer == end.node &&
startOffset == start.offset &&
endOffset == end.offset
);
} else {
const { range } = sel2;

return (
startContainer == range.startContainer &&
endContainer == range.endContainer &&
startOffset == range.startOffset &&
endOffset == range.endOffset
);
}
} else {
return false;
}
}
}

function isCacheSelection(
sel: RangeSelectionForCache | RangeSelection
): sel is RangeSelectionForCache {
return !!(sel as RangeSelectionForCache).start;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,13 @@ import type {
SelectionPluginState,
EditorOptions,
DOMHelper,
MouseUpEvent,
ParsedTable,
TableSelectionInfo,
TableCellCoordinate,
RangeSelection,
} from 'roosterjs-content-model-types';

const MouseLeftButton = 0;
const MouseMiddleButton = 1;
const MouseRightButton = 2;
const Up = 'ArrowUp';
const Down = 'ArrowDown';
const Left = 'ArrowLeft';
Expand All @@ -48,7 +45,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
private state: SelectionPluginState;
private disposer: (() => void) | null = null;
private isSafari = false;
private isMac = false;
private scrollTopCache: number = 0;

constructor(options: EditorOptions) {
Expand Down Expand Up @@ -99,7 +95,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
const document = this.editor.getDocument();

this.isSafari = !!env.isSafari;
this.isMac = !!env.isMac;
document.addEventListener('selectionchange', this.onSelectionChange);
if (this.isSafari) {
this.disposer = this.editor.attachDomEvent({
Expand Down Expand Up @@ -142,7 +137,7 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
break;

case 'mouseUp':
this.onMouseUp(event);
this.onMouseUp();
break;

case 'keyDown':
Expand All @@ -166,17 +161,9 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
let image: HTMLImageElement | null;

// Image selection
if (
rawEvent.button === MouseRightButton &&
(image =
this.getClickingImage(rawEvent) ??
this.getContainedTargetImage(rawEvent, selection)) &&
image.isContentEditable
) {
if ((image = this.getClickingImage(rawEvent)) && image.isContentEditable) {
this.selectImageWithRange(image, rawEvent);
return;
} else if (selection?.type == 'image' && selection.image !== rawEvent.target) {
this.selectBeforeOrAfterElement(editor, selection.image);

return;
}

Expand Down Expand Up @@ -297,20 +284,7 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
}
}

private onMouseUp(event: MouseUpEvent) {
let image: HTMLImageElement | null;

if (
(image = this.getClickingImage(event.rawEvent)) &&
image.isContentEditable &&
event.rawEvent.button != MouseMiddleButton &&
(event.rawEvent.button ==
MouseRightButton /* it's not possible to drag using right click */ ||
event.isClicking)
) {
this.selectImageWithRange(image, event.rawEvent);
}

private onMouseUp() {
this.detachMouseEvent();
}

Expand Down Expand Up @@ -552,27 +526,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
: null;
}

// MacOS will not create a mouseUp event if contextMenu event is not prevent defaulted.
// Make sure we capture image target even if image is wrapped
private getContainedTargetImage = (
event: MouseEvent,
previousSelection: DOMSelection | null
): HTMLImageElement | null => {
if (!this.isMac || !previousSelection || previousSelection.type !== 'image') {
return null;
}

const target = event.target as Node;
if (
isNodeOfType(target, 'ELEMENT_NODE') &&
isElementOfType(target, 'span') &&
target.firstChild === previousSelection.image
) {
return previousSelection.image;
}
return null;
};

private onFocus = () => {
if (!this.state.skipReselectOnFocus && this.state.selection) {
this.setDOMSelection(this.state.selection, this.state.tableSelection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,13 +17,16 @@ import {
isElementOfType,
isNodeOfType,
mutateSegment,
toArray,
unwrap,
} from 'roosterjs-content-model-dom';
import type { DragAndDropHelper } from '../pluginUtils/DragAndDrop/DragAndDropHelper';
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,
Expand All @@ -42,6 +46,7 @@ const DefaultOptions: Partial<ImageEditOptions> = {
};

const IMAGE_EDIT_CHANGE_SOURCE = 'ImageEdit';
const MouseLeftButton = 0;

/**
* ImageEdit plugin handles the following image editing features:
Expand Down Expand Up @@ -86,15 +91,15 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
initialize(editor: IEditor) {
this.editor = editor;
this.disposer = editor.attachDomEvent({
blur: {
beforeDispatch: () => {
this.formatImageWithContentModel(
editor,
true /* shouldSelectImage */,
true /* shouldSelectAsImageSelection*/
);
},
},
// blur: {
// beforeDispatch: () => {
// this.formatImageWithContentModel(
// editor,
// true /* shouldSelectImage */,
// true /* shouldSelectAsImageSelection*/
// );
// },
// },
});
}

Expand All @@ -118,15 +123,59 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
* exclusively by another plugin.
* @param event The event to handle:
*/
onPluginEvent(_event: PluginEvent) {}
onPluginEvent(event: PluginEvent) {
if (!this.editor) {
return;
}

switch (event.eventType) {
case 'selectionChanged':
{
const selection = event.newSelection;
const previousSelection = event.previousSelection;

if (previousSelection?.type == 'image' && selection && this.shadowSpan) {
const previousImage = previousSelection.image;
if (previousImage) {
this.formatImageWhenSelectionChange(this.editor, selection);
}
}
}

break;

// case 'keyUp':
case 'mouseUp':
{
const selection = this.editor.getDOMSelection();

if (
selection?.type == 'image' &&
(event.eventType != 'mouseUp' || event.rawEvent.button == MouseLeftButton)
) {
const image = selection.image;

if (image) {
this.startRotateAndResize(this.editor, image);
}
}
}
break;
}
}

private startEditing(
editor: IEditor,
image: HTMLImageElement,
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);
Expand Down Expand Up @@ -171,6 +220,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 = [
Expand Down Expand Up @@ -439,21 +489,63 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
image.isSelectedAsImageSelection = shouldSelectAsImageSelection;
}
});

this.cleanInfo();
return true;
}

return false;
},
{
changeSource: IMAGE_EDIT_CHANGE_SOURCE,
onNodeCreated: () => {
this.cleanInfo();
},
}
);
}
}

private formatImageWhenSelectionChange(editor: IEditor, newSelection: DOMSelection) {
const { selectedImage, imageEditInfo, lastSrc, clonedImage } = this;

if (
selectedImage?.parentNode &&
lastSrc &&
imageEditInfo &&
clonedImage &&
this.shadowSpan
) {
const parent = selectedImage.parentNode;
const index = toArray(parent.childNodes).indexOf(selectedImage);
const domIp: DOMInsertPoint = {
node: parent,
offset: index,
};

formatInsertPointWithContentModel(editor, domIp, (model, context, insertPoint) => {
if (insertPoint) {
const { paragraph, marker } = insertPoint;
const markerIndex = paragraph.segments.indexOf(marker);
const image = paragraph.segments[markerIndex + 1];

if (markerIndex >= 0 && image?.segmentType == 'Image') {
applyChange(
editor,
selectedImage,
image,
imageEditInfo,
lastSrc,
this.wasImageResized || this.isCropMode,
clonedImage
);

return true;
}
}

return false;
});
}
}

private removeImageWrapper() {
let image: HTMLImageElement | null = null;
if (this.shadowSpan && this.shadowSpan.parentElement) {
Expand Down
Loading

0 comments on commit 2f70bf8

Please sign in to comment.