{
+ if (this.state.redosettings && this.state.redosettings.length > 0) {
+ this.setState({ redosettings: [] }, () => {
+
+ if (this.props.settingschanged) {
+ this.props.settingschanged(newhist);
+ }
+ });
+ } else {
+
+ if (this.props.settingschanged) {
+ this.props.settingschanged(newhist);
+ }
+ }
+
+
+ }}
+ onRenderItem={historyItem}
+ />);
+ }
+
+ private getFilterSettings(): JSX.Element {
+ return (
+ {
+ Object.keys(filterTypeData).map((key, index) => {
+ return ( this.toggleFilter((+key) as FilterType)}
+ />);
+ })
+ }
);
+
+ }
+
+ private toggleFilter(type: FilterType, nvalue: number = undefined, svalue: string = undefined): void {
+ let tmpsettings = clone(this.props.settings);
+ if (!tmpsettings) { tmpsettings = []; }
+ if (tmpsettings.filter((f) => f.type === ManipulationType.Filter && (f as IFilterSettings).filterType === type).length > 0) {
+ const removeindex = findIndex(tmpsettings, (f) => f.type === ManipulationType.Filter && (f as IFilterSettings).filterType === type);
+ tmpsettings.splice(removeindex, 1);
+ } else {
+ tmpsettings.push({
+ type: ManipulationType.Filter,
+ filterType: type,
+ nvalue: nvalue,
+ svalue: svalue
+ });
+ }
+ if (this.props.settingschanged) {
+ this.props.settingschanged(tmpsettings);
+ }
+ }
+
+ private getFlipSettings(): JSX.Element {
+ return (
+
{ return ( ); }}
+ title={strings.FlipHorizontal}
+ ariaLabel={strings.FlipHorizontal}
+ onClick={() => {
+
+ let last = this.getLastManipulation();
+ if (last && last.type === ManipulationType.Flip) {
+ (last as IFlipSettings).flipX = !(last as IFlipSettings).flipX;
+ if ((last as IFlipSettings).flipX === false &&
+ (last as IFlipSettings).flipY === false) {
+ this.removeLastManipulation();
+ } else {
+ this.addOrUpdateLastManipulation(last);
+ }
+ } else {
+ this.addOrUpdateLastManipulation({ type: ManipulationType.Flip, flipX: true, flipY: false });
+ }
+ }}
+ />
+ { return ( ); }}
+ title={strings.FlipVertical}
+ ariaLabel={strings.FlipVertical}
+ onClick={() => {
+ let last = this.getLastManipulation();
+ if (last && last.type === ManipulationType.Flip) {
+ (last as IFlipSettings).flipY = !(last as IFlipSettings).flipY;
+ if ((last as IFlipSettings).flipX === false &&
+ (last as IFlipSettings).flipY === false) {
+ this.removeLastManipulation();
+ } else {
+ this.addOrUpdateLastManipulation(last);
+ }
+ } else {
+ this.addOrUpdateLastManipulation({ type: ManipulationType.Flip, flipX: false, flipY: true });
+ }
+ }}
+ />
+
+ );
+ }
+ private getRotateSettings(): JSX.Element {
+ const lastvalue = this.getLastManipulation();
+ let rotatevalue = 0;
+ if (lastvalue && lastvalue.type === ManipulationType.Rotate) {
+ rotatevalue = (lastvalue as IRotateSettings).rotate ? (lastvalue as IRotateSettings).rotate : 0;
+ }
+ return (
+
+ {this.props.configsettings.rotateButtons.map((value: number, index: number) => {
+ let icon: string = 'CompassNW';
+ if (value !== 0) { icon = 'Rotate'; }
+
+
+ return ( {
+ if (value === 0) {
+ this.setRotate(value);
+ } else {
+ this.calcRotate(value);
+ }
+ }}
+ className={styles.iconbtn}
+ >
+
+ {'' + value} );
+ })}
+
+
+
+
{
+ //Initial Value has a bug 0 is min value only min value is negative
+ const correctBugComponent = component as any;
+ if (correctBugComponent && correctBugComponent.state && correctBugComponent.value != correctBugComponent.props.value) {
+ correctBugComponent.setState({ value: 0, renderedValue: 0 });
+ }
+
+ }}
+ //originFromZero
+ />
+ { this.removeLastManipulation(); }
+ }
+ />
+ );
+ }
+
+ private getCropSettings(): JSX.Element {
+ let crop: ICropSettings = this.getCropValues();
+ return (
+ {
+ if (isNaN(crop.aspect)) {
+ this.setCrop(undefined, undefined, undefined, undefined, this.getAspect());
+ } else {
+ this.setCrop(undefined, undefined, undefined, undefined, undefined);
+ }
+
+ }}
+
+ />
+ this.setCrop(parseInt(x), undefined, undefined, undefined, crop.aspect)} />
+ this.setCrop(undefined, parseInt(y), undefined, undefined, crop.aspect)} />
+ this.setCrop(undefined, undefined, parseInt(w), undefined, crop.aspect)} />
+ this.setCrop(undefined, undefined, undefined, parseInt(h), crop.aspect)} />
+
+
);
+ }
+
+ private getResizeSettings(): JSX.Element {
+ let resize: IResizeSettings = this.getResizeValues();
+ return (
+
+ {
+ if (isNaN(resize.aspect)) {
+ this.setResize(undefined, undefined, this.getAspect());
+ } else {
+ this.setResize(undefined, undefined, undefined);
+ }
+
+ }}
+
+ />
+ this.setResize(parseInt(w), undefined, resize.aspect)} />
+ this.setResize(undefined, parseInt(h), resize.aspect)} />
+
+
);
+ }
+ private getAspect(): number {
+ return this.canvasRef.width / this.canvasRef.height;
+ }
+
+ private getScaleSettings(): JSX.Element {
+ const lastvalue = this.getLastManipulation();
+ let scalevalue = 1;
+ if (lastvalue && lastvalue.type === ManipulationType.Scale) {
+ scalevalue = (lastvalue as IScaleSettings).scale ? (lastvalue as IScaleSettings).scale : 1;
+ }
+ return (
+
+
+ { this.setScale(1); }
+ }
+ />
+
);
+ }
+
+ private getResizeValues(): IResizeSettings {
+ let state: IImageManipulationSettings = this.getLastManipulation();
+ let values: IResizeSettings = {
+ type: ManipulationType.Resize,
+ height: this.bufferRef.height,
+ width: this.bufferRef.width
+ };
+ if (state && state.type === ManipulationType.Resize) {
+ values = state as IResizeSettings;
+ }
+ return values;
+ }
+
+ private setResize(width: number, height: number, aspect: number): void {
+ let values: IResizeSettings = this.getResizeValues();
+ if (width) {
+ values.width = width;
+ if (aspect) {
+ values.height = values.width / aspect;
+ }
+ }
+ if (height) {
+ values.height = height;
+ if (aspect) {
+ values.width = values.height * aspect;
+ }
+ }
+ values.aspect = aspect;
+ this.addOrUpdateLastManipulation(values);
+ }
+
+ private getCropValues(): ICropSettings {
+ let state: IImageManipulationSettings = this.getLastManipulation();
+ let values: ICropSettings = {
+ type: ManipulationType.Crop,
+ sx: 0,
+ sy: 0,
+ height: this.bufferRef.height,
+ width: this.bufferRef.width
+ };
+ if (state && state.type === ManipulationType.Crop) {
+ values = state as ICropSettings;
+ }
+ return values;
+ }
+
+ private setCrop(sx: number, sy: number, width: number, height: number, aspect: number): void {
+ let values = this.getCropValues();
+ const currentheight: number = this.bufferRef.height;
+ const currentwidth: number = this.bufferRef.width;
+ if (!isNaN(sx) && sx >= 0) {
+ if (sx >= currentwidth) {
+ values.sx = currentwidth - 1;
+ } else {
+ values.sx = sx;
+ }
+
+ // limit max width
+ if ((values.width + values.sx) > currentwidth) {
+ values.width = currentwidth - values.sx;
+ }
+
+ }
+ if (!isNaN(sy) && sy >= 0) {
+ if (sy >= currentheight) {
+ values.sy = currentheight - 1;
+ } else {
+ values.sy = sy;
+ }
+
+ // limit max height
+ if ((values.height + values.sy) > currentheight) {
+ values.height = currentheight - values.sy;
+ }
+ }
+ if (!isNaN(width) && width >= 0) {
+ if ((width + values.sx) > currentwidth) {
+ values.width = currentwidth - values.sx;
+ } else {
+ values.width = width;
+ }
+ }
+ if (!isNaN(height) && height >= 0) {
+ if ((height + values.sy) > currentheight) {
+ values.height = currentheight - values.sy;
+ } else {
+ values.height = height;
+ }
+ }
+ if (isNaN(values.aspect) && !isNaN(aspect)) {
+ //aspect added
+
+ //limit w
+ if ((values.width + values.sx) > currentwidth) {
+ values.width = currentwidth - values.sx;
+ }
+
+ values.height = values.width / aspect;
+ //limit h adn recalulate w
+ if ((values.height + values.sy) > currentheight) {
+ values.height = currentheight - values.sy;
+ values.width = values.height * aspect;
+ }
+
+
+ }
+ values.aspect = aspect;
+ if (aspect && (!isNaN(sx) || !isNaN(width))) {
+ values.height = values.width / aspect;
+ }
+ if (aspect && (!isNaN(sy) || !isNaN(height))) {
+ values.width = values.height * aspect;
+ }
+ this.addOrUpdateLastManipulation(values);
+ }
+
+ private setRotate(value: number): void {
+ this.addOrUpdateLastManipulation({
+ type: ManipulationType.Rotate,
+ rotate: value
+ });
+
+ }
+ private setScale(value: number): void {
+ this.addOrUpdateLastManipulation({
+ type: ManipulationType.Scale,
+ scale: value
+ });
+ }
+ private calcRotate(value: number): void {
+ const lastVal = this.getLastManipulation();
+ let cvalue = 0;
+ if (lastVal && lastVal.type === ManipulationType.Rotate) {
+ cvalue = (lastVal as IRotateSettings).rotate;
+ }
+ cvalue = cvalue + value;
+ if (cvalue < -180) { cvalue = -180; }
+ if (cvalue > 180) { cvalue = 180; }
+ this.addOrUpdateLastManipulation({
+ type: ManipulationType.Rotate,
+ rotate: cvalue
+ });
+ }
+
+ private setCanvasRef(element: HTMLCanvasElement): void {
+ this.canvasRef = element;
+ if (this.canvasRef) {
+ this.canvasCtx = element.getContext('2d');
+ } else {
+ console.log('no canvas context ');
+ }
+ }
+ private setBufferRef(element: HTMLCanvasElement): void {
+ this.bufferRef = element;
+ if (this.bufferRef) {
+ this.bufferCtx = element.getContext('2d');
+ } else {
+ console.log('no buffer context ');
+ }
+
+ }
+
+ private setManipulateRef(element: HTMLCanvasElement): void {
+ this.manipulateRef = element;
+ if (this.manipulateRef) {
+ this.manipulateCtx = element.getContext('2d');
+ } else {
+ console.log('no manipulation context ');
+ }
+ }
+
+ private getLastManipulation(): IImageManipulationSettings {
+ if (this.props.settings && this.props.settings.length > 0) {
+ return this.props.settings[this.props.settings.length - 1];
+ }
+ return undefined;
+ }
+ private addOrUpdateLastManipulation(changed: IImageManipulationSettings): void {
+ let state = clone(this.props.settings);
+ if (!state) {
+ state = [];
+ }
+
+ if (state.length > 0 && state[state.length - 1].type === changed.type) {
+ state[state.length - 1] = changed;
+
+ } else {
+ state.push(changed);
+
+ }
+ if (this.state.redosettings && this.state.redosettings.length > 0) {
+ this.setState({ redosettings: [] }, () => {
+ if (this.props.settingschanged) {
+ this.props.settingschanged(state);
+ }
+ });
+ } else {
+ if (this.props.settingschanged) {
+ this.props.settingschanged(state);
+ }
+ }
+ }
+
+ private removeLastManipulation(): void {
+ if (this.props.settings && this.props.settings.length > 0) {
+ let state = clone(this.props.settings);
+ state.splice(state.length - 1, 1);
+ if (this.props.settingschanged) {
+ this.props.settingschanged(clone(state));
+ }
+ }
+ }
+
+ private getActionHeaderButton(options: IManipulationTypeDataDetails): JSX.Element {
+ return ( {
+ if (options.svgIcon) {
+ return ( );
+ }
+ return defaultrenderer(p);
+ }}
+ title={options.text}
+ ariaLabel={options.text}
+ onClick={() => this.openPanel(options.settingPanelType)}
+ />);
+ }
+
+ private getCommandBar(): JSX.Element {
+ return (
+ {this.getActionHeaderButton(manipulationTypeData[ManipulationType.Resize])}
+ {this.getActionHeaderButton(manipulationTypeData[ManipulationType.Crop])}
+ {this.getActionHeaderButton(manipulationTypeData[ManipulationType.Flip])}
+ {this.getActionHeaderButton(manipulationTypeData[ManipulationType.Rotate])}
+ {this.getActionHeaderButton(manipulationTypeData[ManipulationType.Scale])}
+ {this.getActionHeaderButton(manipulationTypeData[ManipulationType.Filter])}
+
+
{
+ const settings = clone(this.props.settings);
+ const last = settings.pop();
+ const redo = clone(this.state.redosettings);
+ redo.push(last);
+ this.setState({ redosettings: redo },
+ () => {
+ if (this.props.settingschanged) {
+ this.props.settingschanged(settings);
+ }
+ });
+
+ }}
+ />
+ {
+ const redosettings = clone(this.state.redosettings);
+ const redo = redosettings.pop();
+ const settings = clone(this.props.settings);
+ settings.push(redo);
+ this.setState({ redosettings: redosettings },
+ () => {
+ if (this.props.settingschanged) {
+ this.props.settingschanged(settings);
+ }
+ });
+
+ }}
+ />
+ {
+ this.setState({ redosettings: [] },
+ () => {
+ if (this.props.settingschanged) {
+ this.props.settingschanged([]);
+ }
+ });
+
+ }}
+ />
+ this.openPanel(SettingPanelType.History)}
+ />
+
+ {this.renderPanelContent()}
+
+ );
+ }
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.types.tsx b/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.types.tsx
new file mode 100644
index 000000000..4c5b6fd45
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.types.tsx
@@ -0,0 +1,160 @@
+import * as React from 'react';
+
+const colorFilterIcon: any = require('../../svg/colorFilter.svg');
+const cropIcon: any = require('../../svg/crop.svg');
+const flipVerticalIcon: any = require('../../svg/flipVertical.svg');
+const resizeIcon: any = require('../../svg/resize.svg');
+import * as strings from 'ImageManipulationStrings';
+
+export enum ManipulationType {
+ Crop,
+ Scale,
+ Rotate,
+ Flip,
+ Filter,
+ Resize
+}
+
+export enum SettingPanelType {
+ Closed = 1,
+ Filter = 2,
+ Flip = 3,
+ Rotate = 4,
+ Scale = 5,
+ Crop = 6,
+ Resize = 7,
+ History = 99
+}
+
+export enum FilterType {
+ Grayscale,
+ Sepia,
+ /*
+ Blur,
+ Emboss,
+ Sepia2,
+ Invert,
+ Sharpen,
+ RemoteWhite,
+ Brightness,
+ Noise,
+ Pixelate,
+ ColorOverLay*/
+}
+
+
+export interface IManipulationBase {
+ type: ManipulationType;
+}
+
+export interface ICrop {
+ sx: number;
+ sy: number;
+ width: number;
+ height: number;
+ aspect?: number;
+}
+
+export interface IResize {
+ width: number;
+ height: number;
+ aspect?: number;
+}
+
+export interface ICropSettings extends IManipulationBase, ICrop {
+
+}
+export interface IFlipSettings extends IManipulationBase {
+ flipX: boolean;
+ flipY: boolean;
+}
+export interface IScaleSettings extends IManipulationBase {
+ scale: number;
+}
+export interface IRotateSettings extends IManipulationBase {
+ rotate: number;
+}
+
+export interface IFilterSettings extends IManipulationBase {
+ filterType: FilterType;
+ nvalue?: number;
+ svalue?: string;
+}
+export interface IResizeSettings extends IManipulationBase, IResize {
+
+}
+
+export type IImageManipulationSettings = IFilterSettings | IRotateSettings | IScaleSettings | IFlipSettings | ICropSettings | IResizeSettings;
+
+
+export const filterTypeData: IFilterTypeData = {
+ 0: strings.FilterTypeGrayscale,
+ 1: strings.FilterTypeSepia
+};
+
+export interface IFilterTypeData {
+ [key: string]: string;
+}
+
+export interface IManipulationTypeDataBase {
+ text: string;
+ iconName?: string;
+ svgIcon?: any;
+ settingPanelType: SettingPanelType;
+}
+
+export interface IManipulationTypeData {
+ [key: string]: IManipulationTypeDataDetails;
+}
+
+export interface IManipulationTypeDataDetails extends IManipulationTypeDataBase {
+ toHTML: (item: IImageManipulationSettings) => JSX.Element;
+}
+
+export const manipulationTypeData: IManipulationTypeData = {
+ 0: {
+ text: strings.ManipulationTypeCrop,
+ svgIcon: cropIcon,
+ toHTML: (item: ICropSettings) => {
+ return ( );
+ //return ({`X:${item.sx} Y:${item.sy}`} );
+ },
+ settingPanelType: SettingPanelType.Crop
+ },
+ 1: {
+ text: strings.ManipulationTypeScale,
+ iconName: 'Zoom',
+ toHTML: (item: IScaleSettings) => { return ( ); },
+ settingPanelType: SettingPanelType.Scale
+ },
+ 2: {
+ text: strings.ManipulationTypeRotate,
+ iconName: 'Rotate',
+ toHTML: (item: IRotateSettings) => { return ( ); },
+ settingPanelType: SettingPanelType.Rotate
+ },
+ 3: {
+ text: strings.ManipulationTypeFlip,
+ svgIcon: flipVerticalIcon,
+ toHTML: (item: IFlipSettings) => { return ( ); },
+ settingPanelType: SettingPanelType.Flip
+ },
+ 4: {
+ text: strings.ManipulationTypeFilter,
+ svgIcon: colorFilterIcon,
+ toHTML: (item: IFilterSettings) => {
+ return ({filterTypeData[item.filterType]} );
+ },
+ settingPanelType: SettingPanelType.Filter
+ },
+ 5: {
+ text: strings.ManipulationTypeResize,
+ iconName: 'SizeLegacy',
+ svgIcon: resizeIcon,
+ toHTML: (item: IResizeSettings) => { return ( ); },
+ settingPanelType: SettingPanelType.Resize
+ },
+
+};
+
+
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/Enums.ts b/samples/react-image-editor/src/components/ImageManipulation/components/Enums.ts
new file mode 100644
index 000000000..d89ef974f
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/Enums.ts
@@ -0,0 +1,10 @@
+export enum nodePoition {
+ NW,
+ N,
+ NE,
+ E,
+ SE,
+ S,
+ SW,
+ W
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/ImageCrop.module.scss b/samples/react-image-editor/src/components/ImageManipulation/components/ImageCrop.module.scss
new file mode 100644
index 000000000..010f0b4c6
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/ImageCrop.module.scss
@@ -0,0 +1,243 @@
+$drag-handle-width: 10px !default;
+$drag-handle-height: 10px !default;
+$drag-bar-size: 6px !default;
+
+// Query to kick us into "mobile" mode with larger drag handles/bars.
+// See: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer
+$mobile-media-query: '(pointer: coarse)' !default;
+
+// Mobile handle/bar sizes. Override as above.
+$drag-handle-mobile-width: 24px !default;
+$drag-handle-mobile-height: 24px !default;
+
+// Handle color/border.
+$drag-handle-background-colour: rgba(0, 0, 0, 0.2) !default;
+$drag-handle-border: 1px solid rgba(255, 255, 255, 0.7) !default;
+
+.ImgGridShadowOverlay{
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+}
+
+.ImgGridVisible {
+ position: absolute;
+ top:0;
+ left:0;
+right: 0;
+bottom:0;
+cursor: crosshair;
+touch-action: manipulation;
+
+ .CropContrainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform: translate3d(0, 0, 0);
+ box-sizing: border-box;
+ cursor: move;
+ box-shadow: 0 0 0 9999em rgba(0, 0, 0, 0.5);
+ touch-action: manipulation;
+ border: 1px dashed white;
+
+ .ruleOfThirdsVT,
+ .ruleOfThirdsHZ{
+ &::before,
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ background-color: rgba(255, 255, 255, 0.4);
+ }
+ }
+ .ruleOfThirdsVT{
+ &::before,
+ &::after {
+ width: 1px;
+ height: 100%;
+ }
+
+ &::before {
+ left: 33.3333%;
+ left: calc(100% / 3);
+ }
+
+ &::after {
+ left: 66.6666%;
+ left: calc(100% / 3 * 2);
+ }
+ }
+ .ruleOfThirdsHZ{
+ &::before,
+ &::after {
+ width: 100%;
+ height: 1px;
+ }
+
+ &::before {
+ top: 33.3333%;
+ top: calc(100% / 3);
+ }
+
+ &::after {
+ top: 66.6666%;
+ top: calc(100% / 3 * 2);
+ }
+ }
+
+.dragHandle{
+ position: absolute;
+
+ &::after {
+ position: absolute;
+ content: '';
+ display: block;
+ width: $drag-handle-width;
+ height: $drag-handle-height;
+ background-color: $drag-handle-background-colour;
+ border: $drag-handle-border;
+ box-sizing: border-box;
+
+ // This stops the borders disappearing when keyboard
+ // nudging.
+ outline: 1px solid transparent;
+ }
+}
+
+ .nw{
+ top: 0;
+ left: 0;
+ margin-top: -(ceil($drag-handle-height / 2));
+ margin-left: -(ceil($drag-handle-width / 2));
+ cursor: nwse-resize;
+
+ &::after {
+ top: 0;
+ left: 0;
+ }
+ }
+ .n {
+ top: 0;
+ left: 50%;
+ margin-top: -(ceil($drag-handle-height / 2));
+ margin-left: -(ceil($drag-handle-width / 2));
+ cursor: ns-resize;
+
+ &::after {
+ top: 0;
+ }
+ }
+ .ne {
+ top: 0;
+ right: 0;
+ margin-top: -(ceil($drag-handle-height / 2));
+ margin-right: -(ceil($drag-handle-width / 2));
+ cursor: nesw-resize;
+
+ &::after {
+ top: 0;
+ right: 0;
+ }
+ }
+ .e {
+ top: 50%;
+ right: 0;
+ margin-top: -(ceil($drag-handle-height / 2));
+ margin-right: -(ceil($drag-handle-width / 2));
+ cursor: ew-resize;
+
+ &::after {
+ right: 0;
+ }
+ }
+ .se {
+ bottom: 0;
+ right: 0;
+ margin-bottom: -(ceil($drag-handle-height / 2));
+ margin-right: -(ceil($drag-handle-width / 2));
+ cursor: nwse-resize;
+
+ &::after {
+ bottom: 0;
+ right: 0;
+ }
+ }
+ .s {
+ bottom: 0;
+ left: 50%;
+ margin-bottom: -(ceil($drag-handle-height / 2));
+ margin-left: -(ceil($drag-handle-width / 2));
+ cursor: ns-resize;
+
+ &::after {
+ bottom: 0;
+ }
+ }
+ .sw {
+ bottom: 0;
+ left: 0;
+ margin-bottom: -(ceil($drag-handle-height / 2));
+ margin-left: -(ceil($drag-handle-width / 2));
+ cursor: nesw-resize;
+
+ &::after {
+ bottom: 0;
+ left: 0;
+ }
+ }
+ .w {
+ top: 50%;
+ left: 0;
+ margin-top: -(ceil($drag-handle-height / 2));
+ margin-left: -(ceil($drag-handle-width / 2));
+ cursor: ew-resize;
+
+ &::after {
+ left: 0;
+ }
+ }
+ .dragBar_n {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: $drag-bar-size;
+ margin-top: -($drag-bar-size / 2);
+ }
+
+
+
+ .dragBar_e {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: $drag-bar-size;
+ height: 100%;
+ margin-right: -($drag-bar-size / 2);
+ }
+ .dragBar_s {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: $drag-bar-size;
+ margin-bottom: -($drag-bar-size / 2);
+ }
+ .dragBar_w {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: $drag-bar-size;
+ height: 100%;
+ margin-left: -($drag-bar-size / 2);
+ }
+
+
+
+}
+
+
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/ImageCrop.tsx b/samples/react-image-editor/src/components/ImageManipulation/components/ImageCrop.tsx
new file mode 100644
index 000000000..bd14ae797
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/ImageCrop.tsx
@@ -0,0 +1,492 @@
+import { noWrap } from 'office-ui-fabric-react';
+import { IPosition } from 'office-ui-fabric-react/lib-es2015/utilities/positioning';
+import * as React from 'react';
+import { ICrop } from '../ImageManipulation.types';
+
+import { nodePoition } from './Enums';
+import styles from './ImageCrop.module.scss';
+import { ICropData, IMousePosition } from './Interfaces';
+
+function clamp(num, min, max) {
+ return Math.min(Math.max(num, min), max);
+}
+
+
+export interface IImageCropProps {
+ crop: ICrop;
+
+ sourceHeight: number;
+ sourceWidth: number;
+ showRuler?: boolean;
+ onDragStart?: (e: MouseEvent) => void;
+ onComplete?: (crop: ICrop) => void;
+ onChange?: (crop: ICrop) => void;
+ onDragEnd?: (e) => void;
+}
+
+export interface IImageCropState {
+ cropIsActive: boolean;
+ newCropIsBeingDrawn: boolean;
+ reloadtimestamp: string;
+}
+
+
+
+
+
+// Feature detection
+// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners
+let passiveSupported = false;
+
+export default class ImageCrop extends React.Component {
+
+ private controlRef: HTMLDivElement = null;
+
+ private dragStarted: boolean = false;
+ private mouseDownOnCrop: boolean = false;
+ private evData: ICropData;
+
+ constructor(props: IImageCropProps) {
+ super(props);
+ this.state = {
+ cropIsActive: false,
+ newCropIsBeingDrawn: false,
+ reloadtimestamp: ''
+ };
+ this.onDocMouseTouchMove = this.onDocMouseTouchMove.bind(this);
+ this.onDocMouseTouchEnd = this.onDocMouseTouchEnd.bind(this);
+ this.onCropMouseTouchDown = this.onCropMouseTouchDown.bind(this);
+ this.setControlRef = this.setControlRef.bind(this);
+ this.onMouseTouchDown = this.onMouseTouchDown.bind(this);
+ }
+
+ public componentDidMount(): void {
+ const { crop, sourceHeight, sourceWidth } = this.props;
+ if (crop && this.isValid(crop) &&
+ (crop.sx !== 0 || crop.sy !== 0 || crop.width !== 0 && crop.height !== 0)
+ ) {
+ this.setState({ cropIsActive: true });
+ } else {
+ //Requireed because first renderer has no ref
+ this.setState({ reloadtimestamp: new Date().getTime().toString() });
+
+ }
+
+ }
+
+ public componentWillUnmount(): void {
+
+ }
+
+
+
+
+ public render(): React.ReactElement {
+ const { crop } = this.props;
+ const { cropIsActive, newCropIsBeingDrawn } = this.state;
+ const cropSelection = this.isValid(crop) && this.controlRef ? this.createSelectionGrid() : null;
+
+ return (
+
+ );
+ }
+
+ private createSelectionGrid(): JSX.Element {
+ const { showRuler } = this.props;
+ const style = this.getCropStyle();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showRuler && (
+
+ )}
+
+ );
+ }
+
+ private makeNewCrop(): ICrop {
+ const crop: ICrop = { ...{ sx: 0, sy: 0, height: 0, width: 0 }, ...this.props.crop };
+ return crop;
+ }
+
+ private getCropStyle() {
+ const crop = this.makeNewCrop();
+ const unit = 'px';
+ return {
+ top: `${crop.sy}${unit}`,
+ left: `${crop.sx}${unit}`,
+ width: `${crop.width}${unit}`,
+ height: `${crop.height}${unit}`,
+ };
+ }
+
+ private getCurrentPosition(e: MouseEvent | any): IMousePosition {
+ let { pageX, pageY } = e;
+ if (e.touches) {
+ [{ pageX, pageY }] = e.touches;
+ }
+
+ let refpos = this.controlRef.getBoundingClientRect();
+ let startx: number = pageX - refpos.left;
+ let starty: number = pageY - refpos.top;
+ return ({
+ x: startx,
+ y: starty
+ });
+ }
+
+ private onDocMouseTouchMove(e: React.MouseEvent | any): void {
+ const { crop, onChange, onDragStart } = this.props;
+ if (!this.mouseDownOnCrop) {
+ return;
+ }
+
+ e.preventDefault();
+ if (!this.dragStarted) {
+ this.dragStarted = true;
+ if (onDragStart) {
+ onDragStart(e as any);
+ }
+ }
+ const pos = this.getCurrentPosition(e);
+
+
+
+
+ const clientPos = this.getClientPos(e);
+ /*
+ if (this.evData.isResize && this.props.aspect && this.evData.cropOffset) {
+ clientPos.y = this.straightenYPath(clientPos.x);
+ }
+ */
+
+ this.evData.xDiff = clientPos.x - this.evData.clientStartX;
+ this.evData.yDiff = clientPos.y - this.evData.clientStartY;
+
+ let nextCrop;
+
+ if (this.evData.isResize) {
+ nextCrop = this.resizeCrop();
+ } else {
+ nextCrop = this.dragCrop();
+ }
+
+ if (nextCrop !== crop) {
+ if (onChange) {
+ onChange(nextCrop);
+ }
+
+ }
+
+ }
+
+ private dragCrop() {
+
+ const { evData } = this;
+ let nextCrop: ICrop = this.makeNewCrop();
+ const width: number = this.controlRef.clientWidth;
+ const height: number = this.controlRef.clientHeight;
+ nextCrop.sx = clamp(evData.cropStartX + evData.xDiff, 0, width - nextCrop.width);
+ nextCrop.sy = clamp(evData.cropStartY + evData.yDiff, 0, height - nextCrop.height);
+
+ return nextCrop;
+ }
+
+ private resizeCrop() {
+ const { evData } = this;
+ let nextCrop = this.makeNewCrop();
+ const { pos } = evData;
+
+ if (evData.xInversed) {
+ evData.xDiff -= evData.cropStartWidth * 2;
+
+ }
+ if (evData.yInversed) {
+ evData.yDiff -= evData.cropStartHeight * 2;
+
+ }
+ const newSize = this.getNewSize();
+
+
+ let newX = evData.cropStartX;
+ let newY = evData.cropStartY;
+
+ if (evData.xInversed) {
+ newX = nextCrop.sx + (nextCrop.width - newSize.width);
+ }
+
+ if (evData.yInversed) {
+ newY = nextCrop.sy + (nextCrop.height - newSize.height);
+ }
+
+ const containedCrop: ICrop = {
+ sx: newX,
+ sy: newY,
+ width: newSize.width,
+ height: newSize.height,
+ aspect: this.props.crop.aspect
+ };
+
+ if (this.props.crop.aspect || (pos === nodePoition.NW || pos === nodePoition.SE || pos === nodePoition.SW || pos === nodePoition.NE)) {
+ nextCrop.sx = containedCrop.sx;
+ nextCrop.sy = containedCrop.sy;
+ nextCrop.width = containedCrop.width;
+ nextCrop.height = containedCrop.height;
+ } else if (pos === nodePoition.E || pos === nodePoition.W) {
+ nextCrop.sx = containedCrop.sx;
+ nextCrop.width = containedCrop.width;
+ } else if (pos === nodePoition.N || pos === nodePoition.S) {
+ nextCrop.sy = containedCrop.sy;
+ nextCrop.height = containedCrop.height;
+ }
+ return nextCrop;
+ }
+
+
+ private getNewSize(): { width: number, height: number } {
+ const { crop, sourceWidth, sourceHeight } = this.props;
+ const { evData } = this;
+
+ let newWidth = evData.cropStartWidth + evData.xDiff;
+
+ if (evData.xInversed) {
+ newWidth = Math.abs(newWidth);
+ }
+
+ newWidth = clamp(newWidth, 0, sourceWidth);
+
+ // New height.
+ let newHeight;
+
+ if (crop.aspect) {
+ newHeight = newWidth / crop.aspect;
+ } else {
+ newHeight = evData.cropStartHeight + evData.yDiff;
+ }
+
+ if (evData.yInversed) {
+ // Cap if polarity is inversed and the height fills the y space.
+ newHeight = Math.min(Math.abs(newHeight), evData.cropStartY);
+ }
+
+ newHeight = clamp(newHeight, 0, sourceHeight);
+
+ if (crop.aspect) {
+ newWidth = clamp(newHeight * crop.aspect, 0, sourceWidth);
+ }
+
+ return {
+ width: newWidth,
+ height: newHeight,
+ };
+ }
+
+ private onDocMouseTouchEnd(e: MouseEvent | any): void {
+ const { crop, onDragEnd, onComplete } = this.props;
+
+ let elecord = this.controlRef.getBoundingClientRect();
+
+
+ if (this.mouseDownOnCrop) {
+ this.mouseDownOnCrop = false;
+ this.dragStarted = false;
+ if (onDragEnd) {
+ onDragEnd(e);
+ }
+ if (onComplete) {
+ onComplete(crop);
+ }
+ this.setState({ cropIsActive: false, newCropIsBeingDrawn: false });
+
+ }
+ }
+
+ private onCropMouseTouchDown(e: MouseEvent | any): void {
+ const { crop } = this.props;
+
+ e.preventDefault(); // Stop drag selection.
+ const mousepos = this.getClientPos(e);
+ const { ord } = e.target.dataset;
+
+ let xInversed: boolean = false;
+ let yInversed: boolean = false;
+ let pos: nodePoition = undefined;
+ if (ord && !isNaN(+ord)) {
+ pos = +ord;
+ xInversed = pos === nodePoition.NW || pos === nodePoition.W || pos === nodePoition.SW;
+ yInversed = pos === nodePoition.NW || pos === nodePoition.N || pos === nodePoition.NE;
+ }
+
+ this.evData = {
+ clientStartX: mousepos.x,
+ clientStartY: mousepos.y,
+ cropStartWidth: crop.width,
+ cropStartHeight: crop.height,
+ cropStartX: xInversed ? crop.sx + crop.width : crop.sx,
+ cropStartY: yInversed ? crop.sy + crop.height : crop.sy,
+ xInversed: xInversed,
+ yInversed: yInversed,
+ isResize: (ord && !isNaN(ord)),
+ pos: pos,
+ xDiff: 0,
+ yDiff: 0
+ };
+
+ this.mouseDownOnCrop = true;
+ this.setState({ cropIsActive: true });
+ }
+
+
+ private setControlRef(element: HTMLDivElement): void {
+ this.controlRef = element;
+ }
+
+ private getClientPos(e: MouseEvent | any): IMousePosition {
+ let pageX;
+ let pageY;
+
+ if (e.touches) {
+ [{ pageX, pageY }] = e.touches;
+ } else {
+ ({ pageX, pageY } = e);
+ }
+
+ return {
+ x: pageX,
+ y: pageY,
+ };
+ }
+
+ private isValid(crop: ICrop) {
+ return crop && !isNaN(crop.width) && !isNaN(crop.height);
+ }
+
+ private makeAspectCrop(crop: ICrop) {
+ if (isNaN(this.props.crop.aspect)) {
+ return crop;
+ }
+
+ const calcCrop: ICrop = crop;
+
+ if (crop.width) {
+ calcCrop.height = calcCrop.width / this.props.crop.aspect;
+ }
+
+ if (crop.height) {
+ calcCrop.width = calcCrop.height * this.props.crop.aspect;
+ }
+
+ if (calcCrop.sy + calcCrop.height > this.props.sourceHeight) {
+ calcCrop.height = this.props.sourceHeight - calcCrop.sy;
+ calcCrop.width = calcCrop.height * this.props.crop.aspect;
+ }
+
+ if (calcCrop.sx + calcCrop.width > this.props.sourceWidth) {
+ calcCrop.width = this.props.sourceWidth - calcCrop.sx;
+ calcCrop.height = calcCrop.width / this.props.crop.aspect;
+ }
+
+ return calcCrop;
+ }
+ private resolveCrop(pixelCrop: ICrop) {
+ if (this.props.crop.aspect && (!pixelCrop.width || !pixelCrop.height)) {
+ return this.makeAspectCrop(pixelCrop);
+ }
+ return pixelCrop;
+ }
+
+ private onMouseTouchDown(e: MouseEvent | any): void {
+ const { crop, onChange } = this.props;
+ e.preventDefault(); // Stop drag selection.
+ const mousepos = this.getClientPos(e);
+
+ let refpos = this.controlRef.getBoundingClientRect();
+ let startx: number = mousepos.x - refpos.left;
+ let starty: number = mousepos.y - refpos.top;
+ //is mousePos in current pos
+ if (crop) {
+ if (crop.sx - 5 <= startx && crop.sx + crop.width + 5 >= startx &&
+ crop.sy - 5 <= starty && crop.sy + crop.height + 5 >= starty
+ ) {
+ //Position in current crop do Nothing
+ return;
+ }
+ }
+
+ const nextCrop: ICrop = {
+ sx: startx,
+ sy: starty,
+ width: 0,
+ height: 0,
+ aspect: crop.aspect
+ };
+
+ this.evData = {
+ clientStartX: mousepos.x,
+ clientStartY: mousepos.y,
+ cropStartWidth: nextCrop.width,
+ cropStartHeight: nextCrop.height,
+ cropStartX: nextCrop.sx,
+ cropStartY: nextCrop.sy,
+ xInversed: false,
+ yInversed: false,
+ isResize: true,
+ xDiff: 0,
+ yDiff: 0,
+ pos: nodePoition.NW,
+ };
+
+ this.mouseDownOnCrop = true;
+
+ onChange(nextCrop);
+
+ this.setState({ cropIsActive: true, newCropIsBeingDrawn: true });
+ }
+
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/ImageGrid.module.scss b/samples/react-image-editor/src/components/ImageManipulation/components/ImageGrid.module.scss
new file mode 100644
index 000000000..dd8181bb9
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/ImageGrid.module.scss
@@ -0,0 +1,129 @@
+.ImgGridShadowOverlay{
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ //background-color: rgba(0,0,0,0.4);
+ overflow: hidden;
+}
+.ImgGridVisible {
+ position: absolute;
+ top:0;
+ left:0;
+right: 0;
+bottom:0;
+ // box-sizing: border-box;
+ // box-shadow:0 0 0 9999em;
+ box-shadow: 0 0 0 9999em rgba(0, 0, 0, 0.5);
+
+ .ImgGridTabel {
+ border:2px solid white;
+ display: table;
+ height: 100%;
+ width: 100%;
+ .ImgGridRow {
+ display: table-row;
+ height: 33.33%;
+
+ .ImgGridCell {
+ display: table-cell;
+ width: 33.33%;
+ }
+.ImgLeftTop{
+ border-right: 1px solid white;
+ border-bottom: 1px solid white;
+ .bubble{
+ cursor:nwse-resize;
+ left: 0;
+ top:0;
+ }
+}
+.ImgRightTop{
+ border-left: 1px solid white;
+ border-bottom: 1px solid white;
+ .bubble{
+ cursor:nesw-resize;
+ right: 0;
+ top:0;
+ }
+}
+.ImgLeftBottom {
+ border-right: 1px solid white;
+ border-top: 1px solid white;
+ .bubble{
+ cursor:nesw-resize;
+ left: 0;
+ bottom:0;
+ }
+}
+.ImgRightBottom {
+ border-left: 1px solid white;
+ border-top: 1px solid white;
+ .bubble{
+ cursor:nwse-resize;
+ right: 0;
+ bottom:0;
+ }
+}
+.ImgCenterTop {
+ border-bottom: 1px dashed white;
+ .bubble{
+ cursor:ns-resize;
+ left: 50%;
+ top:0;
+ height: 10px;
+ }
+}
+.ImgCenterBottom{
+ border-top: 1px dashed white;
+ .bubble{
+ cursor:ns-resize;
+ left: 50%;
+ bottom:0;
+ height: 10px;
+ }
+}
+.ImgLeftCenter{
+ border-right: 1px dashed white;
+ .bubble{
+ cursor:ew-resize;
+ left: 0;
+ top:50%;
+ width: 10px;
+}
+}
+.ImgRightCenter{
+ border-left: 1px dashed white;
+ .bubble{
+ cursor:ew-resize;
+ right: 0;
+ top:50%;
+ width: 10px;
+ }
+}
+ }
+ .bubble{
+ height: 16px;
+ width: 16px;
+ background-color: white;
+ //border-radius: 50%;
+ border:1px solid black;
+ display: block;
+ position:absolute;
+ // margin: -8px 0 0 -8px;
+
+
+ }
+
+ /*
+ .ImgGridRowTop {
+ .ImgGridCellLeft {}
+ .ImgGridCellCenter {}
+ .ImgGridCellRight {}
+ }
+ .ImgGridRowMiddle {}
+ .ImgGridRowBottom {}
+*/
+ }
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/ImageGrid.tsx b/samples/react-image-editor/src/components/ImageManipulation/components/ImageGrid.tsx
new file mode 100644
index 000000000..d1c3bccc1
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/ImageGrid.tsx
@@ -0,0 +1,220 @@
+import { Overlay } from 'office-ui-fabric-react';
+import * as React from 'react';
+import { IResize } from '../ImageManipulation.types';
+
+import { nodePoition } from './Enums';
+import styles from './ImageGrid.module.scss';
+import { IMousePosition } from './Interfaces';
+
+export interface IImageGridProps {
+ width: number;
+ height: number;
+ aspect?: number;
+ onChange: (size: IResize) => void;
+ onComplete?: (size: IResize) => void;
+ onDragEnd?: (e: MouseEvent | any) => void;
+ onDragStart?: (e: MouseEvent | any) => void;
+}
+
+export interface IImageGridState { }
+
+export interface IResizeData {
+ pos: nodePoition;
+ width: number;
+ height: number;
+ xInverse: boolean;
+ yInverse: boolean;
+ clientStartX: number;
+ clientStartY: number;
+}
+
+export default class ImageGrid extends React.Component {
+
+ private evData: IResizeData = null;
+ private dragStarted: boolean = false;
+ constructor(props: IImageGridProps) {
+ super(props);
+
+ this.state = {};
+
+ this.onStartResizing = this.onStartResizing.bind(this);
+ this.onDocMouseTouchMove = this.onDocMouseTouchMove.bind(this);
+ this.onDocMouseTouchEnd = this.onDocMouseTouchEnd.bind(this);
+ }
+
+ public componentDidMount(): void {
+ window.document.addEventListener("mousemove", this.onDocMouseTouchMove);
+ window.document.addEventListener("touchmove", this.onDocMouseTouchMove);
+ window.document.addEventListener('mouseup', this.onDocMouseTouchEnd);
+ window.document.addEventListener('touchend', this.onDocMouseTouchEnd);
+ window.document.addEventListener('touchcancel', this.onDocMouseTouchEnd);
+
+ }
+ public componentWillUnmount(): void {
+ window.document.removeEventListener("mousemove", this.onDocMouseTouchMove);
+ window.document.removeEventListener("touchmove", this.onDocMouseTouchMove);
+ window.document.removeEventListener('mouseup', this.onDocMouseTouchEnd);
+ window.document.removeEventListener('touchend', this.onDocMouseTouchEnd);
+ window.document.removeEventListener('touchcancel', this.onDocMouseTouchEnd);
+ }
+
+ public render(): React.ReactElement {
+ return (
+
+ );
+ }
+
+ private onStartResizing(e: MouseEvent | any): void {
+ const mousePos = this.getClientPos(e);
+ let xInversed: boolean = false;
+ let yInversed: boolean = false;
+ const { ord } = e.target.dataset;
+ let pos: nodePoition = undefined;
+ if (ord && !isNaN(+ord)) {
+ pos = +ord;
+ xInversed = pos === nodePoition.NW || pos === nodePoition.W || pos === nodePoition.SW;
+ yInversed = pos === nodePoition.NW || pos === nodePoition.N || pos === nodePoition.NE;
+ } else {
+ return;
+ }
+ this.dragStarted = true;
+ if (this.props.onDragStart) {
+ this.props.onDragStart(e);
+ }
+ this.evData = {
+ clientStartX: mousePos.x,
+ clientStartY: mousePos.y,
+ xInverse: xInversed,
+ yInverse: yInversed,
+ pos: pos,
+ width: this.props.width,
+ height: this.props.height
+
+ };
+ }
+
+ private onDocMouseTouchMove(e: React.MouseEvent | any): void {
+ const { aspect ,onChange } = this.props;
+ if (!this.dragStarted) {
+ return;
+ }
+ if (!this.evData) {
+ return;
+ }
+ e.preventDefault();
+
+ const mousePos = this.getClientPos(e);
+
+ let xDiff: number = 0;
+ let yDiff: number = 0;
+
+ if (this.evData.pos == nodePoition.E || this.evData.pos == nodePoition.SE || this.evData.pos == nodePoition.NE) {
+ xDiff = mousePos.x - this.evData.clientStartX;
+ } else if (this.evData.pos == nodePoition.W || this.evData.pos == nodePoition.SW || this.evData.pos == nodePoition.NW) {
+ xDiff = this.evData.clientStartX - mousePos.x;
+ }
+
+ if (this.evData.pos == nodePoition.N || this.evData.pos == nodePoition.NW || this.evData.pos == nodePoition.NE) {
+ yDiff = this.evData.clientStartY - mousePos.y;
+ } else if (this.evData.pos == nodePoition.S || this.evData.pos == nodePoition.SW || this.evData.pos == nodePoition.SE) {
+ yDiff = mousePos.y - this.evData.clientStartY;
+ }
+
+ let nextsize: IResize = {
+ width: this.evData.width + xDiff,
+ height: this.evData.height + yDiff
+ };
+ if(aspect) {
+ if(this.evData.pos !== nodePoition.N && this.evData.pos !== nodePoition.S) {
+ nextsize.height = nextsize.width / aspect;
+ } else {
+ nextsize.width = nextsize.height * aspect;
+ }
+ }
+ if (onChange) {
+ onChange(nextsize);
+ }
+ }
+
+ private onDocMouseTouchEnd(e: MouseEvent | any): void {
+ const { width, height, onDragEnd, onComplete } = this.props;
+ if (this.dragStarted) {
+ this.dragStarted = false;
+ if (onDragEnd) {
+ onDragEnd(e);
+ }
+ this.evData = undefined;
+ if (onComplete) {
+ onComplete({ width: width, height: height });
+ this.setState({ cropIsActive: false, newCropIsBeingDrawn: false });
+ }
+
+
+ }
+ }
+
+ private getClientPos(e: MouseEvent | any): IMousePosition {
+ let pageX;
+ let pageY;
+
+ if (e.touches) {
+ [{ pageX, pageY }] = e.touches;
+ } else {
+ ({ pageX, pageY } = e);
+ }
+
+ return {
+ x: pageX,
+ y: pageY,
+ };
+ }
+
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/Interfaces.ts b/samples/react-image-editor/src/components/ImageManipulation/components/Interfaces.ts
new file mode 100644
index 000000000..80c0fe347
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/Interfaces.ts
@@ -0,0 +1,25 @@
+import { nodePoition } from "./Enums";
+
+export interface IMousePosition {
+ x: number;
+ y: number;
+}
+
+
+
+
+export interface ICropData {
+ clientStartX: number;
+ clientStartY: number;
+ cropStartWidth: number;
+ cropStartHeight: number;
+ cropStartX: number;
+ cropStartY: number;
+ xInversed: boolean;
+ yInversed: boolean;
+ isResize: boolean;
+ pos?: nodePoition;
+ xDiff: number;
+ yDiff: number;
+}
+
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/ItemOrder.module.scss b/samples/react-image-editor/src/components/ImageManipulation/components/ItemOrder.module.scss
new file mode 100644
index 000000000..7b716228d
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/ItemOrder.module.scss
@@ -0,0 +1,65 @@
+$ms-color-themePrimary: '[theme:themePrimary, default:#0078d7]';
+$ms-color-neutralLight: '[theme:neutralLight, default:#eaeaea]';
+$ms-color-neutralLighter: '[theme:neutralLighter, default:#f4f4f4]';
+$ms-color-neutralTertiary: '[theme:neutralTertiary, default:#a6a6a6]';
+$ms-color-white: '[theme:white, default:#ffffff]';
+
+
+.propertyFieldOrder {
+ margin-bottom: 2px;
+
+ ul {
+ padding: 0.5px;
+ margin: 0;
+ overflow-y: auto;
+ }
+
+ .disabled {
+
+ li {
+ color: $ms-color-neutralTertiary;
+ }
+
+ }
+
+ li {
+ list-style: none;
+ background-color: $ms-color-white;
+ border: 0.5px solid;
+ border-color: $ms-color-neutralLight;
+ outline: 0.5px solid;
+ outline-color: $ms-color-neutralLight;
+
+ .enabled & :hover {
+ background-color: $ms-color-neutralLighter;
+ }
+
+ & > div {
+ padding: 3px 6px;
+ display: flex;
+ flex-direction: row;
+ }
+
+ }
+
+ .itemBox {
+ flex-grow: 1;
+ }
+
+ .dragEnter {
+ background-color: $ms-color-neutralLight;
+ border-top: 2px dashed;
+ border-top-color: $ms-color-themePrimary;
+ }
+
+ .dragLast {
+ background-color: $ms-color-neutralLight;
+ border-bottom: 2px dashed;
+ border-bottom-color: $ms-color-themePrimary;
+ }
+
+ .lastBox {
+ height: 8px;
+ }
+
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/components/ItemOrder.tsx b/samples/react-image-editor/src/components/ImageManipulation/components/ItemOrder.tsx
new file mode 100644
index 000000000..4f73532dc
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/components/ItemOrder.tsx
@@ -0,0 +1,283 @@
+import { isEqual } from '@microsoft/sp-lodash-subset';
+import { EventGroup, IButtonStyles, IconButton, ISelection, Label } from 'office-ui-fabric-react';
+import { DragDropHelper, IDragDropContext, IDragDropHelperParams } from 'office-ui-fabric-react/lib-es2015/utilities/dragdrop';
+import * as React from 'react';
+import styles from './ItemOrder.module.scss';
+
+export interface IItemOrderProps {
+ label: string;
+ disabled: boolean;
+ items: Array;
+ textProperty?: string;
+ moveUpIconName: string;
+ moveDownIconName: string;
+ disableDragAndDrop: boolean;
+ removeArrows: boolean;
+ maxHeight?: number;
+ valueChanged: (newValue: Array) => void;
+ onRenderItem?: (item: any, index: number) => JSX.Element;
+}
+
+export interface IItemOrderState {
+ items: Array;
+}
+
+
+export default class ItemOrder extends React.Component {
+
+ private _draggedItem: any;
+ private _selection: ISelection;
+ private _ddHelper: DragDropHelper;
+ private _refs: Array;
+ private _ddSubs: Array;
+ private _lastBox: HTMLElement;
+
+
+ constructor(props: IItemOrderProps) {
+ super(props);
+
+ this._selection = null;
+ this._ddHelper = new DragDropHelper({
+ selection: this._selection
+ });
+
+ this._refs = new Array();
+ this._ddSubs = new Array();
+
+ this._draggedItem = null;
+
+ this.state = {
+ items: []
+ };
+ }
+
+
+
+ public render(): JSX.Element {
+ const {
+ items
+ } = this.state;
+ return (
+
+ {this.props.label &&
{this.props.label} }
+
+ {
+ (items && items.length > 0) && (
+ items.map((value: any, index: number) => {
+ return (
+ {this.renderItem(value, index)}
+ );
+ })
+ )
+ }
+ {
+ (items && items.length > 0) && { this._lastBox = ref; }} />
+ }
+
+
+ );
+ }
+
+ private renderItem(item: any, index: number): JSX.Element {
+ return (
+
+
+ {this.renderDisplayValue(item, index)}
+
+ {!this.props.removeArrows &&
+
{this.renderArrows(index)}
+ }
+
+ );
+ }
+
+ private renderDisplayValue(item: any, index: number): JSX.Element {
+ if (typeof this.props.onRenderItem === "function") {
+ return this.props.onRenderItem(item, index);
+ } else {
+ return (
+ {this.props.textProperty ? item[this.props.textProperty] : item.toString()}
+ );
+ }
+ }
+
+ private renderArrows(index: number): JSX.Element {
+ let arrowButtonStyles: Partial = {
+ root: {
+ width: '14px',
+ height: '100%',
+ display: 'inline-block !important'
+ },
+ rootDisabled: {
+ backgroundColor: 'transparent'
+ },
+ icon: {
+ fontSize: "10px"
+ }
+ };
+
+ return (
+
+ { this.onMoveUpClick(index); }}
+ styles={arrowButtonStyles}
+ />
+ { this.onMoveDownClick(index); }}
+ styles={arrowButtonStyles}
+ />
+
+ );
+ }
+
+ public componentWillMount(): void {
+ this.setState({
+ items: this.props.items || []
+ });
+ }
+
+ public componentDidMount(): void {
+ this.setupSubscriptions();
+ }
+
+ public componentWillUpdate(nextProps: IItemOrderProps): void {
+ // Check if the provided items are still the same
+ if (!isEqual(nextProps.items, this.state.items)) {
+ this.setState({
+ items: this.props.items || []
+ });
+ }
+ }
+
+ public componentDidUpdate(): void {
+ this.cleanupSubscriptions();
+ this.setupSubscriptions();
+ }
+
+ public componentWillUnmount(): void {
+ this.cleanupSubscriptions();
+ }
+
+ private registerRef = (ref: HTMLElement): void => {
+ this._refs.push(ref);
+ }
+
+ private setupSubscriptions = (): void => {
+ if (!this.props.disableDragAndDrop && !this.props.disabled) {
+ this._refs.forEach((value: HTMLElement, index: number) => {
+ this._ddSubs.push(this._ddHelper.subscribe(value, new EventGroup(value), {
+ eventMap: [
+ {
+ callback: (context: IDragDropContext, event?: any) => {
+ this._draggedItem = context.data;
+ },
+ eventName: 'dragstart'
+ }
+ ],
+ selectionIndex: index,
+ context: { data: this.state.items[index], index: index },
+ updateDropState: (isDropping: boolean, event: DragEvent) => {
+ if (isDropping) {
+ value.classList.add(styles.dragEnter);
+ } else {
+ value.classList.remove(styles.dragEnter);
+ }
+ },
+ canDrop: (dropContext?: IDragDropContext, dragContext?: IDragDropContext) => {
+ return true;
+ },
+ canDrag: (item?: any) => {
+ return true;
+ },
+ onDrop: (item?: any, event?: DragEvent) => {
+ if (this._draggedItem) {
+ this.insertBeforeItem(item);
+ }
+ },
+ /*onDragStart: (item?: any, itemIndex?: number, selectedItems?: any[], event?: MouseEvent) => {
+ //Never called for some reason, so using eventMap above
+ this._draggedItem = item;
+ },*/
+ onDragEnd: (item?: any, event?: DragEvent) => {
+ this._draggedItem = null;
+ }
+ }));
+ });
+
+ //Create dropable area below list to allow items to be dragged to the bottom
+ if (this._refs.length && typeof this._lastBox !== "undefined") {
+ this._ddSubs.push(this._ddHelper.subscribe(this._lastBox, new EventGroup(this._lastBox), {
+ selectionIndex: this._refs.length,
+ context: { data: {}, index: this._refs.length },
+ updateDropState: (isDropping: boolean, event: DragEvent) => {
+ if (isDropping) {
+ this._refs[this._refs.length - 1].classList.add(styles.dragLast);
+ } else {
+ this._refs[this._refs.length - 1].classList.remove(styles.dragLast);
+ }
+ },
+ canDrop: (dropContext?: IDragDropContext, dragContext?: IDragDropContext) => {
+ return true;
+ },
+ onDrop: (item?: any, event?: DragEvent) => {
+ if (this._draggedItem) {
+ let itemIndex: number = this.state.items.indexOf(this._draggedItem);
+ this.moveItemAtIndexToTargetIndex(itemIndex, this.state.items.length - 1);
+ }
+ }
+ }));
+ }
+ }
+ }
+
+ private cleanupSubscriptions = (): void => {
+ while (this._ddSubs.length) {
+ let sub: any = this._ddSubs.pop();
+ sub.dispose();
+ }
+ }
+
+ private insertBeforeItem = (item: any) => {
+ let itemIndex: number = this.state.items.indexOf(this._draggedItem);
+ let targetIndex: number = this.state.items.indexOf(item);
+ if (itemIndex < targetIndex) {
+ targetIndex -= 1;
+ }
+ this.moveItemAtIndexToTargetIndex(itemIndex, targetIndex);
+ }
+
+
+ private onMoveUpClick = (itemIndex: number): void => {
+ if (itemIndex > 0) {
+ this.moveItemAtIndexToTargetIndex(itemIndex, itemIndex - 1);
+ }
+ }
+
+ private onMoveDownClick = (itemIndex: number): void => {
+ if (itemIndex < this.state.items.length - 1) {
+ this.moveItemAtIndexToTargetIndex(itemIndex, itemIndex + 1);
+ }
+ }
+
+ private moveItemAtIndexToTargetIndex = (itemIndex: number, targetIndex: number): void => {
+ if (itemIndex !== targetIndex && itemIndex > -1 && targetIndex > -1 && itemIndex < this.state.items.length && targetIndex < this.state.items.length) {
+ let items: Array = this.state.items;
+ items.splice(targetIndex, 0, ...items.splice(itemIndex, 1)[0]);
+
+ this.setState({
+ items: items
+ });
+
+ this.props.valueChanged(items);
+ }
+ }
+}
diff --git a/samples/react-image-editor/src/components/ImageManipulation/index.ts b/samples/react-image-editor/src/components/ImageManipulation/index.ts
new file mode 100644
index 000000000..4f9d2e702
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/index.ts
@@ -0,0 +1,13 @@
+/*ImageManipulation
+
+FilterType
+*/
+
+export { ImageManipulation, IImageManipulationConfig } from './ImageManipulation';
+
+export {
+ IImageManipulationSettings, IManipulationBase,
+ IFilterSettings, IRotateSettings, IScaleSettings, IFlipSettings, ICropSettings, IResizeSettings,
+ FilterType
+} from './ImageManipulation.types';
+
diff --git a/samples/react-image-editor/src/components/ImageManipulation/loc/de-de.js b/samples/react-image-editor/src/components/ImageManipulation/loc/de-de.js
new file mode 100644
index 000000000..35ad79a4c
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/loc/de-de.js
@@ -0,0 +1,24 @@
+define([], function () {
+ return {
+ "ManipulationTypeFilter": "Filter",
+ "ManipulationTypeFlip": "Spiegeln",
+ "ManipulationTypeRotate": "Drehen",
+ "ManipulationTypeScale": "Skalieren",
+ "ManipulationTypeCrop": "Zuschneiden",
+ "ManipulationTypeResize": "Größe ändern",
+ "FilterTypeGrayscale": "Graustufen",
+ "FilterTypeSepia": "Sepia",
+ "SettingPanelClose": "Schließen",
+ "SettingPanelHistory": "Verlauf",
+ "CommandBarRedo": "Erneut ausführen",
+ "CommandBarUndo": "Rückgängig machen",
+ "CommandBarReset": "Zurücksetzen",
+ "FlipVertical": "Vertikal",
+ "FlipHorizontal": "Horizontal",
+ "LockAspect": "Verhältnis sperren",
+ "Width": "Breite",
+ "Height": "Höhe",
+ "SourceX": "SourceX",
+ "SourceY": "SourceY",
+ }
+});
diff --git a/samples/react-image-editor/src/components/ImageManipulation/loc/en-us.js b/samples/react-image-editor/src/components/ImageManipulation/loc/en-us.js
new file mode 100644
index 000000000..21a40b31a
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/loc/en-us.js
@@ -0,0 +1,24 @@
+define([], function () {
+ return {
+ "ManipulationTypeFilter": "Filter",
+ "ManipulationTypeFlip": "Flip",
+ "ManipulationTypeRotate": "Rotate",
+ "ManipulationTypeScale": "Scale",
+ "ManipulationTypeCrop": "Crop",
+ "ManipulationTypeResize": "Resize",
+ "FilterTypeGrayscale": "Grayscale",
+ "FilterTypeSepia": "Sepia",
+ "SettingPanelClose": "Close",
+ "SettingPanelHistory": "History",
+ "CommandBarRedo": "Redo",
+ "CommandBarUndo": "Undo",
+ "CommandBarReset": "Reset",
+ "FlipVertical": "Vertical",
+ "FlipHorizontal": "Horizontal",
+ "LockAspect": "Lock aspect",
+ "Width": "Width",
+ "Height": "Height",
+ "SourceX": "SourceX",
+ "SourceY": "SourceY",
+ }
+});
diff --git a/samples/react-image-editor/src/components/ImageManipulation/loc/mystrings.d.ts b/samples/react-image-editor/src/components/ImageManipulation/loc/mystrings.d.ts
new file mode 100644
index 000000000..df65f3b6a
--- /dev/null
+++ b/samples/react-image-editor/src/components/ImageManipulation/loc/mystrings.d.ts
@@ -0,0 +1,35 @@
+declare interface IImageManipulationStrings {
+
+ ManipulationTypeFilter: string;
+ ManipulationTypeFlip: string;
+ ManipulationTypeRotate: string;
+ ManipulationTypeScale: string;
+ ManipulationTypeCrop: string;
+ ManipulationTypeResize: string;
+
+
+ FilterTypeGrayscale: string;
+ FilterTypeSepia: string;
+
+ SettingPanelClose: string;
+ SettingPanelHistory: string;
+
+ CommandBarRedo: string;
+ CommandBarUndo: string;
+ CommandBarReset: string;
+
+ FlipVertical: string;
+ FlipHorizontal: string;
+
+ LockAspect: string;
+ Width: string;
+ Height: string;
+ SourceX: string;
+ SourceY: string;
+
+}
+
+declare module 'ImageManipulationStrings' {
+ const strings: IImageManipulationStrings;
+ export = strings;
+}
diff --git a/samples/react-image-editor/src/components/index.ts b/samples/react-image-editor/src/components/index.ts
new file mode 100644
index 000000000..2c876a285
--- /dev/null
+++ b/samples/react-image-editor/src/components/index.ts
@@ -0,0 +1 @@
+export * from './ImageManipulation';
diff --git a/samples/react-image-editor/src/svg/colorFilter.svg b/samples/react-image-editor/src/svg/colorFilter.svg
new file mode 100644
index 000000000..48d44603d
--- /dev/null
+++ b/samples/react-image-editor/src/svg/colorFilter.svg
@@ -0,0 +1 @@
+
diff --git a/samples/react-image-editor/src/svg/crop.svg b/samples/react-image-editor/src/svg/crop.svg
new file mode 100644
index 000000000..7bce4156b
--- /dev/null
+++ b/samples/react-image-editor/src/svg/crop.svg
@@ -0,0 +1 @@
+
diff --git a/samples/react-image-editor/src/svg/flipHorizontal.svg b/samples/react-image-editor/src/svg/flipHorizontal.svg
new file mode 100644
index 000000000..cb09ca541
--- /dev/null
+++ b/samples/react-image-editor/src/svg/flipHorizontal.svg
@@ -0,0 +1 @@
+
diff --git a/samples/react-image-editor/src/svg/flipVertical.svg b/samples/react-image-editor/src/svg/flipVertical.svg
new file mode 100644
index 000000000..42da1bdd8
--- /dev/null
+++ b/samples/react-image-editor/src/svg/flipVertical.svg
@@ -0,0 +1 @@
+
diff --git a/samples/react-image-editor/src/svg/focus.svg b/samples/react-image-editor/src/svg/focus.svg
new file mode 100644
index 000000000..d12d8df6b
--- /dev/null
+++ b/samples/react-image-editor/src/svg/focus.svg
@@ -0,0 +1 @@
+
diff --git a/samples/react-image-editor/src/svg/resize.svg b/samples/react-image-editor/src/svg/resize.svg
new file mode 100644
index 000000000..67e7dfbcb
--- /dev/null
+++ b/samples/react-image-editor/src/svg/resize.svg
@@ -0,0 +1 @@
+
diff --git a/samples/react-image-editor/src/webparts/reactImageEditor/ReactImageEditorWebPart.ts b/samples/react-image-editor/src/webparts/reactImageEditor/ReactImageEditorWebPart.ts
index 245394f48..41a9993c9 100644
--- a/samples/react-image-editor/src/webparts/reactImageEditor/ReactImageEditorWebPart.ts
+++ b/samples/react-image-editor/src/webparts/reactImageEditor/ReactImageEditorWebPart.ts
@@ -10,8 +10,9 @@ import {
import * as strings from 'ReactImageEditorWebPartStrings';
import ReactImageEditor, { IReactImageEditorBaseProps, IReactImageEditorProps } from './components/ReactImageEditor';
+import { IImageManipulationSettings } from '../../components';
+
-import { IImageManipulationSettings } from 'react-image-manipulation-spfx';
export interface IReactImageEditorWebPartProps extends IReactImageEditorBaseProps {
diff --git a/samples/react-image-editor/src/webparts/reactImageEditor/components/ReactImageEditor.tsx b/samples/react-image-editor/src/webparts/reactImageEditor/components/ReactImageEditor.tsx
index 4a6183bc0..b9c1cb5c4 100644
--- a/samples/react-image-editor/src/webparts/reactImageEditor/components/ReactImageEditor.tsx
+++ b/samples/react-image-editor/src/webparts/reactImageEditor/components/ReactImageEditor.tsx
@@ -7,7 +7,7 @@ import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { FilePicker, IFilePickerResult } from '@pnp/spfx-controls-react/lib/FilePicker';
-import { ImageManipulation, IImageManipulationSettings } from 'react-image-manipulation-spfx'
+import { ImageManipulation, IImageManipulationSettings } from '../../../components/ImageManipulation'
export interface IReactImageEditorProps extends IReactImageEditorBaseProps {
@@ -60,7 +60,7 @@ export default class ReactImageEditor extends React.Component this._onUrlChanged(filePickerResult.fileAbsoluteUrl))
}}
onCancel={() => {
- this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(''))
+ this.setState({ isFilePickerOpen: false })
}}
onChanged={(filePickerResult: IFilePickerResult) => {
this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl))