1 | import { Component, h } from 'preact'
|
2 | import { connect } from 'redux-bundler-preact'
|
3 | import Button from './button'
|
4 | import './image-uploader.css'
|
5 |
|
6 | const preventDefault = e => e.preventDefault()
|
7 | const noOp = () => { }
|
8 |
|
9 | class ImageUploader extends Component {
|
10 | constructor (props) {
|
11 | super(props)
|
12 | this.state = this.getInitialState()
|
13 | this.onRef = this.onRef.bind(this)
|
14 | this.onInputRef = this.onInputRef.bind(this)
|
15 | this.onTouchStart = this.onTouchStart.bind(this)
|
16 | this.onTouchMove = this.onTouchMove.bind(this)
|
17 | this.onMouseDown = this.onMouseDown.bind(this)
|
18 | this.onMouseMove = this.onMouseMove.bind(this)
|
19 | this.onRangeChange = this.onRangeChange.bind(this)
|
20 | this.onEnd = this.onEnd.bind(this)
|
21 | this.imageOnLoad = this.imageOnLoad.bind(this)
|
22 | this.onChange = this.onChange.bind(this)
|
23 | this.onCancelClick = this.onCancelClick.bind(this)
|
24 | }
|
25 |
|
26 |
|
27 | shouldComponentUpdate (nextProps, nextState) {
|
28 | if (nextProps.previousSrc !== this.props.previousSrc) {
|
29 | return true
|
30 | }
|
31 | for (const item in nextState) {
|
32 | if (nextState[item] !== this.state[item]) {
|
33 | return true
|
34 | }
|
35 | }
|
36 |
|
37 |
|
38 | this.props.updateSrc = nextProps.updateSrc
|
39 | this.props.updateValue = nextProps.updateValue
|
40 |
|
41 | return false
|
42 | }
|
43 |
|
44 | getInitialState () {
|
45 | return {
|
46 | src: null,
|
47 | zoom: 1,
|
48 | dragStartX: 0,
|
49 | dragStartY: 0,
|
50 | midDragX: 0,
|
51 | midDragY: 0,
|
52 | x: 0,
|
53 | y: 0,
|
54 | isMoving: false
|
55 | }
|
56 | }
|
57 |
|
58 | onChange ({ target }) {
|
59 | this.onFile(target.files && target.files[0])
|
60 | }
|
61 |
|
62 | onInputRef (el) {
|
63 | if (el) {
|
64 | this.fileInput = el
|
65 | }
|
66 | }
|
67 |
|
68 | onFile (file) {
|
69 | if (!file) {
|
70 | return
|
71 | }
|
72 | const reader = new window.FileReader()
|
73 | reader.onload = (e) => {
|
74 | const src = e.target.result
|
75 | this.setState({ src })
|
76 | }
|
77 | reader.readAsDataURL(file)
|
78 | }
|
79 |
|
80 | onCancelClick () {
|
81 | this.setState(this.getInitialState())
|
82 | delete this.image
|
83 | if (this.fileInput) {
|
84 | this.fileInput.value = ''
|
85 | }
|
86 | }
|
87 |
|
88 | onRangeChange ({ target }) {
|
89 | this.setState({ zoom: target.value })
|
90 | }
|
91 |
|
92 | onDragOver (e) {
|
93 | e.preventDefault()
|
94 | }
|
95 |
|
96 | onFileDrop (e) {
|
97 | e.preventDefault()
|
98 | this.onFile(e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0])
|
99 | }
|
100 |
|
101 | componentDidMount () {
|
102 | this.canvas.addEventListener('touchstart', this.onTouchStart)
|
103 | this.canvas.addEventListener('touchmove', this.onTouchMove)
|
104 | this.canvas.addEventListener('touchend', this.onEnd)
|
105 | this.canvas.addEventListener('mousedown', this.onMouseDown)
|
106 | this.canvas.addEventListener('mousemove', this.onMouseMove)
|
107 | document.addEventListener('mouseup', this.onEnd)
|
108 | }
|
109 |
|
110 | componentWillUnmount () {
|
111 | this.canvas.removeEventListener('touchstart', this.onTouchStart)
|
112 | this.canvas.removeEventListener('touchmove', this.onTouchMove)
|
113 | this.canvas.removeEventListener('touchend', this.onEnd)
|
114 | this.canvas.removeEventListener('mousedown', this.onMouseDown)
|
115 | this.canvas.removeEventListener('mousemove', this.onMouseMove)
|
116 | document.removeEventListener('mouseup', this.onEnd)
|
117 | }
|
118 |
|
119 | getDiff (newPageX, newPageY) {
|
120 | const { dragStartX, dragStartY, x, y } = this.state
|
121 | const deltaX = (dragStartX - newPageX) * -2
|
122 | const deltaY = (dragStartY - newPageY) * -2
|
123 | return {
|
124 | midDragX: x + deltaX,
|
125 | midDragY: y + deltaY
|
126 | }
|
127 | }
|
128 |
|
129 | onMove (newPageX, newPageY) {
|
130 | this.setState(this.getDiff(newPageX, newPageY))
|
131 | }
|
132 |
|
133 | onTouchMove (e) {
|
134 | e.preventDefault()
|
135 | const touch = e.targetTouches[0]
|
136 | this.onMove(touch.pageX, touch.pageY)
|
137 | }
|
138 |
|
139 | onTouchStart (e) {
|
140 | const { pageX, pageY } = e.targetTouches[0]
|
141 | this.setState({
|
142 | isMoving: true,
|
143 | dragStartX: pageX,
|
144 | dragStartY: pageY
|
145 | })
|
146 | }
|
147 |
|
148 | onMouseDown (e) {
|
149 | this.setState({
|
150 | isMoving: true,
|
151 | dragStartX: e.pageX,
|
152 | dragStartY: e.pageY
|
153 | })
|
154 | }
|
155 |
|
156 | onEnd () {
|
157 | if (!this.state.isMoving) {
|
158 | return
|
159 | }
|
160 | this.setState({
|
161 | isMoving: false,
|
162 | dragStartX: 0,
|
163 | dragStartY: 0,
|
164 | midDragX: 0,
|
165 | midDragY: 0,
|
166 | x: this.state.midDragX,
|
167 | y: this.state.midDragY
|
168 | })
|
169 | }
|
170 |
|
171 | onMouseMove (e) {
|
172 | if (!this.state.isMoving) {
|
173 | return
|
174 | }
|
175 | this.onMove(e.pageX, e.pageY)
|
176 | }
|
177 |
|
178 | imageOnLoad () {
|
179 | const { height, width } = this.image
|
180 | const max = Math.max(height, width)
|
181 |
|
182 | window.comp = this
|
183 |
|
184 | let zoom = max > 512 ? 512 / max : 1
|
185 |
|
186 | this.setState({ zoom, loaded: true })
|
187 | }
|
188 |
|
189 | componentDidUpdate (prevProps, prevState) {
|
190 | if (prevState && prevState.src !== this.state.src) {
|
191 | const once = (el) => {
|
192 | this.image.removeEventListener('load', once)
|
193 | this.imageOnLoad()
|
194 | }
|
195 | this.image.addEventListener('load', once)
|
196 | }
|
197 |
|
198 | const { canvas } = this
|
199 | if (canvas) {
|
200 | const ctx = canvas.getContext('2d')
|
201 | window.requestAnimationFrame(() => {
|
202 | if (this.image && this.image.height) {
|
203 | ctx.clearRect(0, 0, canvas.width, canvas.height)
|
204 | const { width, height } = this.image
|
205 | const { zoom, x, y, midDragX, midDragY } = this.state
|
206 | const targetX = midDragX || x
|
207 | const targetY = midDragY || y
|
208 | ctx.drawImage(this.image, targetX, targetY, width * zoom, height * zoom)
|
209 | }
|
210 |
|
211 | const { previousSrc, updateSrc, updateValue } = this.props
|
212 | const { src } = this.state
|
213 | const newDataUrl = canvas.toDataURL('image/png')
|
214 |
|
215 | if (!src) {
|
216 | updateSrc(previousSrc)
|
217 | } else if (src !== newDataUrl) {
|
218 | updateSrc(newDataUrl)
|
219 | canvas.toBlob(updateValue)
|
220 | }
|
221 | })
|
222 | }
|
223 | }
|
224 |
|
225 | onRef (el) {
|
226 | if (el) {
|
227 | this.canvas = el
|
228 | }
|
229 | }
|
230 |
|
231 | render () {
|
232 | const { src } = this.state
|
233 | const { previousSrc, width, height } = this.props
|
234 | const noImageAtAll = (!previousSrc && !src)
|
235 | const onlyPreviousImage = previousSrc && !src
|
236 | const message = noImageAtAll ? 'Select an image from your computer' : 'Select a different image'
|
237 |
|
238 | return (
|
239 | h('div', {
|
240 | onDragOver: preventDefault,
|
241 | onDrop: this.onFileDrop,
|
242 | className: 'imageUploader cf'
|
243 | },
|
244 | h('label', { htmlFor: 'imageUpload' },
|
245 | h('img', {
|
246 | src: previousSrc,
|
247 | className: `w3 h3 v-mid mr2 ${!onlyPreviousImage ? 'dn' : ''}`
|
248 | }),
|
249 | h('input', {
|
250 | disabled: !!src,
|
251 | onChange: this.onChange,
|
252 | ref: this.onInputRef,
|
253 | type: 'file',
|
254 | name: 'file',
|
255 | id: 'imageUpload'
|
256 | }),
|
257 | h('p', {
|
258 | className: !src ? 'dn' : ''
|
259 | }, 'Drag to adjust position, use the slider to zoom.'),
|
260 | h('img', {
|
261 | className: 'dn',
|
262 | src,
|
263 | ref: (el) => {
|
264 | if (el) {
|
265 | this.image = el
|
266 | }
|
267 | }
|
268 | }),
|
269 | h('canvas', {
|
270 | className: `${src ? '' : 'dn'} ba br3`,
|
271 | ref: this.onRef,
|
272 | width,
|
273 | height
|
274 | }),
|
275 | h(Button, {
|
276 | extraClasses: src ? 'dn' : '',
|
277 | tagName: 'span',
|
278 | empty: true
|
279 | }, message)
|
280 | ),
|
281 | h('input', {
|
282 | className: !src ? 'dn' : '',
|
283 | onInput: this.onRangeChange,
|
284 | value: this.state.zoom,
|
285 | type: 'range',
|
286 | max: '2',
|
287 | min: '0',
|
288 | step: '0.05'
|
289 | }),
|
290 | h('div', {
|
291 | className: `pt2 ${!src ? 'dn' : ''}`
|
292 | },
|
293 | h(Button, {
|
294 | type: 'button',
|
295 | empty: true,
|
296 | onClick: this.onCancelClick
|
297 | }, 'Cancel')
|
298 | )
|
299 | )
|
300 | )
|
301 | }
|
302 | }
|
303 |
|
304 | ImageUploader.defaultProps = {
|
305 | width: 200,
|
306 | height: 200,
|
307 | previousSrc: '',
|
308 | updateValue: noOp,
|
309 | updateSrc: noOp
|
310 | }
|
311 |
|
312 | export default connect('doUploadFile', ImageUploader)
|