diff --git a/samples/react-image-editor/README.md b/samples/react-image-editor/README.md index 40c6c5947..53fbc6ce8 100644 --- a/samples/react-image-editor/README.md +++ b/samples/react-image-editor/README.md @@ -16,15 +16,34 @@ extensions: # React Image Editor -## Summary -TODO -IMAGE Animated GIF + + +## Summary + +This solution contains an SPFx webpart that shows a HTML Image Editor based on canvas and [Office UI Fabric](https://developer.microsoft.com/fluentui/). + +Key features of the Editor +* Resize +* Crop +* Flip +* Rotate +* Scale +* Filter (Grayscale / Sepia) +* Redo / Undo +* Histoy of Actions + +The Placeholder and FilePicker is a component from [sp-dev-fx-controls-react ](https://pnp.github.io/sp-dev-fx-controls-react/) + +![react-image-editor in action](assets/react-image-editor.gif) ## Used SharePoint Framework Version ![version](https://img.shields.io/badge/version-1.4.0-green.svg) +> SharePoint 2019 and SharePoint Online + +References to office-ui-fabric-react version 5.x because of SharePoint 2019 Support ## Applies to @@ -35,7 +54,7 @@ IMAGE Animated GIF ## Prerequisites -> Any special pre-requisites? +> SharePoint Online or SharePoint 2019 ## Solution @@ -43,6 +62,8 @@ Solution|Author(s) --------|--------- react-image-editor | Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at)) +Thanks to [celum](https://www.celum.com/) and [cubido](https://www.cubido.at/) to allow to share this code. + ## Version history Version|Date|Comments @@ -61,6 +82,7 @@ Version|Date|Comments - Ensure that you are at the solution folder - in the command-line run: - **npm install** + - **edit config\serve.json set "initialPage": "https://{tenant}.sharepoint.com/_layouts/15/workbench.aspx" - **gulp serve** > Include any additional steps as needed. @@ -69,7 +91,6 @@ Version|Date|Comments * PNP Placeholder control if not Configured * PNP WebpartTitle control (toggle Show/Hide in property pane) * PNP FilePicker control to pick Images (is mocked on localworkbench) -* ImageManipulation from [react-image-manipulation-spfx](https://github.com/petkir/react-image-manipulation-spfx) * Office UI Fabric diff --git a/samples/react-image-editor/assets/react-image-editor.gif b/samples/react-image-editor/assets/react-image-editor.gif new file mode 100644 index 000000000..aa8209766 Binary files /dev/null and b/samples/react-image-editor/assets/react-image-editor.gif differ diff --git a/samples/react-image-editor/assets/sample.json b/samples/react-image-editor/assets/sample.json index 5e04924bf..ba9da4523 100644 --- a/samples/react-image-editor/assets/sample.json +++ b/samples/react-image-editor/assets/sample.json @@ -3,10 +3,19 @@ "name": "pnp-sp-dev-spfx-web-parts-react-image-editor", "source": "pnp", "title": "React Image Editor", - "shortDescription": "", + "shortDescription": "This solution contains an SPFx webpart that shows a HTML Image Editor based on canvas and Office UI Fabric", "url": "https://github.com/pnp/sp-dev-fx-webparts/tree/master/samples/react-image-editor", "longDescription": [ - "" + "This solution contains an SPFx webpart that shows a HTML Image Editor based on canvas and Office UI Fabric ", +"Key features of the Editor", +"* Resize", +"* Crop", +"* Flip", +"* Rotate", +"* Scale", +"* Filter (Grayscale / Sepia)", +"* Redo / Undo", +"* Histoy of Actions" ], "created": "2021-03-17", "modified": "2021-03-17", @@ -32,7 +41,7 @@ { "type": "image", "order": 100, - "url": "https://github.com/pnp/sp-dev-fx-webparts/raw/master/samples/react-image-editor/assets/ReactImageEditorWebPart.gif", + "url": "https://github.com/pnp/sp-dev-fx-webparts/raw/master/samples/react-image-editor/assets/react-image-editor.gif", "alt": "React Image Editor Web part" } ], diff --git a/samples/react-image-editor/config/config.json b/samples/react-image-editor/config/config.json index 2293aa7dd..4c0f2f25a 100644 --- a/samples/react-image-editor/config/config.json +++ b/samples/react-image-editor/config/config.json @@ -15,6 +15,6 @@ "localizedResources": { "ReactImageEditorWebPartStrings": "lib/webparts/reactImageEditor/loc/{locale}.js", "ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js", - "ImageManipulationStrings": "node_modules/react-image-manipulation-spfx/lib/components/ImageManipulation/loc/{locale}.js" + "ImageManipulationStrings": "lib/components/ImageManipulation/loc/{locale}.js" } } diff --git a/samples/react-image-editor/config/serve.json b/samples/react-image-editor/config/serve.json index 090cfe9e6..07971794f 100644 --- a/samples/react-image-editor/config/serve.json +++ b/samples/react-image-editor/config/serve.json @@ -2,7 +2,7 @@ "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json", "port": 4321, "https": true, - "initialPage": "https://localhost:5432/workbench", + "initialPage": "https://contoso.sharepoint.com/_layouts/15/workbench.aspx", "api": { "port": 5432, "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" diff --git a/samples/react-image-editor/package-lock.json b/samples/react-image-editor/package-lock.json index 86b9f3899..2f2f984a1 100644 --- a/samples/react-image-editor/package-lock.json +++ b/samples/react-image-editor/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-image-editor", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -534,6 +534,22 @@ "react": "15.6.2", "react-dom": "15.6.2", "tslib": "~1.8.0" + }, + "dependencies": { + "office-ui-fabric-react": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.21.0.tgz", + "integrity": "sha512-9IBLK6JUWYvdCumKywjkSP7mf/MqTRorP1qXtb2c+w2fQJH4NkdBjanO0zr37c5VPwlqc0KvPf51E9t1NU7oTg==", + "requires": { + "@microsoft/load-themed-styles": "^1.7.2", + "@uifabric/icons": ">=5.2.0 <6.0.0", + "@uifabric/merge-styles": ">=5.6.0 <6.0.0", + "@uifabric/styling": ">=5.7.0 <6.0.0", + "@uifabric/utilities": ">=5.3.3 <6.0.0", + "prop-types": "^15.5.10", + "tslib": "^1.7.1" + } + } } }, "@microsoft/package-deps-hash": { @@ -1066,6 +1082,22 @@ "react": "15.6.2", "react-dom": "15.6.2", "requirejs": "2.1.20" + }, + "dependencies": { + "office-ui-fabric-react": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.21.0.tgz", + "integrity": "sha512-9IBLK6JUWYvdCumKywjkSP7mf/MqTRorP1qXtb2c+w2fQJH4NkdBjanO0zr37c5VPwlqc0KvPf51E9t1NU7oTg==", + "requires": { + "@microsoft/load-themed-styles": "^1.7.2", + "@uifabric/icons": ">=5.2.0 <6.0.0", + "@uifabric/merge-styles": ">=5.6.0 <6.0.0", + "@uifabric/styling": ">=5.7.0 <6.0.0", + "@uifabric/utilities": ">=5.3.3 <6.0.0", + "prop-types": "^15.5.10", + "tslib": "^1.7.1" + } + } } }, "@microsoft/sp-lodash-subset": { @@ -1190,6 +1222,22 @@ "react-dom": "15.6.2", "scrollreveal": "3.3.6", "tslib": "~1.8.0" + }, + "dependencies": { + "office-ui-fabric-react": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.21.0.tgz", + "integrity": "sha512-9IBLK6JUWYvdCumKywjkSP7mf/MqTRorP1qXtb2c+w2fQJH4NkdBjanO0zr37c5VPwlqc0KvPf51E9t1NU7oTg==", + "requires": { + "@microsoft/load-themed-styles": "^1.7.2", + "@uifabric/icons": ">=5.2.0 <6.0.0", + "@uifabric/merge-styles": ">=5.6.0 <6.0.0", + "@uifabric/styling": ">=5.7.0 <6.0.0", + "@uifabric/utilities": ">=5.3.3 <6.0.0", + "prop-types": "^15.5.10", + "tslib": "^1.7.1" + } + } } }, "@microsoft/sp-webpart-workbench": { @@ -1218,6 +1266,23 @@ "react": "15.6.2", "react-dom": "15.6.2", "tslib": "~1.8.0" + }, + "dependencies": { + "office-ui-fabric-react": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.21.0.tgz", + "integrity": "sha512-9IBLK6JUWYvdCumKywjkSP7mf/MqTRorP1qXtb2c+w2fQJH4NkdBjanO0zr37c5VPwlqc0KvPf51E9t1NU7oTg==", + "dev": true, + "requires": { + "@microsoft/load-themed-styles": "^1.7.2", + "@uifabric/icons": ">=5.2.0 <6.0.0", + "@uifabric/merge-styles": ">=5.6.0 <6.0.0", + "@uifabric/styling": ">=5.7.0 <6.0.0", + "@uifabric/utilities": ">=5.3.3 <6.0.0", + "prop-types": "^15.5.10", + "tslib": "^1.7.1" + } + } } }, "@microsoft/stream-collator": { @@ -3638,11 +3703,6 @@ } } }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" - }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -12110,17 +12170,33 @@ "integrity": "sha512-VwGbHIT89e+9XdGcb2gdPgKgYQGjRG5peyPNO982znlCSB/UIM9g7gF6JmHpaMHb8xvEScY7UGJ8GRz6GQVBRg==" }, "office-ui-fabric-react": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.21.0.tgz", - "integrity": "sha512-9IBLK6JUWYvdCumKywjkSP7mf/MqTRorP1qXtb2c+w2fQJH4NkdBjanO0zr37c5VPwlqc0KvPf51E9t1NU7oTg==", + "version": "5.131.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.131.0.tgz", + "integrity": "sha512-QOYu1uf92qhTTIlBAj8teKvRpCmpliRZjynYtgeeUbDm4C4GtXdb/O1rPNFsfT0PNtPC8dCNeQ7/CXjQenUkyw==", "requires": { - "@microsoft/load-themed-styles": "^1.7.2", - "@uifabric/icons": ">=5.2.0 <6.0.0", - "@uifabric/merge-styles": ">=5.6.0 <6.0.0", - "@uifabric/styling": ">=5.7.0 <6.0.0", - "@uifabric/utilities": ">=5.3.3 <6.0.0", + "@microsoft/load-themed-styles": "^1.7.13", + "@uifabric/icons": ">=5.8.0 <6.0.0", + "@uifabric/merge-styles": ">=5.17.1 <6.0.0", + "@uifabric/styling": ">=5.36.0 <6.0.0", + "@uifabric/utilities": ">=5.34.2 <6.0.0", "prop-types": "^15.5.10", "tslib": "^1.7.1" + }, + "dependencies": { + "@microsoft/load-themed-styles": { + "version": "1.10.152", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.152.tgz", + "integrity": "sha512-NLF5+jkpfr/cCyxqqrPrpuL1YvYGMhPa4fSD8s76eI5/jpIyr1cM3PJ3vk03/XLOjSMuOqYntipB5FD1tKadyQ==" + }, + "@uifabric/icons": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-5.8.0.tgz", + "integrity": "sha512-EUhKxYlIPJshg4fQvCNTYSk0p7RhzEWeEAJBV4sao1SKmN0/pZBnkLbDqWjU5VUfdwZZYiIdaLRpM+pyzhniZw==", + "requires": { + "@uifabric/styling": ">=5.30.1 <6.0.0", + "tslib": "^1.7.1" + } + } } }, "on-finished": { @@ -13623,51 +13699,6 @@ "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.2.tgz", "integrity": "sha1-63cFxNs2+1AbOqOP91lhaqD/luA=" }, - "react-image-manipulation-spfx": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/react-image-manipulation-spfx/-/react-image-manipulation-spfx-0.0.4.tgz", - "integrity": "sha512-5VE1Hfn4xg4Q98k4aRFnwxB2hhK/CkSh3TTB/FnEqAyqB04Buz0ZxdspVsVBB41mY57UwehWaP/VBHX5IBvc5w==", - "requires": { - "@microsoft/sp-core-library": "~1.4.0", - "@microsoft/sp-lodash-subset": "~1.4.0", - "@microsoft/sp-office-ui-fabric-core": "~1.4.0", - "@microsoft/sp-webpart-base": "~1.4.0", - "classnames": "^2.2.6", - "office-ui-fabric-react": "5.131.0", - "react": "15.6.2", - "react-dom": "15.6.2" - }, - "dependencies": { - "@microsoft/load-themed-styles": { - "version": "1.10.151", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.151.tgz", - "integrity": "sha512-LCRBzFxmxX94cNNS3ATWu877Y17WJO2/Cg9DQKHrCtrW0tnr7+k6IIGXWAu4L5l9tsa5sQ1YThV6WUS4rWEsFA==" - }, - "@uifabric/icons": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-5.8.0.tgz", - "integrity": "sha512-EUhKxYlIPJshg4fQvCNTYSk0p7RhzEWeEAJBV4sao1SKmN0/pZBnkLbDqWjU5VUfdwZZYiIdaLRpM+pyzhniZw==", - "requires": { - "@uifabric/styling": ">=5.30.1 <6.0.0", - "tslib": "^1.7.1" - } - }, - "office-ui-fabric-react": { - "version": "5.131.0", - "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.131.0.tgz", - "integrity": "sha512-QOYu1uf92qhTTIlBAj8teKvRpCmpliRZjynYtgeeUbDm4C4GtXdb/O1rPNFsfT0PNtPC8dCNeQ7/CXjQenUkyw==", - "requires": { - "@microsoft/load-themed-styles": "^1.7.13", - "@uifabric/icons": ">=5.8.0 <6.0.0", - "@uifabric/merge-styles": ">=5.17.1 <6.0.0", - "@uifabric/styling": ">=5.36.0 <6.0.0", - "@uifabric/utilities": ">=5.34.2 <6.0.0", - "prop-types": "^15.5.10", - "tslib": "^1.7.1" - } - } - } - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/samples/react-image-editor/package.json b/samples/react-image-editor/package.json index 67cbeb32b..f05839d88 100644 --- a/samples/react-image-editor/package.json +++ b/samples/react-image-editor/package.json @@ -1,6 +1,6 @@ { "name": "react-image-editor", - "version": "0.0.1", + "version": "1.0.0", "private": true, "main": "lib/index.js", "engines": { @@ -17,9 +17,9 @@ "@microsoft/sp-office-ui-fabric-core": "~1.4.0", "@microsoft/sp-webpart-base": "~1.4.0", "@pnp/spfx-controls-react": "^1.21.1", + "office-ui-fabric-react": "5.131.0", "react": "15.6.2", - "react-dom": "15.6.2", - "react-image-manipulation-spfx": "0.0.4" + "react-dom": "15.6.2" }, "devDependencies": { "@types/react": "15.6.6", diff --git a/samples/react-image-editor/src/components/ImageManipulation/Filter/GrayscaleFilter.ts b/samples/react-image-editor/src/components/ImageManipulation/Filter/GrayscaleFilter.ts new file mode 100644 index 000000000..44d46babb --- /dev/null +++ b/samples/react-image-editor/src/components/ImageManipulation/Filter/GrayscaleFilter.ts @@ -0,0 +1,25 @@ +import { IImageFilter } from "./IImageFilter"; + + +export class GrayscaleFilter implements IImageFilter { + public process(imageData: ImageData, width: number, height: number, nvalue?: number, svalue?: string): ImageData { + var data = imageData.data; + + //Get length of all pixels in image each pixel made up of 4 elements for each pixel, one for Red, Green, Blue and Alpha + var arraylength = width * height * 4; + //Common formula for converting to grayscale. + //gray = 0.3*R + 0.59*G + 0.11*B + for (var i = arraylength - 1; i > 0; i -= 4) { + //R= i-3, G = i-2 and B = i-1 + //Get our gray shade using the formula + var gray = 0.3 * data[i - 3] + 0.59 * data[i - 2] + 0.11 * data[i - 1]; + data[i - 3] = gray; + data[i - 2] = gray; + data[i - 1] = gray; + + } + + return (imageData); + } + +} diff --git a/samples/react-image-editor/src/components/ImageManipulation/Filter/IImageFilter.ts b/samples/react-image-editor/src/components/ImageManipulation/Filter/IImageFilter.ts new file mode 100644 index 000000000..74166c0f7 --- /dev/null +++ b/samples/react-image-editor/src/components/ImageManipulation/Filter/IImageFilter.ts @@ -0,0 +1,6 @@ +export interface IImageFilter { + //describing function, + //parameters are in left side parenthesis, + //right side 'string' is return type + process(imageData: ImageData, width: number, height: number, nvalue?: number, svalue?: string): ImageData; +} diff --git a/samples/react-image-editor/src/components/ImageManipulation/Filter/SepiaFilter.ts b/samples/react-image-editor/src/components/ImageManipulation/Filter/SepiaFilter.ts new file mode 100644 index 000000000..484701e8c --- /dev/null +++ b/samples/react-image-editor/src/components/ImageManipulation/Filter/SepiaFilter.ts @@ -0,0 +1,29 @@ +import { IImageFilter } from "./IImageFilter"; + +export class SepiaFilter implements IImageFilter { + private r: number[] = [0, 0, 0, 1, 1, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 17, 18, 19, 19, 20, 21, 22, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 39, 40, 41, 42, 44, 45, 47, 48, 49, 52, 54, 55, 57, 59, 60, 62, 65, 67, 69, 70, 72, 74, 77, 79, 81, 83, 86, 88, 90, 92, 94, 97, 99, 101, 103, 107, 109, 111, 112, 116, 118, 120, 124, 126, 127, 129, 133, 135, 136, 140, 142, 143, 145, 149, 150, 152, 155, 157, 159, 162, 163, 165, 167, 170, 171, 173, 176, 177, 178, 180, 183, 184, 185, 188, 189, 190, 192, 194, 195, 196, 198, 200, 201, 202, 203, 204, 206, 207, 208, 209, 211, 212, 213, 214, 215, 216, 218, 219, 219, 220, 221, 222, 223, 224, 225, 226, 227, 227, 228, 229, 229, 230, 231, 232, 232, 233, 234, 234, 235, 236, 236, 237, 238, 238, 239, 239, 240, 241, 241, 242, 242, 243, 244, 244, 245, 245, 245, 246, 247, 247, 248, 248, 249, 249, 249, 250, 251, 251, 252, 252, 252, 253, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]; + private g: number[] = [0, 0, 1, 2, 2, 3, 5, 5, 6, 7, 8, 8, 10, 11, 11, 12, 13, 15, 15, 16, 17, 18, 18, 19, 21, 22, 22, 23, 24, 26, 26, 27, 28, 29, 31, 31, 32, 33, 34, 35, 35, 37, 38, 39, 40, 41, 43, 44, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58, 59, 60, 61, 63, 64, 65, 66, 67, 68, 69, 71, 72, 73, 74, 75, 76, 77, 79, 80, 81, 83, 84, 85, 86, 88, 89, 90, 92, 93, 94, 95, 96, 97, 100, 101, 102, 103, 105, 106, 107, 108, 109, 111, 113, 114, 115, 117, 118, 119, 120, 122, 123, 124, 126, 127, 128, 129, 131, 132, 133, 135, 136, 137, 138, 140, 141, 142, 144, 145, 146, 148, 149, 150, 151, 153, 154, 155, 157, 158, 159, 160, 162, 163, 164, 166, 167, 168, 169, 171, 172, 173, 174, 175, 176, 177, 178, 179, 181, 182, 183, 184, 186, 186, 187, 188, 189, 190, 192, 193, 194, 195, 195, 196, 197, 199, 200, 201, 202, 202, 203, 204, 205, 206, 207, 208, 208, 209, 210, 211, 212, 213, 214, 214, 215, 216, 217, 218, 219, 219, 220, 221, 222, 223, 223, 224, 225, 226, 226, 227, 228, 228, 229, 230, 231, 232, 232, 232, 233, 234, 235, 235, 236, 236, 237, 238, 238, 239, 239, 240, 240, 241, 242, 242, 242, 243, 244, 245, 245, 246, 246, 247, 247, 248, 249, 249, 249, 250, 251, 251, 252, 252, 252, 253, 254, 255]; + private b: number[] = [53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 57, 57, 57, 58, 58, 58, 59, 59, 59, 60, 61, 61, 61, 62, 62, 63, 63, 63, 64, 65, 65, 65, 66, 66, 67, 67, 67, 68, 69, 69, 69, 70, 70, 71, 71, 72, 73, 73, 73, 74, 74, 75, 75, 76, 77, 77, 78, 78, 79, 79, 80, 81, 81, 82, 82, 83, 83, 84, 85, 85, 86, 86, 87, 87, 88, 89, 89, 90, 90, 91, 91, 93, 93, 94, 94, 95, 95, 96, 97, 98, 98, 99, 99, 100, 101, 102, 102, 103, 104, 105, 105, 106, 106, 107, 108, 109, 109, 110, 111, 111, 112, 113, 114, 114, 115, 116, 117, 117, 118, 119, 119, 121, 121, 122, 122, 123, 124, 125, 126, 126, 127, 128, 129, 129, 130, 131, 132, 132, 133, 134, 134, 135, 136, 137, 137, 138, 139, 140, 140, 141, 142, 142, 143, 144, 145, 145, 146, 146, 148, 148, 149, 149, 150, 151, 152, 152, 153, 153, 154, 155, 156, 156, 157, 157, 158, 159, 160, 160, 161, 161, 162, 162, 163, 164, 164, 165, 165, 166, 166, 167, 168, 168, 169, 169, 170, 170, 171, 172, 172, 173, 173, 174, 174, 175, 176, 176, 177, 177, 177, 178, 178, 179, 180, 180, 181, 181, 181, 182, 182, 183, 184, 184, 184, 185, 185, 186, 186, 186, 187, 188, 188, 188, 189, 189, 189, 190, 190, 191, 191, 192, 192, 193, 193, 193, 194, 194, 194, 195, 196, 196, 196, 197, 197, 197, 198, 199]; + private noise: number = 0; + public process(imageData: ImageData, width: number, height: number, nvalue?: number, svalue?: string): ImageData { + //var data: Uint8ClampedArray = imageData.data; + this.noise = nvalue; + for (var i = 0; i < imageData.data.length; i += 4) { + // change image colors + imageData.data[i] = this.r[imageData.data[i]]; + imageData.data[i + 1] = this.g[imageData.data[i + 1]]; + imageData.data[i + 2] = this.b[imageData.data[i + 2]]; + + if (this.noise > 0) { + this.noise = Math.round(this.noise - Math.random() * this.noise); + for (var j = 0; j < 3; j++) { + var iPN = this.noise + imageData.data[i + j]; + imageData.data[i + j] = (iPN > 255) ? 255 : iPN; + } + } + } + + + return (imageData); + } +} diff --git a/samples/react-image-editor/src/components/ImageManipulation/HistoryItem.tsx b/samples/react-image-editor/src/components/ImageManipulation/HistoryItem.tsx new file mode 100644 index 000000000..304428091 --- /dev/null +++ b/samples/react-image-editor/src/components/ImageManipulation/HistoryItem.tsx @@ -0,0 +1,21 @@ +import { Icon } from 'office-ui-fabric-react'; +import * as React from 'react'; +import styles from './ImageManipulation.module.scss'; + +import { IImageManipulationSettings, manipulationTypeData } from './ImageManipulation.types'; + +export const historyItem = (item:IImageManipulationSettings, index:number): JSX.Element => { + if(!item) { + return undefined; + } + const data=manipulationTypeData[item.type]; + + const detailrender = data.toHTML(item); + return ( + + {data.svgIcon ? : } + {data.text} + {detailrender} + + ); +}; diff --git a/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.module.scss b/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.module.scss new file mode 100644 index 000000000..cb50d632f --- /dev/null +++ b/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.module.scss @@ -0,0 +1,63 @@ + + +.historyItem { + .historyItemText, + .historyItemIcon{ +padding-right: 6px; + } + .historyItemDetails{ +display: inline; + } + .historyItemSvg{ + margin-bottom: -3px; + height: 20px; + } +} +.svgbuttonPanel { + height: 40px; +} +.buttonHolderPanel { + &>button { + margin-right: 30px; + } + +} + + +.imageEditor{ + width: 100%; + position: relative; + .commandBar{ + position: absolute; + top:-32px; + button { + background-color: lightgray; + } + .svgbutton { + height: 20px; + } + } + .imageplaceholder{ + position: relative; + } + .canvasmaxwidth{ + max-width: 100%; + } +} +.iconbtn { + min-width: 32px; + width: 32px; + padding: 10px 2px; + margin-right: 3px; + height: 50px; + &>div{ + display: inline-block; + } + .imgtext{ + display: block; + } + .imgicon{ + display: block; + } + +} diff --git a/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.tsx b/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.tsx new file mode 100644 index 000000000..824ca6e24 --- /dev/null +++ b/samples/react-image-editor/src/components/ImageManipulation/ImageManipulation.tsx @@ -0,0 +1,945 @@ +import { DisplayMode } from '@microsoft/sp-core-library'; +import { clone } from '@microsoft/sp-lodash-subset'; +import { Checkbox, DefaultButton, findIndex, Icon, IconButton, IsFocusVisibleClassName, ISlider, Panel, PanelType, Slider, TextField } from 'office-ui-fabric-react'; +import * as React from 'react'; + +import ImageCrop from './components/ImageCrop'; +import ImageGrid from './components/ImageGrid'; +import ItemOrder from './components/ItemOrder'; + +import { GrayscaleFilter } from './Filter/GrayscaleFilter'; +import { SepiaFilter } from './Filter/SepiaFilter'; +import { historyItem } from './HistoryItem'; +import styles from './ImageManipulation.module.scss'; +import { FilterType, filterTypeData, ICrop, ICropSettings, IFilterSettings, IFlipSettings, IImageManipulationSettings, IManipulationTypeDataDetails, IResizeSettings, IRotateSettings, IScaleSettings, ManipulationType, manipulationTypeData, SettingPanelType } from './ImageManipulation.types'; + + +const flipVerticalIcon: any = require('../../svg/flipVertical.svg'); +const flipHorizontalIcon: any = require('../../svg/flipHorizontal.svg'); + +import * as strings from 'ImageManipulationStrings'; + +export interface IImageManipulationConfig { + rotateButtons: number[]; +} + +export interface IImageManipulationProps { + src: string; + settings?: IImageManipulationSettings[]; + settingschanged?: (settings: IImageManipulationSettings[]) => void; + imgLoadError?: () => void; + editMode?: (mode: boolean) => void; + configsettings: IImageManipulationConfig; + displyMode: DisplayMode; +} + +export interface IImageManipulationState { + settingPanel: SettingPanelType; + redosettings: IImageManipulationSettings[]; +} + +export class ImageManipulation extends React.Component { + private img: HTMLImageElement = null; + private wrapperRef: HTMLDivElement = null; + private bufferRef: HTMLCanvasElement = null; + private bufferCtx: CanvasRenderingContext2D = null; + private canvasRef: HTMLCanvasElement = null; + private canvasCtx: CanvasRenderingContext2D = null; + private manipulateRef: HTMLCanvasElement = null; + private manipulateCtx: CanvasRenderingContext2D = null; + + + + constructor(props: IImageManipulationProps) { + super(props); + + this.state = { + settingPanel: SettingPanelType.Closed, + redosettings: [] + }; + this.openPanel = this.openPanel.bind(this); + this.setRotate = this.setRotate.bind(this); + this.calcRotate = this.calcRotate.bind(this); + this.setCanvasRef = this.setCanvasRef.bind(this); + this.setBufferRef = this.setBufferRef.bind(this); + this.setManipulateRef = this.setManipulateRef.bind(this); + this.setScale = this.setScale.bind(this); + this.closeFilter = this.closeFilter.bind(this); + + } + public componentDidMount(): void { + this.imageChanged(this.props.src); + } + public componentDidUpdate(prevProps: IImageManipulationProps): void { + if (prevProps.src !== this.props.src) { + this.imageChanged(this.props.src); + } else { + this.applySettings(); + } + } + + private imageChanged(url: string) { + this.img = new Image(); + this.img.src = url; + this.img.crossOrigin = "Anonymous"; + this.img.onload = () => { + + this.applySettings(); + }; + this.img.onerror = (event: Event | string) => { + if (this.props.imgLoadError) { + this.props.imgLoadError(); + } + }; + } + private applySettings(): void { + this.canvasRef.width = this.img.width; + this.canvasRef.height = this.img.height; + this.bufferRef.width = this.img.width; + this.bufferRef.height = this.img.height; + this.manipulateRef.width = this.img.width; + this.manipulateRef.height = this.img.height; + + let currentwidth = this.img.width; + let currentheight = this.img.height; + let newwidth = currentwidth; + let newheight = currentheight; + this.bufferCtx.clearRect(0, 0, currentwidth, currentheight); + this.bufferCtx.drawImage(this.img, 0, 0); + + + if (this.props.settings) { + this.props.settings.forEach((element, index) => { + this.manipulateCtx.clearRect(0, 0, currentwidth, currentheight); + this.manipulateRef.width = currentwidth; + this.manipulateRef.height = currentheight; + this.manipulateCtx.save(); + let nothingToDo: boolean = false; + switch (element.type) { + case ManipulationType.Flip: + const filp = element as IFlipSettings; + if (filp.flipY) { + this.manipulateCtx.translate(0, currentheight); + this.manipulateCtx.scale(1, -1); + } + if (filp.flipX) { + this.manipulateCtx.translate(currentwidth, 0); + this.manipulateCtx.scale(-1, 1); + } + this.manipulateCtx.drawImage(this.bufferRef, 0, 0); + break; + case ManipulationType.Rotate: + const rotate = element as IRotateSettings; + if (!rotate.rotate || rotate.rotate === 0 || isNaN(rotate.rotate)) { + nothingToDo = true; + break; + } + if (rotate.rotate) { + const angelcalc = rotate.rotate * Math.PI / 180; + const oldwidth = currentwidth; + const oldheight = currentheight; + let offsetwidth = 0; + let offsetheight = 0; + + var a = oldwidth * Math.abs(Math.cos(angelcalc)); + var b = oldheight * Math.abs(Math.sin(angelcalc)); + + var p = oldwidth * Math.abs(Math.sin(angelcalc)); + var q = oldheight * Math.abs(Math.cos(angelcalc)); + newwidth = a + b; + newheight = p + q; + + offsetwidth = (newwidth - oldwidth) / 2; + offsetheight = (newheight - oldheight) / 2; + + this.manipulateRef.width = newwidth; + this.manipulateRef.height = newheight; + + this.manipulateCtx.translate(newwidth / 2, newheight / 2); + this.manipulateCtx.rotate(angelcalc); + this.manipulateCtx.translate(newwidth / 2 * -1, newheight / 2 * -1); + + this.manipulateCtx.drawImage(this.bufferRef, offsetwidth, offsetheight); + + } + break; + case ManipulationType.Scale: + const scale = element as IScaleSettings; + if (scale.scale) { + this.manipulateCtx.translate(currentwidth / 2, currentheight / 2); + this.manipulateCtx.scale(scale.scale, scale.scale); + this.manipulateCtx.translate(currentwidth / 2 * -1, currentheight / 2 * -1); + + this.manipulateCtx.drawImage(this.bufferRef, 0, 0); + } + break; + case ManipulationType.Filter: + nothingToDo = true; + const filter = element as IFilterSettings; + var imageData = this.bufferCtx.getImageData(0, 0, currentwidth, currentheight); + switch (filter.filterType) { + case FilterType.Grayscale: + imageData = new GrayscaleFilter().process(imageData, currentwidth, currentheight, undefined, undefined); + break; + case FilterType.Sepia: + imageData = new SepiaFilter().process(imageData, currentwidth, currentheight, undefined, undefined); + break; + } + this.bufferCtx.putImageData(imageData, 0, 0); + break; + case ManipulationType.Crop: + const last = this.props.settings.length === index + 1; + if (last && this.state.settingPanel === SettingPanelType.Crop) { + //Do nothingis last and current edit + nothingToDo = true; + } else { + const crop = element as ICropSettings; + const sourceX = crop.sx; + const sourceY = crop.sy; + newwidth = crop.width; + newheight = crop.height; + this.manipulateRef.width = newwidth; + this.manipulateRef.height = newheight; + this.manipulateCtx.drawImage(this.bufferRef, sourceX, sourceY, newwidth, newheight, 0, 0, newwidth, newheight); + } + break; + + case ManipulationType.Resize: + const resize = element as IResizeSettings; + newwidth = resize.width; + newheight = resize.height; + this.manipulateCtx.drawImage(this.bufferRef, 0, 0); + } + this.manipulateCtx.restore(); + + if (!nothingToDo) { + this.bufferCtx.clearRect(0, 0, currentwidth, currentheight); + + this.bufferRef.width = newwidth; + this.bufferRef.height = newheight; + + this.bufferCtx.clearRect(0, 0, newwidth, newheight); + this.bufferCtx.drawImage(this.manipulateRef, 0, 0, newwidth, newheight); + + + currentwidth = newwidth; + currentheight = newheight; + } + + }); + + + } + + /*this.canvasCtx.clearRect(0, 0, this.canvasRef.width, this.canvasRef.height) + // this.canvasCtx.drawImage(this.bufferRef, 0, 0); + const sourceX = 400; + const sourceY = 200; + const sourceWidth = 1200; + const sourceHeight = 600; +this.canvasCtx.drawImage(this.bufferRef, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, this.canvasRef.width, this.canvasRef.height); +*/ + this.canvasCtx.clearRect(0, 0, currentwidth, currentheight); + this.canvasRef.width = currentwidth; + this.canvasRef.height = currentheight; + this.canvasCtx.drawImage(this.bufferRef, 0, 0); + // this.canvasCtx.drawImage(this.bufferRef, 0, 0, currentwidth, currentheight); + this.wrapperRef.style.width = currentwidth + 'px'; + //this.wrapperRef.style.height = currentheight + 'px'; + // let height = this.canvasRef.height; + // let width = this.canvasRef.width; + + //this.canvasctx.translate(this.canvasRef.width / 2 * -1, this.canvasRef.height / 2 * -1); + } + + + + + public render(): React.ReactElement { + return ( +
+ {this.props.displyMode === DisplayMode.Edit && this.getCommandBar()} +
{ this.wrapperRef = element; }} + style={this.canvasRef && { width: '' + this.canvasRef.width + 'px' }} + > + + + + + {this.state.settingPanel === SettingPanelType.Crop && (this.getCropGrid())} + {this.state.settingPanel === SettingPanelType.Resize && (this.getResizeGrid())} + +
+
+ ); + } + private getCropGrid(): JSX.Element { + const lastset = this.getLastManipulation() as ICropSettings; + let lastdata: ICrop = { sx: 0, sy: 0, width: 0, height: 0 }; + + if (lastset && lastset.type === ManipulationType.Crop) { + lastdata = lastset; + } + return ( { + this.setCrop(crop.sx, crop.sy, crop.width, crop.height, crop.aspect); + } + } + />); + } + + private getResizeGrid(): JSX.Element { + const lastset = this.getLastManipulation() as IResizeSettings; + if (lastset && lastset.type === ManipulationType.Resize) { + return ( this.setResize(size.width, size.height, lastset.aspect)} + />); + } + return ( this.setResize(size.width, size.height, undefined)} + //aspect={this.getAspect()} + width={this.canvasRef.width} height={this.canvasRef.height} />); + } + + private getMaxWidth(): string { + const { settingPanel } = this.state; + if (settingPanel === SettingPanelType.Crop || settingPanel === SettingPanelType.Resize) { + return ''; + } + return styles.canvasmaxwidth; + } + + private isFilterActive(type: FilterType): boolean { + return (this.props.settings && this.props.settings.filter((f) => f.type === ManipulationType.Filter && (f as IFilterSettings).filterType === type).length > 0); + } + private closeFilter(): void { + this.setState({ + settingPanel: SettingPanelType.Closed + }, () => this.toggleEditMode(false)); + } + private getPanelHeader(settingPanel: SettingPanelType): string { + switch (settingPanel) { + case SettingPanelType.Filter: + return strings.ManipulationTypeFilter; + case SettingPanelType.Flip: + return strings.ManipulationTypeFlip; + case SettingPanelType.Rotate: + return strings.ManipulationTypeRotate; + case SettingPanelType.Scale: + return strings.ManipulationTypeScale; + case SettingPanelType.Crop: + return strings.ManipulationTypeCrop; + case SettingPanelType.Resize: + return strings.ManipulationTypeResize; + case SettingPanelType.History: + return strings.SettingPanelHistory; + } + } + private onRenderFooterContent(): JSX.Element { + return ( +
+ ); + } + private renderPanelContent(): JSX.Element { + switch (this.state.settingPanel) { + case SettingPanelType.Filter: + return this.getFilterSettings(); + case SettingPanelType.Flip: + return this.getFlipSettings(); + case SettingPanelType.Rotate: + return this.getRotateSettings(); + case SettingPanelType.Scale: + return this.getScaleSettings(); + case SettingPanelType.Crop: + return this.getCropSettings(); + case SettingPanelType.Resize: + return this.getResizeSettings(); + + case SettingPanelType.History: + return this.getHistorySettings(); + } + } + + private openPanel(settingPanel: SettingPanelType): void { + this.setState({ + settingPanel: settingPanel + }, () => this.toggleEditMode(true)); + } + + private toggleEditMode(mode: boolean) { + if (this.props.editMode) { + this.props.editMode(mode); + } + } + + private getHistorySettings(): JSX.Element { + return ( { + 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 ( +
+
+ {cropSelection} +
+
+ ); + } + + 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 && } +
    + { + (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))