Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Image Edit Operations #2670

Merged
merged 62 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
52f30e4
WIP
juliaroldi Apr 5, 2024
5bb6b15
fix conflicts
juliaroldi Apr 5, 2024
36f592c
conflict
juliaroldi Apr 5, 2024
ec80d60
WIP
juliaroldi Apr 5, 2024
4aa4828
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
juliaroldi Apr 5, 2024
ec97315
WIP
juliaroldi Apr 5, 2024
531985b
WIP
juliaroldi Apr 8, 2024
b91e403
croppers
juliaroldi Apr 10, 2024
7a06999
porting
juliaroldi Apr 11, 2024
091f2db
WIPP
juliaroldi Apr 12, 2024
c11771f
fix conflicts
juliaroldi Apr 17, 2024
13dcca1
WIP
juliaroldi Apr 18, 2024
816b769
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
juliaroldi Apr 22, 2024
cc260bc
WIP
juliaroldi Apr 23, 2024
b7a0ca8
conflicts
juliaroldi Apr 23, 2024
a1586a5
fixes
juliaroldi Apr 23, 2024
89d51b1
WIP
juliaroldi Apr 24, 2024
63125da
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
juliaroldi Apr 24, 2024
21599f9
fix build
juliaroldi Apr 25, 2024
cca526e
remove function
juliaroldi Apr 25, 2024
7b5cd18
wip: clean/refactor
juliaroldi Apr 26, 2024
75bea9e
wip: clean
juliaroldi Apr 26, 2024
554e3da
clean
juliaroldi Apr 26, 2024
207657d
wip
juliaroldi Apr 26, 2024
3806813
Merge branch 'u/juliaroldi/image-span' of https://github.com/microsof…
juliaroldi Apr 29, 2024
3221722
WIP
juliaroldi Apr 29, 2024
8a50224
Merge branch 'u/juliaroldi/image-span' of https://github.com/microsof…
juliaroldi Apr 30, 2024
abe2b18
wip
juliaroldi Apr 30, 2024
5c15d3e
Merge branch 'u/juliaroldi/image-span' of https://github.com/microsof…
juliaroldi Apr 30, 2024
f91b48b
port image
juliaroldi Apr 30, 2024
f45bf71
wip
juliaroldi May 2, 2024
69b4204
conflicts
juliaroldi May 2, 2024
ee28ea4
test
juliaroldi May 20, 2024
d5e6775
conflict
juliaroldi May 20, 2024
4766498
WIP
juliaroldi May 21, 2024
3642b10
unit test
juliaroldi May 23, 2024
7ea11d5
test
juliaroldi May 24, 2024
da849be
Merge branch 'master' of https://github.com/microsoft/roosterjs into …
juliaroldi May 24, 2024
98d3520
test
juliaroldi May 24, 2024
cb87afd
test
juliaroldi May 28, 2024
814b54f
remove console.log
juliaroldi May 28, 2024
dd732bb
Merge branch 'master' into u/juliaroldi/port-image
juliaroldi May 28, 2024
d95360c
tests
juliaroldi May 29, 2024
71f830b
Merge branch 'u/juliaroldi/port-image' of https://github.com/microsof…
juliaroldi May 29, 2024
6251d8d
tests
juliaroldi May 29, 2024
ac8790f
changed to protected
juliaroldi May 29, 2024
6d86454
image operations
juliaroldi May 29, 2024
2ec3974
fix test
juliaroldi May 29, 2024
5e1be98
remove code
juliaroldi May 29, 2024
85198f2
Merge branch 'master' into u/juliaroldi/image-edit-operations
juliaroldi May 30, 2024
8b5961c
remove editor
juliaroldi Jun 3, 2024
357c7c7
Merge branch 'u/juliaroldi/image-edit-operations' of https://github.c…
juliaroldi Jun 3, 2024
53b044b
fix test
juliaroldi Jun 3, 2024
4c5963a
WIP
juliaroldi Jun 3, 2024
f86541c
fixes
juliaroldi Jun 4, 2024
3c465c6
fix test
juliaroldi Jun 4, 2024
27792a0
status
juliaroldi Jun 4, 2024
9a9abae
test
juliaroldi Jun 4, 2024
baff339
fix conflicts
juliaroldi Jun 4, 2024
4c8d7eb
add mutate block
juliaroldi Jun 4, 2024
26d6e90
fixes texts
juliaroldi Jun 5, 2024
9dd412a
Merge branch 'master' into u/juliaroldi/image-edit-operations
juliaroldi Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 8 additions & 17 deletions demo/scripts/controlsV2/demoButtons/createImageEditButtons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@ function createImageCropButton(handler: ImageEditor): RibbonButton<'buttonNameCr
iconName: 'Crop',
isDisabled: formatState =>
!formatState.canAddImageAltText || !handler.isOperationAllowed('crop'),
onClick: editor => {
const selection = editor.getDOMSelection();
if (selection.type === 'image' && selection.image) {
handler.cropImage(selection.image);
}
onClick: () => {
handler.cropImage();
},
};
}
Expand All @@ -39,13 +36,10 @@ function createImageRotateButton(handler: ImageEditor): RibbonButton<'buttonName
items: directions,
},
isDisabled: formatState => !formatState.canAddImageAltText,
onClick: (editor, direction) => {
const selection = editor.getDOMSelection();
if (selection.type === 'image' && selection.image) {
const rotateDirection = direction as 'left' | 'right';
const rad = degreeToRad(rotateDirection == 'left' ? 270 : 90);
handler.rotateImage(selection.image, rad);
}
onClick: (_editor, direction) => {
const rotateDirection = direction as 'left' | 'right';
const rad = degreeToRad(rotateDirection == 'left' ? 270 : 90);
handler.rotateImage(rad);
},
};
}
Expand All @@ -68,11 +62,8 @@ function createImageFlipButton(handler: ImageEditor): RibbonButton<'buttonNameFl
items: flipDirections,
},
isDisabled: formatState => !formatState.canAddImageAltText,
onClick: (editor, flipDirection) => {
const selection = editor.getDOMSelection();
if (selection.type === 'image' && selection.image) {
handler.flipImage(selection.image, flipDirection as 'horizontal' | 'vertical');
}
onClick: (_editor, flipDirection) => {
handler.flipImage(flipDirection as 'horizontal' | 'vertical');
},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ const ImageRotateMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEdit
imageEditor.canRegenerateImage(node as HTMLImageElement)
);
},
onClick: (key, _editor, node, strings, uiUtilities, imageEdit) => {
onClick: (key, _editor, _node, _strings, _uiUtilities, imageEdit) => {
switch (key) {
case 'menuNameImageRotateLeft':
imageEdit?.rotateImage(node as HTMLImageElement, -Math.PI / 2);
imageEdit?.rotateImage(-Math.PI / 2);
break;
case 'menuNameImageRotateRight':
imageEdit?.rotateImage(node as HTMLImageElement, Math.PI / 2);
imageEdit?.rotateImage(Math.PI / 2);
break;
}
},
Expand All @@ -116,13 +116,13 @@ const ImageFlipMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEditor
imageEditor.canRegenerateImage(node as HTMLImageElement)
);
},
onClick: (key, _editor, node, strings, uiUtilities, imageEdit) => {
onClick: (key, _editor, _node, _strings, _uiUtilities, imageEdit) => {
switch (key) {
case 'menuNameImageRotateFlipHorizontally':
imageEdit?.flipImage(node as HTMLImageElement, 'horizontal');
imageEdit?.flipImage('horizontal');
break;
case 'menuNameImageRotateFlipVertically':
imageEdit?.flipImage(node as HTMLImageElement, 'vertical');
imageEdit?.flipImage('vertical');
break;
}
},
Expand All @@ -137,8 +137,8 @@ const ImageCropMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEditor
imageEditor.canRegenerateImage(node as HTMLImageElement)
);
},
onClick: (_, _editor, node, strings, uiUtilities, imageEdit) => {
imageEdit?.cropImage(node as HTMLImageElement);
onClick: (_, _editor, _node, _strings, _uiUtilities, imageEdit) => {
imageEdit?.cropImage();
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface OptionState {
// Editor options
isRtl: boolean;
disableCache: boolean;
experimentalFeatures: Set<ExperimentalFeature>;
}

export interface OptionPaneProps extends OptionState, SidePaneElementProps {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { Code } from './Code';
import { DefaultFormatPane } from './DefaultFormatPane';
import { EditorCode } from './codes/EditorCode';
import { ExperimentalFeatures } from './ExperimentalFeatures';
import { MainPane } from '../../mainPane/MainPane';
import { OptionPaneProps, OptionState } from './OptionState';
import { Plugins } from './Plugins';
Expand Down Expand Up @@ -55,7 +56,12 @@ export class OptionsPane extends React.Component<OptionPaneProps, OptionState> {
</summary>
<Plugins state={this.state} resetState={this.resetState} />
</details>
<details></details>
<details>
<summary>
<b>Experimental features</b>
</summary>
<ExperimentalFeatures state={this.state} resetState={this.resetState} />
</details>
<div>
<br />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { isElementOfType } from './isElementOfType';
import { isNodeOfType } from './isNodeOfType';
import { wrap } from './wrap';
import { isElementOfType, isNodeOfType, wrap } from 'roosterjs-content-model-dom';

/**
* @internal
* Ensure image is wrapped by a span element
* @param image
* @returns the image
*/
export function ensureImageHasSpanParent(
image: HTMLImageElement,
entryPoint?: string
): HTMLImageElement {
export function ensureImageHasSpanParent(image: HTMLImageElement): HTMLImageElement {
const parent = image.parentElement;
// console.log(parent, entryPoint);

if (
parent &&
isNodeOfType(parent, 'ELEMENT_NODE') &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { addRangeToSelection } from './addRangeToSelection';
import { ensureImageHasSpanParent } from './ensureImageHasSpanParent';
import { ensureUniqueId } from '../setEditorStyle/ensureUniqueId';
import { findLastedCoInMergedCell } from './findLastedCoInMergedCell';
import { findTableCellElement } from './findTableCellElement';
import {
ensureImageHasSpanParent,
isNodeOfType,
parseTableCells,
toArray,
} from 'roosterjs-content-model-dom';
import { isNodeOfType, parseTableCells, toArray } from 'roosterjs-content-model-dom';
import type {
ParsedTable,
SelectionChangedEvent,
Expand Down Expand Up @@ -55,8 +51,8 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
core.api.setEditorStyle(
core,
DOM_SELECTION_CSS_KEY,
`outline-style:auto!important; outline-color:${imageSelectionColor}!important;display: ${
core.environment.isSafari ? 'inline-block' : 'inline-flex'
`outline-style:solid!important; outline-color:${imageSelectionColor}!important;display: ${
core.environment.isSafari ? '-webkit-inline-flex' : 'inline-flex'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

};`,
[`span:has(>img#${ensureUniqueId(image, IMAGE_ID)})`]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ describe('setDOMSelection', () => {
expect(setEditorStyleSpy).toHaveBeenCalledWith(
core,
'_DOMSelection',
'outline-style:auto!important; outline-color:#DB626C!important;display: inline-flex;',
'outline-style:solid!important; outline-color:#DB626C!important;display: inline-flex;',
['span:has(>img#image_0)']
);
expect(setEditorStyleSpy).toHaveBeenCalledWith(
Expand Down Expand Up @@ -370,7 +370,7 @@ describe('setDOMSelection', () => {
expect(setEditorStyleSpy).toHaveBeenCalledWith(
core,
'_DOMSelection',
'outline-style:auto!important; outline-color:red!important;display: inline-flex;',
'outline-style:solid!important; outline-color:red!important;display: inline-flex;',
['span:has(>img#image_0)']
);
expect(setEditorStyleSpy).toHaveBeenCalledWith(
Expand Down Expand Up @@ -437,7 +437,7 @@ describe('setDOMSelection', () => {
expect(setEditorStyleSpy).toHaveBeenCalledWith(
coreValue,
'_DOMSelection',
'outline-style:auto!important; outline-color:DarkColorMock-red!important;display: inline-flex;',
'outline-style:solid!important; outline-color:DarkColorMock-red!important;display: inline-flex;',
['span:has(>img#image_0)']
);
expect(setEditorStyleSpy).toHaveBeenCalledWith(
Expand Down Expand Up @@ -498,7 +498,7 @@ describe('setDOMSelection', () => {
expect(setEditorStyleSpy).toHaveBeenCalledWith(
core,
'_DOMSelection',
'outline-style:auto!important; outline-color:#DB626C!important;display: inline-flex;',
'outline-style:solid!important; outline-color:#DB626C!important;display: inline-flex;',
['span:has(>img#image_0)']
);
expect(setEditorStyleSpy).toHaveBeenCalledWith(
Expand Down Expand Up @@ -559,7 +559,7 @@ describe('setDOMSelection', () => {
expect(setEditorStyleSpy).toHaveBeenCalledWith(
core,
'_DOMSelection',
'outline-style:auto!important; outline-color:#DB626C!important;display: inline-flex;',
'outline-style:solid!important; outline-color:#DB626C!important;display: inline-flex;',
['span:has(>img#image_0_0)']
);
expect(setEditorStyleSpy).toHaveBeenCalledWith(
Expand Down
1 change: 0 additions & 1 deletion packages/roosterjs-content-model-dom/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export { toArray } from './domUtils/toArray';
export { moveChildNodes, wrapAllChildNodes } from './domUtils/moveChildNodes';
export { wrap } from './domUtils/wrap';
export { unwrap } from './domUtils/unwrap';
export { ensureImageHasSpanParent } from './domUtils/ensureImageHasSpanParent';
export {
isEntityElement,
findClosestEntityWrapper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const BooleanDefinition = createBooleanDefinition(true);
* @internal
* Definition of ImageMetadataFormat
*/
export const ImageMetadataFormatDefinition = createObjectDefinition<Required<ImageMetadataFormat>>({
const ImageMetadataFormatDefinition = createObjectDefinition<Required<ImageMetadataFormat>>({
widthPx: NumberDefinition,
heightPx: NumberDefinition,
leftPercent: NumberDefinition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { updateImageEditInfo } from './utils/updateImageEditInfo';
import { updateRotateHandle } from './Rotator/updateRotateHandle';
import { updateWrapper } from './utils/updateWrapper';
import {
ensureImageHasSpanParent,
getSelectedSegmentsAndParagraphs,
isElementOfType,
isNodeOfType,
mutateSegment,
unwrap,
} from 'roosterjs-content-model-dom';
import type { DragAndDropHelper } from '../pluginUtils/DragAndDrop/DragAndDropHelper';
Expand All @@ -39,7 +39,7 @@ const DefaultOptions: Partial<ImageEditOptions> = {
preserveRatio: true,
disableRotate: false,
disableSideResize: false,
onSelectState: 'resizeAndRotate',
onSelectState: 'resize',
};

const IMAGE_EDIT_CHANGE_SOURCE = 'ImageEdit';
Expand Down Expand Up @@ -127,7 +127,6 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
apiOperation?: ImageEditOperation
) {
const contentModelImage = getContentModelImage(editor);
ensureImageHasSpanParent(image);
const imageSpan = image.parentElement;
if (
!contentModelImage ||
Expand Down Expand Up @@ -293,13 +292,15 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
}

public canRegenerateImage(image: HTMLImageElement): boolean {
return canRegenerateImage(image) || canRegenerateImage(this.selectedImage);
return canRegenerateImage(image);
}

public cropImage(image: HTMLImageElement) {
if (!this.editor) {
public cropImage() {
const selection = this.editor?.getDOMSelection();
if (!this.editor || !selection || selection.type !== 'image') {
return;
}
let image = selection.image;
if (this.wrapper && this.selectedImage && this.shadowSpan) {
image = this.removeImageWrapper() ?? image;
}
Expand Down Expand Up @@ -411,35 +412,39 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
this.shadowSpan
) {
editor.formatContentModel(
(model, _) => {
(model, context) => {
const selectedSegmentsAndParagraphs = getSelectedSegmentsAndParagraphs(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here we edit image based on current selection, then why we need to pass in image object in other editing functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pass the image element to add and remove the wrapper element. Here we edit the selected content model image

model,
false
);
if (!selectedSegmentsAndParagraphs[0]) {
return false;
}
const segment = selectedSegmentsAndParagraphs[0][0];

if (
this.lastSrc &&
this.selectedImage &&
this.imageEditInfo &&
this.clonedImage &&
segment.segmentType == 'Image'
) {
applyChange(
editor,
this.selectedImage,
segment,
this.imageEditInfo,
this.lastSrc,
this.wasImageResized || this.isCropMode,
this.clonedImage
);
segment.isSelected = shouldSelectImage;
segment.isSelectedAsImageSelection = shouldSelectAsImageSelection;

const segment = selectedSegmentsAndParagraphs[0][0];
const paragraph = selectedSegmentsAndParagraphs[0][1];

if (paragraph && segment.segmentType == 'Image') {
mutateSegment(paragraph, segment, image => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice.

And I think we only need to return true for formatContentModel when image is really changed, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you can move this down into the "if" block below.

if (
this.lastSrc &&
this.selectedImage &&
this.imageEditInfo &&
this.clonedImage
) {
applyChange(
editor,
this.selectedImage,
image,
this.imageEditInfo,
this.lastSrc,
this.wasImageResized || this.isCropMode,
this.clonedImage
);
image.isSelected = shouldSelectImage;
image.isSelectedAsImageSelection = shouldSelectAsImageSelection;
}
});
return true;
}

Expand Down Expand Up @@ -473,7 +478,12 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
return image;
}

public flipImage(image: HTMLImageElement, direction: 'horizontal' | 'vertical') {
public flipImage(direction: 'horizontal' | 'vertical') {
const selection = this.editor?.getDOMSelection();
if (!this.editor || !selection || selection.type !== 'image') {
return;
}
const image = selection.image;
if (this.editor) {
this.editImage(this.editor, image, 'flip', imageEditInfo => {
const angleRad = imageEditInfo.angleRad || 0;
Expand All @@ -497,7 +507,12 @@ export class ImageEditPlugin implements ImageEditor, EditorPlugin {
}
}

public rotateImage(image: HTMLImageElement, angleRad: number) {
public rotateImage(angleRad: number) {
const selection = this.editor?.getDOMSelection();
if (!this.editor || !selection || selection.type !== 'image') {
return;
}
const image = selection.image;
if (this.editor) {
this.editImage(this.editor, image, 'rotate', imageEditInfo => {
imageEditInfo.angleRad = (imageEditInfo.angleRad || 0) + angleRad;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export function createImageWrapper(
const doc = editor.getDocument();

let rotators: HTMLDivElement[] = [];
if (!options.disableRotate && (operation === 'resizeAndRotate' || operation === 'rotate')) {
if (!options.disableRotate && operation === 'rotate') {
rotators = createImageRotator(doc, htmlOptions);
}
let resizers: HTMLDivElement[] = [];
if (operation === 'resize' || operation === 'resizeAndRotate') {
if (operation === 'resize') {
resizers = createImageResizer(doc);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import type { ContentModelImage, IEditor } from 'roosterjs-content-model-types';
* @internal
*/
export function getContentModelImage(editor: IEditor): ContentModelImage | null {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is this used? This will give you a cloned object but not the original image object. Is that ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used to get the image dataset

const model = editor.getContentModelCopy('disconnected' /*mode*/);
const model = editor.getContentModelCopy('disconnected');
const selectedSegments = getSelectedSegments(model, false /*includeFormatHolder*/);
if (selectedSegments.length == 1 && selectedSegments[0].segmentType == 'Image') {
return selectedSegments[0];
return selectedSegments[0] as ContentModelImage;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine here, but if we do this kind of type cast in other place, it will be dangerous.

Actually, if you only want dataset, just get it directly from the connect model, and return the dataset instead.

Disconnected model actually need a clone, which is not as good perf as connected one.

function getSelectedImageDataset() {
  let result;

  editor.formatContentModel(model => {
    const image = getSelectedImage(model);
    result = getImageMetadata(image);
    return false;
  });

  return result;

Something like this.

}
return null;
}
Loading
Loading