block_editor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +147 -0
- data/Rakefile +18 -0
- data/app/assets/config/block_editor_manifest.js +1 -0
- data/app/assets/stylesheets/block_editor/backend.scss +4 -0
- data/app/assets/stylesheets/block_editor/backend/blocks.scss +0 -0
- data/app/assets/stylesheets/block_editor/frontend.scss +1 -0
- data/app/assets/stylesheets/block_editor/frontend/blocks.scss +0 -0
- data/app/controllers/block_editor/application_controller.rb +4 -0
- data/app/helpers/block_editor/application_helper.rb +11 -0
- data/app/javascript/block_editor/blocks/button/edit.js +240 -0
- data/app/javascript/block_editor/blocks/column/edit.js +93 -0
- data/app/javascript/block_editor/blocks/image/edit.js +656 -0
- data/app/javascript/block_editor/blocks/index.js +263 -0
- data/app/javascript/block_editor/components/block-editor/index.js +88 -0
- data/app/javascript/block_editor/components/block-editor/styles.scss +39 -0
- data/app/javascript/block_editor/components/header/index.js +45 -0
- data/app/javascript/block_editor/components/header/redo.js +36 -0
- data/app/javascript/block_editor/components/header/styles.scss +14 -0
- data/app/javascript/block_editor/components/header/undo.js +36 -0
- data/app/javascript/block_editor/components/media-upload/index.js +37 -0
- data/app/javascript/block_editor/components/notices/index.js +26 -0
- data/app/javascript/block_editor/components/notices/styles.scss +9 -0
- data/app/javascript/block_editor/components/sidebar/index.js +31 -0
- data/app/javascript/block_editor/components/sidebar/styles.scss +43 -0
- data/app/javascript/block_editor/stores/action-types.js +4 -0
- data/app/javascript/block_editor/stores/actions.js +41 -0
- data/app/javascript/block_editor/stores/controls.js +21 -0
- data/app/javascript/block_editor/stores/index.js +30 -0
- data/app/javascript/block_editor/stores/reducer.js +20 -0
- data/app/javascript/block_editor/stores/resolvers.js +10 -0
- data/app/javascript/block_editor/stores/selectors.js +13 -0
- data/app/javascript/controllers/block_editor_controller.jsx +42 -0
- data/app/javascript/controllers/index.js +6 -0
- data/app/javascript/packs/block_editor/application.js +2 -0
- data/app/javascript/packs/block_editor/application.scss +108 -0
- data/app/jobs/block_editor/application_job.rb +4 -0
- data/app/mailers/block_editor/application_mailer.rb +6 -0
- data/app/models/block_editor/application_record.rb +5 -0
- data/app/models/block_editor/block_list.rb +7 -0
- data/app/models/concerns/block_editor/listable.rb +24 -0
- data/app/views/layouts/block_editor/application.html.erb +15 -0
- data/config/initializers/webpacker_extension.rb +12 -0
- data/config/routes.rb +2 -0
- data/config/webpack/development.js +5 -0
- data/config/webpack/environment.js +3 -0
- data/config/webpack/production.js +5 -0
- data/config/webpack/test.js +5 -0
- data/config/webpacker.yml +92 -0
- data/db/migrate/20210312032114_create_block_lists.rb +11 -0
- data/lib/block_editor.rb +26 -0
- data/lib/block_editor/block_list_renderer.rb +43 -0
- data/lib/block_editor/blocks/base.rb +32 -0
- data/lib/block_editor/engine.rb +34 -0
- data/lib/block_editor/instance.rb +19 -0
- data/lib/block_editor/version.rb +3 -0
- data/lib/tasks/block_editor_tasks.rake +59 -0
- 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 );
|