block_editor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +147 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/block_editor_manifest.js +1 -0
  6. data/app/assets/stylesheets/block_editor/backend.scss +4 -0
  7. data/app/assets/stylesheets/block_editor/backend/blocks.scss +0 -0
  8. data/app/assets/stylesheets/block_editor/frontend.scss +1 -0
  9. data/app/assets/stylesheets/block_editor/frontend/blocks.scss +0 -0
  10. data/app/controllers/block_editor/application_controller.rb +4 -0
  11. data/app/helpers/block_editor/application_helper.rb +11 -0
  12. data/app/javascript/block_editor/blocks/button/edit.js +240 -0
  13. data/app/javascript/block_editor/blocks/column/edit.js +93 -0
  14. data/app/javascript/block_editor/blocks/image/edit.js +656 -0
  15. data/app/javascript/block_editor/blocks/index.js +263 -0
  16. data/app/javascript/block_editor/components/block-editor/index.js +88 -0
  17. data/app/javascript/block_editor/components/block-editor/styles.scss +39 -0
  18. data/app/javascript/block_editor/components/header/index.js +45 -0
  19. data/app/javascript/block_editor/components/header/redo.js +36 -0
  20. data/app/javascript/block_editor/components/header/styles.scss +14 -0
  21. data/app/javascript/block_editor/components/header/undo.js +36 -0
  22. data/app/javascript/block_editor/components/media-upload/index.js +37 -0
  23. data/app/javascript/block_editor/components/notices/index.js +26 -0
  24. data/app/javascript/block_editor/components/notices/styles.scss +9 -0
  25. data/app/javascript/block_editor/components/sidebar/index.js +31 -0
  26. data/app/javascript/block_editor/components/sidebar/styles.scss +43 -0
  27. data/app/javascript/block_editor/stores/action-types.js +4 -0
  28. data/app/javascript/block_editor/stores/actions.js +41 -0
  29. data/app/javascript/block_editor/stores/controls.js +21 -0
  30. data/app/javascript/block_editor/stores/index.js +30 -0
  31. data/app/javascript/block_editor/stores/reducer.js +20 -0
  32. data/app/javascript/block_editor/stores/resolvers.js +10 -0
  33. data/app/javascript/block_editor/stores/selectors.js +13 -0
  34. data/app/javascript/controllers/block_editor_controller.jsx +42 -0
  35. data/app/javascript/controllers/index.js +6 -0
  36. data/app/javascript/packs/block_editor/application.js +2 -0
  37. data/app/javascript/packs/block_editor/application.scss +108 -0
  38. data/app/jobs/block_editor/application_job.rb +4 -0
  39. data/app/mailers/block_editor/application_mailer.rb +6 -0
  40. data/app/models/block_editor/application_record.rb +5 -0
  41. data/app/models/block_editor/block_list.rb +7 -0
  42. data/app/models/concerns/block_editor/listable.rb +24 -0
  43. data/app/views/layouts/block_editor/application.html.erb +15 -0
  44. data/config/initializers/webpacker_extension.rb +12 -0
  45. data/config/routes.rb +2 -0
  46. data/config/webpack/development.js +5 -0
  47. data/config/webpack/environment.js +3 -0
  48. data/config/webpack/production.js +5 -0
  49. data/config/webpack/test.js +5 -0
  50. data/config/webpacker.yml +92 -0
  51. data/db/migrate/20210312032114_create_block_lists.rb +11 -0
  52. data/lib/block_editor.rb +26 -0
  53. data/lib/block_editor/block_list_renderer.rb +43 -0
  54. data/lib/block_editor/blocks/base.rb +32 -0
  55. data/lib/block_editor/engine.rb +34 -0
  56. data/lib/block_editor/instance.rb +19 -0
  57. data/lib/block_editor/version.rb +3 -0
  58. data/lib/tasks/block_editor_tasks.rake +59 -0
  59. metadata +131 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+ import React from 'react';
6
+ import ReactDOM from 'react-dom';
7
+
8
+ /**
9
+ * WordPress dependencies
10
+ */
11
+ import {
12
+ InnerBlocks,
13
+ BlockControls,
14
+ BlockVerticalAlignmentToolbar,
15
+ InspectorControls,
16
+ __experimentalBlock as Block,
17
+ } from '@wordpress/block-editor';
18
+ import { PanelBody, RangeControl } from '@wordpress/components';
19
+ import { withDispatch, withSelect } from '@wordpress/data';
20
+ import { compose } from '@wordpress/compose';
21
+ import { __ } from '@wordpress/i18n';
22
+
23
+ function ColumnEdit( {
24
+ attributes,
25
+ setAttributes,
26
+ updateAlignment,
27
+ hasChildBlocks,
28
+ } ) {
29
+ const { verticalAlignment, width } = attributes;
30
+
31
+ const classes = classnames( 'block-core-columns', {
32
+ [ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment,
33
+ } );
34
+
35
+ const hasWidth = Number.isFinite( width );
36
+
37
+ return (
38
+ <>
39
+ <BlockControls>
40
+ <BlockVerticalAlignmentToolbar
41
+ onChange={ updateAlignment }
42
+ value={ verticalAlignment }
43
+ />
44
+ </BlockControls>
45
+ <InnerBlocks
46
+ templateLock={ false }
47
+ renderAppender={
48
+ hasChildBlocks
49
+ ? undefined
50
+ : () => <InnerBlocks.ButtonBlockAppender />
51
+ }
52
+ __experimentalTagName={ Block.div }
53
+ __experimentalPassedProps={ {
54
+ className: classes,
55
+ style: hasWidth ? { flexBasis: width + '%' } : undefined,
56
+ } }
57
+ />
58
+ </>
59
+ );
60
+ }
61
+
62
+ export default compose(
63
+ withSelect( ( select, ownProps ) => {
64
+ const { clientId } = ownProps;
65
+ const { getBlockOrder } = select( 'core/block-editor' );
66
+
67
+ return {
68
+ hasChildBlocks: getBlockOrder( clientId ).length > 0,
69
+ };
70
+ } ),
71
+ withDispatch( ( dispatch, ownProps, registry ) => {
72
+ return {
73
+ updateAlignment( verticalAlignment ) {
74
+ const { clientId, setAttributes } = ownProps;
75
+ const { updateBlockAttributes } = dispatch(
76
+ 'core/block-editor'
77
+ );
78
+ const { getBlockRootClientId } = registry.select(
79
+ 'core/block-editor'
80
+ );
81
+
82
+ // Update own alignment.
83
+ setAttributes( { verticalAlignment } );
84
+
85
+ // Reset Parent Columns Block
86
+ const rootClientId = getBlockRootClientId( clientId );
87
+ updateBlockAttributes( rootClientId, {
88
+ verticalAlignment: null,
89
+ } );
90
+ },
91
+ };
92
+ } )
93
+ )( ColumnEdit );
@@ -0,0 +1,656 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classnames from 'classnames';
5
+ import React from 'react';
6
+ import ReactDOM from 'react-dom';
7
+ import { get, filter, map, last, omit, pick, includes } from 'lodash';
8
+
9
+ /**
10
+ * WordPress dependencies
11
+ */
12
+ import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob';
13
+ import {
14
+ ExternalLink,
15
+ PanelBody,
16
+ ResizableBox,
17
+ Spinner,
18
+ TextareaControl,
19
+ TextControl,
20
+ ToolbarGroup,
21
+ withNotices,
22
+ } from '@wordpress/components';
23
+ import { useViewportMatch } from '@wordpress/compose';
24
+ import { useSelect, useDispatch } from '@wordpress/data';
25
+ import {
26
+ BlockAlignmentToolbar,
27
+ BlockControls,
28
+ BlockIcon,
29
+ InspectorControls,
30
+ InspectorAdvancedControls,
31
+ MediaPlaceholder,
32
+ MediaReplaceFlow,
33
+ RichText,
34
+ __experimentalBlock as Block,
35
+ __experimentalImageSizeControl as ImageSizeControl,
36
+ __experimentalImageURLInputUI as ImageURLInputUI,
37
+ } from '@wordpress/block-editor';
38
+ import { useEffect, useState, useRef } from '@wordpress/element';
39
+ import { __, sprintf } from '@wordpress/i18n';
40
+ import { getPath } from '@wordpress/url';
41
+ import { image as icon } from '@wordpress/icons';
42
+ import { createBlock } from '@wordpress/blocks';
43
+
44
+ /**
45
+ * Internal dependencies
46
+ */
47
+ import { calculatePreferedImageSize } from '@wordpress/block-library/src/image/utils';
48
+
49
+ /**
50
+ * Module constants
51
+ */
52
+ import {
53
+ MIN_SIZE,
54
+ LINK_DESTINATION_MEDIA,
55
+ LINK_DESTINATION_ATTACHMENT,
56
+ ALLOWED_MEDIA_TYPES,
57
+ DEFAULT_SIZE_SLUG,
58
+ } from '@wordpress/block-library/src/image/constants';
59
+
60
+ export const pickRelevantMediaFiles = ( image ) => {
61
+ const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] );
62
+ imageProps.url =
63
+ get( image, [ 'sizes', 'large', 'url' ] ) ||
64
+ get( image, [ 'media_details', 'sizes', 'large', 'source_url' ] ) ||
65
+ image.url;
66
+ return imageProps;
67
+ };
68
+
69
+
70
+ const useImageSize = ( ref, src, dependencies ) => {
71
+ const [ state, setState ] = useState( {
72
+ imageWidth: null,
73
+ imageHeight: null,
74
+ imageWidthWithinContainer: null,
75
+ imageHeightWithinContainer: null,
76
+ } );
77
+
78
+ useEffect( () => {
79
+ if ( ! src ) {
80
+ return;
81
+ }
82
+
83
+ const { defaultView } = ref.current.ownerDocument;
84
+ const image = new defaultView.Image();
85
+
86
+ function calculateSize() {
87
+ const { width, height } = calculatePreferedImageSize(
88
+ image,
89
+ ref.current
90
+ );
91
+
92
+ setState( {
93
+ imageWidth: image.width,
94
+ imageHeight: image.height,
95
+ imageWidthWithinContainer: width,
96
+ imageHeightWithinContainer: height,
97
+ } );
98
+ }
99
+
100
+ defaultView.addEventListener( 'resize', calculateSize );
101
+ image.addEventListener( 'load', calculateSize );
102
+ image.src = src;
103
+
104
+ return () => {
105
+ defaultView.removeEventListener( 'resize', calculateSize );
106
+ image.removeEventListener( 'load', calculateSize );
107
+ };
108
+ }, [ src, ...dependencies ] );
109
+
110
+ return state;
111
+ }
112
+
113
+ /**
114
+ * Is the URL a temporary blob URL? A blob URL is one that is used temporarily
115
+ * while the image is being uploaded and will not have an id yet allocated.
116
+ *
117
+ * @param {number=} id The id of the image.
118
+ * @param {string=} url The url of the image.
119
+ *
120
+ * @return {boolean} Is the URL a Blob URL
121
+ */
122
+ const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url );
123
+
124
+ /**
125
+ * Is the url for the image hosted externally. An externally hosted image has no
126
+ * id and is not a blob url.
127
+ *
128
+ * @param {number=} id The id of the image.
129
+ * @param {string=} url The url of the image.
130
+ *
131
+ * @return {boolean} Is the url an externally hosted url?
132
+ */
133
+ const isExternalImage = ( id, url ) => url && ! id && ! isBlobURL( url );
134
+
135
+ function getFilename( url ) {
136
+ const path = getPath( url );
137
+ if ( path ) {
138
+ return last( path.split( '/' ) );
139
+ }
140
+ }
141
+
142
+ export function ImageEdit( {
143
+ attributes: {
144
+ url = '',
145
+ alt,
146
+ caption,
147
+ align,
148
+ id,
149
+ href,
150
+ rel,
151
+ linkClass,
152
+ linkDestination,
153
+ title,
154
+ width,
155
+ height,
156
+ linkTarget,
157
+ sizeSlug,
158
+ },
159
+ setAttributes,
160
+ isSelected,
161
+ className,
162
+ noticeUI,
163
+ insertBlocksAfter,
164
+ noticeOperations,
165
+ onReplace,
166
+ } ) {
167
+ const ref = useRef();
168
+ const { image, maxWidth, isRTL, imageSizes, mediaUpload } = useSelect(
169
+ ( select ) => {
170
+ const { getMedia } = select( 'core' );
171
+ const { getSettings } = select( 'core/block-editor' );
172
+ return {
173
+ ...pick( getSettings(), [
174
+ 'mediaUpload',
175
+ 'imageSizes',
176
+ 'isRTL',
177
+ 'maxWidth',
178
+ ] ),
179
+ image: id && isSelected ? getMedia( id ) : null,
180
+ };
181
+ },
182
+ [ id, isSelected ]
183
+ );
184
+ const { toggleSelection } = useDispatch( 'core/block-editor' );
185
+ const isLargeViewport = useViewportMatch( 'medium' );
186
+ const [ captionFocused, setCaptionFocused ] = useState( false );
187
+ const isWideAligned = includes( [ 'wide', 'full' ], align );
188
+
189
+ function onResizeStart() {
190
+ toggleSelection( false );
191
+ }
192
+
193
+ function onResizeStop() {
194
+ toggleSelection( true );
195
+ }
196
+
197
+ function onUploadError( message ) {
198
+ noticeOperations.removeAllNotices();
199
+ noticeOperations.createErrorNotice( message );
200
+ }
201
+
202
+ function onSelectImage( media ) {
203
+ if ( ! media || ! media.url ) {
204
+ setAttributes( {
205
+ url: undefined,
206
+ alt: undefined,
207
+ id: undefined,
208
+ title: undefined,
209
+ caption: undefined,
210
+ } );
211
+ return;
212
+ }
213
+
214
+ let mediaAttributes = pickRelevantMediaFiles( media );
215
+
216
+ // If the current image is temporary but an alt text was meanwhile
217
+ // written by the user, make sure the text is not overwritten.
218
+ if ( isTemporaryImage( id, url ) ) {
219
+ if ( alt ) {
220
+ mediaAttributes = omit( mediaAttributes, [ 'alt' ] );
221
+ }
222
+ }
223
+
224
+ // If a caption text was meanwhile written by the user,
225
+ // make sure the text is not overwritten by empty captions.
226
+ if ( caption && ! get( mediaAttributes, [ 'caption' ] ) ) {
227
+ mediaAttributes = omit( mediaAttributes, [ 'caption' ] );
228
+ }
229
+
230
+ let additionalAttributes;
231
+ // Reset the dimension attributes if changing to a different image.
232
+ if ( ! media.id || media.id !== id ) {
233
+ additionalAttributes = {
234
+ width: undefined,
235
+ height: undefined,
236
+ sizeSlug: DEFAULT_SIZE_SLUG,
237
+ };
238
+ } else {
239
+ // Keep the same url when selecting the same file, so "Image Size"
240
+ // option is not changed.
241
+ additionalAttributes = { url };
242
+ }
243
+
244
+ // Check if the image is linked to it's media.
245
+ if ( linkDestination === LINK_DESTINATION_MEDIA ) {
246
+ // Update the media link.
247
+ mediaAttributes.href = media.url;
248
+ }
249
+
250
+ // Check if the image is linked to the attachment page.
251
+ if ( linkDestination === LINK_DESTINATION_ATTACHMENT ) {
252
+ // Update the media link.
253
+ mediaAttributes.href = media.link;
254
+ }
255
+
256
+ setAttributes( {
257
+ ...mediaAttributes,
258
+ ...additionalAttributes,
259
+ } );
260
+ }
261
+
262
+ function onSelectURL( newURL ) {
263
+ if ( newURL !== url ) {
264
+ setAttributes( {
265
+ url: newURL,
266
+ id: undefined,
267
+ sizeSlug: DEFAULT_SIZE_SLUG,
268
+ } );
269
+ }
270
+ }
271
+
272
+ function onImageError() {
273
+ console.log('Block Editor - ImageBlock: An image error occurred.')
274
+ // // Check if there's an embed block that handles this URL.
275
+ // const embedBlock = createUpgradedEmbedBlock( { attributes: { url } } );
276
+ // if ( undefined !== embedBlock ) {
277
+ // onReplace( embedBlock );
278
+ // }
279
+ }
280
+
281
+ function onSetHref( props ) {
282
+ setAttributes( props );
283
+ }
284
+
285
+ function onSetTitle( value ) {
286
+ // This is the HTML title attribute, separate from the media object
287
+ // title.
288
+ setAttributes( { title: value } );
289
+ }
290
+
291
+ function onFocusCaption() {
292
+ if ( ! captionFocused ) {
293
+ setCaptionFocused( true );
294
+ }
295
+ }
296
+
297
+ function onImageClick() {
298
+ if ( captionFocused ) {
299
+ setCaptionFocused( false );
300
+ }
301
+ }
302
+
303
+ function updateAlt( newAlt ) {
304
+ setAttributes( { alt: newAlt } );
305
+ }
306
+
307
+ function updateAlignment( nextAlign ) {
308
+ const extraUpdatedAttributes = isWideAligned
309
+ ? { width: undefined, height: undefined }
310
+ : {};
311
+ setAttributes( {
312
+ ...extraUpdatedAttributes,
313
+ align: nextAlign,
314
+ } );
315
+ }
316
+
317
+ function updateImage( newSizeSlug ) {
318
+ const newUrl = get( image, [
319
+ 'media_details',
320
+ 'sizes',
321
+ newSizeSlug,
322
+ 'source_url',
323
+ ] );
324
+ if ( ! newUrl ) {
325
+ return null;
326
+ }
327
+
328
+ setAttributes( {
329
+ url,
330
+ width: undefined,
331
+ height: undefined,
332
+ sizeSlug: newSizeSlug,
333
+ } );
334
+ }
335
+
336
+ function getImageSizeOptions() {
337
+ return map(
338
+ filter( imageSizes, ( { slug } ) =>
339
+ get( image, [ 'media_details', 'sizes', slug, 'source_url' ] )
340
+ ),
341
+ ( { name, slug } ) => ( { value: slug, label: name } )
342
+ );
343
+ }
344
+
345
+ const isTemp = isTemporaryImage( id, url );
346
+
347
+ // Upload a temporary image on mount.
348
+ useEffect( () => {
349
+ if ( ! isTemp ) {
350
+ return;
351
+ }
352
+
353
+ const file = getBlobByURL( url );
354
+
355
+ if ( file ) {
356
+ mediaUpload( {
357
+ filesList: [ file ],
358
+ onFileChange: ( [ img ] ) => {
359
+ onSelectImage( img );
360
+ },
361
+ allowedTypes: ALLOWED_MEDIA_TYPES,
362
+ onError: ( message ) => {
363
+ noticeOperations.createErrorNotice( message );
364
+ },
365
+ } );
366
+ }
367
+ }, [] );
368
+
369
+ // If an image is temporary, revoke the Blob url when it is uploaded (and is
370
+ // no longer temporary).
371
+ useEffect( () => {
372
+ if ( ! isTemp ) {
373
+ return;
374
+ }
375
+
376
+ return () => {
377
+ revokeBlobURL( url );
378
+ };
379
+ }, [ isTemp ] );
380
+
381
+ useEffect( () => {
382
+ if ( ! isSelected ) {
383
+ setCaptionFocused( false );
384
+ }
385
+ }, [ isSelected ] );
386
+
387
+ const isExternal = isExternalImage( id, url );
388
+ const controls = (
389
+ <BlockControls>
390
+ <BlockAlignmentToolbar
391
+ value={ align }
392
+ onChange={ updateAlignment }
393
+ />
394
+ { url && (
395
+ <MediaReplaceFlow
396
+ mediaId={ id }
397
+ mediaURL={ url }
398
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
399
+ accept="image/*"
400
+ onSelect={ onSelectImage }
401
+ onSelectURL={ onSelectURL }
402
+ onError={ onUploadError }
403
+ />
404
+ ) }
405
+ { url && (
406
+ <ToolbarGroup>
407
+ <ImageURLInputUI
408
+ url={ href || '' }
409
+ onChangeUrl={ onSetHref }
410
+ linkDestination={ linkDestination }
411
+ mediaUrl={ image && image.source_url }
412
+ mediaLink={ image && image.link }
413
+ linkTarget={ linkTarget }
414
+ linkClass={ linkClass }
415
+ rel={ rel }
416
+ />
417
+ </ToolbarGroup>
418
+ ) }
419
+ </BlockControls>
420
+ );
421
+ const src = isExternal ? url : undefined;
422
+ const mediaPreview = !! url && (
423
+ <img
424
+ alt={ __( 'Edit image' ) }
425
+ title={ __( 'Edit image' ) }
426
+ className={ 'edit-image-preview' }
427
+ src={ url }
428
+ />
429
+ );
430
+
431
+ const mediaPlaceholder = (
432
+ <MediaPlaceholder
433
+ icon={ <BlockIcon icon={ icon } /> }
434
+ onSelect={ onSelectImage }
435
+ onSelectURL={ onSelectURL }
436
+ notices={ noticeUI }
437
+ onError={ onUploadError }
438
+ accept="image/*"
439
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
440
+ value={ { id, src } }
441
+ mediaPreview={ mediaPreview }
442
+ disableMediaButtons={ url }
443
+ />
444
+ );
445
+
446
+ const {
447
+ imageWidthWithinContainer,
448
+ imageHeightWithinContainer,
449
+ imageWidth,
450
+ imageHeight,
451
+ } = useImageSize( ref, url, [ align ] );
452
+
453
+ if ( ! url ) {
454
+ return (
455
+ <>
456
+ { controls }
457
+ <Block.div>{ mediaPlaceholder }</Block.div>
458
+ </>
459
+ );
460
+ }
461
+
462
+ const classes = classnames( className, {
463
+ 'is-transient': isBlobURL( url ),
464
+ 'is-resized': !! width || !! height,
465
+ 'is-focused': isSelected,
466
+ [ `size-${ sizeSlug }` ]: sizeSlug,
467
+ } );
468
+
469
+ const isResizable = false;
470
+ const imageSizeOptions = getImageSizeOptions();
471
+
472
+ const inspectorControls = (
473
+ <>
474
+ <InspectorControls>
475
+ <PanelBody title={ __( 'Image settings' ) }>
476
+ <TextareaControl
477
+ label={ __( 'Alt text (alternative text)' ) }
478
+ value={ alt }
479
+ onChange={ updateAlt }
480
+ help={
481
+ <>
482
+ <ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
483
+ { __(
484
+ 'Describe the purpose of the image'
485
+ ) }
486
+ </ExternalLink>
487
+ { __(
488
+ 'Leave empty if the image is purely decorative.'
489
+ ) }
490
+ </>
491
+ }
492
+ />
493
+ </PanelBody>
494
+ </InspectorControls>
495
+ <InspectorAdvancedControls>
496
+ <TextControl
497
+ label={ __( 'Title attribute' ) }
498
+ value={ title || '' }
499
+ onChange={ onSetTitle }
500
+ help={
501
+ <>
502
+ { __(
503
+ 'Describe the role of this image on the page.'
504
+ ) }
505
+ <ExternalLink href="https://www.w3.org/TR/html52/dom.html#the-title-attribute">
506
+ { __(
507
+ '(Note: many devices and browsers do not display this text.)'
508
+ ) }
509
+ </ExternalLink>
510
+ </>
511
+ }
512
+ />
513
+ </InspectorAdvancedControls>
514
+ </>
515
+ );
516
+
517
+ const filename = getFilename( url );
518
+ let defaultedAlt;
519
+
520
+ if ( alt ) {
521
+ defaultedAlt = alt;
522
+ } else if ( filename ) {
523
+ defaultedAlt = sprintf(
524
+ /* translators: %s: file name */
525
+ __( 'This image has an empty alt attribute; its file name is %s' ),
526
+ filename
527
+ );
528
+ } else {
529
+ defaultedAlt = __( 'This image has an empty alt attribute' );
530
+ }
531
+
532
+ let img = (
533
+ // Disable reason: Image itself is not meant to be interactive, but
534
+ // should direct focus to block.
535
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
536
+ <>
537
+ { inspectorControls }
538
+ <img
539
+ src={ url }
540
+ alt={ defaultedAlt }
541
+ onClick={ onImageClick }
542
+ onError={ () => onImageError() }
543
+ />
544
+ { isBlobURL( url ) && <Spinner /> }
545
+ </>
546
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
547
+ );
548
+
549
+ if ( ! isResizable || ! imageWidthWithinContainer ) {
550
+ img = <div style={ { width, height } }>{ img }</div>;
551
+ } else {
552
+ const currentWidth = width || imageWidthWithinContainer;
553
+ const currentHeight = height || imageHeightWithinContainer;
554
+
555
+ const ratio = imageWidth / imageHeight;
556
+ const minWidth = imageWidth < imageHeight ? MIN_SIZE : MIN_SIZE * ratio;
557
+ const minHeight =
558
+ imageHeight < imageWidth ? MIN_SIZE : MIN_SIZE / ratio;
559
+
560
+ // With the current implementation of ResizableBox, an image needs an
561
+ // explicit pixel value for the max-width. In absence of being able to
562
+ // set the content-width, this max-width is currently dictated by the
563
+ // vanilla editor style. The following variable adds a buffer to this
564
+ // vanilla style, so 3rd party themes have some wiggleroom. This does,
565
+ // in most cases, allow you to scale the image beyond the width of the
566
+ // main column, though not infinitely.
567
+ // @todo It would be good to revisit this once a content-width variable
568
+ // becomes available.
569
+ const maxWidthBuffer = maxWidth * 2.5;
570
+
571
+ let showRightHandle = false;
572
+ let showLeftHandle = false;
573
+
574
+ /* eslint-disable no-lonely-if */
575
+ // See https://github.com/WordPress/gutenberg/issues/7584.
576
+ if ( align === 'center' ) {
577
+ // When the image is centered, show both handles.
578
+ showRightHandle = true;
579
+ showLeftHandle = true;
580
+ } else if ( isRTL ) {
581
+ // In RTL mode the image is on the right by default.
582
+ // Show the right handle and hide the left handle only when it is
583
+ // aligned left. Otherwise always show the left handle.
584
+ if ( align === 'left' ) {
585
+ showRightHandle = true;
586
+ } else {
587
+ showLeftHandle = true;
588
+ }
589
+ } else {
590
+ // Show the left handle and hide the right handle only when the
591
+ // image is aligned right. Otherwise always show the right handle.
592
+ if ( align === 'right' ) {
593
+ showLeftHandle = true;
594
+ } else {
595
+ showRightHandle = true;
596
+ }
597
+ }
598
+ /* eslint-enable no-lonely-if */
599
+
600
+ img = (
601
+ <ResizableBox
602
+ size={ { width, height } }
603
+ showHandle={ isSelected }
604
+ minWidth={ minWidth }
605
+ maxWidth={ maxWidthBuffer }
606
+ minHeight={ minHeight }
607
+ maxHeight={ maxWidthBuffer / ratio }
608
+ lockAspectRatio
609
+ enable={ {
610
+ top: false,
611
+ right: showRightHandle,
612
+ bottom: true,
613
+ left: showLeftHandle,
614
+ } }
615
+ onResizeStart={ onResizeStart }
616
+ onResizeStop={ ( event, direction, elt, delta ) => {
617
+ onResizeStop();
618
+ setAttributes( {
619
+ width: parseInt( currentWidth + delta.width, 10 ),
620
+ height: parseInt( currentHeight + delta.height, 10 ),
621
+ } );
622
+ } }
623
+ >
624
+ { img }
625
+ </ResizableBox>
626
+ );
627
+ }
628
+
629
+ return (
630
+ <>
631
+ { controls }
632
+ <Block.figure ref={ ref } className={ classes }>
633
+ { img }
634
+ { ( ! RichText.isEmpty( caption ) || isSelected ) && (
635
+ <RichText
636
+ tagName="figcaption"
637
+ placeholder={ __( 'Write caption…' ) }
638
+ value={ caption }
639
+ unstableOnFocus={ onFocusCaption }
640
+ onChange={ ( value ) =>
641
+ setAttributes( { caption: value } )
642
+ }
643
+ isSelected={ captionFocused }
644
+ inlineToolbar
645
+ __unstableOnSplitAtEnd={ () =>
646
+ insertBlocksAfter( createBlock( 'core/paragraph' ) )
647
+ }
648
+ />
649
+ ) }
650
+ { mediaPlaceholder }
651
+ </Block.figure>
652
+ </>
653
+ );
654
+ }
655
+
656
+ export default withNotices( ImageEdit );