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

Shadow dom image wrapper #1520

Merged
merged 10 commits into from
Jan 23, 2023
2 changes: 1 addition & 1 deletion packages/roosterjs-editor-dom/lib/utils/createElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const KnownCreateElementData: Record<KnownCreateElementDataIndex, CreateE
},
[KnownCreateElementDataIndex.ImageEditWrapper]: {
tag: 'span',
style: 'max-width:100%;position:fixed',
style: 'max-width:100%',
children: [
{
tag: 'div',
Expand Down
112 changes: 33 additions & 79 deletions packages/roosterjs-editor-plugins/lib/plugins/ImageEdit/ImageEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,22 @@ import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from './types/DragAn
import DragAndDropHandler from '../../pluginUtils/DragAndDropHandler';
import DragAndDropHelper from '../../pluginUtils/DragAndDropHelper';
import getGeneratedImageSize from './editInfoUtils/getGeneratedImageSize';
import getLatestZIndex from './editInfoUtils/getLastZIndex';
import ImageEditInfo from './types/ImageEditInfo';
import ImageHtmlOptions from './types/ImageHtmlOptions';
import { Cropper, getCropHTML } from './imageEditors/Cropper';
import { deleteEditInfo, getEditInfoFromImage } from './editInfoUtils/editInfo';
import { getRotateHTML, Rotator } from './imageEditors/Rotator';
import { getRotateHTML, Rotator, updateRotateHandlePosition } from './imageEditors/Rotator';
import { ImageEditElementClass } from './types/ImageEditElementClass';
import {
arrayPush,
Browser,
createElement,
getComputedStyle,
getObjectKeys,
removeGlobalCssStyle,
safeInstanceOf,
setGlobalCssStyles,
toArray,
unwrap,
wrap,
} from 'roosterjs-editor-dom';
import {
Resizer,
Expand Down Expand Up @@ -125,11 +124,6 @@ export default class ImageEdit implements EditorPlugin {
*/
private wasResized: boolean;

/**
* Editor zoom scale
*/
private zoomWrapper: HTMLElement;

/**
* Create a new instance of ImageEdit
* @param options Image editing options
Expand Down Expand Up @@ -163,7 +157,10 @@ export default class ImageEdit implements EditorPlugin {
*/
initialize(editor: IEditor) {
this.editor = editor;
this.disposer = editor.addDomEventHandler('blur', this.onBlur);
this.disposer = editor.addDomEventHandler({
blur: () => this.onBlur,
drop: e => e.preventDefault(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should disable onDrag instead, and only disable when image is in editing.

});
}

/**
Expand Down Expand Up @@ -207,9 +204,8 @@ export default class ImageEdit implements EditorPlugin {
deleteEditInfo(img as HTMLImageElement);
});
break;

case PluginEventType.Scroll:
this.setEditingImage(null);
case PluginEventType.BeforeDispose:
this.removeWrapper();
break;
}
}
Expand Down Expand Up @@ -307,7 +303,6 @@ export default class ImageEdit implements EditorPlugin {
];

this.editor.select(this.image);
this.toggleImageVisibility(this.image, false /** showImage */);
}
}

Expand Down Expand Up @@ -374,43 +369,28 @@ export default class ImageEdit implements EditorPlugin {
}
});

this.insertImageWrapper(this.editor, this.image, this.wrapper, this.editor.getZoomScale());
this.insertImageWrapper(this.wrapper);
}

private toggleImageVisibility(image: HTMLImageElement, showImage: boolean) {
const editorId = this.editor.getEditorDomAttribute('id');
const doc = this.editor.getDocument();
const editingId = 'editingId' + editorId;
if (showImage) {
removeGlobalCssStyle(doc, editingId);
} else {
const cssRule = `#${editorId} #${image.id} {visibility: hidden}`;
setGlobalCssStyles(doc, cssRule, editingId);
}
}
private insertImageWrapper(wrapper: HTMLSpanElement) {
const span = wrap(this.image, 'span');
const shadowRoot = span.attachShadow({
mode: 'open',
});

private insertImageWrapper(
editor: IEditor,
image: HTMLImageElement,
wrapper: HTMLSpanElement,
scale: number
) {
this.zoomWrapper = copyElementRect(image, createZoomWrapper(editor, wrapper, scale));
this.zoomWrapper.style.zIndex = `${getLatestZIndex(editor.getScrollContainer()) + 1}`;
this.editor.getDocument().body.appendChild(this.zoomWrapper);
span.style.verticalAlign = 'bottom';

shadowRoot.appendChild(wrapper);
}

/**
* Remove the temp wrapper of the image
*/
private removeWrapper = () => {
const doc = this.editor.getDocument();
if (this.zoomWrapper && doc.body?.contains(this.zoomWrapper)) {
doc.body?.removeChild(this.zoomWrapper);
this.toggleImageVisibility(this.image, true /** showImage */);
if (this.editor.contains(this.image) && this.wrapper) {
unwrap(this.image.parentNode);
}
this.wrapper = null;
this.zoomWrapper = null;
};

/**
Expand All @@ -424,6 +404,8 @@ export default class ImageEdit implements EditorPlugin {
const cropContainers = getEditElements(wrapper, ImageEditElementClass.CropContainer);
const cropOverlays = getEditElements(wrapper, ImageEditElementClass.CropOverlay);
const resizeHandles = getEditElements(wrapper, ImageEditElementClass.ResizeHandle);
const rotateCenter = getEditElements(wrapper, ImageEditElementClass.RotateCenter)[0];
const rotateHandle = getEditElements(wrapper, ImageEditElementClass.RotateHandle)[0];
const cropHandles = getEditElements(wrapper, ImageEditElementClass.CropHandle);

// Cropping and resizing will show different UI, so check if it is cropping here first
Expand Down Expand Up @@ -457,9 +439,8 @@ export default class ImageEdit implements EditorPlugin {
wrapper.style.height = getPx(visibleHeight);
wrapper.style.margin = `${marginVertical}px ${marginHorizontal}px`;
wrapper.style.transform = `rotate(${angleRad}rad)`;
this.zoomWrapper.style.width = getPx(visibleWidth);
this.zoomWrapper.style.height = getPx(visibleHeight);
fitImageContainer(this.editor, this.zoomWrapper, angleRad);
this.wrapper.style.width = getPx(visibleWidth);
this.wrapper.style.height = getPx(visibleHeight);

// Update the text-alignment to avoid the image to overflow if the parent element have align center or right
// or if the direction is Right To Left
Expand All @@ -484,6 +465,7 @@ export default class ImageEdit implements EditorPlugin {
setSize(cropOverlays[1], undefined, 0, 0, cropBottomPx, cropRightPx, undefined);
setSize(cropOverlays[2], cropLeftPx, undefined, 0, 0, undefined, cropBottomPx);
setSize(cropOverlays[3], 0, cropTopPx, undefined, 0, cropLeftPx, undefined);

updateHandleCursor(cropHandles, angleRad);
} else {
// For rotate/resize, set the margin of the image so that cropped part won't be visible
Expand All @@ -504,6 +486,14 @@ export default class ImageEdit implements EditorPlugin {
this.updateWrapper();
}

updateRotateHandlePosition(
this.editInfo,
this.editor.getVisibleViewport(),
marginVertical,
rotateCenter,
rotateHandle
);

updateHandleCursor(resizeHandles, angleRad);
}
}
Expand Down Expand Up @@ -646,42 +636,6 @@ function getColorString(color: string | ModeIndependentColor, isDarkMode: boolea
return isDarkMode ? color.darkModeColor.trim() : color.lightModeColor.trim();
}

function fitImageContainer(editor: IEditor, zoomWrapper: HTMLElement, angle: number) {
const angleIndex = handleRadIndexCalculator(angle);
const isVertical = (angleIndex >= 2 && angleIndex < 4) || angleIndex >= 6;
const editorTop = editor.getScrollContainer()?.getBoundingClientRect()?.top;
const { top, width, height } = zoomWrapper?.getBoundingClientRect();
if (editorTop > top) {
const rotatePercent = 100 * Math.abs(angle);
const zoomWrapperHeight = editorTop - top;
const zoomWrapperHeightPercent = isVertical
? rotatePercent * (zoomWrapperHeight / width)
: 100 * (zoomWrapperHeight / height);

zoomWrapper.style.clipPath = `polygon(0 ${zoomWrapperHeightPercent}%, 100% ${zoomWrapperHeightPercent}%, 100% ${
isVertical ? rotatePercent : '100'
}%, 0 ${isVertical ? rotatePercent : '100'}%)`;
}
}

function copyElementRect(originalElement: HTMLElement, element: HTMLElement) {
const { top, left, right, bottom } = originalElement.getBoundingClientRect();
element.style.top = `${top}px`;
element.style.bottom = `${bottom}px`;
element.style.right = `${right}px`;
element.style.left = `${left}px`;
return element;
}

function createZoomWrapper(editor: IEditor, wrapper: HTMLSpanElement, scale: number) {
const zoomWrapper = editor.getDocument().createElement('div');
zoomWrapper.style.transform = `scale(${scale || 1})`;
zoomWrapper.style.transformOrigin = 'top left';
zoomWrapper.style.position = 'fixed';
zoomWrapper.appendChild(wrapper);
return zoomWrapper;
}

function getStylePropertyValue(element: HTMLElement, property: string): string {
return element.ownerDocument.defaultView.getComputedStyle(element).getPropertyValue(property);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import DragAndDropContext from '../types/DragAndDropContext';
import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler';
import ImageEditInfo, { RotateInfo } from '../types/ImageEditInfo';
import ImageHtmlOptions from '../types/ImageHtmlOptions';
import { CreateElementData } from 'roosterjs-editor-types';
import { CreateElementData, Rect } from 'roosterjs-editor-types';
import { ImageEditElementClass } from '../types/ImageEditElementClass';
import { RotateInfo } from '../types/ImageEditInfo';

const ROTATE_SIZE = 32;
const ROTATE_GAP = 15;
Expand Down Expand Up @@ -39,6 +39,33 @@ export const Rotator: DragAndDropHandler<DragAndDropContext, RotateInfo> = {
},
};

/**
* @internal
* Move rotate handle. When image is very close to the border of editor, rotate handle may not be visible.
* Fix it by reduce the distance from image to rotate handle
*/
export function updateRotateHandlePosition(
editInfo: ImageEditInfo,
editorRect: Rect,
marginVertical: number,
rotateCenter: HTMLElement,
rotateHandle: HTMLElement
) {
const top = rotateHandle.getBoundingClientRect()?.top - editorRect?.top;
const { angleRad, heightPx } = editInfo;
const cosAngle = Math.cos(angleRad);
const adjustedDistance =
cosAngle <= 0
? Number.MAX_SAFE_INTEGER
: (top + heightPx / 2 + marginVertical) / cosAngle - heightPx / 2;

const rotateGap = Math.max(Math.min(ROTATE_GAP, adjustedDistance), 0);
const rotateTop = Math.max(Math.min(ROTATE_SIZE, adjustedDistance - rotateGap), 0);
rotateCenter.style.top = -rotateGap + 'px';
rotateCenter.style.height = rotateGap + 'px';
rotateHandle.style.top = -rotateTop + 'px';
}

/**
* @internal
* Get HTML for rotate elements, including the rotate handle with icon, and a line between the handle and the image
Expand Down