Skip to content

Commit

Permalink
Support resize image on mobile and custom resize handle style (#988)
Browse files Browse the repository at this point in the history
* Support Image edit on Mobile and custom resize handle

* Fix code

* Pass parameters

* Fix lint

* Fix lint

* Fix lint

* Fix lint

* Fix lint

* Fix lint

* Fix lint

* Use browserInfo to detect isMobile and refactor how to customize handle

* Fix circular dependencies

* Fix test

* Fix lint

* Fix lint

* Refine code

* Refine

* Fix test

* Fix type

Co-authored-by: Bryan Valverde U <bvalverde@microsoft.com>
  • Loading branch information
haven2world and BryanValverdeU authored May 25, 2022
1 parent 1b7f062 commit ecb9098
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 71 deletions.
19 changes: 17 additions & 2 deletions packages/roosterjs-editor-dom/lib/utils/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ const isAndroidRegex = /android/i;
* Get current browser information from user agent string
* @param userAgent The userAgent string of a browser
* @param appVersion The appVersion string of a browser
* @param vendor The vendor string of a browser
* @returns The BrowserInfo object calculated from the given userAgent and appVersion
*/
export function getBrowserInfo(userAgent: string, appVersion: string): BrowserInfo {
export function getBrowserInfo(userAgent: string, appVersion: string, vendor?: string): BrowserInfo {
// checks whether the browser is running in IE
// IE11 will use rv in UA instead of MSIE. Unfortunately Firefox also uses this. We should also look for "Trident" to confirm this.
// There have been cases where companies using older version of IE and custom UserAgents have broken this logic (e.g. IE 10 and KellyServices)
Expand All @@ -22,6 +23,19 @@ export function getBrowserInfo(userAgent: string, appVersion: string): BrowserIn
let isSafari = false;
let isEdge = false;
let isWebKit = userAgent.indexOf('WebKit') != -1;
let isMobileOrTablet = false;

// Reference: http://detectmobilebrowsers.com/
// The default regex on the website doesn't consider tablet.
// To support tablet, add |android|ipad|playbook|silk to the first regex according to the info in /about page
((userAgentOrVendor: string) => {
if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(userAgentOrVendor)
|| /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(userAgentOrVendor.substr(0, 4))
) {
isMobileOrTablet = true;
}
})(userAgent || vendor || "");


if (!isIE) {
isChrome = userAgent.indexOf('Chrome') != -1;
Expand Down Expand Up @@ -56,12 +70,13 @@ export function getBrowserInfo(userAgent: string, appVersion: string): BrowserIn
isEdge,
isIEOrEdge: isIE || isEdge,
isAndroid,
isMobileOrTablet,
};
}

/**
* Browser object contains browser and operating system information of current environment
*/
export const Browser = window
? getBrowserInfo(window.navigator.userAgent, window.navigator.appVersion)
? getBrowserInfo(window.navigator.userAgent, window.navigator.appVersion, window.navigator.vendor)
: {};
8 changes: 7 additions & 1 deletion packages/roosterjs-editor-dom/test/utils/BrowserTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BrowserInfo } from 'roosterjs-editor-types';
import { getBrowserInfo } from '../../lib/utils/Browser';

function runBrowserDataTest(userAgent: string, appVersion: string, expected: BrowserInfo): void {
let b = getBrowserInfo(userAgent, appVersion);
let b = getBrowserInfo(userAgent, appVersion, '');
expect(b.isChrome).toBe(expected.isChrome);
expect(b.isEdge).toBe(expected.isEdge);
expect(b.isFirefox).toBe(expected.isFirefox);
Expand All @@ -28,6 +28,7 @@ describe('getBrowserData', () => {
isSafari: true,
isWebKit: true,
isWin: false,
isMobileOrTablet: false,
}
);
});
Expand All @@ -46,6 +47,7 @@ describe('getBrowserData', () => {
isSafari: false,
isWebKit: false,
isWin: true,
isMobileOrTablet: false,
}
);
});
Expand All @@ -64,6 +66,7 @@ describe('getBrowserData', () => {
isSafari: false,
isWebKit: true,
isWin: true,
isMobileOrTablet: false,
}
);
});
Expand All @@ -82,6 +85,7 @@ describe('getBrowserData', () => {
isSafari: false,
isWebKit: false,
isWin: true,
isMobileOrTablet: false,
}
);
});
Expand All @@ -100,6 +104,7 @@ describe('getBrowserData', () => {
isSafari: false,
isWebKit: false,
isWin: true,
isMobileOrTablet: false,
}
);
});
Expand All @@ -118,6 +123,7 @@ describe('getBrowserData', () => {
isSafari: false,
isWebKit: false,
isWin: true,
isMobileOrTablet: false,
}
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
import { Browser } from 'roosterjs-editor-dom/lib';
import Disposable from './Disposable';
import DragAndDropHandler from './DragAndDropHandler';

/**
* @internal
* Compatible mouse event names for different platform
*/
interface MouseEventNames {
MOUSEDOWN: string;
MOUSEMOVE: string;
MOUSEUP: string;
}

/**
* Generate event names based on different platforms to be compatible with desktop and mobile browsers
*/
const MOUSE_EVENT_NAMES: MouseEventNames = (() => {
if (Browser.isMobileOrTablet) {
return {
MOUSEDOWN: 'touchstart',
MOUSEMOVE: 'touchmove',
MOUSEUP: 'touchend',
}
} else {
return {
MOUSEDOWN: 'mousedown',
MOUSEMOVE: 'mousemove',
MOUSEUP: 'mouseup',
}
}
})()


/**
* @internal
* A helper class to help manage drag and drop to an HTML element
Expand All @@ -26,27 +57,27 @@ export default class DragAndDropHelper<TContext, TInitValue> implements Disposab
private handler: DragAndDropHandler<TContext, TInitValue>,
private zoomScale: number
) {
trigger.addEventListener('mousedown', this.onMouseDown);
trigger.addEventListener(MOUSE_EVENT_NAMES.MOUSEDOWN, this.onMouseDown);
}

/**
* Dispose this object, remove all event listeners that has been attached
*/
dispose() {
this.trigger.removeEventListener('mousedown', this.onMouseDown);
this.trigger.removeEventListener(MOUSE_EVENT_NAMES.MOUSEDOWN, this.onMouseDown);
this.removeDocumentEvents();
}

private addDocumentEvents() {
const doc = this.trigger.ownerDocument;
doc.addEventListener('mousemove', this.onMouseMove, true /*useCapture*/);
doc.addEventListener('mouseup', this.onMouseUp, true /*useCapture*/);
doc.addEventListener(MOUSE_EVENT_NAMES.MOUSEMOVE, this.onMouseMove, true /*useCapture*/);
doc.addEventListener(MOUSE_EVENT_NAMES.MOUSEUP, this.onMouseUp, true /*useCapture*/);
}

private removeDocumentEvents() {
const doc = this.trigger.ownerDocument;
doc.removeEventListener('mousemove', this.onMouseMove, true /*useCapture*/);
doc.removeEventListener('mouseup', this.onMouseUp, true /*useCapture*/);
doc.removeEventListener(MOUSE_EVENT_NAMES.MOUSEMOVE, this.onMouseMove, true /*useCapture*/);
doc.removeEventListener(MOUSE_EVENT_NAMES.MOUSEUP, this.onMouseUp, true /*useCapture*/);
}

private onMouseDown = (e: MouseEvent) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import applyChange from './editInfoUtils/applyChange';
import canRegenerateImage from './api/canRegenerateImage';
import DragAndDropContext, { X, Y } from './types/DragAndDropContext';
import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from './types/DragAndDropContext';
import DragAndDropHandler from '../../pluginUtils/DragAndDropHandler';
import DragAndDropHelper from '../../pluginUtils/DragAndDropHelper';
import getGeneratedImageSize from './editInfoUtils/getGeneratedImageSize';
Expand Down Expand Up @@ -29,6 +29,7 @@ import {
getSideResizeHTML,
getCornerResizeHTML,
getResizeBordersHTML,
OnShowResizeHandle,
} from './imageEditors/Resizer';
import {
ExperimentalFeatures,
Expand Down Expand Up @@ -138,8 +139,12 @@ export default class ImageEdit implements EditorPlugin {
/**
* Create a new instance of ImageEdit
* @param options Image editing options
* @param onShowResizeHandle An optional callback to allow customize resize handle element of image resizing.
* To customize the resize handle element, add this callback and change the attributes of elementData then it
* will be picked up by ImageEdit code
*/
constructor(options?: ImageEditOptions) {
constructor(options?: ImageEditOptions,
private onShowResizeHandle?: OnShowResizeHandle) {
this.options = {
...DefaultOptions,
...(options || {}),
Expand Down Expand Up @@ -390,7 +395,7 @@ export default class ImageEdit implements EditorPlugin {
((Object.keys(ImageEditHTMLMap) as any[]) as (keyof typeof ImageEditHTMLMap)[]).forEach(
thisOperation => {
if ((operation & thisOperation) == thisOperation) {
arrayPush(htmlData, ImageEditHTMLMap[thisOperation](options));
arrayPush(htmlData, ImageEditHTMLMap[thisOperation](options, this.onShowResizeHandle));
}
}
);
Expand Down Expand Up @@ -560,8 +565,8 @@ export default class ImageEdit implements EditorPlugin {
element,
{
...commonContext,
x: element.dataset.x as X,
y: element.dataset.y as Y,
x: element.dataset.x as DNDDirectionX,
y: element.dataset.y as DnDDirectionY,
},
this.updateWrapper,
dragAndDrop,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import DragAndDropContext, { X, Y } from '../types/DragAndDropContext';
import DragAndDropContext, { DNDDirectionX, DnDDirectionY } from '../types/DragAndDropContext';
import DragAndDropHandler from '../../../pluginUtils/DragAndDropHandler';
import { CreateElementData } from 'roosterjs-editor-types';
import { CropInfo } from '../types/ImageEditInfo';
Expand All @@ -7,8 +7,8 @@ import { rotateCoordinate } from './Resizer';

const CROP_HANDLE_SIZE = 22;
const CROP_HANDLE_WIDTH = 7;
const Xs: X[] = ['w', 'e'];
const Ys: Y[] = ['s', 'n'];
const Xs: DNDDirectionX[] = ['w', 'e'];
const Ys: DnDDirectionY[] = ['s', 'n'];
const ROTATION: Record<string, number> = {
sw: 0,
nw: 90,
Expand Down Expand Up @@ -106,7 +106,7 @@ export function getCropHTML(): CreateElementData[] {
return [containerHTML, overlayHTML, overlayHTML, overlayHTML, overlayHTML];
}

function getCropHTMLInternal(x: X, y: Y): CreateElementData {
function getCropHTMLInternal(x: DNDDirectionX, y: DnDDirectionY): CreateElementData {
const leftOrRight = x == 'w' ? 'left' : 'right';
const topOrBottom = y == 'n' ? 'top' : 'bottom';
const rotation = ROTATION[y + x];
Expand Down
Loading

0 comments on commit ecb9098

Please sign in to comment.