Merge pull request #1776 from petkir/petkir-react-image-editor

This commit is contained in:
Hugo Bernier 2021-03-21 00:33:06 -04:00 committed by GitHub
commit 3d0aefe72d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 21091 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-image-editor/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,11 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "onprem19",
"version": "1.11.0",
"libraryName": "react-image-editor",
"libraryId": "e859f86f-7a12-40e3-94fb-97ee47419aed",
"packageManager": "npm",
"componentType": "webpart"
}
}

View File

@ -0,0 +1,108 @@
---
page_type: sample
products:
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
platforms:
- React
createdDate: 3/17/2021 12:00:00 AM
---
# React Image Editor
## Summary
This solution contains an SPFx web part that shows an 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 are components from the [sp-dev-fx-controls-react ](https://pnp.github.io/sp-dev-fx-controls-react/)
![react-image-editor in action](assets/react-image-editor.gif)
## Compatibility
![SPFx 1.4.0](https://img.shields.io/badge/SPFx-1.4.0-green.svg)
![Node.js LTS 6.x](https://img.shields.io/badge/Node.js-LTS%206.x-green.svg)
![SharePoint 2016 | 2019 | Online](https://img.shields.io/badge/SharePoint-2016%20%7C%202019%20%7C%20Online-green.svg)
![Teams No: Not designed for Microsoft Teams](https://img.shields.io/badge/Teams-No-red.svg "Not designed for Microsoft Teams")
![Workbench Local | Hosted](https://img.shields.io/badge/Workbench-Local%20%7C%20Hosted-green.svg)
References to office-ui-fabric-react version 5.x because of SharePoint 2019 Support
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
## Prerequisites
> SharePoint Online or SharePoint 2019
## Solution
Solution|Author(s)
--------|---------
react-image-editor | Peter Paul Kirschner ([@petkir_at](https://twitter.com/petkir_at))
Thanks to [celum](https://www.celum.com/) and [cubido](https://www.cubido.at/) to allow to share this code.
## Version history
Version|Date|Comments
-------|----|--------
1.0.0.0|Mar 17, 2021|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- edit config\serve.json set "initialPage": "https://{tenant}.sharepoint.com/_layouts/15/workbench.aspx"
- **gulp serve**
> Include any additional steps as needed.
## Usage
* PNP Placeholder control if not Configured
* PNP WebpartTitle control (toggle Show/Hide in property pane)
* PNP FilePicker control to pick Images (is mocked on local workbench)
* Office UI Fabric
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-image-editor" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

View File

@ -0,0 +1,65 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-image-editor",
"source": "pnp",
"title": "Image Editor",
"shortDescription": "This solution contains an SPFx web part that shows an HTML Image Editor based on canvas and Office UI Fabric",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/master/samples/react-image-editor",
"longDescription": [
"This solution contains an SPFx web part that shows an HTML Image Editor based on canvas and Office UI Fabric ",
"Key features of the Editor",
"* Resize",
"* Crop",
"* Flip",
"* Rotate",
"* Scale",
"* Filter (Grayscale / Sepia)",
"* Redo / Undo",
"* Histoy of Actions"
],
"created": "2021-03-17",
"modified": "2021-03-17",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.4.0"
},
{
"key": "SPFX-TEAMSTAB",
"value": "false"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"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"
}
],
"authors": [
{
"gitHubAccount": "petkir",
"company": "Cubido Business Solutions GmbH",
"pictureUrl": "https://github.com/petkir.png",
"name": "Peter Paul Kirschner",
"twitter": "petkir_at"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"react-image-editor-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/reactImageEditor/ReactImageEditorWebPart.js",
"manifest": "./src/webparts/reactImageEditor/ReactImageEditorWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ReactImageEditorWebPartStrings": "lib/webparts/reactImageEditor/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js",
"ImageManipulationStrings": "lib/components/ImageManipulation/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-image-editor",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-image-editor-client-side-solution",
"id": "e859f86f-7a12-40e3-94fb-97ee47419aed",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-image-editor.sppkg"
}
}

View File

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

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(require('gulp'));

17590
samples/react-image-editor/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
{
"name": "react-image-editor",
"version": "1.0.0",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@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",
"@pnp/spfx-controls-react": "^1.21.1",
"office-ui-fabric-react": "5.131.0",
"react": "15.6.2",
"react-dom": "15.6.2"
},
"devDependencies": {
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
}
}

View File

@ -0,0 +1,23 @@
import { IImageFilter } from './IImageFilter';
export class GrayscaleFilter implements IImageFilter {
public process(imageData: ImageData, width: number, height: number, nvalue?: number, svalue?: string): ImageData {
const data: Uint8ClampedArray = 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
const arraylength: number = width * height * 4;
// Common formula for converting to grayscale.
// gray = 0.3*R + 0.59*G + 0.11*B
for (let i: number = arraylength - 1; i > 0; i -= 4) {
// R= i-3, G = i-2 and B = i-1
// Get our gray shade using the formula
const gray: number = 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,39 @@
import { IImageFilter } from './IImageFilter';
export class SepiaFilter implements IImageFilter {
// tslint:disable-next-line: max-line-length
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
];
// tslint:disable-next-line: max-line-length
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
];
// tslint:disable-next-line: max-line-length
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 (let i: number = 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 (let j: number = 0; j < 3; j++) {
const iPN: number = this.noise + imageData.data[i + j];
imageData.data[i + j] = (iPN > 255) ? 255 : iPN;
}
}
}
return (imageData);
}
}

View File

@ -0,0 +1,28 @@
import { Icon } from 'office-ui-fabric-react';
import * as React from 'react';
import styles from './ImageManipulation.module.scss';
import {
IImageManipulationSettings,
manipulationTypeData,
IManipulationTypeDataDetails } from './ImageManipulation.types';
// tslint:disable-next-line: typedef
export const historyItem = (item: IImageManipulationSettings, _index: number): JSX.Element => {
if (!item) {
return undefined;
}
const data: IManipulationTypeDataDetails = manipulationTypeData[item.type];
const detailrender: JSX.Element = data.toHTML(item);
return (
<span className={styles.historyItem}>
<span className={styles.historyItemIcon}>{data.svgIcon ?
// tslint:disable-next-line: react-a11y-img-has-alt
<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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
import * as React from 'react';
// tslint:disable-next-line: no-any
const colorFilterIcon: any = require('../../svg/colorFilter.svg');
// tslint:disable-next-line: no-any
const cropIcon: any = require('../../svg/crop.svg');
// tslint:disable-next-line: no-any
const flipVerticalIcon: any = require('../../svg/flipVertical.svg');
// tslint:disable-next-line: no-any
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;
// tslint:disable-next-line: no-any
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,432 @@
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: number, min: number, max: number): number {
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;
// tslint:disable-next-line: no-any
onDragEnd?: (e: any) => void;
}
export interface IImageCropState {
cropIsActive: boolean;
newCropIsBeingDrawn: boolean;
reloadtimestamp: string;
}
// Feature detection
// tslint:disable-next-line: max-line-length
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners
export default class ImageCrop extends
React.Component<IImageCropProps, IImageCropState> {
private controlRef: HTMLDivElement = undefined;
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 } = 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 render(): React.ReactElement<IImageCropProps> {
const { crop } = this.props;
const cropSelection: JSX.Element = this.isValid(crop) && this.controlRef ? this.createSelectionGrid() : undefined;
// tslint:disable:react-a11y-event-has-role
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>
);
// tslint:
}
private createSelectionGrid(): JSX.Element {
const { showRuler } = this.props;
const style: { top: string, left: string, width: string, height: string } = this.getCropStyle();
// tslint:disable:react-a11y-event-has-role
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>
);
// tslint:enable
}
private makeNewCrop(): ICrop {
const crop: ICrop = { ...{ sx: 0, sy: 0, height: 0, width: 0 }, ...this.props.crop };
return crop;
}
private getCropStyle(): { top: string, left: string, width: string, height: string } {
const crop: ICrop = this.makeNewCrop();
const unit: string = 'px';
return {
top: `${crop.sy}${unit}`,
left: `${crop.sx}${unit}`,
width: `${crop.width}${unit}`,
height: `${crop.height}${unit}`
};
}
// tslint:disable-next-line: no-any
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) {
// tslint:disable-next-line: no-any
onDragStart(e as any);
}
}
const clientPos: IMousePosition = 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: ICrop;
if (this.evData.isResize) {
nextCrop = this.resizeCrop();
} else {
nextCrop = this.dragCrop();
}
if (nextCrop !== crop) {
if (onChange) {
onChange(nextCrop);
}
}
}
private dragCrop(): ICrop {
const { evData } = this;
const 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(): ICrop {
const { evData } = this;
const nextCrop: ICrop = this.makeNewCrop();
const { pos } = evData;
if (evData.xInversed) {
evData.xDiff -= evData.cropStartWidth * 2;
}
if (evData.yInversed) {
evData.yDiff -= evData.cropStartHeight * 2;
}
const newSize: { width: number, height: number } = this.getNewSize();
let newX: number = evData.cropStartX;
let newY: number = 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: number = evData.cropStartWidth + evData.xDiff;
if (evData.xInversed) {
newWidth = Math.abs(newWidth);
}
newWidth = clamp(newWidth, 0, sourceWidth);
// New height.
let newHeight: number;
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
};
}
// tslint:disable-next-line: no-any
private onDocMouseTouchEnd(e: MouseEvent | any): void {
const { crop, onDragEnd, onComplete } = this.props;
if (this.mouseDownOnCrop) {
this.mouseDownOnCrop = false;
this.dragStarted = false;
if (onDragEnd) {
onDragEnd(e);
}
if (onComplete) {
onComplete(crop);
}
this.setState({ cropIsActive: false, newCropIsBeingDrawn: false });
}
}
// tslint:disable-next-line: no-any
private onCropMouseTouchDown(e: MouseEvent | any): void {
const { crop } = this.props;
e.preventDefault(); // Stop drag selection.
const mousepos: IMousePosition = 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;
}
// tslint:disable-next-line: no-any
private getClientPos(e: MouseEvent | any): IMousePosition {
let pageX: number;
let pageY: number;
if (e.touches) {
[{ pageX, pageY }] = e.touches;
} else {
({ pageX, pageY } = e);
}
return {
x: pageX,
y: pageY
};
}
private isValid(crop: ICrop): boolean {
return crop && !isNaN(crop.width) && !isNaN(crop.height);
}
// tslint:disable-next-line: no-any
private onMouseTouchDown(e: MouseEvent | any): void {
const { crop, onChange } = this.props;
e.preventDefault(); // Stop drag selection.
const mousepos: IMousePosition = this.getClientPos(e);
// tslint:disable-next-line: no-any
const refpos: any = this.controlRef.getBoundingClientRect();
const startx: number = mousepos.x - refpos.left;
const 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,230 @@
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;
// tslint:disable-next-line: no-any
onDragEnd?: (e: MouseEvent | any) => void;
// tslint:disable-next-line: no-any
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 = undefined;
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> {
// tslint:disable:react-a11y-event-has-role
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>
);
// tslint:enable
}
// tslint:disable-next-line: no-any
private onStartResizing(e: MouseEvent | any): void {
const mousePos: IMousePosition = 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
};
}
// tslint:disable-next-line: no-any
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: IMousePosition = 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;
}
const 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);
}
}
// tslint:disable-next-line: no-any
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 });
}
}
}
// tslint:disable-next-line: no-any
private getClientPos(e: MouseEvent | any): IMousePosition {
let pageX: number;
let pageY: number;
if (e.touches) {
[{ pageX, pageY }] = e.touches;
} else {
({ pageX, pageY } = e);
}
return {
x: pageX,
y: pageY
};
}
}

View File

@ -0,0 +1,21 @@
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,303 @@
import { isEqual } from '@microsoft/sp-lodash-subset';
import { EventGroup, IButtonStyles, IconButton, ISelection, Label } from 'office-ui-fabric-react';
import { DragDropHelper, IDragDropContext } 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;
// tslint:disable-next-line: no-any
items: Array<any>;
textProperty?: string;
moveUpIconName: string;
moveDownIconName: string;
disableDragAndDrop: boolean;
removeArrows: boolean;
maxHeight?: number;
// tslint:disable-next-line: no-any
valueChanged: (newValue: Array<any>) => void;
// tslint:disable-next-line: no-any
onRenderItem?: (item: any, index: number) => JSX.Element;
}
export interface IItemOrderState {
// tslint:disable-next-line: no-any
items: Array<any>;
}
export default class ItemOrder extends React.Component<IItemOrderProps, IItemOrderState> {
// tslint:disable-next-line: no-any
private _draggedItem: any;
private _selection: ISelection;
private _ddHelper: DragDropHelper;
private _refs: Array<HTMLElement>;
// tslint:disable-next-line: no-any
private _ddSubs: Array<any>;
private _lastBox: HTMLElement;
constructor(props: IItemOrderProps) {
super(props);
this._selection = undefined;
this._ddHelper = new DragDropHelper({
selection: this._selection
});
this._refs = new Array<HTMLElement>();
// tslint:disable-next-line: no-any
this._ddSubs = new Array<any>();
this._draggedItem = undefined;
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) && (
// tslint:disable-next-line: no-any
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>
);
}
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();
}
// tslint:disable-next-line: no-any
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>
);
}
// tslint:disable-next-line: no-any
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 {
const 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>
);
}
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: [
{
// tslint:disable-next-line: no-any
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;
},
// tslint:disable-next-line: no-any
canDrag: (_item?: any) => {
return true;
},
// tslint:disable-next-line: no-any
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;
},*/
// tslint:disable-next-line: no-any
onDragEnd: (_item?: any, _event?: DragEvent) => {
this._draggedItem = undefined;
}
}));
});
// Create droppable 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;
},
// tslint:disable-next-line: no-any
onDrop: (_item?: any, _event?: DragEvent) => {
if (this._draggedItem) {
const itemIndex: number = this.state.items.indexOf(this._draggedItem);
this.moveItemAtIndexToTargetIndex(itemIndex, this.state.items.length - 1);
}
}
}));
}
}
}
private cleanupSubscriptions = (): void => {
while (this._ddSubs.length) {
// tslint:disable-next-line: no-any
const sub: any = this._ddSubs.pop();
sub.dispose();
}
}
// tslint:disable-next-line: no-any
private insertBeforeItem = (item: any) => {
const 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) {
// tslint:disable-next-line: no-any
const 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,12 @@
/*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,34 @@
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 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

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

@ -0,0 +1,25 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "51c2d476-67e1-453f-888c-ae193bad7bc6",
"alias": "ReactImageEditorWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Other" },
"title": { "default": "react-image-editor" },
"description": { "default": "react-image-editor description" },
"officeFabricIconFontName": "Page",
"properties": {
"title": "react-image-editor Sample",
"showTitle":true,
"settings":[],
"url":""
}
}]
}

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneToggle
} from '@microsoft/sp-webpart-base';
import * as strings from 'ReactImageEditorWebPartStrings';
import ReactImageEditor, { IReactImageEditorBaseProps, IReactImageEditorProps } from './components/ReactImageEditor';
import { IImageManipulationSettings } from '../../components';
export interface IReactImageEditorWebPartProps extends IReactImageEditorBaseProps {
}
export default class ReactImageEditorWebPart extends BaseClientSideWebPart<IReactImageEditorWebPartProps> {
public render(): void {
const element: React.ReactElement<IReactImageEditorProps> = React.createElement(
ReactImageEditor,
{
context: this.context,
displayMode: this.displayMode,
showTitle: this.properties.showTitle,
title: this.properties.title,
url: this.properties.url,
settings: this.properties.settings,
updateTitleProperty: (value: string) => { this.properties.title = value; },
updateUrlProperty: (value: string) => {
// tslint:disable-next-line: curly
if (this.properties.url !== value)
this.properties.url = value;
this.properties.settings = [];
this.render();
},
updateManipulationSettingsProperty: (value: IImageManipulationSettings[]) => {
this.properties.settings = value;
this.render();
}
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneToggle('showTitle', {
label: strings.ShowTitleFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,5 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.reactImageEditor {
display: block;
}

View File

@ -0,0 +1,108 @@
import * as React from 'react';
import styles from './ReactImageEditor.module.scss';
import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle';
import { DisplayMode, Environment, EnvironmentType } from '@microsoft/sp-core-library';
import { Placeholder } from '@pnp/spfx-controls-react/lib/Placeholder';
import { WebPartContext } from '@microsoft/sp-webpart-base';
import { FilePicker, IFilePickerResult } from '@pnp/spfx-controls-react/lib/FilePicker';
import { ImageManipulation, IImageManipulationSettings } from '../../../components/ImageManipulation';
export interface IReactImageEditorBaseProps {
showTitle: boolean;
title: string;
url?: string;
settings?: IImageManipulationSettings[];
}
export interface IReactImageEditorProps extends IReactImageEditorBaseProps {
displayMode: DisplayMode;
context: WebPartContext;
updateTitleProperty: (value: string) => void;
updateUrlProperty: (value: string) => void;
updateManipulationSettingsProperty: (value: IImageManipulationSettings[]) => void;
}
export interface IReactImageEditorState {
isFilePickerOpen: boolean;
statekey: string;
}
export default class ReactImageEditor extends React.Component<IReactImageEditorProps, IReactImageEditorState> {
constructor(props: IReactImageEditorProps) {
super(props);
this.state = {
isFilePickerOpen: false,
statekey: 'init'
};
this._onConfigure = this._onConfigure.bind(this);
this._onUrlChanged = this._onUrlChanged.bind(this);
this._onSettingsChanged = this._onSettingsChanged.bind(this);
}
public render(): React.ReactElement<IReactImageEditorProps> {
const { url, settings } = this.props;
const { isFilePickerOpen } = this.state;
const isConfigured: boolean = !!url && url.length > 0;
return (
<div className={styles.reactImageEditor}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateTitleProperty} />
{(isFilePickerOpen || isConfigured) && Environment.type !== EnvironmentType.Local &&
<FilePicker
isPanelOpen={isFilePickerOpen}
accepts={['.gif', '.jpg', '.jpeg', '.png']}
buttonIcon='FileImage'
onSave={(filePickerResult: IFilePickerResult) => {
this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl));
}}
onCancel={() => {
this.setState({ isFilePickerOpen: false });
}}
onChanged={(filePickerResult: IFilePickerResult) => {
this.setState({ isFilePickerOpen: false }, () => this._onUrlChanged(filePickerResult.fileAbsoluteUrl));
}}
context={this.props.context}
/>}
{!isConfigured ? (<Placeholder iconName='Edit'
iconText='Configure your web part'
description='Please configure the web part.'
buttonLabel='Configure'
onConfigure={this._onConfigure} />) :
(<ImageManipulation
settings={settings}
configsettings={{
rotateButtons: [-90, -45, -30, 0, 30, 45, 90]
}
}
displyMode={DisplayMode.Edit}
settingschanged={this.props.updateManipulationSettingsProperty}
src={url}
/>)}
</div >
);
}
private _onConfigure = () => {
if (Environment.type === EnvironmentType.Local) {
this.setState({ isFilePickerOpen: false }, () => {
this._onUrlChanged(
'https://media.gettyimages.com/photos/'
+ 'whitewater-paddlers-descend-vertical-waterfall-in-kayak-picture-id1256321293?s=2048x2048'
);
});
} else {
this.setState({ isFilePickerOpen: true });
}
}
private _onUrlChanged = (url: string) => {
this.props.updateUrlProperty(url);
}
private _onSettingsChanged = (settings: IImageManipulationSettings[]) => {
this.props.updateManipulationSettingsProperty(settings);
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"ShowTitleFieldLabel": "Show webpart title"
}
});

View File

@ -0,0 +1,10 @@
declare interface IReactImageEditorWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
ShowTitleFieldLabel: string;
}
declare module 'ReactImageEditorWebPartStrings' {
const strings: IReactImageEditorWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}

View File

@ -0,0 +1,31 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false,
"react-a11y-event-has-role": true
}
}