0.0.0.4
This commit is contained in:
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
## ISC License
|
||||
|
||||
Copyright (c) 2015, Dominic Tobias (https://github.com/dominictobias)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
Generated
Vendored
+284
@@ -0,0 +1,284 @@
|
||||
# React Image Crop
|
||||
|
||||
An image cropping tool for React with no dependencies.
|
||||
|
||||
[](https://www.npmjs.com/package/react-image-crop)
|
||||
|
||||
[CodeSandbox Demo](https://codesandbox.io/s/react-image-crop-demo-with-react-hooks-y831o)
|
||||
|
||||

|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Features](#features)
|
||||
2. [Installation](#installation)
|
||||
3. [Usage](#usage)
|
||||
4. [Example](#example)
|
||||
5. [CDN](#cdn)
|
||||
6. [Props](#props)
|
||||
7. [FAQ](#faq)
|
||||
1. [How can I generate a crop preview in the browser?](#how-can-i-generate-a-crop-preview-in-the-browser)
|
||||
2. [How to correct image EXIF orientation/rotation?](#how-to-correct-image-exif-orientationrotation)
|
||||
3. [How to filter, rotate and annotate?](#how-to-filter-rotate-and-annotate)
|
||||
4. [How can I center the crop?](#how-can-i-center-the-crop)
|
||||
8. [Contributing / Developing](#contributing--developing)
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive (you can use pixels or percentages).
|
||||
- Touch enabled.
|
||||
- Free-form or fixed aspect crops.
|
||||
- Fully keyboard accessible (a11y).
|
||||
- No dependencies/small footprint (<5KB gzip).
|
||||
- Min/max crop size.
|
||||
- Crop anything, not just images.
|
||||
|
||||
If React Crop doesn't cover your requirements then take a look at [Pintura](https://pqina.nl/pintura/?ref=react-image-crop) (our sponsor). It features cropping, rotating, filtering, annotation, and lots more.
|
||||
|
||||
[Learn more about Pintura here](https://pqina.nl/pintura/?ref=react-image-crop)
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
npm i react-image-crop --save
|
||||
yarn add react-image-crop
|
||||
pnpm add react-image-crop
|
||||
```
|
||||
|
||||
This library works with all modern browsers. It does not work with IE.
|
||||
|
||||
## Usage
|
||||
|
||||
Include the main js module:
|
||||
|
||||
```js
|
||||
import ReactCrop from 'react-image-crop'
|
||||
```
|
||||
|
||||
Include either `dist/ReactCrop.css` or `ReactCrop.scss`.
|
||||
|
||||
```js
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
// or scss:
|
||||
import 'react-image-crop/src/ReactCrop.scss'
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
import ReactCrop, { type Crop } from 'react-image-crop'
|
||||
|
||||
function CropDemo({ src }) {
|
||||
const [crop, setCrop] = useState<Crop>()
|
||||
return (
|
||||
<ReactCrop crop={crop} onChange={c => setCrop(c)}>
|
||||
<img src={src} />
|
||||
</ReactCrop>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
See the [sandbox demo](https://codesandbox.io/s/react-image-crop-demo-with-react-hooks-y831o) for a more complete example.
|
||||
|
||||
## CDN
|
||||
|
||||
```html
|
||||
<link href="https://unpkg.com/react-image-crop/dist/ReactCrop.css" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/react-image-crop/dist/index.umd.cjs"></script>
|
||||
```
|
||||
|
||||
Note when importing the script globally using a `<script>` tag access the component with `ReactCrop.Component`.
|
||||
|
||||
## Props
|
||||
|
||||
**`onChange: (crop: PixelCrop, percentCrop: PercentCrop) => void`**
|
||||
|
||||
A callback which happens for every change of the crop (i.e. many times as you are dragging/resizing). Passes the current crop state object.
|
||||
|
||||
Note you _must_ implement this callback and update your crop state, otherwise nothing will change!
|
||||
|
||||
```tsx
|
||||
<ReactCrop crop={crop} onChange={(crop, percentCrop) => setCrop(crop)} />
|
||||
```
|
||||
|
||||
`crop` and `percentCrop` are interchangeable. `crop` uses pixels and `percentCrop` uses percentages to position and size itself. Percent crops are resistant to image/media resizing.
|
||||
|
||||
**`crop?: Crop`**
|
||||
|
||||
Starting with no crop:
|
||||
|
||||
```tsx
|
||||
const [crop, setCrop] = useState<Crop>()
|
||||
|
||||
<ReactCrop crop={crop} onChange={c => setCrop(c)}>
|
||||
<img src={src} />
|
||||
</ReactCrop>
|
||||
```
|
||||
|
||||
Starting with a preselected crop:
|
||||
|
||||
```tsx
|
||||
const [crop, setCrop] = useState<Crop>({
|
||||
unit: '%', // Can be 'px' or '%'
|
||||
x: 25,
|
||||
y: 25,
|
||||
width: 50,
|
||||
height: 50
|
||||
})
|
||||
|
||||
<ReactCrop crop={crop} onChange={c => setCrop(c)}>
|
||||
<img src={src} />
|
||||
</ReactCrop>
|
||||
```
|
||||
|
||||
⚠️ You must ensure the crop is in bounds and correct to the aspect ratio if manually setting. Aspect ratios can be tricky when using %. You can make use of `centerCrop` and `makeAspectCrop` helpers. See [How can I center the crop?](#how-can-i-center-the-crop) or the [CodeSanbox Demo](https://codesandbox.io/s/react-image-crop-demo-with-react-hooks-y831o) for examples.
|
||||
|
||||
**`aspect?: number`**
|
||||
|
||||
The aspect ratio of the crop, e.g. `1` for a square or `16 / 9` for landscape. Omit/pass undefined for a free-form crop.
|
||||
|
||||
**`minWidth?: number`**
|
||||
|
||||
A minimum crop width, in pixels.
|
||||
|
||||
**`minHeight?: number`**
|
||||
|
||||
A minimum crop height, in pixels.
|
||||
|
||||
**`maxWidth?: number`**
|
||||
|
||||
A maximum crop width, in pixels.
|
||||
|
||||
**`maxHeight?: number`**
|
||||
|
||||
A maximum crop height, in pixels.
|
||||
|
||||
**`keepSelection?: boolean`**
|
||||
|
||||
If true is passed then selection can't be disabled if the user clicks outside the selection area.
|
||||
|
||||
**`disabled?: boolean`**
|
||||
|
||||
If true then the user cannot resize or draw a new crop. A class of `ReactCrop--disabled` is also added to the container for user styling.
|
||||
|
||||
**`locked?: boolean`**
|
||||
|
||||
If true then the user cannot create or resize a crop, but can still drag the existing crop around. A class of `ReactCrop--locked` is also added to the container for user styling.
|
||||
|
||||
**`className?: string`**
|
||||
|
||||
A string of classes to add to the main `ReactCrop` element.
|
||||
|
||||
**`style?: React.CSSProperties`**
|
||||
|
||||
Inline styles object to be passed to the image wrapper element.
|
||||
|
||||
**`onComplete?: (crop: PixelCrop, percentCrop: PercentCrop) => void`**
|
||||
|
||||
A callback which happens after a resize, drag, or nudge. Passes the current crop state object.
|
||||
|
||||
`percentCrop` is the crop as a percentage. A typical use case for it would be to save it so that the user's crop can be restored regardless of the size of the image (for example saving it on desktop, and then using it on a mobile where the image is smaller).
|
||||
|
||||
**`onDragStart?: (e: PointerEvent) => void`**
|
||||
|
||||
A callback which happens when a user starts dragging or resizing. It is convenient to manipulate elements outside this component.
|
||||
|
||||
**`onDragEnd?: (e: PointerEvent) => void`**
|
||||
|
||||
A callback which happens when a user releases the cursor or touch after dragging or resizing.
|
||||
|
||||
**`renderSelectionAddon?: (state: ReactCropState) => React.ReactNode`**
|
||||
|
||||
Render a custom element inside the crop selection.
|
||||
|
||||
**`ruleOfThirds?: boolean`**
|
||||
|
||||
Show [rule of thirds](https://en.wikipedia.org/wiki/Rule_of_thirds) lines in the cropped area. Defaults to `false`.
|
||||
|
||||
**`circularCrop?: boolean`**
|
||||
|
||||
Show the crop area as a circle. If your `aspect` is not `1` (a square) then the circle will be warped into an oval shape. Defaults to `false`.
|
||||
|
||||
## FAQ
|
||||
|
||||
### How can I generate a crop preview in the browser?
|
||||
|
||||
This isn't part of the library but there is an example over here [CodeSandbox Demo](https://codesandbox.io/s/react-image-crop-demo-with-react-hooks-y831o).
|
||||
|
||||
### How to correct image EXIF orientation/rotation?
|
||||
|
||||
You might find that some images are rotated incorrectly. Unfortunately this is a browser wide issue not related to this library. You need to fix your image before passing it in.
|
||||
|
||||
You can use the following library to load images, which will correct the rotation for you: https://github.com/blueimp/JavaScript-Load-Image/
|
||||
|
||||
You can read an issue on this subject here: https://github.com/dominictobias/react-image-crop/issues/181
|
||||
|
||||
If you're looking for a complete out of the box image editor which already handles EXIF rotation then consider using [Pintura](https://pqina.nl/pintura/?ref=react-image-crop).
|
||||
|
||||
<h3>How to filter, rotate and annotate?</h3>
|
||||
|
||||
This library is deliberately lightweight and minimal for you to build features on top of. If you wish to perform more advanced image editing out of the box then consider using [Pintura](https://pqina.nl/pintura/?ref=react-image-crop).
|
||||
|
||||

|
||||
|
||||
### How can I center the crop?
|
||||
|
||||
The easiest way is to use the percentage unit:
|
||||
|
||||
```js
|
||||
crop: {
|
||||
unit: '%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
x: 25,
|
||||
y: 25
|
||||
}
|
||||
```
|
||||
|
||||
Centering an aspect ratio crop is trickier especially when dealing with `%`. However two helper functions are provided:
|
||||
|
||||
1. Listen to the load event of your media to get its size:
|
||||
|
||||
```jsx
|
||||
<ReactCrop crop={crop} aspect={16 / 9}>
|
||||
<img src={src} onLoad={onImageLoad} />
|
||||
</ReactCrop>
|
||||
```
|
||||
|
||||
2. Use `makeAspectCrop` to create your desired aspect and then `centerCrop` to center it:
|
||||
|
||||
```js
|
||||
function onImageLoad(e) {
|
||||
const { naturalWidth: width, naturalHeight: height } = e.currentTarget
|
||||
|
||||
const crop = centerCrop(
|
||||
makeAspectCrop(
|
||||
{
|
||||
// You don't need to pass a complete crop into
|
||||
// makeAspectCrop or centerCrop.
|
||||
unit: '%',
|
||||
width: 90,
|
||||
},
|
||||
16 / 9,
|
||||
width,
|
||||
height
|
||||
),
|
||||
width,
|
||||
height
|
||||
)
|
||||
|
||||
setCrop(crop)
|
||||
}
|
||||
```
|
||||
|
||||
Also remember to set your crop using the percentCrop on changes:
|
||||
|
||||
```js
|
||||
const onCropChange = (crop, percentCrop) => setCrop(percentCrop)
|
||||
```
|
||||
|
||||
And your `aspect` prop should be set to the same value: `<ReactCrop aspect={16 / 9} ... />`.
|
||||
|
||||
## Contributing / Developing
|
||||
|
||||
To develop run `pnpm install && pnpm dev` and open the localhost server in your browser. Update code and it will reload. When you're ready, open a pull request.
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+202
@@ -0,0 +1,202 @@
|
||||
import { default as default_2 } from 'react';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
export declare const areCropsEqual: (cropA: Partial<Crop>, cropB: Partial<Crop>) => boolean;
|
||||
|
||||
export declare function centerCrop(crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>, containerWidth: number, containerHeight: number): PercentCrop;
|
||||
|
||||
export declare function centerCrop(crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>, containerWidth: number, containerHeight: number): PixelCrop;
|
||||
|
||||
export declare const clamp: (num: number, min: number, max: number) => number;
|
||||
|
||||
export declare const cls: (...args: unknown[]) => string;
|
||||
|
||||
export declare function containCrop(pixelCrop: PixelCrop, aspect: number, ord: Ords, containerWidth: number, containerHeight: number, minWidth?: number, minHeight?: number, maxWidth?: number, maxHeight?: number): {
|
||||
unit: "px";
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export declare function convertToPercentCrop(crop: Partial<Crop>, containerWidth: number, containerHeight: number): PercentCrop;
|
||||
|
||||
export declare function convertToPixelCrop(crop: Partial<Crop>, containerWidth: number, containerHeight: number): PixelCrop;
|
||||
|
||||
export declare interface Crop {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
unit: 'px' | '%';
|
||||
}
|
||||
|
||||
export declare const defaultCrop: PixelCrop;
|
||||
|
||||
declare interface EVData {
|
||||
startClientX: number;
|
||||
startClientY: number;
|
||||
startCropX: number;
|
||||
startCropY: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
isResize: boolean;
|
||||
ord?: Ords;
|
||||
}
|
||||
|
||||
export declare function makeAspectCrop(crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>, aspect: number, containerWidth: number, containerHeight: number): PercentCrop;
|
||||
|
||||
export declare function makeAspectCrop(crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>, aspect: number, containerWidth: number, containerHeight: number): PixelCrop;
|
||||
|
||||
export declare function nudgeCrop(pixelCrop: PixelCrop, key: string, offset: number, ord: Ords): {
|
||||
unit: "px";
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export declare type Ords = XOrds | YOrds | XYOrds;
|
||||
|
||||
export declare interface PercentCrop extends Crop {
|
||||
unit: '%';
|
||||
}
|
||||
|
||||
export declare interface PixelCrop extends Crop {
|
||||
unit: 'px';
|
||||
}
|
||||
|
||||
declare class ReactCrop extends PureComponent<ReactCropProps, ReactCropState> {
|
||||
static xOrds: string[];
|
||||
static yOrds: string[];
|
||||
static xyOrds: string[];
|
||||
static nudgeStep: number;
|
||||
static nudgeStepMedium: number;
|
||||
static nudgeStepLarge: number;
|
||||
static defaultProps: {
|
||||
ariaLabels: {
|
||||
cropArea: string;
|
||||
nwDragHandle: string;
|
||||
nDragHandle: string;
|
||||
neDragHandle: string;
|
||||
eDragHandle: string;
|
||||
seDragHandle: string;
|
||||
sDragHandle: string;
|
||||
swDragHandle: string;
|
||||
wDragHandle: string;
|
||||
};
|
||||
};
|
||||
get document(): Document;
|
||||
docMoveBound: boolean;
|
||||
mouseDownOnCrop: boolean;
|
||||
dragStarted: boolean;
|
||||
evData: EVData;
|
||||
componentRef: default_2.RefObject<HTMLDivElement | null>;
|
||||
mediaRef: default_2.RefObject<HTMLDivElement | null>;
|
||||
resizeObserver?: ResizeObserver;
|
||||
initChangeCalled: boolean;
|
||||
instanceId: string;
|
||||
state: ReactCropState;
|
||||
getBox(): Rectangle;
|
||||
componentDidUpdate(prevProps: ReactCropProps): void;
|
||||
componentWillUnmount(): void;
|
||||
bindDocMove(): void;
|
||||
unbindDocMove(): void;
|
||||
onCropPointerDown: (e: default_2.PointerEvent<HTMLDivElement>) => void;
|
||||
onComponentPointerDown: (e: default_2.PointerEvent<HTMLDivElement>) => void;
|
||||
onDocPointerMove: (e: PointerEvent) => void;
|
||||
onComponentKeyDown: (e: default_2.KeyboardEvent<HTMLDivElement>) => void;
|
||||
onHandlerKeyDown: (e: default_2.KeyboardEvent<HTMLDivElement>, ord: Ords) => void;
|
||||
onDocPointerDone: (e: PointerEvent) => void;
|
||||
onDragFocus: () => void;
|
||||
getCropStyle(): {
|
||||
top: string;
|
||||
left: string;
|
||||
width: string;
|
||||
height: string;
|
||||
} | undefined;
|
||||
dragCrop(): PixelCrop;
|
||||
getPointRegion(box: Rectangle, origOrd: Ords | undefined, minWidth: number, minHeight: number): XYOrds;
|
||||
resolveMinDimensions(box: Rectangle, aspect: number, minWidth?: number, minHeight?: number): number[];
|
||||
resizeCrop(): PixelCrop;
|
||||
renderCropSelection(): default_2.JSX.Element | undefined;
|
||||
makePixelCrop(box: Rectangle): PixelCrop;
|
||||
render(): default_2.JSX.Element;
|
||||
}
|
||||
export { ReactCrop as Component }
|
||||
export { ReactCrop }
|
||||
export default ReactCrop;
|
||||
|
||||
export declare interface ReactCropProps {
|
||||
/** An object of labels to override the built-in English ones */
|
||||
ariaLabels?: {
|
||||
cropArea: string;
|
||||
nwDragHandle: string;
|
||||
nDragHandle: string;
|
||||
neDragHandle: string;
|
||||
eDragHandle: string;
|
||||
seDragHandle: string;
|
||||
sDragHandle: string;
|
||||
swDragHandle: string;
|
||||
wDragHandle: string;
|
||||
};
|
||||
/** The aspect ratio of the crop, e.g. `1` for a square or `16 / 9` for landscape. */
|
||||
aspect?: number;
|
||||
/** Classes to pass to the `ReactCrop` element. */
|
||||
className?: string;
|
||||
/** The elements that you want to perform a crop on. For example
|
||||
* an image or video. */
|
||||
children?: default_2.ReactNode;
|
||||
/** Show the crop area as a circle. If your aspect is not 1 (a square) then the circle will be warped into an oval shape. Defaults to false. */
|
||||
circularCrop?: boolean;
|
||||
/** Since v10 all crop params are required except for aspect. Omit the entire crop object if you don't want a crop. See README on how to create an aspect crop with a % crop. */
|
||||
crop?: Crop;
|
||||
/** If true then the user cannot resize or draw a new crop. A class of `ReactCrop--disabled` is also added to the container for user styling. */
|
||||
disabled?: boolean;
|
||||
/** If true then the user cannot create or resize a crop, but can still drag the existing crop around. A class of `ReactCrop--locked` is also added to the container for user styling. */
|
||||
locked?: boolean;
|
||||
/** If true is passed then selection can't be disabled if the user clicks outside the selection area. */
|
||||
keepSelection?: boolean;
|
||||
/** A minimum crop width, in pixels. */
|
||||
minWidth?: number;
|
||||
/** A minimum crop height, in pixels. */
|
||||
minHeight?: number;
|
||||
/** A maximum crop width, in pixels. */
|
||||
maxWidth?: number;
|
||||
/** A maximum crop height, in pixels. */
|
||||
maxHeight?: number;
|
||||
/** A callback which happens for every change of the crop. You should set the crop to state and pass it back into the library via the `crop` prop. */
|
||||
onChange: (crop: PixelCrop, percentageCrop: PercentCrop) => void;
|
||||
/** A callback which happens after a resize, drag, or nudge. Passes the current crop state object in pixels and percent. */
|
||||
onComplete?: (crop: PixelCrop, percentageCrop: PercentCrop) => void;
|
||||
/** A callback which happens when a user starts dragging or resizing. It is convenient to manipulate elements outside this component. */
|
||||
onDragStart?: (e: PointerEvent) => void;
|
||||
/** A callback which happens when a user releases the cursor or touch after dragging or resizing. */
|
||||
onDragEnd?: (e: PointerEvent) => void;
|
||||
/** Render a custom element in crop selection. */
|
||||
renderSelectionAddon?: (state: ReactCropState) => default_2.ReactNode;
|
||||
/** Show rule of thirds lines in the cropped area. Defaults to false. */
|
||||
ruleOfThirds?: boolean;
|
||||
/** Inline styles object to be passed to the `ReactCrop` element. */
|
||||
style?: default_2.CSSProperties;
|
||||
}
|
||||
|
||||
export declare interface ReactCropState {
|
||||
cropIsActive: boolean;
|
||||
newCropIsBeingDrawn: boolean;
|
||||
}
|
||||
|
||||
declare interface Rectangle {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export declare type XOrds = 'e' | 'w';
|
||||
|
||||
export declare type XYOrds = 'nw' | 'ne' | 'se' | 'sw';
|
||||
|
||||
export declare type YOrds = 'n' | 's';
|
||||
|
||||
export { }
|
||||
node_modules/.pnpm/react-image-crop@11.0.10_react@19.2.0/node_modules/react-image-crop/dist/index.js
Generated
Vendored
+459
@@ -0,0 +1,459 @@
|
||||
var _ = Object.defineProperty;
|
||||
var $ = (a, h, e) => h in a ? _(a, h, { enumerable: !0, configurable: !0, writable: !0, value: e }) : a[h] = e;
|
||||
var m = (a, h, e) => $(a, typeof h != "symbol" ? h + "" : h, e);
|
||||
import u, { PureComponent as K, createRef as P } from "react";
|
||||
const E = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
unit: "px"
|
||||
}, b = (a, h, e) => Math.min(Math.max(a, h), e), H = (...a) => a.filter((h) => h && typeof h == "string").join(" "), X = (a, h) => a === h || a.width === h.width && a.height === h.height && a.x === h.x && a.y === h.y && a.unit === h.unit;
|
||||
function B(a, h, e, n) {
|
||||
const t = D(a, e, n);
|
||||
return a.width && (t.height = t.width / h), a.height && (t.width = t.height * h), t.y + t.height > n && (t.height = n - t.y, t.width = t.height * h), t.x + t.width > e && (t.width = e - t.x, t.height = t.width / h), a.unit === "%" ? v(t, e, n) : t;
|
||||
}
|
||||
function L(a, h, e) {
|
||||
const n = D(a, h, e);
|
||||
return n.x = (h - n.width) / 2, n.y = (e - n.height) / 2, a.unit === "%" ? v(n, h, e) : n;
|
||||
}
|
||||
function v(a, h, e) {
|
||||
return a.unit === "%" ? { ...E, ...a, unit: "%" } : {
|
||||
unit: "%",
|
||||
x: a.x ? a.x / h * 100 : 0,
|
||||
y: a.y ? a.y / e * 100 : 0,
|
||||
width: a.width ? a.width / h * 100 : 0,
|
||||
height: a.height ? a.height / e * 100 : 0
|
||||
};
|
||||
}
|
||||
function D(a, h, e) {
|
||||
return a.unit ? a.unit === "px" ? { ...E, ...a, unit: "px" } : {
|
||||
unit: "px",
|
||||
x: a.x ? a.x * h / 100 : 0,
|
||||
y: a.y ? a.y * e / 100 : 0,
|
||||
width: a.width ? a.width * h / 100 : 0,
|
||||
height: a.height ? a.height * e / 100 : 0
|
||||
} : { ...E, ...a, unit: "px" };
|
||||
}
|
||||
function k(a, h, e, n, t, d = 0, r = 0, o = n, w = t) {
|
||||
const i = { ...a };
|
||||
let s = Math.min(d, n), c = Math.min(r, t), g = Math.min(o, n), p = Math.min(w, t);
|
||||
h && (h > 1 ? (s = r ? r * h : s, c = s / h, g = o * h) : (c = d ? d / h : c, s = c * h, p = w / h)), i.y < 0 && (i.height = Math.max(i.height + i.y, c), i.y = 0), i.x < 0 && (i.width = Math.max(i.width + i.x, s), i.x = 0);
|
||||
const l = n - (i.x + i.width);
|
||||
l < 0 && (i.x = Math.min(i.x, n - s), i.width += l);
|
||||
const C = t - (i.y + i.height);
|
||||
if (C < 0 && (i.y = Math.min(i.y, t - c), i.height += C), i.width < s && ((e === "sw" || e == "nw") && (i.x -= s - i.width), i.width = s), i.height < c && ((e === "nw" || e == "ne") && (i.y -= c - i.height), i.height = c), i.width > g && ((e === "sw" || e == "nw") && (i.x -= g - i.width), i.width = g), i.height > p && ((e === "nw" || e == "ne") && (i.y -= p - i.height), i.height = p), h) {
|
||||
const y = i.width / i.height;
|
||||
if (y < h) {
|
||||
const f = Math.max(i.width / h, c);
|
||||
(e === "nw" || e == "ne") && (i.y -= f - i.height), i.height = f;
|
||||
} else if (y > h) {
|
||||
const f = Math.max(i.height * h, s);
|
||||
(e === "sw" || e == "nw") && (i.x -= f - i.width), i.width = f;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
function I(a, h, e, n) {
|
||||
const t = { ...a };
|
||||
return h === "ArrowLeft" ? n === "nw" ? (t.x -= e, t.y -= e, t.width += e, t.height += e) : n === "w" ? (t.x -= e, t.width += e) : n === "sw" ? (t.x -= e, t.width += e, t.height += e) : n === "ne" ? (t.y += e, t.width -= e, t.height -= e) : n === "e" ? t.width -= e : n === "se" && (t.width -= e, t.height -= e) : h === "ArrowRight" && (n === "nw" ? (t.x += e, t.y += e, t.width -= e, t.height -= e) : n === "w" ? (t.x += e, t.width -= e) : n === "sw" ? (t.x += e, t.width -= e, t.height -= e) : n === "ne" ? (t.y -= e, t.width += e, t.height += e) : n === "e" ? t.width += e : n === "se" && (t.width += e, t.height += e)), h === "ArrowUp" ? n === "nw" ? (t.x -= e, t.y -= e, t.width += e, t.height += e) : n === "n" ? (t.y -= e, t.height += e) : n === "ne" ? (t.y -= e, t.width += e, t.height += e) : n === "sw" ? (t.x += e, t.width -= e, t.height -= e) : n === "s" ? t.height -= e : n === "se" && (t.width -= e, t.height -= e) : h === "ArrowDown" && (n === "nw" ? (t.x += e, t.y += e, t.width -= e, t.height -= e) : n === "n" ? (t.y += e, t.height -= e) : n === "ne" ? (t.y += e, t.width -= e, t.height -= e) : n === "sw" ? (t.x -= e, t.width += e, t.height += e) : n === "s" ? t.height += e : n === "se" && (t.width += e, t.height += e)), t;
|
||||
}
|
||||
const M = { capture: !0, passive: !1 };
|
||||
let N = 0;
|
||||
const x = class x extends K {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
m(this, "docMoveBound", !1);
|
||||
m(this, "mouseDownOnCrop", !1);
|
||||
m(this, "dragStarted", !1);
|
||||
m(this, "evData", {
|
||||
startClientX: 0,
|
||||
startClientY: 0,
|
||||
startCropX: 0,
|
||||
startCropY: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
isResize: !0
|
||||
});
|
||||
m(this, "componentRef", P());
|
||||
m(this, "mediaRef", P());
|
||||
m(this, "resizeObserver");
|
||||
m(this, "initChangeCalled", !1);
|
||||
m(this, "instanceId", `rc-${N++}`);
|
||||
m(this, "state", {
|
||||
cropIsActive: !1,
|
||||
newCropIsBeingDrawn: !1
|
||||
});
|
||||
m(this, "onCropPointerDown", (e) => {
|
||||
const { crop: n, disabled: t } = this.props, d = this.getBox();
|
||||
if (!n)
|
||||
return;
|
||||
const r = D(n, d.width, d.height);
|
||||
if (t)
|
||||
return;
|
||||
e.cancelable && e.preventDefault(), this.bindDocMove(), this.componentRef.current.focus({ preventScroll: !0 });
|
||||
const o = e.target.dataset.ord, w = !!o;
|
||||
let i = e.clientX, s = e.clientY, c = r.x, g = r.y;
|
||||
if (o) {
|
||||
const p = e.clientX - d.x, l = e.clientY - d.y;
|
||||
let C = 0, y = 0;
|
||||
o === "ne" || o == "e" ? (C = p - (r.x + r.width), y = l - r.y, c = r.x, g = r.y + r.height) : o === "se" || o === "s" ? (C = p - (r.x + r.width), y = l - (r.y + r.height), c = r.x, g = r.y) : o === "sw" || o == "w" ? (C = p - r.x, y = l - (r.y + r.height), c = r.x + r.width, g = r.y) : (o === "nw" || o == "n") && (C = p - r.x, y = l - r.y, c = r.x + r.width, g = r.y + r.height), i = c + d.x + C, s = g + d.y + y;
|
||||
}
|
||||
this.evData = {
|
||||
startClientX: i,
|
||||
startClientY: s,
|
||||
startCropX: c,
|
||||
startCropY: g,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
isResize: w,
|
||||
ord: o
|
||||
}, this.mouseDownOnCrop = !0, this.setState({ cropIsActive: !0 });
|
||||
});
|
||||
m(this, "onComponentPointerDown", (e) => {
|
||||
const { crop: n, disabled: t, locked: d, keepSelection: r, onChange: o } = this.props, w = this.getBox();
|
||||
if (t || d || r && n)
|
||||
return;
|
||||
e.cancelable && e.preventDefault(), this.bindDocMove(), this.componentRef.current.focus({ preventScroll: !0 });
|
||||
const i = e.clientX - w.x, s = e.clientY - w.y, c = {
|
||||
unit: "px",
|
||||
x: i,
|
||||
y: s,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
this.evData = {
|
||||
startClientX: e.clientX,
|
||||
startClientY: e.clientY,
|
||||
startCropX: i,
|
||||
startCropY: s,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
isResize: !0
|
||||
}, this.mouseDownOnCrop = !0, o(D(c, w.width, w.height), v(c, w.width, w.height)), this.setState({ cropIsActive: !0, newCropIsBeingDrawn: !0 });
|
||||
});
|
||||
m(this, "onDocPointerMove", (e) => {
|
||||
const { crop: n, disabled: t, onChange: d, onDragStart: r } = this.props, o = this.getBox();
|
||||
if (t || !n || !this.mouseDownOnCrop)
|
||||
return;
|
||||
e.cancelable && e.preventDefault(), this.dragStarted || (this.dragStarted = !0, r && r(e));
|
||||
const { evData: w } = this;
|
||||
w.clientX = e.clientX, w.clientY = e.clientY;
|
||||
let i;
|
||||
w.isResize ? i = this.resizeCrop() : i = this.dragCrop(), X(n, i) || d(
|
||||
D(i, o.width, o.height),
|
||||
v(i, o.width, o.height)
|
||||
);
|
||||
});
|
||||
m(this, "onComponentKeyDown", (e) => {
|
||||
const { crop: n, disabled: t, onChange: d, onComplete: r } = this.props;
|
||||
if (t)
|
||||
return;
|
||||
const o = e.key;
|
||||
let w = !1;
|
||||
if (!n)
|
||||
return;
|
||||
const i = this.getBox(), s = this.makePixelCrop(i), g = (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) ? x.nudgeStepLarge : e.shiftKey ? x.nudgeStepMedium : x.nudgeStep;
|
||||
if (o === "ArrowLeft" ? (s.x -= g, w = !0) : o === "ArrowRight" ? (s.x += g, w = !0) : o === "ArrowUp" ? (s.y -= g, w = !0) : o === "ArrowDown" && (s.y += g, w = !0), w) {
|
||||
e.cancelable && e.preventDefault(), s.x = b(s.x, 0, i.width - s.width), s.y = b(s.y, 0, i.height - s.height);
|
||||
const p = D(s, i.width, i.height), l = v(s, i.width, i.height);
|
||||
d(p, l), r && r(p, l);
|
||||
}
|
||||
});
|
||||
m(this, "onHandlerKeyDown", (e, n) => {
|
||||
const {
|
||||
aspect: t = 0,
|
||||
crop: d,
|
||||
disabled: r,
|
||||
minWidth: o = 0,
|
||||
minHeight: w = 0,
|
||||
maxWidth: i,
|
||||
maxHeight: s,
|
||||
onChange: c,
|
||||
onComplete: g
|
||||
} = this.props, p = this.getBox();
|
||||
if (r || !d)
|
||||
return;
|
||||
if (e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight")
|
||||
e.stopPropagation(), e.preventDefault();
|
||||
else
|
||||
return;
|
||||
const C = (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) ? x.nudgeStepLarge : e.shiftKey ? x.nudgeStepMedium : x.nudgeStep, y = D(d, p.width, p.height), f = I(y, e.key, C, n), R = k(
|
||||
f,
|
||||
t,
|
||||
n,
|
||||
p.width,
|
||||
p.height,
|
||||
o,
|
||||
w,
|
||||
i,
|
||||
s
|
||||
);
|
||||
if (!X(d, R)) {
|
||||
const Y = v(R, p.width, p.height);
|
||||
c(R, Y), g && g(R, Y);
|
||||
}
|
||||
});
|
||||
m(this, "onDocPointerDone", (e) => {
|
||||
const { crop: n, disabled: t, onComplete: d, onDragEnd: r } = this.props, o = this.getBox();
|
||||
this.unbindDocMove(), !(t || !n) && this.mouseDownOnCrop && (this.mouseDownOnCrop = !1, this.dragStarted = !1, r && r(e), d && d(D(n, o.width, o.height), v(n, o.width, o.height)), this.setState({ cropIsActive: !1, newCropIsBeingDrawn: !1 }));
|
||||
});
|
||||
m(this, "onDragFocus", () => {
|
||||
var e;
|
||||
(e = this.componentRef.current) == null || e.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
get document() {
|
||||
return document;
|
||||
}
|
||||
// We unfortunately get the bounding box every time as x+y changes
|
||||
// due to scrolling.
|
||||
getBox() {
|
||||
const e = this.mediaRef.current;
|
||||
if (!e)
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
const { x: n, y: t, width: d, height: r } = e.getBoundingClientRect();
|
||||
return { x: n, y: t, width: d, height: r };
|
||||
}
|
||||
componentDidUpdate(e) {
|
||||
const { crop: n, onComplete: t } = this.props;
|
||||
if (t && !e.crop && n) {
|
||||
const { width: d, height: r } = this.getBox();
|
||||
d && r && t(D(n, d, r), v(n, d, r));
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.resizeObserver && this.resizeObserver.disconnect(), this.unbindDocMove();
|
||||
}
|
||||
bindDocMove() {
|
||||
this.docMoveBound || (this.document.addEventListener("pointermove", this.onDocPointerMove, M), this.document.addEventListener("pointerup", this.onDocPointerDone, M), this.document.addEventListener("pointercancel", this.onDocPointerDone, M), this.docMoveBound = !0);
|
||||
}
|
||||
unbindDocMove() {
|
||||
this.docMoveBound && (this.document.removeEventListener("pointermove", this.onDocPointerMove, M), this.document.removeEventListener("pointerup", this.onDocPointerDone, M), this.document.removeEventListener("pointercancel", this.onDocPointerDone, M), this.docMoveBound = !1);
|
||||
}
|
||||
getCropStyle() {
|
||||
const { crop: e } = this.props;
|
||||
if (e)
|
||||
return {
|
||||
top: `${e.y}${e.unit}`,
|
||||
left: `${e.x}${e.unit}`,
|
||||
width: `${e.width}${e.unit}`,
|
||||
height: `${e.height}${e.unit}`
|
||||
};
|
||||
}
|
||||
dragCrop() {
|
||||
const { evData: e } = this, n = this.getBox(), t = this.makePixelCrop(n), d = e.clientX - e.startClientX, r = e.clientY - e.startClientY;
|
||||
return t.x = b(e.startCropX + d, 0, n.width - t.width), t.y = b(e.startCropY + r, 0, n.height - t.height), t;
|
||||
}
|
||||
getPointRegion(e, n, t, d) {
|
||||
const { evData: r } = this, o = r.clientX - e.x, w = r.clientY - e.y;
|
||||
let i;
|
||||
d && n ? i = n === "nw" || n === "n" || n === "ne" : i = w < r.startCropY;
|
||||
let s;
|
||||
return t && n ? s = n === "nw" || n === "w" || n === "sw" : s = o < r.startCropX, s ? i ? "nw" : "sw" : i ? "ne" : "se";
|
||||
}
|
||||
resolveMinDimensions(e, n, t = 0, d = 0) {
|
||||
const r = Math.min(t, e.width), o = Math.min(d, e.height);
|
||||
return !n || !r && !o ? [r, o] : n > 1 ? r ? [r, r / n] : [o * n, o] : o ? [o * n, o] : [r, r / n];
|
||||
}
|
||||
resizeCrop() {
|
||||
const { evData: e } = this, { aspect: n = 0, maxWidth: t, maxHeight: d } = this.props, r = this.getBox(), [o, w] = this.resolveMinDimensions(r, n, this.props.minWidth, this.props.minHeight);
|
||||
let i = this.makePixelCrop(r);
|
||||
const s = this.getPointRegion(r, e.ord, o, w), c = e.ord || s;
|
||||
let g = e.clientX - e.startClientX, p = e.clientY - e.startClientY;
|
||||
(o && c === "nw" || c === "w" || c === "sw") && (g = Math.min(g, -o)), (w && c === "nw" || c === "n" || c === "ne") && (p = Math.min(p, -w));
|
||||
const l = {
|
||||
unit: "px",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
s === "ne" ? (l.x = e.startCropX, l.width = g, n ? (l.height = l.width / n, l.y = e.startCropY - l.height) : (l.height = Math.abs(p), l.y = e.startCropY - l.height)) : s === "se" ? (l.x = e.startCropX, l.y = e.startCropY, l.width = g, n ? l.height = l.width / n : l.height = p) : s === "sw" ? (l.x = e.startCropX + g, l.y = e.startCropY, l.width = Math.abs(g), n ? l.height = l.width / n : l.height = p) : s === "nw" && (l.x = e.startCropX + g, l.width = Math.abs(g), n ? (l.height = l.width / n, l.y = e.startCropY - l.height) : (l.height = Math.abs(p), l.y = e.startCropY + p));
|
||||
const C = k(
|
||||
l,
|
||||
n,
|
||||
s,
|
||||
r.width,
|
||||
r.height,
|
||||
o,
|
||||
w,
|
||||
t,
|
||||
d
|
||||
);
|
||||
return n || x.xyOrds.indexOf(c) > -1 ? i = C : x.xOrds.indexOf(c) > -1 ? (i.x = C.x, i.width = C.width) : x.yOrds.indexOf(c) > -1 && (i.y = C.y, i.height = C.height), i.x = b(i.x, 0, r.width - i.width), i.y = b(i.y, 0, r.height - i.height), i;
|
||||
}
|
||||
renderCropSelection() {
|
||||
const {
|
||||
ariaLabels: e = x.defaultProps.ariaLabels,
|
||||
disabled: n,
|
||||
locked: t,
|
||||
renderSelectionAddon: d,
|
||||
ruleOfThirds: r,
|
||||
crop: o
|
||||
} = this.props, w = this.getCropStyle();
|
||||
if (o)
|
||||
return /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
style: w,
|
||||
className: "ReactCrop__crop-selection",
|
||||
onPointerDown: this.onCropPointerDown,
|
||||
"aria-label": e.cropArea,
|
||||
tabIndex: 0,
|
||||
onKeyDown: this.onComponentKeyDown,
|
||||
role: "group"
|
||||
},
|
||||
!n && !t && /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__drag-elements", onFocus: this.onDragFocus }, /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__drag-bar ord-n", "data-ord": "n" }), /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__drag-bar ord-e", "data-ord": "e" }), /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__drag-bar ord-s", "data-ord": "s" }), /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__drag-bar ord-w", "data-ord": "w" }), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-nw",
|
||||
"data-ord": "nw",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.nwDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "nw"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-n",
|
||||
"data-ord": "n",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.nDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "n"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-ne",
|
||||
"data-ord": "ne",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.neDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "ne"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-e",
|
||||
"data-ord": "e",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.eDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "e"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-se",
|
||||
"data-ord": "se",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.seDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "se"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-s",
|
||||
"data-ord": "s",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.sDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "s"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-sw",
|
||||
"data-ord": "sw",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.swDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "sw"),
|
||||
role: "button"
|
||||
}
|
||||
), /* @__PURE__ */ u.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "ReactCrop__drag-handle ord-w",
|
||||
"data-ord": "w",
|
||||
tabIndex: 0,
|
||||
"aria-label": e.wDragHandle,
|
||||
onKeyDown: (i) => this.onHandlerKeyDown(i, "w"),
|
||||
role: "button"
|
||||
}
|
||||
)),
|
||||
d && /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__selection-addon", onPointerDown: (i) => i.stopPropagation() }, d(this.state)),
|
||||
r && /* @__PURE__ */ u.createElement(u.Fragment, null, /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__rule-of-thirds-hz" }), /* @__PURE__ */ u.createElement("div", { className: "ReactCrop__rule-of-thirds-vt" }))
|
||||
);
|
||||
}
|
||||
makePixelCrop(e) {
|
||||
const n = { ...E, ...this.props.crop || {} };
|
||||
return D(n, e.width, e.height);
|
||||
}
|
||||
render() {
|
||||
const { aspect: e, children: n, circularCrop: t, className: d, crop: r, disabled: o, locked: w, style: i, ruleOfThirds: s } = this.props, { cropIsActive: c, newCropIsBeingDrawn: g } = this.state, p = r ? this.renderCropSelection() : null, l = H(
|
||||
"ReactCrop",
|
||||
d,
|
||||
c && "ReactCrop--active",
|
||||
o && "ReactCrop--disabled",
|
||||
w && "ReactCrop--locked",
|
||||
g && "ReactCrop--new-crop",
|
||||
r && e && "ReactCrop--fixed-aspect",
|
||||
r && t && "ReactCrop--circular-crop",
|
||||
r && s && "ReactCrop--rule-of-thirds",
|
||||
!this.dragStarted && r && !r.width && !r.height && "ReactCrop--invisible-crop",
|
||||
t && "ReactCrop--no-animate"
|
||||
);
|
||||
return /* @__PURE__ */ u.createElement("div", { ref: this.componentRef, className: l, style: i }, /* @__PURE__ */ u.createElement("div", { ref: this.mediaRef, className: "ReactCrop__child-wrapper", onPointerDown: this.onComponentPointerDown }, n), r ? /* @__PURE__ */ u.createElement("svg", { className: "ReactCrop__crop-mask", width: "100%", height: "100%" }, /* @__PURE__ */ u.createElement("defs", null, /* @__PURE__ */ u.createElement("mask", { id: `hole-${this.instanceId}` }, /* @__PURE__ */ u.createElement("rect", { width: "100%", height: "100%", fill: "white" }), t ? /* @__PURE__ */ u.createElement(
|
||||
"ellipse",
|
||||
{
|
||||
cx: `${r.x + r.width / 2}${r.unit}`,
|
||||
cy: `${r.y + r.height / 2}${r.unit}`,
|
||||
rx: `${r.width / 2}${r.unit}`,
|
||||
ry: `${r.height / 2}${r.unit}`,
|
||||
fill: "black"
|
||||
}
|
||||
) : /* @__PURE__ */ u.createElement(
|
||||
"rect",
|
||||
{
|
||||
x: `${r.x}${r.unit}`,
|
||||
y: `${r.y}${r.unit}`,
|
||||
width: `${r.width}${r.unit}`,
|
||||
height: `${r.height}${r.unit}`,
|
||||
fill: "black"
|
||||
}
|
||||
))), /* @__PURE__ */ u.createElement("rect", { fill: "black", fillOpacity: 0.5, width: "100%", height: "100%", mask: `url(#hole-${this.instanceId})` })) : void 0, p);
|
||||
}
|
||||
};
|
||||
m(x, "xOrds", ["e", "w"]), m(x, "yOrds", ["n", "s"]), m(x, "xyOrds", ["nw", "ne", "se", "sw"]), m(x, "nudgeStep", 1), m(x, "nudgeStepMedium", 10), m(x, "nudgeStepLarge", 100), m(x, "defaultProps", {
|
||||
ariaLabels: {
|
||||
cropArea: "Use the arrow keys to move the crop selection area",
|
||||
nwDragHandle: "Use the arrow keys to move the north west drag handle to change the crop selection area",
|
||||
nDragHandle: "Use the up and down arrow keys to move the north drag handle to change the crop selection area",
|
||||
neDragHandle: "Use the arrow keys to move the north east drag handle to change the crop selection area",
|
||||
eDragHandle: "Use the up and down arrow keys to move the east drag handle to change the crop selection area",
|
||||
seDragHandle: "Use the arrow keys to move the south east drag handle to change the crop selection area",
|
||||
sDragHandle: "Use the up and down arrow keys to move the south drag handle to change the crop selection area",
|
||||
swDragHandle: "Use the arrow keys to move the south west drag handle to change the crop selection area",
|
||||
wDragHandle: "Use the up and down arrow keys to move the west drag handle to change the crop selection area"
|
||||
}
|
||||
});
|
||||
let S = x;
|
||||
export {
|
||||
S as Component,
|
||||
S as ReactCrop,
|
||||
X as areCropsEqual,
|
||||
L as centerCrop,
|
||||
b as clamp,
|
||||
H as cls,
|
||||
k as containCrop,
|
||||
v as convertToPercentCrop,
|
||||
D as convertToPixelCrop,
|
||||
S as default,
|
||||
E as defaultCrop,
|
||||
B as makeAspectCrop,
|
||||
I as nudgeCrop
|
||||
};
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "react-image-crop",
|
||||
"version": "11.0.10",
|
||||
"description": "A responsive image cropping tool for React",
|
||||
"repository": "https://github.com/dominictobias/react-image-crop",
|
||||
"type": "module",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./dist/index.umd.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.umd.cjs"
|
||||
},
|
||||
"./dist/ReactCrop.css": "./dist/ReactCrop.css",
|
||||
"./src/ReactCrop.scss": "./src/ReactCrop.scss",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"browserslist": "last 3 versions, not IE > 0",
|
||||
"style": "dist/ReactCrop.css",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"author": "Dominic Tobias (https://github.com/dominictobias)",
|
||||
"keywords": [
|
||||
"react",
|
||||
"reactjs",
|
||||
"image",
|
||||
"crop",
|
||||
"react-component"
|
||||
],
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.1",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||
"@typescript-eslint/parser": "^8.29.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"sass": "^1.86.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-dts": "^4.5.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+310
@@ -0,0 +1,310 @@
|
||||
@use 'sass:math';
|
||||
|
||||
// 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;
|
||||
|
||||
// Moved to resolve SASS 1.77.7 deprecation warnings
|
||||
$antWidth: 10px;
|
||||
$doubleAntWidth: 10px * 2;
|
||||
|
||||
@keyframes marching-ants {
|
||||
0% {
|
||||
background-position: 0 0, 0 100%, 0 0, 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: $doubleAntWidth 0, (-$doubleAntWidth) 100%, 0 (-$doubleAntWidth), 100% $doubleAntWidth;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--rc-drag-handle-size: 12px;
|
||||
--rc-drag-handle-mobile-size: 24px;
|
||||
--rc-drag-handle-bg-colour: rgba(0, 0, 0, 0.2);
|
||||
--rc-drag-bar-size: 6px; // The invisible grip size of the crop selection edges
|
||||
--rc-border-color: rgba(255, 255, 255, 0.7);
|
||||
--rc-focus-color: #0088ff;
|
||||
}
|
||||
|
||||
.ReactCrop {
|
||||
$root: &;
|
||||
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: crosshair;
|
||||
max-width: 100%;
|
||||
|
||||
& *,
|
||||
& *::before,
|
||||
& *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&--disabled,
|
||||
&--locked {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
&__child-wrapper {
|
||||
overflow: hidden;
|
||||
max-height: inherit;
|
||||
|
||||
& > img,
|
||||
& > video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(#{$root}--disabled) {
|
||||
#{$root}__child-wrapper {
|
||||
& > img,
|
||||
& > video {
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
#{$root}__crop-selection {
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__crop-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
// Workaround an SVG precision issue: #611
|
||||
width: calc(100% + 0.5px);
|
||||
height: calc(100% + 0.5px);
|
||||
}
|
||||
|
||||
&__crop-selection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
cursor: move;
|
||||
|
||||
.ReactCrop--disabled & {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.ReactCrop--circular-crop & {
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
left: -1px;
|
||||
border: 1px solid var(--rc-border-color);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.ReactCrop--no-animate & {
|
||||
// border: 1px dashed white;
|
||||
outline: 1px dashed white;
|
||||
}
|
||||
&:not(.ReactCrop--no-animate &) {
|
||||
animation: marching-ants 1s;
|
||||
background-image: linear-gradient(to right, #fff 50%, #444 50%), linear-gradient(to right, #fff 50%, #444 50%),
|
||||
linear-gradient(to bottom, #fff 50%, #444 50%), linear-gradient(to bottom, #fff 50%, #444 50%);
|
||||
background-size: $antWidth 1px, $antWidth 1px, 1px $antWidth, 1px $antWidth;
|
||||
background-position: 0 0, 0 100%, 0 0, 100% 0;
|
||||
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
|
||||
color: #fff;
|
||||
animation-play-state: running;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--rc-focus-color);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
&--invisible-crop &__crop-mask,
|
||||
&--invisible-crop &__crop-selection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__rule-of-thirds-vt::before,
|
||||
&__rule-of-thirds-vt::after,
|
||||
&__rule-of-thirds-hz::before,
|
||||
&__rule-of-thirds-hz::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
&__rule-of-thirds-vt {
|
||||
&::before,
|
||||
&::after {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 33.3333%;
|
||||
left: calc(100% / 3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 66.6666%;
|
||||
left: calc(100% / 3 * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&__rule-of-thirds-hz {
|
||||
&::before,
|
||||
&::after {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 33.3333%;
|
||||
top: calc(100% / 3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 66.6666%;
|
||||
top: calc(100% / 3 * 2);
|
||||
}
|
||||
}
|
||||
|
||||
&__drag-handle {
|
||||
position: absolute;
|
||||
width: var(--rc-drag-handle-size);
|
||||
height: var(--rc-drag-handle-size);
|
||||
background-color: var(--rc-drag-handle-bg-colour);
|
||||
border: 1px solid var(--rc-border-color);
|
||||
|
||||
&:focus {
|
||||
background: var(--rc-focus-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ord-nw {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: nw-resize;
|
||||
}
|
||||
.ord-n {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: n-resize;
|
||||
}
|
||||
.ord-ne {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, -50%);
|
||||
cursor: ne-resize;
|
||||
}
|
||||
.ord-e {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(50%, -50%);
|
||||
cursor: e-resize;
|
||||
}
|
||||
.ord-se {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, 50%);
|
||||
cursor: se-resize;
|
||||
}
|
||||
.ord-s {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
cursor: s-resize;
|
||||
}
|
||||
.ord-sw {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
cursor: sw-resize;
|
||||
}
|
||||
.ord-w {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
// Use the same specificity as the ords above but just
|
||||
// come after.
|
||||
&__disabled &__drag-handle {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
&__drag-bar {
|
||||
position: absolute;
|
||||
|
||||
&.ord-n {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--rc-drag-bar-size);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
&.ord-e {
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: var(--rc-drag-bar-size);
|
||||
height: 100%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
&.ord-s {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--rc-drag-bar-size);
|
||||
transform: translateY(50%);
|
||||
}
|
||||
&.ord-w {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--rc-drag-bar-size);
|
||||
height: 100%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&--new-crop &__drag-bar,
|
||||
&--new-crop &__drag-handle,
|
||||
&--fixed-aspect &__drag-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--fixed-aspect &__drag-handle.ord-n,
|
||||
&--fixed-aspect &__drag-handle.ord-e,
|
||||
&--fixed-aspect &__drag-handle.ord-s,
|
||||
&--fixed-aspect &__drag-handle.ord-w {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media #{$mobile-media-query} {
|
||||
.ord-n,
|
||||
.ord-e,
|
||||
.ord-s,
|
||||
.ord-w {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__drag-handle {
|
||||
width: var(--rc-drag-handle-mobile-size);
|
||||
height: var(--rc-drag-handle-mobile-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+850
@@ -0,0 +1,850 @@
|
||||
import React, { PureComponent, createRef } from 'react'
|
||||
|
||||
import { Ords, XYOrds, Crop, PixelCrop, PercentCrop } from './types'
|
||||
import {
|
||||
defaultCrop,
|
||||
clamp,
|
||||
cls,
|
||||
areCropsEqual,
|
||||
convertToPercentCrop,
|
||||
convertToPixelCrop,
|
||||
containCrop,
|
||||
nudgeCrop,
|
||||
} from './utils'
|
||||
|
||||
import './ReactCrop.scss'
|
||||
|
||||
interface EVData {
|
||||
startClientX: number
|
||||
startClientY: number
|
||||
startCropX: number
|
||||
startCropY: number
|
||||
clientX: number
|
||||
clientY: number
|
||||
isResize: boolean
|
||||
ord?: Ords
|
||||
}
|
||||
|
||||
interface Rectangle {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const DOC_MOVE_OPTS = { capture: true, passive: false }
|
||||
let instanceCount = 0
|
||||
|
||||
export interface ReactCropProps {
|
||||
/** An object of labels to override the built-in English ones */
|
||||
ariaLabels?: {
|
||||
cropArea: string
|
||||
nwDragHandle: string
|
||||
nDragHandle: string
|
||||
neDragHandle: string
|
||||
eDragHandle: string
|
||||
seDragHandle: string
|
||||
sDragHandle: string
|
||||
swDragHandle: string
|
||||
wDragHandle: string
|
||||
}
|
||||
/** The aspect ratio of the crop, e.g. `1` for a square or `16 / 9` for landscape. */
|
||||
aspect?: number
|
||||
/** Classes to pass to the `ReactCrop` element. */
|
||||
className?: string
|
||||
/** The elements that you want to perform a crop on. For example
|
||||
* an image or video. */
|
||||
children?: React.ReactNode
|
||||
/** Show the crop area as a circle. If your aspect is not 1 (a square) then the circle will be warped into an oval shape. Defaults to false. */
|
||||
circularCrop?: boolean
|
||||
/** Since v10 all crop params are required except for aspect. Omit the entire crop object if you don't want a crop. See README on how to create an aspect crop with a % crop. */
|
||||
crop?: Crop
|
||||
/** If true then the user cannot resize or draw a new crop. A class of `ReactCrop--disabled` is also added to the container for user styling. */
|
||||
disabled?: boolean
|
||||
/** If true then the user cannot create or resize a crop, but can still drag the existing crop around. A class of `ReactCrop--locked` is also added to the container for user styling. */
|
||||
locked?: boolean
|
||||
/** If true is passed then selection can't be disabled if the user clicks outside the selection area. */
|
||||
keepSelection?: boolean
|
||||
/** A minimum crop width, in pixels. */
|
||||
minWidth?: number
|
||||
/** A minimum crop height, in pixels. */
|
||||
minHeight?: number
|
||||
/** A maximum crop width, in pixels. */
|
||||
maxWidth?: number
|
||||
/** A maximum crop height, in pixels. */
|
||||
maxHeight?: number
|
||||
/** A callback which happens for every change of the crop. You should set the crop to state and pass it back into the library via the `crop` prop. */
|
||||
onChange: (crop: PixelCrop, percentageCrop: PercentCrop) => void
|
||||
/** A callback which happens after a resize, drag, or nudge. Passes the current crop state object in pixels and percent. */
|
||||
onComplete?: (crop: PixelCrop, percentageCrop: PercentCrop) => void
|
||||
/** A callback which happens when a user starts dragging or resizing. It is convenient to manipulate elements outside this component. */
|
||||
onDragStart?: (e: PointerEvent) => void
|
||||
/** A callback which happens when a user releases the cursor or touch after dragging or resizing. */
|
||||
onDragEnd?: (e: PointerEvent) => void
|
||||
/** Render a custom element in crop selection. */
|
||||
renderSelectionAddon?: (state: ReactCropState) => React.ReactNode
|
||||
/** Show rule of thirds lines in the cropped area. Defaults to false. */
|
||||
ruleOfThirds?: boolean
|
||||
/** Inline styles object to be passed to the `ReactCrop` element. */
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export interface ReactCropState {
|
||||
cropIsActive: boolean
|
||||
newCropIsBeingDrawn: boolean
|
||||
}
|
||||
|
||||
export class ReactCrop extends PureComponent<ReactCropProps, ReactCropState> {
|
||||
static xOrds = ['e', 'w']
|
||||
static yOrds = ['n', 's']
|
||||
static xyOrds = ['nw', 'ne', 'se', 'sw']
|
||||
|
||||
static nudgeStep = 1
|
||||
static nudgeStepMedium = 10
|
||||
static nudgeStepLarge = 100
|
||||
|
||||
static defaultProps = {
|
||||
ariaLabels: {
|
||||
cropArea: 'Use the arrow keys to move the crop selection area',
|
||||
nwDragHandle: 'Use the arrow keys to move the north west drag handle to change the crop selection area',
|
||||
nDragHandle: 'Use the up and down arrow keys to move the north drag handle to change the crop selection area',
|
||||
neDragHandle: 'Use the arrow keys to move the north east drag handle to change the crop selection area',
|
||||
eDragHandle: 'Use the up and down arrow keys to move the east drag handle to change the crop selection area',
|
||||
seDragHandle: 'Use the arrow keys to move the south east drag handle to change the crop selection area',
|
||||
sDragHandle: 'Use the up and down arrow keys to move the south drag handle to change the crop selection area',
|
||||
swDragHandle: 'Use the arrow keys to move the south west drag handle to change the crop selection area',
|
||||
wDragHandle: 'Use the up and down arrow keys to move the west drag handle to change the crop selection area',
|
||||
},
|
||||
}
|
||||
|
||||
get document() {
|
||||
return document
|
||||
}
|
||||
|
||||
docMoveBound = false
|
||||
mouseDownOnCrop = false
|
||||
dragStarted = false
|
||||
evData: EVData = {
|
||||
startClientX: 0,
|
||||
startClientY: 0,
|
||||
startCropX: 0,
|
||||
startCropY: 0,
|
||||
clientX: 0,
|
||||
clientY: 0,
|
||||
isResize: true,
|
||||
}
|
||||
|
||||
componentRef = createRef<HTMLDivElement>()
|
||||
mediaRef = createRef<HTMLDivElement>()
|
||||
resizeObserver?: ResizeObserver
|
||||
initChangeCalled = false
|
||||
instanceId = `rc-${instanceCount++}`
|
||||
|
||||
state: ReactCropState = {
|
||||
cropIsActive: false,
|
||||
newCropIsBeingDrawn: false,
|
||||
}
|
||||
|
||||
// We unfortunately get the bounding box every time as x+y changes
|
||||
// due to scrolling.
|
||||
getBox(): Rectangle {
|
||||
const el = this.mediaRef.current
|
||||
if (!el) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 }
|
||||
}
|
||||
const { x, y, width, height } = el.getBoundingClientRect()
|
||||
return { x, y, width, height }
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ReactCropProps) {
|
||||
const { crop, onComplete } = this.props
|
||||
|
||||
// Useful for when programatically setting a new
|
||||
// crop and wanting to show a preview.
|
||||
if (onComplete && !prevProps.crop && crop) {
|
||||
const { width, height } = this.getBox()
|
||||
if (width && height) {
|
||||
onComplete(convertToPixelCrop(crop, width, height), convertToPercentCrop(crop, width, height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
this.unbindDocMove()
|
||||
}
|
||||
|
||||
bindDocMove() {
|
||||
if (this.docMoveBound) {
|
||||
return
|
||||
}
|
||||
|
||||
this.document.addEventListener('pointermove', this.onDocPointerMove, DOC_MOVE_OPTS)
|
||||
this.document.addEventListener('pointerup', this.onDocPointerDone, DOC_MOVE_OPTS)
|
||||
this.document.addEventListener('pointercancel', this.onDocPointerDone, DOC_MOVE_OPTS)
|
||||
|
||||
this.docMoveBound = true
|
||||
}
|
||||
|
||||
unbindDocMove() {
|
||||
if (!this.docMoveBound) {
|
||||
return
|
||||
}
|
||||
|
||||
this.document.removeEventListener('pointermove', this.onDocPointerMove, DOC_MOVE_OPTS)
|
||||
this.document.removeEventListener('pointerup', this.onDocPointerDone, DOC_MOVE_OPTS)
|
||||
this.document.removeEventListener('pointercancel', this.onDocPointerDone, DOC_MOVE_OPTS)
|
||||
|
||||
this.docMoveBound = false
|
||||
}
|
||||
|
||||
onCropPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const { crop, disabled } = this.props
|
||||
const box = this.getBox()
|
||||
|
||||
if (!crop) {
|
||||
return
|
||||
}
|
||||
|
||||
const pixelCrop = convertToPixelCrop(crop, box.width, box.height)
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.cancelable) e.preventDefault() // Stop drag selection.
|
||||
|
||||
// Bind to doc to follow movements outside of element.
|
||||
this.bindDocMove()
|
||||
|
||||
// Focus for detecting keypress.
|
||||
;(this.componentRef.current as HTMLDivElement).focus({ preventScroll: true })
|
||||
|
||||
const ord = (e.target as HTMLElement).dataset.ord as Ords
|
||||
const isResize = Boolean(ord)
|
||||
let startClientX = e.clientX
|
||||
let startClientY = e.clientY
|
||||
let startCropX = pixelCrop.x
|
||||
let startCropY = pixelCrop.y
|
||||
|
||||
// Set the starting coords to the opposite corner.
|
||||
if (ord) {
|
||||
const relativeX = e.clientX - box.x
|
||||
const relativeY = e.clientY - box.y
|
||||
let fromCornerX = 0
|
||||
let fromCornerY = 0
|
||||
|
||||
if (ord === 'ne' || ord == 'e') {
|
||||
fromCornerX = relativeX - (pixelCrop.x + pixelCrop.width)
|
||||
fromCornerY = relativeY - pixelCrop.y
|
||||
startCropX = pixelCrop.x
|
||||
startCropY = pixelCrop.y + pixelCrop.height
|
||||
} else if (ord === 'se' || ord === 's') {
|
||||
fromCornerX = relativeX - (pixelCrop.x + pixelCrop.width)
|
||||
fromCornerY = relativeY - (pixelCrop.y + pixelCrop.height)
|
||||
startCropX = pixelCrop.x
|
||||
startCropY = pixelCrop.y
|
||||
} else if (ord === 'sw' || ord == 'w') {
|
||||
fromCornerX = relativeX - pixelCrop.x
|
||||
fromCornerY = relativeY - (pixelCrop.y + pixelCrop.height)
|
||||
startCropX = pixelCrop.x + pixelCrop.width
|
||||
startCropY = pixelCrop.y
|
||||
} else if (ord === 'nw' || ord == 'n') {
|
||||
fromCornerX = relativeX - pixelCrop.x
|
||||
fromCornerY = relativeY - pixelCrop.y
|
||||
startCropX = pixelCrop.x + pixelCrop.width
|
||||
startCropY = pixelCrop.y + pixelCrop.height
|
||||
}
|
||||
|
||||
startClientX = startCropX + box.x + fromCornerX
|
||||
startClientY = startCropY + box.y + fromCornerY
|
||||
}
|
||||
|
||||
this.evData = {
|
||||
startClientX,
|
||||
startClientY,
|
||||
startCropX,
|
||||
startCropY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
isResize,
|
||||
ord,
|
||||
}
|
||||
|
||||
this.mouseDownOnCrop = true
|
||||
this.setState({ cropIsActive: true })
|
||||
}
|
||||
|
||||
onComponentPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const { crop, disabled, locked, keepSelection, onChange } = this.props
|
||||
const box = this.getBox()
|
||||
|
||||
if (disabled || locked || (keepSelection && crop)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.cancelable) e.preventDefault() // Stop drag selection.
|
||||
|
||||
// Bind to doc to follow movements outside of element.
|
||||
this.bindDocMove()
|
||||
|
||||
// Focus for detecting keypress.
|
||||
;(this.componentRef.current as HTMLDivElement).focus({ preventScroll: true })
|
||||
|
||||
const cropX = e.clientX - box.x
|
||||
const cropY = e.clientY - box.y
|
||||
const nextCrop: PixelCrop = {
|
||||
unit: 'px',
|
||||
x: cropX,
|
||||
y: cropY,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
|
||||
this.evData = {
|
||||
startClientX: e.clientX,
|
||||
startClientY: e.clientY,
|
||||
startCropX: cropX,
|
||||
startCropY: cropY,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
isResize: true,
|
||||
}
|
||||
|
||||
this.mouseDownOnCrop = true
|
||||
|
||||
onChange(convertToPixelCrop(nextCrop, box.width, box.height), convertToPercentCrop(nextCrop, box.width, box.height))
|
||||
|
||||
this.setState({ cropIsActive: true, newCropIsBeingDrawn: true })
|
||||
}
|
||||
|
||||
onDocPointerMove = (e: PointerEvent) => {
|
||||
const { crop, disabled, onChange, onDragStart } = this.props
|
||||
const box = this.getBox()
|
||||
|
||||
if (disabled || !crop || !this.mouseDownOnCrop) {
|
||||
return
|
||||
}
|
||||
|
||||
// Stop drag selection.
|
||||
if (e.cancelable) e.preventDefault()
|
||||
|
||||
if (!this.dragStarted) {
|
||||
this.dragStarted = true
|
||||
if (onDragStart) {
|
||||
onDragStart(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Update pointer position.
|
||||
const { evData } = this
|
||||
|
||||
evData.clientX = e.clientX
|
||||
evData.clientY = e.clientY
|
||||
|
||||
let nextCrop
|
||||
|
||||
if (evData.isResize) {
|
||||
nextCrop = this.resizeCrop()
|
||||
} else {
|
||||
nextCrop = this.dragCrop()
|
||||
}
|
||||
|
||||
if (!areCropsEqual(crop, nextCrop)) {
|
||||
onChange(
|
||||
convertToPixelCrop(nextCrop, box.width, box.height),
|
||||
convertToPercentCrop(nextCrop, box.width, box.height)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onComponentKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const { crop, disabled, onChange, onComplete } = this.props
|
||||
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const keyCode = e.key
|
||||
let nudged = false
|
||||
|
||||
if (!crop) {
|
||||
return
|
||||
}
|
||||
|
||||
const box = this.getBox()
|
||||
const nextCrop = this.makePixelCrop(box)
|
||||
const ctrlCmdPressed = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
|
||||
const nudgeStep = ctrlCmdPressed
|
||||
? ReactCrop.nudgeStepLarge
|
||||
: e.shiftKey
|
||||
? ReactCrop.nudgeStepMedium
|
||||
: ReactCrop.nudgeStep
|
||||
|
||||
if (keyCode === 'ArrowLeft') {
|
||||
nextCrop.x -= nudgeStep
|
||||
nudged = true
|
||||
} else if (keyCode === 'ArrowRight') {
|
||||
nextCrop.x += nudgeStep
|
||||
nudged = true
|
||||
} else if (keyCode === 'ArrowUp') {
|
||||
nextCrop.y -= nudgeStep
|
||||
nudged = true
|
||||
} else if (keyCode === 'ArrowDown') {
|
||||
nextCrop.y += nudgeStep
|
||||
nudged = true
|
||||
}
|
||||
|
||||
if (nudged) {
|
||||
if (e.cancelable) e.preventDefault() // Stop drag selection.
|
||||
|
||||
nextCrop.x = clamp(nextCrop.x, 0, box.width - nextCrop.width)
|
||||
nextCrop.y = clamp(nextCrop.y, 0, box.height - nextCrop.height)
|
||||
|
||||
const pixelCrop = convertToPixelCrop(nextCrop, box.width, box.height)
|
||||
const percentCrop = convertToPercentCrop(nextCrop, box.width, box.height)
|
||||
|
||||
onChange(pixelCrop, percentCrop)
|
||||
if (onComplete) {
|
||||
onComplete(pixelCrop, percentCrop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onHandlerKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, ord: Ords) => {
|
||||
const {
|
||||
aspect = 0,
|
||||
crop,
|
||||
disabled,
|
||||
minWidth = 0,
|
||||
minHeight = 0,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
onChange,
|
||||
onComplete,
|
||||
} = this.props
|
||||
const box = this.getBox()
|
||||
|
||||
if (disabled || !crop) {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep the event from bubbling up to the container
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
const ctrlCmdPressed = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
|
||||
const offset = ctrlCmdPressed
|
||||
? ReactCrop.nudgeStepLarge
|
||||
: e.shiftKey
|
||||
? ReactCrop.nudgeStepMedium
|
||||
: ReactCrop.nudgeStep
|
||||
|
||||
const pixelCrop = convertToPixelCrop(crop, box.width, box.height)
|
||||
const nudgedCrop = nudgeCrop(pixelCrop, e.key, offset, ord)
|
||||
const containedCrop = containCrop(
|
||||
nudgedCrop,
|
||||
aspect,
|
||||
ord,
|
||||
box.width,
|
||||
box.height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
maxHeight
|
||||
)
|
||||
|
||||
if (!areCropsEqual(crop, containedCrop)) {
|
||||
const percentCrop = convertToPercentCrop(containedCrop, box.width, box.height)
|
||||
onChange(containedCrop, percentCrop)
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(containedCrop, percentCrop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDocPointerDone = (e: PointerEvent) => {
|
||||
const { crop, disabled, onComplete, onDragEnd } = this.props
|
||||
const box = this.getBox()
|
||||
|
||||
this.unbindDocMove()
|
||||
|
||||
if (disabled || !crop) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.mouseDownOnCrop) {
|
||||
this.mouseDownOnCrop = false
|
||||
this.dragStarted = false
|
||||
|
||||
onDragEnd && onDragEnd(e)
|
||||
onComplete &&
|
||||
onComplete(convertToPixelCrop(crop, box.width, box.height), convertToPercentCrop(crop, box.width, box.height))
|
||||
|
||||
this.setState({ cropIsActive: false, newCropIsBeingDrawn: false })
|
||||
}
|
||||
}
|
||||
|
||||
onDragFocus = (/*e: React.FocusEvent<HTMLDivElement, Element>*/) => {
|
||||
// Fixes #491
|
||||
this.componentRef.current?.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
getCropStyle() {
|
||||
const { crop } = this.props
|
||||
|
||||
if (!crop) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
top: `${crop.y}${crop.unit}`,
|
||||
left: `${crop.x}${crop.unit}`,
|
||||
width: `${crop.width}${crop.unit}`,
|
||||
height: `${crop.height}${crop.unit}`,
|
||||
}
|
||||
}
|
||||
|
||||
dragCrop() {
|
||||
const { evData } = this
|
||||
const box = this.getBox()
|
||||
const nextCrop = this.makePixelCrop(box)
|
||||
const xDiff = evData.clientX - evData.startClientX
|
||||
const yDiff = evData.clientY - evData.startClientY
|
||||
|
||||
nextCrop.x = clamp(evData.startCropX + xDiff, 0, box.width - nextCrop.width)
|
||||
nextCrop.y = clamp(evData.startCropY + yDiff, 0, box.height - nextCrop.height)
|
||||
|
||||
return nextCrop
|
||||
}
|
||||
|
||||
getPointRegion(box: Rectangle, origOrd: Ords | undefined, minWidth: number, minHeight: number): XYOrds {
|
||||
const { evData } = this
|
||||
|
||||
const relativeX = evData.clientX - box.x
|
||||
const relativeY = evData.clientY - box.y
|
||||
|
||||
let topHalf: boolean
|
||||
if (minHeight && origOrd) {
|
||||
// Uses orig ord (never flip when minHeight != 0)
|
||||
topHalf = origOrd === 'nw' || origOrd === 'n' || origOrd === 'ne'
|
||||
} else {
|
||||
topHalf = relativeY < evData.startCropY
|
||||
}
|
||||
|
||||
let leftHalf: boolean
|
||||
if (minWidth && origOrd) {
|
||||
// Uses orig ord (never flip when minWidth != 0)
|
||||
leftHalf = origOrd === 'nw' || origOrd === 'w' || origOrd === 'sw'
|
||||
} else {
|
||||
leftHalf = relativeX < evData.startCropX
|
||||
}
|
||||
|
||||
if (leftHalf) {
|
||||
return topHalf ? 'nw' : 'sw'
|
||||
} else {
|
||||
return topHalf ? 'ne' : 'se'
|
||||
}
|
||||
}
|
||||
|
||||
resolveMinDimensions(box: Rectangle, aspect: number, minWidth = 0, minHeight = 0) {
|
||||
const mw = Math.min(minWidth, box.width)
|
||||
const mh = Math.min(minHeight, box.height)
|
||||
|
||||
if (!aspect || (!mw && !mh)) {
|
||||
return [mw, mh]
|
||||
}
|
||||
|
||||
if (aspect > 1) {
|
||||
return mw ? [mw, mw / aspect] : [mh * aspect, mh]
|
||||
} else {
|
||||
return mh ? [mh * aspect, mh] : [mw, mw / aspect]
|
||||
}
|
||||
}
|
||||
|
||||
resizeCrop() {
|
||||
const { evData } = this
|
||||
const { aspect = 0, maxWidth, maxHeight } = this.props
|
||||
const box = this.getBox()
|
||||
const [minWidth, minHeight] = this.resolveMinDimensions(box, aspect, this.props.minWidth, this.props.minHeight)
|
||||
let nextCrop = this.makePixelCrop(box)
|
||||
const area = this.getPointRegion(box, evData.ord, minWidth, minHeight)
|
||||
const ord = evData.ord || area
|
||||
let xDiff = evData.clientX - evData.startClientX
|
||||
let yDiff = evData.clientY - evData.startClientY
|
||||
|
||||
// When min dimensions are set, ensure crop isn't dragged when going
|
||||
// beyond the other side #554
|
||||
if ((minWidth && ord === 'nw') || ord === 'w' || ord === 'sw') {
|
||||
xDiff = Math.min(xDiff, -minWidth)
|
||||
}
|
||||
|
||||
if ((minHeight && ord === 'nw') || ord === 'n' || ord === 'ne') {
|
||||
yDiff = Math.min(yDiff, -minHeight)
|
||||
}
|
||||
|
||||
const tmpCrop: PixelCrop = {
|
||||
unit: 'px',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
|
||||
if (area === 'ne') {
|
||||
tmpCrop.x = evData.startCropX
|
||||
tmpCrop.width = xDiff
|
||||
|
||||
if (aspect) {
|
||||
tmpCrop.height = tmpCrop.width / aspect
|
||||
tmpCrop.y = evData.startCropY - tmpCrop.height
|
||||
} else {
|
||||
tmpCrop.height = Math.abs(yDiff)
|
||||
tmpCrop.y = evData.startCropY - tmpCrop.height
|
||||
}
|
||||
} else if (area === 'se') {
|
||||
tmpCrop.x = evData.startCropX
|
||||
tmpCrop.y = evData.startCropY
|
||||
tmpCrop.width = xDiff
|
||||
|
||||
if (aspect) {
|
||||
tmpCrop.height = tmpCrop.width / aspect
|
||||
} else {
|
||||
tmpCrop.height = yDiff
|
||||
}
|
||||
} else if (area === 'sw') {
|
||||
tmpCrop.x = evData.startCropX + xDiff
|
||||
tmpCrop.y = evData.startCropY
|
||||
tmpCrop.width = Math.abs(xDiff)
|
||||
|
||||
if (aspect) {
|
||||
tmpCrop.height = tmpCrop.width / aspect
|
||||
} else {
|
||||
tmpCrop.height = yDiff
|
||||
}
|
||||
} else if (area === 'nw') {
|
||||
tmpCrop.x = evData.startCropX + xDiff
|
||||
tmpCrop.width = Math.abs(xDiff)
|
||||
|
||||
if (aspect) {
|
||||
tmpCrop.height = tmpCrop.width / aspect
|
||||
tmpCrop.y = evData.startCropY - tmpCrop.height
|
||||
} else {
|
||||
tmpCrop.height = Math.abs(yDiff)
|
||||
tmpCrop.y = evData.startCropY + yDiff
|
||||
}
|
||||
}
|
||||
|
||||
const containedCrop = containCrop(
|
||||
tmpCrop,
|
||||
aspect,
|
||||
area,
|
||||
box.width,
|
||||
box.height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
maxHeight
|
||||
)
|
||||
|
||||
// Apply x/y/width/height changes depending on ordinate
|
||||
// (fixed aspect always applies both).
|
||||
if (aspect || ReactCrop.xyOrds.indexOf(ord) > -1) {
|
||||
nextCrop = containedCrop
|
||||
} else if (ReactCrop.xOrds.indexOf(ord) > -1) {
|
||||
nextCrop.x = containedCrop.x
|
||||
nextCrop.width = containedCrop.width
|
||||
} else if (ReactCrop.yOrds.indexOf(ord) > -1) {
|
||||
nextCrop.y = containedCrop.y
|
||||
nextCrop.height = containedCrop.height
|
||||
}
|
||||
|
||||
// When drawing a new crop with min dimensions we allow flipping, but
|
||||
// ensure we don't flip outside the crop area, just ignore those.
|
||||
nextCrop.x = clamp(nextCrop.x, 0, box.width - nextCrop.width)
|
||||
nextCrop.y = clamp(nextCrop.y, 0, box.height - nextCrop.height)
|
||||
|
||||
return nextCrop
|
||||
}
|
||||
|
||||
renderCropSelection() {
|
||||
const {
|
||||
ariaLabels = ReactCrop.defaultProps.ariaLabels,
|
||||
disabled,
|
||||
locked,
|
||||
renderSelectionAddon,
|
||||
ruleOfThirds,
|
||||
crop,
|
||||
} = this.props
|
||||
const style = this.getCropStyle()
|
||||
|
||||
if (!crop) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className="ReactCrop__crop-selection"
|
||||
onPointerDown={this.onCropPointerDown}
|
||||
aria-label={ariaLabels.cropArea}
|
||||
tabIndex={0}
|
||||
onKeyDown={this.onComponentKeyDown}
|
||||
role="group"
|
||||
>
|
||||
{!disabled && !locked && (
|
||||
<div className="ReactCrop__drag-elements" onFocus={this.onDragFocus}>
|
||||
<div className="ReactCrop__drag-bar ord-n" data-ord="n" />
|
||||
<div className="ReactCrop__drag-bar ord-e" data-ord="e" />
|
||||
<div className="ReactCrop__drag-bar ord-s" data-ord="s" />
|
||||
<div className="ReactCrop__drag-bar ord-w" data-ord="w" />
|
||||
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-nw"
|
||||
data-ord="nw"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.nwDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'nw')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-n"
|
||||
data-ord="n"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.nDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'n')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-ne"
|
||||
data-ord="ne"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.neDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'ne')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-e"
|
||||
data-ord="e"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.eDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'e')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-se"
|
||||
data-ord="se"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.seDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'se')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-s"
|
||||
data-ord="s"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.sDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 's')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-sw"
|
||||
data-ord="sw"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.swDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'sw')}
|
||||
role="button"
|
||||
/>
|
||||
<div
|
||||
className="ReactCrop__drag-handle ord-w"
|
||||
data-ord="w"
|
||||
tabIndex={0}
|
||||
aria-label={ariaLabels.wDragHandle}
|
||||
onKeyDown={e => this.onHandlerKeyDown(e, 'w')}
|
||||
role="button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{renderSelectionAddon && (
|
||||
<div className="ReactCrop__selection-addon" onPointerDown={e => e.stopPropagation()}>
|
||||
{renderSelectionAddon(this.state)}
|
||||
</div>
|
||||
)}
|
||||
{ruleOfThirds && (
|
||||
<>
|
||||
<div className="ReactCrop__rule-of-thirds-hz" />
|
||||
<div className="ReactCrop__rule-of-thirds-vt" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
makePixelCrop(box: Rectangle) {
|
||||
const crop = { ...defaultCrop, ...(this.props.crop || {}) }
|
||||
return convertToPixelCrop(crop, box.width, box.height)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { aspect, children, circularCrop, className, crop, disabled, locked, style, ruleOfThirds } = this.props
|
||||
const { cropIsActive, newCropIsBeingDrawn } = this.state
|
||||
const cropSelection = crop ? this.renderCropSelection() : null
|
||||
|
||||
const componentClasses = cls(
|
||||
'ReactCrop',
|
||||
className,
|
||||
cropIsActive && 'ReactCrop--active',
|
||||
disabled && 'ReactCrop--disabled',
|
||||
locked && 'ReactCrop--locked',
|
||||
newCropIsBeingDrawn && 'ReactCrop--new-crop',
|
||||
crop && aspect && 'ReactCrop--fixed-aspect',
|
||||
crop && circularCrop && 'ReactCrop--circular-crop',
|
||||
crop && ruleOfThirds && 'ReactCrop--rule-of-thirds',
|
||||
!this.dragStarted && crop && !crop.width && !crop.height && 'ReactCrop--invisible-crop',
|
||||
circularCrop && 'ReactCrop--no-animate'
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={this.componentRef} className={componentClasses} style={style}>
|
||||
<div ref={this.mediaRef} className="ReactCrop__child-wrapper" onPointerDown={this.onComponentPointerDown}>
|
||||
{children}
|
||||
</div>
|
||||
{crop ? (
|
||||
<svg className="ReactCrop__crop-mask" width="100%" height="100%">
|
||||
<defs>
|
||||
<mask id={`hole-${this.instanceId}`}>
|
||||
<rect width="100%" height="100%" fill="white" />
|
||||
{circularCrop ? (
|
||||
<ellipse
|
||||
cx={`${crop.x + crop.width / 2}${crop.unit}`}
|
||||
cy={`${crop.y + crop.height / 2}${crop.unit}`}
|
||||
rx={`${crop.width / 2}${crop.unit}`}
|
||||
ry={`${crop.height / 2}${crop.unit}`}
|
||||
fill="black"
|
||||
/>
|
||||
) : (
|
||||
<rect
|
||||
x={`${crop.x}${crop.unit}`}
|
||||
y={`${crop.y}${crop.unit}`}
|
||||
width={`${crop.width}${crop.unit}`}
|
||||
height={`${crop.height}${crop.unit}`}
|
||||
fill="black"
|
||||
/>
|
||||
)}
|
||||
</mask>
|
||||
</defs>
|
||||
<rect fill="black" fillOpacity={0.5} width="100%" height="100%" mask={`url(#hole-${this.instanceId})`} />
|
||||
</svg>
|
||||
) : undefined}
|
||||
{cropSelection}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+145
@@ -0,0 +1,145 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
|
||||
import ReactCrop, { centerCrop, makeAspectCrop, Crop, PixelCrop } from '..'
|
||||
import { canvasPreview } from './canvasPreview'
|
||||
import { useDebounceEffect } from './useDebounceEffect'
|
||||
|
||||
import '../ReactCrop.scss'
|
||||
import './index.scss'
|
||||
|
||||
// This is to demonstate how to make and center a % aspect crop
|
||||
// which is a bit trickier so we use some helper functions.
|
||||
function centerAspectCrop(mediaWidth: number, mediaHeight: number, aspect: number) {
|
||||
return centerCrop(
|
||||
makeAspectCrop(
|
||||
{
|
||||
unit: '%',
|
||||
width: 90,
|
||||
},
|
||||
aspect,
|
||||
mediaWidth,
|
||||
mediaHeight
|
||||
),
|
||||
mediaWidth,
|
||||
mediaHeight
|
||||
)
|
||||
}
|
||||
|
||||
// const defaultAspect = 9 / 16
|
||||
const defaultAspect = 16 / 9
|
||||
|
||||
export function Demo() {
|
||||
const [imgSrc, setImgSrc] = useState('')
|
||||
const previewCanvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const [crop, setCrop] = useState<Crop>()
|
||||
const [completedCrop, setCompletedCrop] = useState<PixelCrop>()
|
||||
const [scale, setScale] = useState(1)
|
||||
const [rotate, setRotate] = useState(0)
|
||||
const [aspect, setAspect] = useState<number | undefined>(defaultAspect)
|
||||
|
||||
function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
setCrop(undefined) // Makes crop preview update between images.
|
||||
const reader = new FileReader()
|
||||
reader.addEventListener('load', () => setImgSrc(reader.result?.toString() || ''))
|
||||
reader.readAsDataURL(e.target.files[0])
|
||||
}
|
||||
}
|
||||
|
||||
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
|
||||
if (aspect) {
|
||||
const { width, height } = e.currentTarget
|
||||
setCrop(centerAspectCrop(width, height, aspect))
|
||||
}
|
||||
}
|
||||
|
||||
useDebounceEffect(
|
||||
async () => {
|
||||
if (completedCrop?.width && completedCrop?.height && imgRef.current && previewCanvasRef.current) {
|
||||
// We use canvasPreview as it's much faster than imgPreview.
|
||||
canvasPreview(imgRef.current, previewCanvasRef.current, completedCrop, scale, rotate)
|
||||
}
|
||||
},
|
||||
100,
|
||||
[completedCrop, scale, rotate]
|
||||
)
|
||||
|
||||
function handleToggleAspectClick() {
|
||||
if (aspect) {
|
||||
setAspect(undefined)
|
||||
} else {
|
||||
setAspect(defaultAspect)
|
||||
if (imgRef.current) {
|
||||
const { width, height } = imgRef.current
|
||||
setCrop(centerAspectCrop(width, height, defaultAspect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="Crop-Controls">
|
||||
<input type="file" accept="image/*" onChange={onSelectFile} />
|
||||
<div>
|
||||
<label htmlFor="scale-input">Scale: </label>
|
||||
<input
|
||||
id="scale-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={scale}
|
||||
disabled={!imgSrc}
|
||||
onChange={e => setScale(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="rotate-input">Rotate: </label>
|
||||
<input
|
||||
id="rotate-input"
|
||||
type="number"
|
||||
value={rotate}
|
||||
disabled={!imgSrc}
|
||||
onChange={e => setRotate(Math.min(180, Math.max(-180, Number(e.target.value))))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={handleToggleAspectClick}>Toggle aspect {aspect ? 'off' : 'on'}</button>
|
||||
</div>
|
||||
</div>
|
||||
{!!imgSrc && (
|
||||
<ReactCrop
|
||||
// className="ReactCrop--no-animate"
|
||||
crop={crop}
|
||||
onChange={(_, percentCrop) => setCrop(percentCrop)}
|
||||
onComplete={c => setCompletedCrop(c)}
|
||||
aspect={aspect}
|
||||
minWidth={400}
|
||||
minHeight={200}
|
||||
circularCrop
|
||||
ruleOfThirds
|
||||
>
|
||||
<img
|
||||
ref={imgRef}
|
||||
alt="Crop me"
|
||||
src={imgSrc}
|
||||
style={{ transform: `scale(${scale}) rotate(${rotate}deg)` }}
|
||||
onLoad={onImageLoad}
|
||||
/>
|
||||
</ReactCrop>
|
||||
)}
|
||||
<div>
|
||||
{!!completedCrop && (
|
||||
<canvas
|
||||
ref={previewCanvasRef}
|
||||
style={{
|
||||
border: '1px solid black',
|
||||
objectFit: 'contain',
|
||||
width: completedCrop.width,
|
||||
height: completedCrop.height,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
import { PixelCrop } from '..'
|
||||
|
||||
const TO_RADIANS = Math.PI / 180
|
||||
|
||||
export async function canvasPreview(
|
||||
image: HTMLImageElement,
|
||||
canvas: HTMLCanvasElement,
|
||||
crop: PixelCrop,
|
||||
scale = 1,
|
||||
rotate = 0
|
||||
) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('No 2d context')
|
||||
}
|
||||
|
||||
const scaleX = image.naturalWidth / image.width
|
||||
const scaleY = image.naturalHeight / image.height
|
||||
// devicePixelRatio slightly increases sharpness on retina devices
|
||||
// at the expense of slightly slower render times and needing to
|
||||
// size the image back down if you want to download/upload and be
|
||||
// true to the images natural size.
|
||||
const pixelRatio = window.devicePixelRatio
|
||||
// const pixelRatio = 1
|
||||
|
||||
canvas.width = Math.floor(crop.width * scaleX * pixelRatio)
|
||||
canvas.height = Math.floor(crop.height * scaleY * pixelRatio)
|
||||
|
||||
ctx.scale(pixelRatio, pixelRatio)
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
|
||||
const cropX = crop.x * scaleX
|
||||
const cropY = crop.y * scaleY
|
||||
|
||||
const rotateRads = rotate * TO_RADIANS
|
||||
const centerX = image.naturalWidth / 2
|
||||
const centerY = image.naturalHeight / 2
|
||||
|
||||
ctx.save()
|
||||
|
||||
// 5) Move the crop origin to the canvas origin (0,0)
|
||||
ctx.translate(-cropX, -cropY)
|
||||
// 4) Move the origin to the center of the original position
|
||||
ctx.translate(centerX, centerY)
|
||||
// 3) Rotate around the origin
|
||||
ctx.rotate(rotateRads)
|
||||
// 2) Scale the image
|
||||
ctx.scale(scale, scale)
|
||||
// 1) Move the center of the image to the origin (0,0)
|
||||
ctx.translate(-centerX, -centerY)
|
||||
ctx.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight, 0, 0, image.naturalWidth, image.naturalHeight)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
import { PixelCrop } from '..'
|
||||
import { canvasPreview } from './canvasPreview'
|
||||
|
||||
let previewUrl = ''
|
||||
|
||||
function toBlob(canvas: HTMLCanvasElement): Promise<Blob | null> {
|
||||
return new Promise(resolve => {
|
||||
canvas.toBlob(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
// Returns an image source you should set to state and pass
|
||||
// `{previewSrc && <img alt="Crop preview" src={previewSrc} />}`
|
||||
export async function imgPreview(image: HTMLImageElement, crop: PixelCrop, scale = 1, rotate = 0) {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvasPreview(image, canvas, crop, scale, rotate)
|
||||
|
||||
const blob = await toBlob(canvas)
|
||||
|
||||
if (!blob) {
|
||||
console.error('Failed to create blob')
|
||||
return ''
|
||||
}
|
||||
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl)
|
||||
}
|
||||
|
||||
previewUrl = URL.createObjectURL(blob)
|
||||
return previewUrl
|
||||
}
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.App {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Demo } from './Demo'
|
||||
|
||||
const root = createRoot(document.getElementById('root')!)
|
||||
root.render(<Demo />)
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { useEffect, DependencyList } from 'react'
|
||||
|
||||
export function useDebounceEffect(fn: () => void, waitTime: number, deps?: DependencyList) {
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
fn.apply(undefined, [...(deps as [])])
|
||||
}, waitTime)
|
||||
|
||||
return () => {
|
||||
clearTimeout(t)
|
||||
}
|
||||
}, deps)
|
||||
}
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
export * from './ReactCrop'
|
||||
|
||||
// For compat with older versions
|
||||
export { ReactCrop as default, ReactCrop as Component } from './ReactCrop'
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
export type XOrds = 'e' | 'w'
|
||||
export type YOrds = 'n' | 's'
|
||||
export type XYOrds = 'nw' | 'ne' | 'se' | 'sw'
|
||||
export type Ords = XOrds | YOrds | XYOrds
|
||||
|
||||
export interface Crop {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
unit: 'px' | '%'
|
||||
}
|
||||
|
||||
export interface PixelCrop extends Crop {
|
||||
unit: 'px'
|
||||
}
|
||||
|
||||
export interface PercentCrop extends Crop {
|
||||
unit: '%'
|
||||
}
|
||||
Generated
Vendored
+347
@@ -0,0 +1,347 @@
|
||||
import { PixelCrop, PercentCrop, Crop, Ords } from './types'
|
||||
|
||||
export const defaultCrop: PixelCrop = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
unit: 'px',
|
||||
}
|
||||
|
||||
export const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max)
|
||||
|
||||
export const cls = (...args: unknown[]) => args.filter(v => v && typeof v === 'string').join(' ')
|
||||
|
||||
export const areCropsEqual = (cropA: Partial<Crop>, cropB: Partial<Crop>) =>
|
||||
cropA === cropB ||
|
||||
(cropA.width === cropB.width &&
|
||||
cropA.height === cropB.height &&
|
||||
cropA.x === cropB.x &&
|
||||
cropA.y === cropB.y &&
|
||||
cropA.unit === cropB.unit)
|
||||
|
||||
export function makeAspectCrop(
|
||||
crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>,
|
||||
aspect: number,
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): PercentCrop
|
||||
export function makeAspectCrop(
|
||||
crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>,
|
||||
aspect: number,
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): PixelCrop
|
||||
export function makeAspectCrop(crop: Partial<Crop>, aspect: number, containerWidth: number, containerHeight: number) {
|
||||
const pixelCrop = convertToPixelCrop(crop, containerWidth, containerHeight)
|
||||
|
||||
if (crop.width) {
|
||||
pixelCrop.height = pixelCrop.width / aspect
|
||||
}
|
||||
|
||||
if (crop.height) {
|
||||
pixelCrop.width = pixelCrop.height * aspect
|
||||
}
|
||||
|
||||
if (pixelCrop.y + pixelCrop.height > containerHeight) {
|
||||
pixelCrop.height = containerHeight - pixelCrop.y
|
||||
pixelCrop.width = pixelCrop.height * aspect
|
||||
}
|
||||
|
||||
if (pixelCrop.x + pixelCrop.width > containerWidth) {
|
||||
pixelCrop.width = containerWidth - pixelCrop.x
|
||||
pixelCrop.height = pixelCrop.width / aspect
|
||||
}
|
||||
|
||||
if (crop.unit === '%') {
|
||||
return convertToPercentCrop(pixelCrop, containerWidth, containerHeight)
|
||||
}
|
||||
|
||||
return pixelCrop
|
||||
}
|
||||
|
||||
export function centerCrop(
|
||||
crop: Pick<PercentCrop, 'unit'> & Partial<Omit<PercentCrop, 'unit'>>,
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): PercentCrop
|
||||
export function centerCrop(
|
||||
crop: Pick<PixelCrop, 'unit'> & Partial<Omit<PixelCrop, 'unit'>>,
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): PixelCrop
|
||||
export function centerCrop(crop: Partial<Crop>, containerWidth: number, containerHeight: number) {
|
||||
const pixelCrop = convertToPixelCrop(crop, containerWidth, containerHeight)
|
||||
|
||||
pixelCrop.x = (containerWidth - pixelCrop.width) / 2
|
||||
pixelCrop.y = (containerHeight - pixelCrop.height) / 2
|
||||
|
||||
if (crop.unit === '%') {
|
||||
return convertToPercentCrop(pixelCrop, containerWidth, containerHeight)
|
||||
}
|
||||
|
||||
return pixelCrop
|
||||
}
|
||||
|
||||
export function convertToPercentCrop(
|
||||
crop: Partial<Crop>,
|
||||
containerWidth: number,
|
||||
containerHeight: number
|
||||
): PercentCrop {
|
||||
if (crop.unit === '%') {
|
||||
return { ...defaultCrop, ...crop, unit: '%' }
|
||||
}
|
||||
|
||||
return {
|
||||
unit: '%',
|
||||
x: crop.x ? (crop.x / containerWidth) * 100 : 0,
|
||||
y: crop.y ? (crop.y / containerHeight) * 100 : 0,
|
||||
width: crop.width ? (crop.width / containerWidth) * 100 : 0,
|
||||
height: crop.height ? (crop.height / containerHeight) * 100 : 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function convertToPixelCrop(crop: Partial<Crop>, containerWidth: number, containerHeight: number): PixelCrop {
|
||||
if (!crop.unit) {
|
||||
return { ...defaultCrop, ...crop, unit: 'px' }
|
||||
}
|
||||
|
||||
if (crop.unit === 'px') {
|
||||
return { ...defaultCrop, ...crop, unit: 'px' }
|
||||
}
|
||||
|
||||
return {
|
||||
unit: 'px',
|
||||
x: crop.x ? (crop.x * containerWidth) / 100 : 0,
|
||||
y: crop.y ? (crop.y * containerHeight) / 100 : 0,
|
||||
width: crop.width ? (crop.width * containerWidth) / 100 : 0,
|
||||
height: crop.height ? (crop.height * containerHeight) / 100 : 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Sorry.
|
||||
export function containCrop(
|
||||
pixelCrop: PixelCrop,
|
||||
aspect: number,
|
||||
ord: Ords,
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
minWidth = 0,
|
||||
minHeight = 0,
|
||||
maxWidth = containerWidth,
|
||||
maxHeight = containerHeight
|
||||
) {
|
||||
const containedCrop = { ...pixelCrop }
|
||||
let _minWidth = Math.min(minWidth, containerWidth)
|
||||
let _minHeight = Math.min(minHeight, containerHeight)
|
||||
let _maxWidth = Math.min(maxWidth, containerWidth)
|
||||
let _maxHeight = Math.min(maxHeight, containerHeight)
|
||||
|
||||
if (aspect) {
|
||||
if (aspect > 1) {
|
||||
// Landscape - increase width min + max.
|
||||
_minWidth = minHeight ? minHeight * aspect : _minWidth
|
||||
_minHeight = _minWidth / aspect
|
||||
_maxWidth = maxWidth * aspect
|
||||
} else {
|
||||
// Portrait - increase height min + max.
|
||||
_minHeight = minWidth ? minWidth / aspect : _minHeight
|
||||
_minWidth = _minHeight * aspect
|
||||
_maxHeight = maxHeight / aspect
|
||||
}
|
||||
}
|
||||
|
||||
// Stop underflow on top.
|
||||
if (containedCrop.y < 0) {
|
||||
containedCrop.height = Math.max(containedCrop.height + containedCrop.y, _minHeight)
|
||||
containedCrop.y = 0
|
||||
}
|
||||
|
||||
// Stop underflow on left.
|
||||
if (containedCrop.x < 0) {
|
||||
containedCrop.width = Math.max(containedCrop.width + containedCrop.x, _minWidth)
|
||||
containedCrop.x = 0
|
||||
}
|
||||
|
||||
// Stop overflow on right.
|
||||
const xOverflow = containerWidth - (containedCrop.x + containedCrop.width)
|
||||
if (xOverflow < 0) {
|
||||
containedCrop.x = Math.min(containedCrop.x, containerWidth - _minWidth)
|
||||
containedCrop.width += xOverflow
|
||||
}
|
||||
|
||||
// Stop overflow on bottom.
|
||||
const yOverflow = containerHeight - (containedCrop.y + containedCrop.height)
|
||||
if (yOverflow < 0) {
|
||||
containedCrop.y = Math.min(containedCrop.y, containerHeight - _minHeight)
|
||||
containedCrop.height += yOverflow
|
||||
}
|
||||
|
||||
// Make crop respect min width generally.
|
||||
if (containedCrop.width < _minWidth) {
|
||||
if (ord === 'sw' || ord == 'nw') {
|
||||
// Stops box moving when min is hit.
|
||||
containedCrop.x -= _minWidth - containedCrop.width
|
||||
}
|
||||
containedCrop.width = _minWidth
|
||||
}
|
||||
|
||||
// Make crop respect min height generally.
|
||||
if (containedCrop.height < _minHeight) {
|
||||
if (ord === 'nw' || ord == 'ne') {
|
||||
// Stops box moving when min is hit.
|
||||
containedCrop.y -= _minHeight - containedCrop.height
|
||||
}
|
||||
containedCrop.height = _minHeight
|
||||
}
|
||||
|
||||
// Make crop respect max width generally.
|
||||
if (containedCrop.width > _maxWidth) {
|
||||
if (ord === 'sw' || ord == 'nw') {
|
||||
// Stops box moving when max is hit.
|
||||
containedCrop.x -= _maxWidth - containedCrop.width
|
||||
}
|
||||
containedCrop.width = _maxWidth
|
||||
}
|
||||
|
||||
// Make crop respect max height generally.
|
||||
if (containedCrop.height > _maxHeight) {
|
||||
if (ord === 'nw' || ord == 'ne') {
|
||||
// Stops box moving when min is hit.
|
||||
containedCrop.y -= _maxHeight - containedCrop.height
|
||||
}
|
||||
containedCrop.height = _maxHeight
|
||||
}
|
||||
|
||||
// Maintain aspect after size fixing.
|
||||
if (aspect) {
|
||||
const currAspect = containedCrop.width / containedCrop.height
|
||||
if (currAspect < aspect) {
|
||||
// Crop is shrunk on the width so adjust the height.
|
||||
const newHeight = Math.max(containedCrop.width / aspect, _minHeight)
|
||||
|
||||
if (ord === 'nw' || ord == 'ne') {
|
||||
// Stops box moving when min is hit.
|
||||
containedCrop.y -= newHeight - containedCrop.height
|
||||
}
|
||||
|
||||
containedCrop.height = newHeight
|
||||
} else if (currAspect > aspect) {
|
||||
// Crop is shrunk on the height so adjust the width.
|
||||
const newWidth = Math.max(containedCrop.height * aspect, _minWidth)
|
||||
|
||||
if (ord === 'sw' || ord == 'nw') {
|
||||
// Stops box moving when max is hit.
|
||||
containedCrop.x -= newWidth - containedCrop.width
|
||||
}
|
||||
|
||||
containedCrop.width = newWidth
|
||||
}
|
||||
}
|
||||
|
||||
return containedCrop
|
||||
}
|
||||
|
||||
export function nudgeCrop(pixelCrop: PixelCrop, key: string, offset: number, ord: Ords) {
|
||||
const nextCrop = { ...pixelCrop }
|
||||
|
||||
if (key === 'ArrowLeft') {
|
||||
if (ord === 'nw') {
|
||||
nextCrop.x -= offset
|
||||
nextCrop.y -= offset
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'w') {
|
||||
nextCrop.x -= offset
|
||||
nextCrop.width += offset
|
||||
} else if (ord === 'sw') {
|
||||
nextCrop.x -= offset
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'ne') {
|
||||
nextCrop.y += offset
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'e') {
|
||||
nextCrop.width -= offset
|
||||
} else if (ord === 'se') {
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
}
|
||||
} else if (key === 'ArrowRight') {
|
||||
if (ord === 'nw') {
|
||||
nextCrop.x += offset
|
||||
nextCrop.y += offset
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'w') {
|
||||
// Niche: Will move right if minWidth hit.
|
||||
nextCrop.x += offset
|
||||
nextCrop.width -= offset
|
||||
} else if (ord === 'sw') {
|
||||
nextCrop.x += offset
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'ne') {
|
||||
nextCrop.y -= offset
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'e') {
|
||||
nextCrop.width += offset
|
||||
} else if (ord === 'se') {
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'ArrowUp') {
|
||||
if (ord === 'nw') {
|
||||
nextCrop.x -= offset
|
||||
nextCrop.y -= offset
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'n') {
|
||||
nextCrop.y -= offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'ne') {
|
||||
nextCrop.y -= offset
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'sw') {
|
||||
nextCrop.x += offset
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 's') {
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'se') {
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
}
|
||||
} else if (key === 'ArrowDown') {
|
||||
if (ord === 'nw') {
|
||||
nextCrop.x += offset
|
||||
nextCrop.y += offset
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'n') {
|
||||
// Niche: Will move down if minHeight hit.
|
||||
nextCrop.y += offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'ne') {
|
||||
nextCrop.y += offset
|
||||
nextCrop.width -= offset
|
||||
nextCrop.height -= offset
|
||||
} else if (ord === 'sw') {
|
||||
nextCrop.x -= offset
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 's') {
|
||||
nextCrop.height += offset
|
||||
} else if (ord === 'se') {
|
||||
nextCrop.width += offset
|
||||
nextCrop.height += offset
|
||||
}
|
||||
}
|
||||
|
||||
return nextCrop
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user