UNPKG

11.1 kBJavaScriptView Raw
1/**
2 * External dependencies
3 */
4import React from 'react';
5import { View, ImageBackground, TextInput, Text, TouchableWithoutFeedback } from 'react-native';
6import {
7 subscribeMediaUpload,
8 requestMediaPickFromMediaLibrary,
9 requestMediaPickFromDeviceLibrary,
10 requestMediaPickFromDeviceCamera,
11 mediaUploadSync,
12 requestImageFailedRetryDialog,
13 requestImageUploadCancelDialog,
14} from 'react-native-gutenberg-bridge';
15
16/**
17 * WordPress dependencies
18 */
19import {
20 Toolbar,
21 ToolbarButton,
22 Spinner,
23 Dashicon,
24} from '@wordpress/components';
25import {
26 MediaPlaceholder,
27 RichText,
28 BlockControls,
29 InspectorControls,
30} from '@wordpress/block-editor';
31import {
32 BottomSheet,
33 Picker,
34} from '@wordpress/editor';
35import { __ } from '@wordpress/i18n';
36import { isURL } from '@wordpress/url';
37
38/**
39 * Internal dependencies
40 */
41import ImageSize from './image-size';
42import styles from './styles.scss';
43
44const MEDIA_UPLOAD_STATE_UPLOADING = 1;
45const MEDIA_UPLOAD_STATE_SUCCEEDED = 2;
46const MEDIA_UPLOAD_STATE_FAILED = 3;
47const MEDIA_UPLOAD_STATE_RESET = 4;
48
49const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE = 'choose_from_device';
50const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO = 'take_photo';
51const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_media_library';
52
53const LINK_DESTINATION_CUSTOM = 'custom';
54const LINK_DESTINATION_NONE = 'none';
55
56class ImageEdit extends React.Component {
57 constructor( props ) {
58 super( props );
59
60 this.state = {
61 showSettings: false,
62 progress: 0,
63 isUploadInProgress: false,
64 isUploadFailed: false,
65 };
66
67 this.mediaUpload = this.mediaUpload.bind( this );
68 this.addMediaUploadListener = this.addMediaUploadListener.bind( this );
69 this.removeMediaUploadListener = this.removeMediaUploadListener.bind( this );
70 this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind( this );
71 this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind( this );
72 this.updateMediaProgress = this.updateMediaProgress.bind( this );
73 this.updateAlt = this.updateAlt.bind( this );
74 this.updateImageURL = this.updateImageURL.bind( this );
75 this.onSetLinkDestination = this.onSetLinkDestination.bind( this );
76 this.onImagePressed = this.onImagePressed.bind( this );
77 this.onClearSettings = this.onClearSettings.bind( this );
78 }
79
80 componentDidMount() {
81 const { attributes } = this.props;
82
83 if ( attributes.id && ! isURL( attributes.url ) ) {
84 this.addMediaUploadListener();
85 mediaUploadSync();
86 }
87 }
88
89 componentWillUnmount() {
90 this.removeMediaUploadListener();
91 }
92
93 onImagePressed() {
94 const { attributes } = this.props;
95
96 if ( this.state.isUploadInProgress ) {
97 requestImageUploadCancelDialog( attributes.id );
98 } else if ( attributes.id && ! isURL( attributes.url ) ) {
99 requestImageFailedRetryDialog( attributes.id );
100 }
101 }
102
103 mediaUpload( payload ) {
104 const { attributes } = this.props;
105
106 if ( payload.mediaId !== attributes.id ) {
107 return;
108 }
109
110 switch ( payload.state ) {
111 case MEDIA_UPLOAD_STATE_UPLOADING:
112 this.updateMediaProgress( payload );
113 break;
114 case MEDIA_UPLOAD_STATE_SUCCEEDED:
115 this.finishMediaUploadWithSuccess( payload );
116 break;
117 case MEDIA_UPLOAD_STATE_FAILED:
118 this.finishMediaUploadWithFailure( payload );
119 break;
120 case MEDIA_UPLOAD_STATE_RESET:
121 this.mediaUploadStateReset( payload );
122 break;
123 }
124 }
125
126 updateMediaProgress( payload ) {
127 const { setAttributes } = this.props;
128 this.setState( { progress: payload.progress, isUploadInProgress: true, isUploadFailed: false } );
129 if ( payload.mediaUrl ) {
130 setAttributes( { url: payload.mediaUrl } );
131 }
132 }
133
134 finishMediaUploadWithSuccess( payload ) {
135 const { setAttributes } = this.props;
136
137 setAttributes( { url: payload.mediaUrl, id: payload.mediaServerId } );
138 this.setState( { isUploadInProgress: false } );
139
140 this.removeMediaUploadListener();
141 }
142
143 finishMediaUploadWithFailure( payload ) {
144 const { setAttributes } = this.props;
145
146 setAttributes( { id: payload.mediaId } );
147 this.setState( { isUploadInProgress: false, isUploadFailed: true } );
148 }
149
150 mediaUploadStateReset( payload ) {
151 const { setAttributes } = this.props;
152
153 setAttributes( { id: payload.mediaId, url: null } );
154 this.setState( { isUploadInProgress: false, isUploadFailed: false } );
155 }
156
157 addMediaUploadListener() {
158 //if we already have a subscription not worth doing it again
159 if ( this.subscriptionParentMediaUpload ) {
160 return;
161 }
162 this.subscriptionParentMediaUpload = subscribeMediaUpload( ( payload ) => {
163 this.mediaUpload( payload );
164 } );
165 }
166
167 removeMediaUploadListener() {
168 if ( this.subscriptionParentMediaUpload ) {
169 this.subscriptionParentMediaUpload.remove();
170 }
171 }
172
173 updateAlt( newAlt ) {
174 this.props.setAttributes( { alt: newAlt } );
175 }
176
177 updateImageURL( url ) {
178 this.props.setAttributes( { url, width: undefined, height: undefined } );
179 }
180
181 onSetLinkDestination( href ) {
182 this.props.setAttributes( {
183 linkDestination: LINK_DESTINATION_CUSTOM,
184 href,
185 } );
186 }
187
188 onClearSettings() {
189 this.props.setAttributes( {
190 alt: '',
191 linkDestination: LINK_DESTINATION_NONE,
192 href: undefined,
193 } );
194 }
195
196 getMediaOptionsItems() {
197 return [
198 { icon: 'format-image', value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, label: __( 'Choose from device' ) },
199 { icon: 'camera', value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO, label: __( 'Take a Photo' ) },
200 { icon: 'wordpress-alt', value: MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, label: __( 'WordPress Media Library' ) },
201 ];
202 }
203
204 render() {
205 const { attributes, isSelected, setAttributes } = this.props;
206 const { url, caption, height, width, alt, href } = attributes;
207
208 const onMediaLibraryButtonPressed = () => {
209 requestMediaPickFromMediaLibrary( ( mediaId, mediaUrl ) => {
210 if ( mediaUrl ) {
211 setAttributes( { id: mediaId, url: mediaUrl } );
212 }
213 } );
214 };
215
216 const onMediaUploadButtonPressed = () => {
217 requestMediaPickFromDeviceLibrary( ( mediaId, mediaUri ) => {
218 if ( mediaUri ) {
219 this.addMediaUploadListener( );
220 setAttributes( { url: mediaUri, id: mediaId } );
221 }
222 } );
223 };
224
225 const onMediaCaptureButtonPressed = () => {
226 requestMediaPickFromDeviceCamera( ( mediaId, mediaUri ) => {
227 if ( mediaUri ) {
228 this.addMediaUploadListener( );
229 setAttributes( { url: mediaUri, id: mediaId } );
230 }
231 } );
232 };
233
234 const onImageSettingsButtonPressed = () => {
235 this.setState( { showSettings: true } );
236 };
237
238 const onImageSettingsClose = () => {
239 this.setState( { showSettings: false } );
240 };
241
242 let picker;
243
244 const onMediaOptionsButtonPressed = () => {
245 picker.presentPicker();
246 };
247
248 const toolbarEditButton = (
249 <Toolbar>
250 <ToolbarButton
251 label={ __( 'Edit image' ) }
252 icon="edit"
253 onClick={ onMediaOptionsButtonPressed }
254 />
255 </Toolbar>
256 );
257
258 const getInspectorControls = () => (
259 <BottomSheet
260 isVisible={ this.state.showSettings }
261 onClose={ onImageSettingsClose }
262 hideHeader
263 >
264 <BottomSheet.Cell
265 icon={ 'admin-links' }
266 label={ __( 'Link To' ) }
267 value={ href || '' }
268 valuePlaceholder={ __( 'Add URL' ) }
269 onChangeValue={ this.onSetLinkDestination }
270 autoCapitalize="none"
271 autoCorrect={ false }
272 />
273 <BottomSheet.Cell
274 icon={ 'editor-textcolor' }
275 label={ __( 'Alt Text' ) }
276 value={ alt || '' }
277 valuePlaceholder={ __( 'None' ) }
278 separatorType={ 'fullWidth' }
279 onChangeValue={ this.updateAlt }
280 />
281 <BottomSheet.Cell
282 label={ __( 'Clear All Settings' ) }
283 labelStyle={ styles.clearSettingsButton }
284 separatorType={ 'none' }
285 onPress={ this.onClearSettings }
286 />
287 </BottomSheet>
288 );
289
290 const mediaOptions = this.getMediaOptionsItems();
291
292 const getMediaOptions = () => (
293 <Picker
294 hideCancelButton={ true }
295 ref={ ( instance ) => picker = instance }
296 options={ mediaOptions }
297 onChange={ ( value ) => {
298 if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) {
299 onMediaUploadButtonPressed();
300 } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO ) {
301 onMediaCaptureButtonPressed();
302 } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) {
303 onMediaLibraryButtonPressed();
304 }
305 } }
306 />
307 );
308
309 if ( ! url ) {
310 return (
311 <View style={ { flex: 1 } } >
312 { getMediaOptions() }
313 <MediaPlaceholder
314 onMediaOptionsPressed={ onMediaOptionsButtonPressed }
315 />
316 </View>
317 );
318 }
319
320 const showSpinner = this.state.isUploadInProgress;
321 const opacity = this.state.isUploadInProgress ? 0.3 : 1;
322 const progress = this.state.progress * 100;
323
324 return (
325 <TouchableWithoutFeedback onPress={ this.onImagePressed } disabled={ ! isSelected }>
326 <View style={ { flex: 1 } }>
327 { showSpinner && <Spinner progress={ progress } /> }
328 <BlockControls>
329 { toolbarEditButton }
330 </BlockControls>
331 <InspectorControls>
332 <ToolbarButton
333 label={ __( 'Image Settings' ) }
334 icon="admin-generic"
335 onClick={ onImageSettingsButtonPressed }
336 />
337 </InspectorControls>
338 <ImageSize src={ url } >
339 { ( sizes ) => {
340 const {
341 imageWidthWithinContainer,
342 imageHeightWithinContainer,
343 } = sizes;
344
345 if ( imageWidthWithinContainer === undefined ) {
346 return (
347 <View style={ styles.imageContainer } >
348 <Dashicon icon={ 'format-image' } size={ 300 } />
349 </View>
350 );
351 }
352
353 let finalHeight = imageHeightWithinContainer;
354 if ( height > 0 && height < imageHeightWithinContainer ) {
355 finalHeight = height;
356 }
357
358 let finalWidth = imageWidthWithinContainer;
359 if ( width > 0 && width < imageWidthWithinContainer ) {
360 finalWidth = width;
361 }
362
363 return (
364 <View style={ { flex: 1 } } >
365 { getInspectorControls() }
366 { getMediaOptions() }
367 <ImageBackground
368 style={ { width: finalWidth, height: finalHeight, opacity } }
369 resizeMethod="scale"
370 source={ { uri: url } }
371 key={ url }
372 >
373 { this.state.isUploadFailed &&
374 <View style={ styles.imageContainer } >
375 <Dashicon icon={ 'image-rotate' } ariaPressed={ 'dashicon-active' } />
376 <Text style={ styles.uploadFailedText }>{ __( 'Failed to insert media.\nPlease tap for options.' ) }</Text>
377 </View>
378 }
379 </ImageBackground>
380 </View>
381 );
382 } }
383 </ImageSize>
384 { ( ! RichText.isEmpty( caption ) > 0 || isSelected ) && (
385 <View style={ { padding: 12, flex: 1 } }>
386 <TextInput
387 style={ { textAlign: 'center' } }
388 fontFamily={ this.props.fontFamily || ( styles[ 'caption-text' ].fontFamily ) }
389 underlineColorAndroid="transparent"
390 value={ caption }
391 placeholder={ __( 'Write caption…' ) }
392 onChangeText={ ( newCaption ) => setAttributes( { caption: newCaption } ) }
393 />
394 </View>
395 ) }
396 </View>
397 </TouchableWithoutFeedback>
398 );
399 }
400}
401
402export default ImageEdit;