@codeleap/web 6.1.2 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CropPicker/components/CropPickerFooter.d.ts +11 -0
- package/dist/components/CropPicker/components/CropPickerFooter.js +12 -0
- package/dist/components/CropPicker/components/CropPickerFooter.js.map +1 -0
- package/dist/components/CropPicker/hooks.d.ts +39 -0
- package/dist/components/CropPicker/hooks.js +184 -0
- package/dist/components/CropPicker/hooks.js.map +1 -0
- package/dist/components/CropPicker/index.d.ts +3 -1
- package/dist/components/CropPicker/index.js +19 -32
- package/dist/components/CropPicker/index.js.map +1 -1
- package/dist/components/CropPicker/styles.d.ts +2 -2
- package/dist/components/CropPicker/types.d.ts +13 -15
- package/dist/components/CropPicker/utils.d.ts +10 -0
- package/dist/components/CropPicker/utils.js +117 -0
- package/dist/components/CropPicker/utils.js.map +1 -0
- package/dist/lib/hooks/index.d.ts +0 -1
- package/dist/lib/hooks/index.js +0 -1
- package/dist/lib/hooks/index.js.map +1 -1
- package/package.json +17 -17
- package/package.json.bak +1 -1
- package/src/components/CropPicker/components/CropPickerFooter.tsx +39 -0
- package/src/components/CropPicker/hooks.ts +173 -0
- package/src/components/CropPicker/index.tsx +49 -77
- package/src/components/CropPicker/styles.ts +8 -5
- package/src/components/CropPicker/types.ts +11 -16
- package/src/components/CropPicker/utils.ts +95 -0
- package/src/components/View/index.tsx +1 -1
- package/src/lib/hooks/index.ts +0 -1
- package/dist/lib/hooks/useCropPicker.d.ts +0 -36
- package/dist/lib/hooks/useCropPicker.js +0 -216
- package/dist/lib/hooks/useCropPicker.js.map +0 -1
- package/src/lib/hooks/useCropPicker.ts +0 -192
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeleap/web",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@codeleap/config": "6.
|
|
13
|
-
"@codeleap/hooks": "6.
|
|
14
|
-
"@codeleap/i18n": "6.
|
|
15
|
-
"@codeleap/logger": "6.
|
|
16
|
-
"@codeleap/query": "6.
|
|
17
|
-
"@codeleap/types": "6.
|
|
18
|
-
"@codeleap/utils": "6.
|
|
19
|
-
"@codeleap/store": "6.
|
|
12
|
+
"@codeleap/config": "6.3.0",
|
|
13
|
+
"@codeleap/hooks": "6.3.0",
|
|
14
|
+
"@codeleap/i18n": "6.3.0",
|
|
15
|
+
"@codeleap/logger": "6.3.0",
|
|
16
|
+
"@codeleap/query": "6.3.0",
|
|
17
|
+
"@codeleap/types": "6.3.0",
|
|
18
|
+
"@codeleap/utils": "6.3.0",
|
|
19
|
+
"@codeleap/store": "6.3.0"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc --build",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"uuid": "13.0.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@codeleap/types": "6.
|
|
51
|
-
"@codeleap/utils": "6.
|
|
52
|
-
"@codeleap/hooks": "6.
|
|
53
|
-
"@codeleap/i18n": "6.
|
|
54
|
-
"@codeleap/query": "6.
|
|
55
|
-
"@codeleap/logger": "6.
|
|
56
|
-
"@codeleap/styles": "6.
|
|
57
|
-
"@codeleap/store": "6.
|
|
50
|
+
"@codeleap/types": "6.3.0",
|
|
51
|
+
"@codeleap/utils": "6.3.0",
|
|
52
|
+
"@codeleap/hooks": "6.3.0",
|
|
53
|
+
"@codeleap/i18n": "6.3.0",
|
|
54
|
+
"@codeleap/query": "6.3.0",
|
|
55
|
+
"@codeleap/logger": "6.3.0",
|
|
56
|
+
"@codeleap/styles": "6.3.0",
|
|
57
|
+
"@codeleap/store": "6.3.0",
|
|
58
58
|
"@emotion/react": "11.14.0",
|
|
59
59
|
"@reach/router": "^1.3.4",
|
|
60
60
|
"dayjs": "1.11.18",
|
package/package.json.bak
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ICSS } from '@codeleap/styles'
|
|
2
|
+
import { Button, ButtonComposition } from '../../Button'
|
|
3
|
+
import { View } from '../../View'
|
|
4
|
+
import { AnyFunction, StylesOf } from '@codeleap/types'
|
|
5
|
+
|
|
6
|
+
export type CropPickerFooterProps = {
|
|
7
|
+
footerStyle: ICSS
|
|
8
|
+
confirmStyles: StylesOf<ButtonComposition>
|
|
9
|
+
cancelStyles: StylesOf<ButtonComposition>
|
|
10
|
+
onConfirmCrop: AnyFunction
|
|
11
|
+
onCancelCrop: AnyFunction
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CropPickerFooter = (props: CropPickerFooterProps) => {
|
|
15
|
+
const {
|
|
16
|
+
footerStyle,
|
|
17
|
+
confirmStyles,
|
|
18
|
+
cancelStyles,
|
|
19
|
+
onConfirmCrop,
|
|
20
|
+
onCancelCrop,
|
|
21
|
+
} = props
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View style={footerStyle}>
|
|
25
|
+
<Button
|
|
26
|
+
text={'Cancel'}
|
|
27
|
+
onPress={onCancelCrop}
|
|
28
|
+
debugName={'cropModal:cancel'}
|
|
29
|
+
style={cancelStyles}
|
|
30
|
+
/>
|
|
31
|
+
<Button
|
|
32
|
+
text={'Confirm'}
|
|
33
|
+
onPress={onConfirmCrop}
|
|
34
|
+
debugName={'cropModal:confirm'}
|
|
35
|
+
style={confirmStyles}
|
|
36
|
+
/>
|
|
37
|
+
</View>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { WebInputFile } from '@codeleap/types'
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { Crop } from 'react-image-crop'
|
|
5
|
+
import { calculateProportionalDimensions, cropImage, readImage } from './utils'
|
|
6
|
+
|
|
7
|
+
export type UseCropPicker = {
|
|
8
|
+
file: File
|
|
9
|
+
aspect?: number
|
|
10
|
+
minWidth?: number
|
|
11
|
+
minHeight?: number
|
|
12
|
+
onCrop?: (croppedFile: WebInputFile | null) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useCropPicker({
|
|
16
|
+
aspect,
|
|
17
|
+
minWidth: minW,
|
|
18
|
+
minHeight: minH,
|
|
19
|
+
file,
|
|
20
|
+
onCrop,
|
|
21
|
+
}: UseCropPicker) {
|
|
22
|
+
const [image, setImage] = useState<HTMLImageElement>(null)
|
|
23
|
+
const [crop, setCrop] = useState<Crop>()
|
|
24
|
+
const [relativeCrop, setRelativeCrop] = useState<Crop>()
|
|
25
|
+
|
|
26
|
+
const [imageDimensions, setImageDimensions] = useState<{
|
|
27
|
+
naturalWidth: number
|
|
28
|
+
naturalHeight: number
|
|
29
|
+
displayWidth: number
|
|
30
|
+
displayHeight: number
|
|
31
|
+
aspectRatio: number
|
|
32
|
+
} | null>(null)
|
|
33
|
+
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
setRelativeCrop(null)
|
|
36
|
+
setCrop(undefined)
|
|
37
|
+
setImageDimensions(null)
|
|
38
|
+
setTimeout(() => setImage(null), 500)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleConfirmCrop = async () => {
|
|
42
|
+
const [preview, croppedBlob] = await cropImage(image, relativeCrop)
|
|
43
|
+
|
|
44
|
+
const newImage: WebInputFile = {
|
|
45
|
+
file: new File([croppedBlob], 'cropped.jpg', {
|
|
46
|
+
type: 'image/jpeg',
|
|
47
|
+
lastModified: Date.now(),
|
|
48
|
+
}),
|
|
49
|
+
preview,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onCrop(newImage)
|
|
53
|
+
|
|
54
|
+
setTimeout(() => cleanup())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleCancelCrop = () => {
|
|
58
|
+
onCrop(null)
|
|
59
|
+
|
|
60
|
+
setTimeout(() => cleanup())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const loadFile = useCallback(async (toReadFile: File) => {
|
|
64
|
+
const imageData = await readImage(toReadFile)
|
|
65
|
+
const { naturalWidth, naturalHeight } = imageData
|
|
66
|
+
|
|
67
|
+
const dimensions = calculateProportionalDimensions(naturalWidth, naturalHeight)
|
|
68
|
+
|
|
69
|
+
setImageDimensions(dimensions)
|
|
70
|
+
|
|
71
|
+
const targetAspect = aspect || dimensions.aspectRatio
|
|
72
|
+
const imageAspect = dimensions.aspectRatio
|
|
73
|
+
|
|
74
|
+
let initialCrop: Crop
|
|
75
|
+
|
|
76
|
+
if (aspect) {
|
|
77
|
+
const v =
|
|
78
|
+
imageAspect >= targetAspect
|
|
79
|
+
? {
|
|
80
|
+
width: ((naturalHeight * targetAspect) / naturalWidth) * 100,
|
|
81
|
+
height: 100,
|
|
82
|
+
}
|
|
83
|
+
: {
|
|
84
|
+
width: 100,
|
|
85
|
+
height: (naturalWidth / targetAspect / naturalHeight) * 100,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
initialCrop = {
|
|
89
|
+
...v,
|
|
90
|
+
x: (100 - v.width) / 2,
|
|
91
|
+
y: (100 - v.height) / 2,
|
|
92
|
+
unit: '%' as const,
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
initialCrop = {
|
|
96
|
+
width: 100,
|
|
97
|
+
height: 100,
|
|
98
|
+
x: 0,
|
|
99
|
+
y: 0,
|
|
100
|
+
unit: '%' as const,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setCrop(initialCrop)
|
|
105
|
+
setRelativeCrop(initialCrop)
|
|
106
|
+
setImage(imageData)
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
const getMinDimensions = useCallback(() => {
|
|
110
|
+
if (!imageDimensions) return { minWidth: minW ?? 100, minHeight: minH ?? 100 }
|
|
111
|
+
|
|
112
|
+
const targetAspect = aspect || imageDimensions.aspectRatio
|
|
113
|
+
const calculatedMinWidth = (minW ?? 100) * targetAspect
|
|
114
|
+
const calculatedMinHeight = minH ?? 100
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
minWidth: Math.max(calculatedMinWidth, 50),
|
|
118
|
+
minHeight: Math.max(calculatedMinHeight, 50),
|
|
119
|
+
}
|
|
120
|
+
}, [aspect, imageDimensions, minW, minH])
|
|
121
|
+
|
|
122
|
+
const handleCropChange = useCallback((newCrop: Crop) => {
|
|
123
|
+
if (!imageDimensions) {
|
|
124
|
+
setCrop(newCrop)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { minWidth, minHeight } = getMinDimensions()
|
|
129
|
+
|
|
130
|
+
setCrop({
|
|
131
|
+
...newCrop,
|
|
132
|
+
width: newCrop.width < minWidth ? minWidth : newCrop.width,
|
|
133
|
+
height: newCrop.height < minHeight ? minHeight : newCrop.height,
|
|
134
|
+
})
|
|
135
|
+
}, [imageDimensions, getMinDimensions])
|
|
136
|
+
|
|
137
|
+
const containerStyles = useMemo(() => {
|
|
138
|
+
if (!imageDimensions) return {}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
width: imageDimensions.displayWidth,
|
|
142
|
+
height: imageDimensions.displayHeight,
|
|
143
|
+
maxWidth: '90vw',
|
|
144
|
+
maxHeight: '70vh',
|
|
145
|
+
margin: '0 auto',
|
|
146
|
+
}
|
|
147
|
+
}, [imageDimensions])
|
|
148
|
+
|
|
149
|
+
const imageStyles = useMemo(() => {
|
|
150
|
+
if (!imageDimensions) return {}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
width: imageDimensions.displayWidth,
|
|
154
|
+
height: imageDimensions.displayHeight,
|
|
155
|
+
objectFit: 'contain' as const,
|
|
156
|
+
}
|
|
157
|
+
}, [imageDimensions])
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
loadFile(file)
|
|
161
|
+
}, [])
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
containerStyles,
|
|
165
|
+
imageStyles,
|
|
166
|
+
handleCropChange,
|
|
167
|
+
handleConfirmCrop,
|
|
168
|
+
handleCancelCrop,
|
|
169
|
+
setRelativeCrop,
|
|
170
|
+
crop,
|
|
171
|
+
image,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -1,116 +1,88 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
import { CropPickerProps } from './types'
|
|
4
|
-
import { useCropPicker } from '../../lib'
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { StyledComponentProps, AnyRecord, IJSX, useCompositionStyles } from '@codeleap/styles'
|
|
5
3
|
import { WebStyleRegistry } from '../../lib/WebStyleRegistry'
|
|
6
4
|
import { useStylesFor } from '../../lib/hooks/useStylesFor'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { LoadingOverlay } from '../LoadingOverlay'
|
|
12
|
-
|
|
13
|
-
const ReactCrop: React.ElementType = require('react-image-crop').Component
|
|
5
|
+
import { View } from '../View'
|
|
6
|
+
import { ActivityIndicator } from '../ActivityIndicator'
|
|
7
|
+
import { CropPickerFooter } from './components/CropPickerFooter'
|
|
8
|
+
import { CropPickerProps } from './types'
|
|
14
9
|
|
|
15
10
|
import 'react-image-crop/dist/ReactCrop.css'
|
|
11
|
+
const ReactCrop: React.ElementType = require('react-image-crop').Component
|
|
16
12
|
|
|
17
13
|
export * from './styles'
|
|
18
14
|
export * from './types'
|
|
15
|
+
export * from './hooks'
|
|
16
|
+
export * from './components/CropPickerFooter'
|
|
19
17
|
|
|
20
18
|
export const CropPicker = (props: CropPickerProps) => {
|
|
21
19
|
const {
|
|
22
|
-
onFileSelect,
|
|
23
|
-
targetCrop,
|
|
24
|
-
modalProps,
|
|
25
|
-
title,
|
|
26
|
-
confirmButton,
|
|
27
|
-
debugName,
|
|
28
|
-
handle,
|
|
29
|
-
withLoading,
|
|
30
20
|
style,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
handle,
|
|
22
|
+
reactCrop,
|
|
23
|
+
aspect,
|
|
24
|
+
children,
|
|
25
|
+
FooterComponent,
|
|
34
26
|
} = {
|
|
35
27
|
...CropPicker.defaultProps,
|
|
36
28
|
...props,
|
|
37
29
|
}
|
|
38
30
|
|
|
31
|
+
const styles = useStylesFor(CropPicker.styleRegistryName, style)
|
|
32
|
+
const compositionStyles = useCompositionStyles(['indicator', 'confirm', 'cancel'], styles)
|
|
33
|
+
|
|
34
|
+
if (!handle) return null
|
|
35
|
+
|
|
39
36
|
const {
|
|
40
|
-
onConfirmCrop,
|
|
41
|
-
onFilesReturned,
|
|
42
|
-
onClose,
|
|
43
|
-
fileInputRef,
|
|
44
|
-
visible,
|
|
45
37
|
image,
|
|
46
38
|
crop,
|
|
47
39
|
setRelativeCrop,
|
|
48
|
-
isLoading,
|
|
49
40
|
handleCropChange,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
handleConfirmCrop,
|
|
42
|
+
handleCancelCrop,
|
|
43
|
+
} = handle
|
|
53
44
|
|
|
54
|
-
|
|
45
|
+
if (!image?.src) return <View style={styles.loadingWrapper}><ActivityIndicator style={compositionStyles.indicator} debugName='CropPickerLoading' /></View>
|
|
55
46
|
|
|
56
47
|
return (
|
|
57
48
|
<>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
onChange={
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<Modal
|
|
66
|
-
visible={visible}
|
|
67
|
-
toggle={onClose}
|
|
68
|
-
title={title}
|
|
69
|
-
footer={
|
|
70
|
-
<Button
|
|
71
|
-
text={confirmButton}
|
|
72
|
-
style={composition?.confirmButton}
|
|
73
|
-
onPress={onConfirmCrop}
|
|
74
|
-
debugName={debugName}
|
|
75
|
-
{...confirmButtonProps}
|
|
76
|
-
/>
|
|
77
|
-
}
|
|
78
|
-
{...modalProps}
|
|
79
|
-
style={composition?.modal}
|
|
49
|
+
<ReactCrop
|
|
50
|
+
{...reactCrop}
|
|
51
|
+
crop={crop}
|
|
52
|
+
onChange={handleCropChange}
|
|
53
|
+
onComplete={(_, relCrop) => setRelativeCrop(relCrop)}
|
|
54
|
+
style={styles.crop}
|
|
55
|
+
aspect={aspect ?? reactCrop?.aspect}
|
|
80
56
|
>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</Modal>
|
|
57
|
+
<img
|
|
58
|
+
src={image.src}
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
css={[styles?.image]}
|
|
61
|
+
/>
|
|
62
|
+
</ReactCrop>
|
|
63
|
+
|
|
64
|
+
{children}
|
|
65
|
+
|
|
66
|
+
<FooterComponent
|
|
67
|
+
footerStyle={styles.footer}
|
|
68
|
+
confirmStyles={compositionStyles.confirm}
|
|
69
|
+
cancelStyles={compositionStyles.cancel}
|
|
70
|
+
onConfirmCrop={handleConfirmCrop}
|
|
71
|
+
onCancelCrop={handleCancelCrop}
|
|
72
|
+
/>
|
|
98
73
|
</>
|
|
99
74
|
)
|
|
100
75
|
}
|
|
101
76
|
|
|
102
77
|
CropPicker.styleRegistryName = 'CropPicker'
|
|
103
|
-
CropPicker.elements = ['
|
|
104
|
-
CropPicker.rootElement = '
|
|
105
|
-
|
|
78
|
+
CropPicker.elements = ['crop', 'image', 'footer', 'confirm', 'cancel', 'indicator', 'loadingWrapper']
|
|
79
|
+
CropPicker.rootElement = 'crop'
|
|
106
80
|
CropPicker.withVariantTypes = <S extends AnyRecord>(styles: S) => {
|
|
107
81
|
return CropPicker as (props: StyledComponentProps<CropPickerProps, typeof styles>) => IJSX
|
|
108
82
|
}
|
|
109
83
|
|
|
110
84
|
CropPicker.defaultProps = {
|
|
111
|
-
|
|
112
|
-
confirmButton: 'Confirm Crop',
|
|
113
|
-
withLoading: false,
|
|
85
|
+
FooterComponent: CropPickerFooter,
|
|
114
86
|
} as Partial<CropPickerProps>
|
|
115
87
|
|
|
116
88
|
WebStyleRegistry.registerComponent(CropPicker)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { ActivityIndicatorComposition } from '../ActivityIndicator'
|
|
1
2
|
import { ButtonComposition } from '../Button'
|
|
2
|
-
import { ModalComposition } from '../Modal'
|
|
3
3
|
|
|
4
4
|
export type CropPickerComposition =
|
|
5
|
-
| '
|
|
6
|
-
| '
|
|
7
|
-
|
|
|
8
|
-
| `
|
|
5
|
+
| 'crop'
|
|
6
|
+
| 'image'
|
|
7
|
+
| 'footer'
|
|
8
|
+
| `indicator${Capitalize<ActivityIndicatorComposition>}`
|
|
9
|
+
| `confirm${Capitalize<ButtonComposition>}`
|
|
10
|
+
| `cancel${Capitalize<ButtonComposition>}`
|
|
11
|
+
| 'loadingWrapper'
|
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import { StyledProp } from '@codeleap/styles'
|
|
2
|
-
import {
|
|
2
|
+
import { FileInputRef } from '../FileInput'
|
|
3
|
+
import type { ReactCropProps } from 'react-image-crop'
|
|
3
4
|
import { CropPickerComposition } from './styles'
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { useCropPicker } from '../../lib'
|
|
7
|
-
import { ButtonProps } from '../Button'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { useCropPicker } from './hooks'
|
|
8
7
|
import { RefObject } from 'react'
|
|
8
|
+
import { CropPickerFooterProps } from './components/CropPickerFooter'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
type NativeCropProps = Omit<ReactCropProps, 'crop' | 'onChange' | 'onComplete' | 'style'>
|
|
11
11
|
|
|
12
12
|
export type CropPickerProps =
|
|
13
|
-
Partial<FileInputProps> &
|
|
14
13
|
{
|
|
15
|
-
|
|
16
|
-
targetCrop?: BaseCropProps
|
|
17
|
-
modalProps?: Partial<ModalProps>
|
|
18
|
-
title?: string
|
|
19
|
-
confirmButton?: string
|
|
14
|
+
FooterComponent?: React.ComponentType<Partial<CropPickerFooterProps>>
|
|
20
15
|
debugName: string
|
|
16
|
+
style?: StyledProp<CropPickerComposition>
|
|
21
17
|
handle?: ReturnType<typeof useCropPicker>
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
aspect?: number
|
|
19
|
+
reactCrop?: NativeCropProps
|
|
20
|
+
children?: React.ReactNode
|
|
24
21
|
ref?: RefObject<FileInputRef | null>
|
|
25
22
|
}
|
|
26
|
-
|
|
27
|
-
export type ImageReading = HTMLImageElement
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Crop } from 'react-image-crop'
|
|
2
|
+
|
|
3
|
+
export function readImage(file: File | Blob): Promise<HTMLImageElement> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const reader = new FileReader()
|
|
6
|
+
|
|
7
|
+
reader.onload = () => {
|
|
8
|
+
const image = new Image()
|
|
9
|
+
image.onload = () => resolve(image)
|
|
10
|
+
image.onerror = () => reject(new Error('Failed to load image'))
|
|
11
|
+
image.src = reader.result as string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
reader.onerror = () => reject(new Error('Failed to read file'))
|
|
15
|
+
reader.readAsDataURL(file)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function cropImage(
|
|
20
|
+
image: HTMLImageElement,
|
|
21
|
+
crop: Crop,
|
|
22
|
+
): Promise<[string, Blob]> {
|
|
23
|
+
if (!crop.width || !crop.height) {
|
|
24
|
+
throw new Error('Invalid crop dimensions')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const canvas = document.createElement('canvas')
|
|
28
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true })
|
|
29
|
+
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
throw new Error('Could not get canvas 2d context')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scaleX = image.naturalWidth / 100
|
|
35
|
+
const scaleY = image.naturalHeight / 100
|
|
36
|
+
|
|
37
|
+
canvas.width = crop.width * scaleX
|
|
38
|
+
canvas.height = crop.height * scaleY
|
|
39
|
+
|
|
40
|
+
ctx.drawImage(
|
|
41
|
+
image,
|
|
42
|
+
crop.x * scaleX,
|
|
43
|
+
crop.y * scaleY,
|
|
44
|
+
canvas.width,
|
|
45
|
+
canvas.height,
|
|
46
|
+
0,
|
|
47
|
+
0,
|
|
48
|
+
canvas.width,
|
|
49
|
+
canvas.height,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
54
|
+
canvas.toBlob(
|
|
55
|
+
blob => blob ? resolve(blob) : reject(new Error('Canvas is empty')),
|
|
56
|
+
'image/png',
|
|
57
|
+
1,
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const croppedImage = await readImage(blob)
|
|
62
|
+
|
|
63
|
+
return [croppedImage.src, blob]
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(`Failed to crop image: ${error instanceof Error ? error.message : String(error)}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const calculateProportionalDimensions = (naturalWidth: number, naturalHeight: number) => {
|
|
70
|
+
const aspectRatio = naturalWidth / naturalHeight
|
|
71
|
+
|
|
72
|
+
const maxWidth = 600
|
|
73
|
+
const maxHeight = 400
|
|
74
|
+
|
|
75
|
+
let displayWidth = naturalWidth
|
|
76
|
+
let displayHeight = naturalHeight
|
|
77
|
+
|
|
78
|
+
if (displayHeight > maxHeight) {
|
|
79
|
+
displayHeight = maxHeight
|
|
80
|
+
displayWidth = displayHeight * aspectRatio
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (displayWidth > maxWidth) {
|
|
84
|
+
displayWidth = maxWidth
|
|
85
|
+
displayHeight = displayWidth / aspectRatio
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
naturalWidth,
|
|
90
|
+
naturalHeight,
|
|
91
|
+
displayWidth: Math.round(displayWidth),
|
|
92
|
+
displayHeight: Math.round(displayHeight),
|
|
93
|
+
aspectRatio,
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/lib/hooks/index.ts
CHANGED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { WebInputFile } from '@codeleap/types';
|
|
2
|
-
import { ImageReading } from '../../components/CropPicker';
|
|
3
|
-
import { FileInputProps, FileInputRef } from '../../components/FileInput';
|
|
4
|
-
import { Crop, ReactCropProps } from 'react-image-crop';
|
|
5
|
-
export type UseCropPickerProps = Partial<ReactCropProps> & {
|
|
6
|
-
onFileSelect: FileInputProps['onFileSelect'];
|
|
7
|
-
ref: React.MutableRefObject<FileInputRef> | React.Ref<FileInputRef>;
|
|
8
|
-
};
|
|
9
|
-
type ImageType = 'png' | 'jpeg' | 'webp';
|
|
10
|
-
export declare function readImage(file: File | Blob): Promise<ImageReading>;
|
|
11
|
-
export declare function cropImage(image: ImageReading, crop: Crop, type: ImageType): Promise<[string, Blob]>;
|
|
12
|
-
export declare function useCropPicker({ onFileSelect, ref, aspect, minWidth: minW, minHeight: minH, }: UseCropPickerProps): {
|
|
13
|
-
onCancel: () => Promise<void>;
|
|
14
|
-
onResolved: (files: WebInputFile[]) => void;
|
|
15
|
-
cleanup: () => void;
|
|
16
|
-
onConfirmCrop: (imageType?: ImageType) => Promise<void>;
|
|
17
|
-
onFilesReturned: (event: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
|
|
18
|
-
onClose: () => void;
|
|
19
|
-
fileInputRef: import("react").RefObject<FileInputRef>;
|
|
20
|
-
visible: boolean;
|
|
21
|
-
toggle: (value?: boolean) => void;
|
|
22
|
-
image: HTMLImageElement;
|
|
23
|
-
setImage: import("react").Dispatch<import("react").SetStateAction<HTMLImageElement>>;
|
|
24
|
-
crop: Crop;
|
|
25
|
-
setCrop: import("react").Dispatch<import("react").SetStateAction<Crop>>;
|
|
26
|
-
relativeCrop: Crop;
|
|
27
|
-
setRelativeCrop: import("react").Dispatch<import("react").SetStateAction<Crop>>;
|
|
28
|
-
isLoading: boolean;
|
|
29
|
-
croppedPromise: {
|
|
30
|
-
_await: () => Promise<WebInputFile[]>;
|
|
31
|
-
resolve: (value: WebInputFile[]) => Promise<void>;
|
|
32
|
-
reject: (err: any) => Promise<void>;
|
|
33
|
-
};
|
|
34
|
-
handleCropChange: (newCrop: Crop) => void;
|
|
35
|
-
};
|
|
36
|
-
export {};
|