UNPKG

7.88 kBJavaScriptView Raw
1import { Component, h } from 'preact'
2import { connect } from 'redux-bundler-preact'
3import Button from './button'
4import './image-uploader.css'
5
6const preventDefault = e => e.preventDefault()
7const noOp = () => { }
8
9class 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 // this is necessary to prevent infinite recursion
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 // hacky ugliness, update references anyway
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
304ImageUploader.defaultProps = {
305 width: 200,
306 height: 200,
307 previousSrc: '',
308 updateValue: noOp,
309 updateSrc: noOp
310}
311
312export default connect('doUploadFile', ImageUploader)