Skip to content

Commit

Permalink
Add image size checkmarks to context menu (#2168)
Browse files Browse the repository at this point in the history
* Allow a max error un percentage size check

* Allow a checkmark to be shown in ctx menu

* Add checkmark to image sizes

* Add missing type

* Fix types

* Add comment

* Attempt to fix image selection

* Revert unneeded changes in image selection

* Use attr as backup in resize calc

* Rely entirely on image selection

* Revert changes to domeventplugin

* Copy changes into content model adapter
  • Loading branch information
ianeli1 authored Nov 2, 2023
1 parent 8c444f3 commit 37e8fb5
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-editor-types'

const Escape = 'Escape';
const Delete = 'Delete';
const mouseLeftButton = 0;
const mouseMiddleButton = 1;

/**
* @internal
Expand Down Expand Up @@ -44,7 +44,7 @@ export default class ImageSelection implements EditorPlugin {
if (
safeInstanceOf(target, 'HTMLImageElement') &&
target.isContentEditable &&
event.rawEvent.button === mouseLeftButton
event.rawEvent.button != mouseMiddleButton
) {
this.editor.select(target);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import createContextMenuProvider from '../utils/createContextMenuProvider';
import showInputDialog from '../../inputDialog/utils/showInputDialog';
import { canRegenerateImage, resetImage, resizeByPercentage } from 'roosterjs-editor-plugins';
import { DocumentCommand, ImageEditOperation } from 'roosterjs-editor-types';
import { safeInstanceOf } from 'roosterjs-editor-dom';
import {
canRegenerateImage,
isResizedTo,
resetImage,
resizeByPercentage,
} from 'roosterjs-editor-plugins';
import { DocumentCommand, ImageEditOperation, SelectionRangeTypes } from 'roosterjs-editor-types';
import { getObjectKeys } from 'roosterjs-editor-dom';
import { setImageAltText } from 'roosterjs-editor-api';
import type ContextMenuItem from '../types/ContextMenuItem';
import type { EditorPlugin, IEditor } from 'roosterjs-editor-types';
Expand Down Expand Up @@ -40,6 +45,13 @@ const ImageAltTextMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEdi
},
};

const sizeMap: { [key in ImageEditMenuItemStringKey]?: number } = {
menuNameImageSizeBestFit: 0,
menuNameImageSizeSmall: 0.25,
menuNameImageSizeMedium: 0.5,
menuNameImageSizeOriginal: 1,
};

const ImageResizeMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEdit> = {
key: 'menuNameImageResize',
unlocalizedText: 'Size',
Expand All @@ -49,34 +61,40 @@ const ImageResizeMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEdit
menuNameImageSizeMedium: 'Medium',
menuNameImageSizeOriginal: 'Original',
},
onClick: (key, editor, node) => {
onClick: (key, editor, _) => {
const selection = editor.getSelectionRangeEx();
if (selection.type !== SelectionRangeTypes.ImageSelection) {
return;
}
editor.addUndoSnapshot(() => {
let percentage = 0;
switch (key) {
case 'menuNameImageSizeSmall':
percentage = 0.25;
break;
case 'menuNameImageSizeMedium':
percentage = 0.5;
break;
case 'menuNameImageSizeOriginal':
percentage = 1;
break;
}
const percentage = sizeMap[key];

if (percentage > 0) {
if (percentage != undefined && percentage > 0) {
resizeByPercentage(
editor,
node as HTMLImageElement,
selection.image,
percentage,
10 /*minWidth*/,
10 /*minHeight*/
);
} else {
resetImage(editor, node as HTMLImageElement);
resetImage(editor, selection.image);
}
});
},
getSelectedId: (editor, _) => {
const selection = editor.getSelectionRangeEx();
return (
(selection.type === SelectionRangeTypes.ImageSelection &&
getObjectKeys(sizeMap).find(key => {
return key == 'menuNameImageSizeBestFit'
? !selection.image.hasAttribute('width') &&
!selection.image.hasAttribute('height')
: isResizedTo(selection.image, sizeMap[key]!);
})) ||
null
);
},
};

const ImageRotateMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEdit> = {
Expand Down Expand Up @@ -184,8 +202,9 @@ const ImageCutMenuItem: ContextMenuItem<ImageEditMenuItemStringKey, ImageEdit> =
},
};

function shouldShowImageEditItems(editor: IEditor, node: Node) {
return safeInstanceOf(node, 'HTMLImageElement') && node.isContentEditable;
function shouldShowImageEditItems(editor: IEditor, _: Node) {
const selection = editor.getSelectionRangeEx();
return selection.type === SelectionRangeTypes.ImageSelection && !!selection.image;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export default interface ContextMenuItem<TString extends string, TContext = neve
*/
shouldShow?: (editor: IEditor, targetNode: Node, context?: TContext) => boolean;

/**
* A callback function to verify which subitem ID should have a checkmark
* @param editor The editor object that triggers this event
* @param targetNode The node that user is clicking onto
* @returns ID to be shown as selected, null for none
*/
getSelectedId?: (editor: IEditor, targetNode: Node) => TString | null;

/**
* A key-value map for sub menu items, key is the key of menu item, value is unlocalized string
* When click on a child item, onClick handler will be triggered with the key of the clicked child item passed in as the second parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,19 @@ class ContextMenuProviderImpl<TString extends string, TContext>
.filter(
item => !item.shouldShow || item.shouldShow(this.editor!, node, this.context)
)
.map(item => this.convertMenuItems(item))
.map(item => this.convertMenuItems(item, node))
: [];
}

setUIUtilities(uiUtilities: UIUtilities) {
this.uiUtilities = uiUtilities;
}

private convertMenuItems(item: ContextMenuItem<TString, TContext>): IContextualMenuItem {
private convertMenuItems(
item: ContextMenuItem<TString, TContext>,
node: Node
): IContextualMenuItem {
const selectedId = item.getSelectedId?.(this.editor!, node);
return {
key: item.key,
data: item,
Expand All @@ -85,6 +89,12 @@ class ContextMenuProviderImpl<TString extends string, TContext>
onRender: item.itemRender
? subItem => item.itemRender?.(subItem, () => this.onClick(item, key))
: undefined,
iconProps:
key == selectedId
? {
iconName: 'Checkmark',
}
: undefined,
})),
...(item.commandBarSubMenuProperties || {}),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-editor-types'

const Escape = 'Escape';
const Delete = 'Delete';
const mouseLeftButton = 0;
const mouseMiddleButton = 1;

/**
* Detect image selection and help highlight the image
Expand Down Expand Up @@ -43,7 +43,7 @@ export default class ImageSelection implements EditorPlugin {
if (
safeInstanceOf(target, 'HTMLImageElement') &&
target.isContentEditable &&
event.rawEvent.button === mouseLeftButton
event.rawEvent.button != mouseMiddleButton
) {
this.editor.select(target);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ import { getEditInfoFromImage } from '../editInfoUtils/editInfo';
* Check if the image is already resized to the given percentage
* @param image The image to check
* @param percentage The percentage to check
* @param maxError Maximum difference of pixels to still be considered the same size
*/
export default function isResizedTo(image: HTMLImageElement, percentage: number): boolean {
export default function isResizedTo(
image: HTMLImageElement,
percentage: number,
maxError: number = 1
): boolean {
const editInfo = getEditInfoFromImage(image);
//Image selection will sometimes return an image which is currently hidden and wrapped. Use HTML attributes as backup
const visibleHeight = editInfo.heightPx || image.height;
const visibleWidth = editInfo.widthPx || image.width;
if (editInfo) {
const { width, height } = getTargetSizeByPercentage(editInfo, percentage);
return (
Math.round(width) == Math.round(editInfo.widthPx) &&
Math.round(height) == Math.round(editInfo.heightPx)
Math.abs(width - visibleWidth) < maxError && Math.abs(height - visibleHeight) < maxError
);
}
return false;
Expand Down

0 comments on commit 37e8fb5

Please sign in to comment.