1 |
|
2 |
|
3 |
|
4 | import React from 'react';
|
5 | import { View, ImageBackground, TextInput, Text, TouchableWithoutFeedback } from 'react-native';
|
6 | import {
|
7 | subscribeMediaUpload,
|
8 | requestMediaPickFromMediaLibrary,
|
9 | requestMediaPickFromDeviceLibrary,
|
10 | requestMediaPickFromDeviceCamera,
|
11 | mediaUploadSync,
|
12 | requestImageFailedRetryDialog,
|
13 | requestImageUploadCancelDialog,
|
14 | } from 'react-native-gutenberg-bridge';
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | import {
|
20 | Toolbar,
|
21 | ToolbarButton,
|
22 | Spinner,
|
23 | Dashicon,
|
24 | } from '@wordpress/components';
|
25 | import {
|
26 | MediaPlaceholder,
|
27 | RichText,
|
28 | BlockControls,
|
29 | InspectorControls,
|
30 | } from '@wordpress/block-editor';
|
31 | import {
|
32 | BottomSheet,
|
33 | Picker,
|
34 | } from '@wordpress/editor';
|
35 | import { __ } from '@wordpress/i18n';
|
36 | import { isURL } from '@wordpress/url';
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | import ImageSize from './image-size';
|
42 | import styles from './styles.scss';
|
43 |
|
44 | const MEDIA_UPLOAD_STATE_UPLOADING = 1;
|
45 | const MEDIA_UPLOAD_STATE_SUCCEEDED = 2;
|
46 | const MEDIA_UPLOAD_STATE_FAILED = 3;
|
47 | const MEDIA_UPLOAD_STATE_RESET = 4;
|
48 |
|
49 | const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE = 'choose_from_device';
|
50 | const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_PHOTO = 'take_photo';
|
51 | const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_media_library';
|
52 |
|
53 | const LINK_DESTINATION_CUSTOM = 'custom';
|
54 | const LINK_DESTINATION_NONE = 'none';
|
55 |
|
56 | class 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 |
|
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 |
|
402 | export default ImageEdit;
|