block_editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 );
|