Documentation include Source files

This commit is contained in:
Peter Paul Kirschner 2021-03-17 19:45:38 +01:00
parent a6a27b0119
commit ffeb462296
35 changed files with 2956 additions and 75 deletions

View File

@ -16,15 +16,34 @@ extensions:
# React Image Editor # 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 ## Used SharePoint Framework Version
![version](https://img.shields.io/badge/version-1.4.0-green.svg) ![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 ## Applies to
@ -35,7 +54,7 @@ IMAGE Animated GIF
## Prerequisites ## Prerequisites
> Any special pre-requisites? > SharePoint Online or SharePoint 2019
## Solution ## Solution
@ -43,6 +62,8 @@ Solution|Author(s)
--------|--------- --------|---------
react-image-editor | Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at)) 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 history
Version|Date|Comments Version|Date|Comments
@ -61,6 +82,7 @@ Version|Date|Comments
- Ensure that you are at the solution folder - Ensure that you are at the solution folder
- in the command-line run: - in the command-line run:
- **npm install** - **npm install**
- **edit config\serve.json set "initialPage": "https://{tenant}.sharepoint.com/_layouts/15/workbench.aspx"
- **gulp serve** - **gulp serve**
> Include any additional steps as needed. > Include any additional steps as needed.
@ -69,7 +91,6 @@ Version|Date|Comments
* PNP Placeholder control if not Configured * PNP Placeholder control if not Configured
* PNP WebpartTitle control (toggle Show/Hide in property pane) * PNP WebpartTitle control (toggle Show/Hide in property pane)
* PNP FilePicker control to pick Images (is mocked on localworkbench) * 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 * Office UI Fabric

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

View File

@ -3,10 +3,19 @@
"name": "pnp-sp-dev-spfx-web-parts-react-image-editor", "name": "pnp-sp-dev-spfx-web-parts-react-image-editor",
"source": "pnp", "source": "pnp",
"title": "React Image Editor", "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", "url": "https://github.com/pnp/sp-dev-fx-webparts/tree/master/samples/react-image-editor",
"longDescription": [ "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", "created": "2021-03-17",
"modified": "2021-03-17", "modified": "2021-03-17",
@ -32,7 +41,7 @@
{ {
"type": "image", "type": "image",
"order": 100, "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" "alt": "React Image Editor Web part"
} }
], ],

View File

@ -15,6 +15,6 @@
"localizedResources": { "localizedResources": {
"ReactImageEditorWebPartStrings": "lib/webparts/reactImageEditor/loc/{locale}.js", "ReactImageEditorWebPartStrings": "lib/webparts/reactImageEditor/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/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"
} }
} }

View File

@ -2,7 +2,7 @@
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json", "$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321, "port": 4321,
"https": true, "https": true,
"initialPage": "https://localhost:5432/workbench", "initialPage": "https://contoso.sharepoint.com/_layouts/15/workbench.aspx",
"api": { "api": {
"port": 5432, "port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/" "entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"

View File

@ -1,6 +1,6 @@
{ {
"name": "react-image-editor", "name": "react-image-editor",
"version": "0.0.1", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -534,6 +534,22 @@
"react": "15.6.2", "react": "15.6.2",
"react-dom": "15.6.2", "react-dom": "15.6.2",
"tslib": "~1.8.0" "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": { "@microsoft/package-deps-hash": {
@ -1066,6 +1082,22 @@
"react": "15.6.2", "react": "15.6.2",
"react-dom": "15.6.2", "react-dom": "15.6.2",
"requirejs": "2.1.20" "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": { "@microsoft/sp-lodash-subset": {
@ -1190,6 +1222,22 @@
"react-dom": "15.6.2", "react-dom": "15.6.2",
"scrollreveal": "3.3.6", "scrollreveal": "3.3.6",
"tslib": "~1.8.0" "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": { "@microsoft/sp-webpart-workbench": {
@ -1218,6 +1266,23 @@
"react": "15.6.2", "react": "15.6.2",
"react-dom": "15.6.2", "react-dom": "15.6.2",
"tslib": "~1.8.0" "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": { "@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": { "clean-css": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
@ -12110,17 +12170,33 @@
"integrity": "sha512-VwGbHIT89e+9XdGcb2gdPgKgYQGjRG5peyPNO982znlCSB/UIM9g7gF6JmHpaMHb8xvEScY7UGJ8GRz6GQVBRg==" "integrity": "sha512-VwGbHIT89e+9XdGcb2gdPgKgYQGjRG5peyPNO982znlCSB/UIM9g7gF6JmHpaMHb8xvEScY7UGJ8GRz6GQVBRg=="
}, },
"office-ui-fabric-react": { "office-ui-fabric-react": {
"version": "5.21.0", "version": "5.131.0",
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.21.0.tgz", "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.131.0.tgz",
"integrity": "sha512-9IBLK6JUWYvdCumKywjkSP7mf/MqTRorP1qXtb2c+w2fQJH4NkdBjanO0zr37c5VPwlqc0KvPf51E9t1NU7oTg==", "integrity": "sha512-QOYu1uf92qhTTIlBAj8teKvRpCmpliRZjynYtgeeUbDm4C4GtXdb/O1rPNFsfT0PNtPC8dCNeQ7/CXjQenUkyw==",
"requires": { "requires": {
"@microsoft/load-themed-styles": "^1.7.2", "@microsoft/load-themed-styles": "^1.7.13",
"@uifabric/icons": ">=5.2.0 <6.0.0", "@uifabric/icons": ">=5.8.0 <6.0.0",
"@uifabric/merge-styles": ">=5.6.0 <6.0.0", "@uifabric/merge-styles": ">=5.17.1 <6.0.0",
"@uifabric/styling": ">=5.7.0 <6.0.0", "@uifabric/styling": ">=5.36.0 <6.0.0",
"@uifabric/utilities": ">=5.3.3 <6.0.0", "@uifabric/utilities": ">=5.34.2 <6.0.0",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"tslib": "^1.7.1" "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": { "on-finished": {
@ -13623,51 +13699,6 @@
"resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.2.tgz", "resolved": "https://registry.npmjs.org/react-dom-factories/-/react-dom-factories-1.0.2.tgz",
"integrity": "sha1-63cFxNs2+1AbOqOP91lhaqD/luA=" "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": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "react-image-editor", "name": "react-image-editor",
"version": "0.0.1", "version": "1.0.0",
"private": true, "private": true,
"main": "lib/index.js", "main": "lib/index.js",
"engines": { "engines": {
@ -17,9 +17,9 @@
"@microsoft/sp-office-ui-fabric-core": "~1.4.0", "@microsoft/sp-office-ui-fabric-core": "~1.4.0",
"@microsoft/sp-webpart-base": "~1.4.0", "@microsoft/sp-webpart-base": "~1.4.0",
"@pnp/spfx-controls-react": "^1.21.1", "@pnp/spfx-controls-react": "^1.21.1",
"office-ui-fabric-react": "5.131.0",
"react": "15.6.2", "react": "15.6.2",
"react-dom": "15.6.2", "react-dom": "15.6.2"
"react-image-manipulation-spfx": "0.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "15.6.6", "@types/react": "15.6.6",

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 (
<span className={styles.historyItem}>
<span className={styles.historyItemIcon}>{data.svgIcon ? <img className={styles.historyItemSvg} src={data.svgIcon} /> : <Icon iconName={data.iconName} />}</span>
<span className={styles.historyItemText}>{data.text}</span>
<span className={styles.historyItemDetails}>{detailrender}</span>
</span>
);
};

View File

@ -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;
}
}

View File

@ -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<IImageManipulationProps, IImageManipulationState> {
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<IImageManipulationProps> {
return (
<div className={styles.imageEditor} style={{
marginTop: this.props.displyMode === DisplayMode.Edit ? '40px' : '0px',
}} >
{this.props.displyMode === DisplayMode.Edit && this.getCommandBar()}
<div className={styles.imageplaceholder + ' ' + this.getMaxWidth()}
ref={(element: HTMLDivElement) => { this.wrapperRef = element; }}
style={this.canvasRef && { width: '' + this.canvasRef.width + 'px' }}
>
<canvas className={this.getMaxWidth()}
style={{ display: 'none' }}
ref={this.setBufferRef}></canvas>
<canvas className={this.getMaxWidth()}
style={{ display: 'none' }}
ref={this.setManipulateRef}></canvas>
<canvas className={this.getMaxWidth()} ref={this.setCanvasRef} ></canvas>
{this.state.settingPanel === SettingPanelType.Crop && (this.getCropGrid())}
{this.state.settingPanel === SettingPanelType.Resize && (this.getResizeGrid())}
</div>
</div>
);
}
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 (<ImageCrop
crop={lastdata}
showRuler
sourceHeight={this.img.height}
sourceWidth={this.img.width}
onChange={(crop) => {
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 (<ImageGrid
width={lastset.width} height={lastset.height}
aspect={lastset.aspect}
onChange={(size) => this.setResize(size.width, size.height, lastset.aspect)}
/>);
}
return (<ImageGrid
onChange={(size) => 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 (
<div> </div>
);
}
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 (<ItemOrder
label={''}
disabled={false}
moveUpIconName={'ChevronUpSmall'}
moveDownIconName={'ChevronDownSmall'}
disableDragAndDrop={false}
removeArrows={false}
items={this.props.settings}
valueChanged={(newhist) => {
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 (<div>
{
Object.keys(filterTypeData).map((key, index) => {
return (<Checkbox
key={'Filter' + index}
label={filterTypeData[key]}
checked={this.isFilterActive((+key) as FilterType)}
onChange={() => this.toggleFilter((+key) as FilterType)}
/>);
})
}</div>);
}
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 (<div className={styles.buttonHolderPanel} >
<IconButton
iconProps={{ iconName: 'SwitcherStartEnd' }}
onRenderIcon={() => { return (<img className={styles.svgbuttonPanel} src={flipVerticalIcon} />); }}
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 });
}
}}
/>
<IconButton
onRenderIcon={() => { return (<img className={styles.svgbuttonPanel} src={flipHorizontalIcon} />); }}
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 });
}
}}
/>
</div>);
}
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 (<div>
<div>
{this.props.configsettings.rotateButtons.map((value: number, index: number) => {
let icon: string = 'CompassNW';
if (value !== 0) { icon = 'Rotate'; }
return (<DefaultButton
key={'rotate' + index}
onClick={() => {
if (value === 0) {
this.setRotate(value);
} else {
this.calcRotate(value);
}
}}
className={styles.iconbtn}
>
<Icon iconName={icon} style={value < 0 ? { transform: 'scaleX(-1)' } : {}} className={styles.imgicon} />
<span className={styles.imgtext} >{'' + value}</span></DefaultButton>);
})}
</div>
<Slider
label=''
min={-180}
max={180}
value={rotatevalue}
onChange={this.setRotate}
showValue={true}
componentRef={(component: ISlider | null) => {
//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
/>
<IconButton
key={'resetrotate'}
disabled={!(lastvalue && lastvalue.type === ManipulationType.Rotate)}
iconProps={{ iconName: 'Undo' }}
ariaLabel={strings.CommandBarReset}
onClick={() => { this.removeLastManipulation(); }
}
/>
</div >);
}
private getCropSettings(): JSX.Element {
let crop: ICropSettings = this.getCropValues();
return (<div>
<Checkbox
label={strings.LockAspect}
checked={!isNaN(crop.aspect)}
onChange={() => {
if (isNaN(crop.aspect)) {
this.setCrop(undefined, undefined, undefined, undefined, this.getAspect());
} else {
this.setCrop(undefined, undefined, undefined, undefined, undefined);
}
}}
/>
<TextField label={strings.SourceX} value={'' + crop.sx} onChanged={(x) => this.setCrop(parseInt(x), undefined, undefined, undefined, crop.aspect)} />
<TextField label={strings.SourceY} value={'' + crop.sy} onChanged={(y) => this.setCrop(undefined, parseInt(y), undefined, undefined, crop.aspect)} />
<TextField label={strings.Width} value={'' + crop.width} onChanged={(w) => this.setCrop(undefined, undefined, parseInt(w), undefined, crop.aspect)} />
<TextField label={strings.Height} value={'' + crop.height} onChanged={(h) => this.setCrop(undefined, undefined, undefined, parseInt(h), crop.aspect)} />
</div>);
}
private getResizeSettings(): JSX.Element {
let resize: IResizeSettings = this.getResizeValues();
return (<div>
<Checkbox
label={strings.LockAspect}
checked={!isNaN(resize.aspect)}
onChange={() => {
if (isNaN(resize.aspect)) {
this.setResize(undefined, undefined, this.getAspect());
} else {
this.setResize(undefined, undefined, undefined);
}
}}
/>
<TextField label={strings.Width} value={'' + resize.width} onChanged={(w) => this.setResize(parseInt(w), undefined, resize.aspect)} />
<TextField label={strings.Height} value={'' + resize.height} onChanged={(h) => this.setResize(undefined, parseInt(h), resize.aspect)} />
</div>);
}
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 (<div>
<Slider
label=''
min={0.1}
max={5}
step={0.1}
value={scalevalue}
onChange={this.setScale}
showValue={true}
/>
<IconButton
key={'resetscale'}
disabled={!(lastvalue && lastvalue.type === ManipulationType.Scale)}
iconProps={{ iconName: 'Undo' }}
title={strings.CommandBarReset}
ariaLabel={strings.CommandBarReset}
onClick={() => { this.setScale(1); }
}
/>
</div>);
}
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 (<IconButton
iconProps={{ iconName: options.iconName }}
onRenderIcon={(p, defaultrenderer) => {
if (options.svgIcon) {
return (<img className={styles.svgbutton} src={options.svgIcon} />);
}
return defaultrenderer(p);
}}
title={options.text}
ariaLabel={options.text}
onClick={() => this.openPanel(options.settingPanelType)}
/>);
}
private getCommandBar(): JSX.Element {
return (<div className={styles.commandBar}>
{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])}
<IconButton
iconProps={{ iconName: 'Undo' }}
title={strings.CommandBarUndo}
ariaLabel={strings.CommandBarUndo}
disabled={!this.props.settings || this.props.settings.length < 1}
onClick={() => {
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);
}
});
}}
/>
<IconButton
iconProps={{ iconName: 'Redo' }}
title={strings.CommandBarRedo}
ariaLabel={strings.CommandBarRedo}
disabled={!this.state.redosettings || this.state.redosettings.length < 1}
onClick={() => {
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);
}
});
}}
/>
<IconButton
iconProps={{ iconName: 'Delete' }}
title={strings.CommandBarReset}
ariaLabel={strings.CommandBarReset}
disabled={!this.props.settings || this.props.settings.length < 1}
onClick={() => {
this.setState({ redosettings: [] },
() => {
if (this.props.settingschanged) {
this.props.settingschanged([]);
}
});
}}
/>
<IconButton
iconProps={{ iconName: 'History' }}
title={strings.SettingPanelHistory}
ariaLabel={strings.SettingPanelHistory}
disabled={!this.props.settings || this.props.settings.length < 1}
onClick={() => this.openPanel(SettingPanelType.History)}
/>
<Panel
isOpen={this.state.settingPanel != SettingPanelType.Closed}
type={PanelType.smallFixedFar}
onDismiss={this.closeFilter}
headerText={this.getPanelHeader(this.state.settingPanel)}
closeButtonAriaLabel={strings.SettingPanelClose}
isBlocking={false}
onRenderFooterContent={this.onRenderFooterContent}
>
{this.renderPanelContent()}
</Panel>
</div>);
}
}

View File

@ -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 (<span></span>);
//return (<span>{`X:${item.sx} Y:${item.sy}`}</span>);
},
settingPanelType: SettingPanelType.Crop
},
1: {
text: strings.ManipulationTypeScale,
iconName: 'Zoom',
toHTML: (item: IScaleSettings) => { return (<span></span>); },
settingPanelType: SettingPanelType.Scale
},
2: {
text: strings.ManipulationTypeRotate,
iconName: 'Rotate',
toHTML: (item: IRotateSettings) => { return (<span></span>); },
settingPanelType: SettingPanelType.Rotate
},
3: {
text: strings.ManipulationTypeFlip,
svgIcon: flipVerticalIcon,
toHTML: (item: IFlipSettings) => { return (<span></span>); },
settingPanelType: SettingPanelType.Flip
},
4: {
text: strings.ManipulationTypeFilter,
svgIcon: colorFilterIcon,
toHTML: (item: IFilterSettings) => {
return (<span>{filterTypeData[item.filterType]}</span>);
},
settingPanelType: SettingPanelType.Filter
},
5: {
text: strings.ManipulationTypeResize,
iconName: 'SizeLegacy',
svgIcon: resizeIcon,
toHTML: (item: IResizeSettings) => { return (<span></span>); },
settingPanelType: SettingPanelType.Resize
},
};

View File

@ -0,0 +1,10 @@
export enum nodePoition {
NW,
N,
NE,
E,
SE,
S,
SW,
W
}

View File

@ -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);
}
}
}

View File

@ -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<IImageCropProps, IImageCropState> {
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<IImageCropProps> {
const { crop } = this.props;
const { cropIsActive, newCropIsBeingDrawn } = this.state;
const cropSelection = this.isValid(crop) && this.controlRef ? this.createSelectionGrid() : null;
return (
<div ref={this.setControlRef}
className={styles.ImgGridShadowOverlay}
onMouseMove={this.onDocMouseTouchMove}
onTouchMove={this.onDocMouseTouchMove}
onMouseUp={this.onDocMouseTouchEnd}
onTouchCancel={this.onDocMouseTouchEnd}
onTouchEnd={this.onDocMouseTouchEnd}
onMouseDown={this.onMouseTouchDown}
onTouchStart={this.onMouseTouchDown}
>
<div className={styles.ImgGridVisible}
style={
{
left: 0,
top: 0,
right: 0,
bottom: 0
}
}>
{cropSelection}
</div>
</div>
);
}
private createSelectionGrid(): JSX.Element {
const { showRuler } = this.props;
const style = this.getCropStyle();
return (
<div
style={style}
className={styles.CropContrainer}
onMouseDown={this.onCropMouseTouchDown}
onTouchStart={this.onCropMouseTouchDown}
>
<div className={styles.dragBar_n} data-ord={nodePoition.N} />
<div className={styles.dragBar_e} data-ord={nodePoition.E} />
<div className={styles.dragBar_s} data-ord={nodePoition.S} />
<div className={styles.dragBar_w} data-ord={nodePoition.W} />
<div className={[styles.dragHandle, styles.nw].join(' ')} data-ord={nodePoition.NW} />
<div className={[styles.dragHandle, styles.n].join(' ')} data-ord={nodePoition.N} />
<div className={[styles.dragHandle, styles.ne].join(' ')} data-ord={nodePoition.NE} />
<div className={[styles.dragHandle, styles.e].join(' ')} data-ord={nodePoition.E} />
<div className={[styles.dragHandle, styles.se].join(' ')} data-ord={nodePoition.SE} />
<div className={[styles.dragHandle, styles.s].join(' ')} data-ord={nodePoition.S} />
<div className={[styles.dragHandle, styles.sw].join(' ')} data-ord={nodePoition.SW} />
<div className={[styles.dragHandle, styles.w].join(' ')} data-ord={nodePoition.W} />
{showRuler && (
<div>
<div className={styles.ruleOfThirdsHZ} />
<div className={styles.ruleOfThirdsVT} />
</div>
)}
</div>
);
}
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<HTMLDivElement> | 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 });
}
}

View File

@ -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 {}
*/
}
}

View File

@ -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<IImageGridProps, IImageGridState> {
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<IImageGridProps> {
return (
<div className={styles.ImgGridShadowOverlay}>
<div className={styles.ImgGridVisible}
style={
{
left: 0,
top: 0,
right: 0,
bottom: 0
}
}>
<div className={styles.ImgGridTabel}>
<div className={styles.ImgGridRow}>
<div className={styles.ImgLeftTop + ' ' + styles.ImgGridCell}>
<div className={styles.bubble}
onMouseDown={this.onStartResizing}
onTouchStart={this.onStartResizing}
data-ord={nodePoition.NW} />
</div>
<div className={styles.ImgCenterTop + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.N} onMouseDown={this.onStartResizing} />
</div>
<div className={styles.ImgRightTop + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.NE} onMouseDown={this.onStartResizing} />
</div>
</div>
<div className={styles.ImgGridRow}>
<div className={styles.ImgLeftCenter + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.W} onMouseDown={this.onStartResizing} />
</div>
<div className={styles.ImgGridCell}></div>
<div className={styles.ImgRightCenter + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.E} onMouseDown={this.onStartResizing} />
</div>
</div>
<div className={styles.ImgGridRow}>
<div className={styles.ImgLeftBottom + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.SW} onMouseDown={this.onStartResizing} />
</div>
<div className={styles.ImgCenterBottom + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.S} onMouseDown={this.onStartResizing} />
</div>
<div className={styles.ImgRightBottom + ' ' + styles.ImgGridCell}>
<div className={styles.bubble} data-ord={nodePoition.SE} onMouseDown={this.onStartResizing} />
</div>
</div>
</div>
</div>
</div>
);
}
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<HTMLDivElement> | 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,
};
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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<any>;
textProperty?: string;
moveUpIconName: string;
moveDownIconName: string;
disableDragAndDrop: boolean;
removeArrows: boolean;
maxHeight?: number;
valueChanged: (newValue: Array<any>) => void;
onRenderItem?: (item: any, index: number) => JSX.Element;
}
export interface IItemOrderState {
items: Array<any>;
}
export default class ItemOrder extends React.Component<IItemOrderProps, IItemOrderState> {
private _draggedItem: any;
private _selection: ISelection;
private _ddHelper: DragDropHelper;
private _refs: Array<HTMLElement>;
private _ddSubs: Array<any>;
private _lastBox: HTMLElement;
constructor(props: IItemOrderProps) {
super(props);
this._selection = null;
this._ddHelper = new DragDropHelper({
selection: this._selection
});
this._refs = new Array<HTMLElement>();
this._ddSubs = new Array<any>();
this._draggedItem = null;
this.state = {
items: []
};
}
public render(): JSX.Element {
const {
items
} = this.state;
return (
<div className={styles.propertyFieldOrder}>
{this.props.label && <Label>{this.props.label}</Label>}
<ul style={{ maxHeight: this.props.maxHeight ? this.props.maxHeight + 'px' : '100%' }} className={!this.props.disabled ? styles.enabled : styles.disabled}>
{
(items && items.length > 0) && (
items.map((value: any, index: number) => {
return (
<li
ref={this.registerRef}
key={index}
draggable={!this.props.disableDragAndDrop && !this.props.disabled}
style={{ cursor: !this.props.disableDragAndDrop && !this.props.disabled ? 'pointer' : 'default' }}
>{this.renderItem(value, index)}</li>
);
})
)
}
{
(items && items.length > 0) && <div className={styles.lastBox} ref={(ref: HTMLElement) => { this._lastBox = ref; }} />
}
</ul>
</div>
);
}
private renderItem(item: any, index: number): JSX.Element {
return (
<div>
<div className={styles.itemBox}>
{this.renderDisplayValue(item, index)}
</div>
{!this.props.removeArrows &&
<div>{this.renderArrows(index)}</div>
}
</div>
);
}
private renderDisplayValue(item: any, index: number): JSX.Element {
if (typeof this.props.onRenderItem === "function") {
return this.props.onRenderItem(item, index);
} else {
return (
<span>{this.props.textProperty ? item[this.props.textProperty] : item.toString()}</span>
);
}
}
private renderArrows(index: number): JSX.Element {
let arrowButtonStyles: Partial<IButtonStyles> = {
root: {
width: '14px',
height: '100%',
display: 'inline-block !important'
},
rootDisabled: {
backgroundColor: 'transparent'
},
icon: {
fontSize: "10px"
}
};
return (
<div>
<IconButton
disabled={this.props.disabled || index === 0}
iconProps={{ iconName: this.props.moveUpIconName }}
onClick={() => { this.onMoveUpClick(index); }}
styles={arrowButtonStyles}
/>
<IconButton
disabled={this.props.disabled || index === this.props.items.length - 1}
iconProps={{ iconName: this.props.moveDownIconName }}
onClick={() => { this.onMoveDownClick(index); }}
styles={arrowButtonStyles}
/>
</div>
);
}
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<any> = this.state.items;
items.splice(targetIndex, 0, ...items.splice(itemIndex, 1)[0]);
this.setState({
items: items
});
this.props.valueChanged(items);
}
}
}

View File

@ -0,0 +1,13 @@
/*ImageManipulation
FilterType
*/
export { ImageManipulation, IImageManipulationConfig } from './ImageManipulation';
export {
IImageManipulationSettings, IManipulationBase,
IFilterSettings, IRotateSettings, IScaleSettings, IFlipSettings, ICropSettings, IResizeSettings,
FilterType
} from './ImageManipulation.types';

View File

@ -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",
}
});

View File

@ -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",
}
});

View File

@ -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;
}

View File

@ -0,0 +1 @@
export * from './ImageManipulation';

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 16 4 C 11.593564 4 8 7.5935677 8 12 C 8 12.195683 8.0153666 12.387845 8.0292969 12.580078 C 5.0876831 13.764072 3 16.643186 3 20 C 3 24.406432 6.5935644 28 11 28 C 12.890388 28 14.628379 27.335631 16 26.232422 C 17.371621 27.335631 19.109612 28 21 28 C 25.406436 28 29 24.406432 29 20 C 29 16.643186 26.912317 13.764072 23.970703 12.580078 C 23.984633 12.387845 24 12.195683 24 12 C 24 7.5935677 20.406436 4 16 4 z M 16 6 C 19.325556 6 22 8.674446 22 12 C 22 12.023102 21.996351 12.045321 21.996094 12.068359 C 21.669097 12.027374 21.337804 12 21 12 C 19.109612 12 17.371621 12.664369 16 13.767578 C 14.628379 12.664369 12.890388 12 11 12 C 10.662196 12 10.330903 12.027374 10.003906 12.068359 C 10.003649 12.045321 10 12.023102 10 12 C 10 8.674446 12.674444 6 16 6 z M 11 14 C 12.360072 14 13.607475 14.45273 14.611328 15.208984 C 14.110052 15.875191 13.710721 16.618398 13.435547 17.421875 C 12.008277 16.748871 10.896895 15.533946 10.359375 14.035156 C 10.569969 14.013008 10.783318 14 11 14 z M 21 14 C 21.216682 14 21.430031 14.013008 21.640625 14.035156 C 21.103105 15.533946 19.991723 16.748871 18.564453 17.421875 C 18.289279 16.618398 17.889948 15.875191 17.388672 15.208984 C 18.392525 14.45273 19.639928 14 21 14 z M 8.4355469 14.578125 C 9.1903993 16.782207 10.878414 18.554196 13.029297 19.419922 C 13.015367 19.612155 13 19.804317 13 20 C 13 21.797096 13.604527 23.452954 14.611328 24.791016 C 13.607475 25.54727 12.360072 26 11 26 C 7.6744439 26 5 23.325554 5 20 C 5 17.594646 6.4028703 15.536599 8.4355469 14.578125 z M 23.564453 14.578125 C 25.59713 15.536599 27 17.594646 27 20 C 27 23.325554 24.325556 26 21 26 C 19.639928 26 18.392525 25.54727 17.388672 24.791016 C 18.395473 23.452954 19 21.797096 19 20 C 19 19.804317 18.984633 19.612155 18.970703 19.419922 C 21.121586 18.554196 22.809601 16.782207 23.564453 14.578125 z M 16 16.675781 C 16.264346 17.073691 16.476452 17.507074 16.640625 17.964844 C 16.430031 17.986992 16.216682 18 16 18 C 15.783318 18 15.569969 17.986992 15.359375 17.964844 C 15.523548 17.507074 15.735654 17.073691 16 16.675781 z M 15.003906 19.931641 C 15.330903 19.972626 15.662196 20 16 20 C 16.337804 20 16.669097 19.972626 16.996094 19.931641 C 16.996351 19.954679 17 19.976898 17 20 C 17 21.232254 16.631467 22.373698 16 23.324219 C 15.368533 22.373698 15 21.232254 15 20 C 15 19.976898 15.003649 19.954679 15.003906 19.931641 z"></path></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 8 4 L 8 8 L 4 8 L 4 10 L 8 10 L 8 24 L 22 24 L 22 28 L 24 28 L 24 24 L 28 24 L 28 22 L 11.4375 22 L 22 11.4375 L 22 21 L 24 21 L 24 9.4375 L 27.71875 5.71875 L 26.28125 4.28125 L 22.5625 8 L 11 8 L 11 10 L 20.5625 10 L 10 20.5625 L 10 4 Z"></path></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 26 3.3125 L 4.5 15 L 26 15 Z M 4.46875 17 L 7.9375 18.875 L 24.53125 27.875 L 26 28.6875 L 26 17 Z M 12.34375 19 L 24 19 L 24 25.34375 Z"></path></svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 15 4.46875 L 13.125 7.9375 L 4.125 24.53125 L 3.3125 26 L 15 26 Z M 17 4.5 L 17 26 L 28.6875 26 Z M 13 12.34375 L 13 24 L 6.65625 24 Z"></path></svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M 46.0625 1.9375 C 45.976563 1.949219 45.894531 1.96875 45.8125 2 C 45.726563 2.019531 45.640625 2.050781 45.5625 2.09375 L 1.5625 24.09375 C 1.210938 24.257813 0.984375 24.613281 0.984375 25 C 0.984375 25.386719 1.210938 25.742188 1.5625 25.90625 L 45.5625 47.90625 C 46.0625 48.148438 46.664063 47.9375 46.90625 47.4375 C 47.148438 46.9375 46.9375 46.335938 46.4375 46.09375 L 39.40625 42.5625 C 41.109375 39.992188 45 33.238281 45 25 C 45 16.761719 41.109375 10.007813 39.40625 7.4375 L 46.4375 3.90625 C 46.960938 3.78125 47.292969 3.269531 47.191406 2.742188 C 47.089844 2.214844 46.59375 1.859375 46.0625 1.9375 Z M 35.75 9.25 C 36.667969 10.550781 41 17.03125 41 25 C 41 32.96875 36.667969 39.449219 35.75 40.75 L 26.59375 36.1875 C 28.097656 34.328125 31 30.078125 31 25 C 31 19.921875 28.097656 15.671875 26.59375 13.8125 Z M 21.6875 16.28125 C 20.347656 18.484375 19 21.542969 19 25 C 19 28.457031 20.347656 31.515625 21.6875 33.71875 L 4.25 25 Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 5 5 L 5 7 L 7 7 L 7 5 Z M 9 5 L 9 7 L 11 7 L 11 5 Z M 13 5 L 13 7 L 15 7 L 15 5 Z M 17 5 L 17 7 L 19 7 L 19 5 Z M 21 5 L 21 7 L 23 7 L 23 5 Z M 25 5 L 25 7 L 27 7 L 27 5 Z M 5 9 L 5 11 L 7 11 L 7 9 Z M 25 9 L 25 11 L 27 11 L 27 9 Z M 14 11 L 14 13 L 17.5625 13 L 13.5625 17 L 5 17 L 5 27 L 15 27 L 15 18.4375 L 19 14.4375 L 19 18 L 21 18 L 21 11 Z M 5 13 L 5 15 L 7 15 L 7 13 Z M 25 13 L 25 15 L 27 15 L 27 13 Z M 25 17 L 25 19 L 27 19 L 27 17 Z M 7 19 L 13 19 L 13 25 L 7 25 Z M 25 21 L 25 23 L 27 23 L 27 21 Z M 17 25 L 17 27 L 19 27 L 19 25 Z M 21 25 L 21 27 L 23 27 L 23 25 Z M 25 25 L 25 27 L 27 27 L 27 25 Z"></path></svg>

After

Width:  |  Height:  |  Size: 700 B

View File

@ -10,8 +10,9 @@ import {
import * as strings from 'ReactImageEditorWebPartStrings'; import * as strings from 'ReactImageEditorWebPartStrings';
import ReactImageEditor, { IReactImageEditorBaseProps, IReactImageEditorProps } from './components/ReactImageEditor'; import ReactImageEditor, { IReactImageEditorBaseProps, IReactImageEditorProps } from './components/ReactImageEditor';
import { IImageManipulationSettings } from '../../components';
import { IImageManipulationSettings } from 'react-image-manipulation-spfx';
export interface IReactImageEditorWebPartProps extends IReactImageEditorBaseProps { export interface IReactImageEditorWebPartProps extends IReactImageEditorBaseProps {

View File

@ -7,7 +7,7 @@ import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartContext } from '@microsoft/sp-webpart-base'; import { WebPartContext } from '@microsoft/sp-webpart-base';
import { FilePicker, IFilePickerResult } from '@pnp/spfx-controls-react/lib/FilePicker'; 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 { export interface IReactImageEditorProps extends IReactImageEditorBaseProps {
@ -60,7 +60,7 @@ export default class ReactImageEditor extends React.Component<IReactImageEditorP
this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl)) this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl))
}} }}
onCancel={() => { onCancel={() => {
this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged('')) this.setState({ isFilePickerOpen: false })
}} }}
onChanged={(filePickerResult: IFilePickerResult) => { onChanged={(filePickerResult: IFilePickerResult) => {
this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl)) this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl))