@wordpress/fields 0.31.1-next.v.202602111440.0 → 0.32.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.
- package/CHANGELOG.md +2 -0
- package/README.md +1 -1
- package/build/actions/duplicate-post.cjs.map +1 -1
- package/build/actions/reorder-page.cjs.map +1 -1
- package/build/components/create-template-part-modal/index.cjs +2 -5
- package/build/components/create-template-part-modal/index.cjs.map +3 -3
- package/build/components/media-edit/index.cjs +372 -157
- package/build/components/media-edit/index.cjs.map +3 -3
- package/build/components/media-edit/use-moving-animation.cjs +75 -0
- package/build/components/media-edit/use-moving-animation.cjs.map +7 -0
- package/build/fields/featured-image/index.cjs +2 -1
- package/build/fields/featured-image/index.cjs.map +2 -2
- package/build/fields/template/hooks.cjs +96 -0
- package/build/fields/template/hooks.cjs.map +7 -0
- package/build/fields/template/index.cjs +2 -0
- package/build/fields/template/index.cjs.map +2 -2
- package/build/fields/template/template-edit.cjs +33 -114
- package/build/fields/template/template-edit.cjs.map +3 -3
- package/build/fields/template/template-view.cjs +68 -0
- package/build/fields/template/template-view.cjs.map +7 -0
- package/build/fields/template/utils.cjs +91 -0
- package/build/fields/template/utils.cjs.map +7 -0
- package/build-module/actions/duplicate-post.mjs.map +1 -1
- package/build-module/actions/reorder-page.mjs.map +1 -1
- package/build-module/components/create-template-part-modal/index.mjs +3 -6
- package/build-module/components/create-template-part-modal/index.mjs.map +2 -2
- package/build-module/components/media-edit/index.mjs +379 -159
- package/build-module/components/media-edit/index.mjs.map +2 -2
- package/build-module/components/media-edit/use-moving-animation.mjs +54 -0
- package/build-module/components/media-edit/use-moving-animation.mjs.map +7 -0
- package/build-module/fields/featured-image/index.mjs +2 -1
- package/build-module/fields/featured-image/index.mjs.map +2 -2
- package/build-module/fields/template/hooks.mjs +71 -0
- package/build-module/fields/template/hooks.mjs.map +7 -0
- package/build-module/fields/template/index.mjs +2 -0
- package/build-module/fields/template/index.mjs.map +2 -2
- package/build-module/fields/template/template-edit.mjs +36 -123
- package/build-module/fields/template/template-edit.mjs.map +2 -2
- package/build-module/fields/template/template-view.mjs +43 -0
- package/build-module/fields/template/template-view.mjs.map +7 -0
- package/build-module/fields/template/utils.mjs +65 -0
- package/build-module/fields/template/utils.mjs.map +7 -0
- package/build-style/style-rtl.css +69 -61
- package/build-style/style.css +69 -61
- package/build-types/actions/duplicate-post.d.ts +1 -1
- package/build-types/actions/duplicate-post.d.ts.map +1 -1
- package/build-types/actions/reorder-page.d.ts +1 -1
- package/build-types/actions/reorder-page.d.ts.map +1 -1
- package/build-types/components/create-template-part-modal/index.d.ts.map +1 -1
- package/build-types/components/media-edit/index.d.ts +1 -1
- package/build-types/components/media-edit/index.d.ts.map +1 -1
- package/build-types/components/media-edit/use-moving-animation.d.ts +13 -0
- package/build-types/components/media-edit/use-moving-animation.d.ts.map +1 -0
- package/build-types/fields/template/hooks.d.ts +10 -0
- package/build-types/fields/template/hooks.d.ts.map +1 -0
- package/build-types/fields/template/index.d.ts.map +1 -1
- package/build-types/fields/template/template-edit.d.ts.map +1 -1
- package/build-types/fields/template/template-view.d.ts +4 -0
- package/build-types/fields/template/template-view.d.ts.map +1 -0
- package/build-types/fields/template/utils.d.ts +28 -0
- package/build-types/fields/template/utils.d.ts.map +1 -0
- package/package.json +27 -26
- package/src/actions/duplicate-post.tsx +1 -1
- package/src/actions/reorder-page.tsx +1 -1
- package/src/components/create-template-part-modal/index.tsx +5 -14
- package/src/components/media-edit/index.tsx +420 -163
- package/src/components/media-edit/style.scss +83 -30
- package/src/components/media-edit/use-moving-animation.ts +77 -0
- package/src/fields/featured-image/index.tsx +1 -1
- package/src/fields/template/hooks.ts +121 -0
- package/src/fields/template/index.ts +2 -0
- package/src/fields/template/template-edit.tsx +38 -149
- package/src/fields/template/template-view.tsx +52 -0
- package/src/fields/template/utils.ts +119 -0
- package/src/style.scss +0 -1
- package/src/fields/template/style.scss +0 -34
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
__experimentalText as Text,
|
|
15
15
|
__experimentalTruncate as Truncate,
|
|
16
16
|
__experimentalVStack as VStack,
|
|
17
|
+
__experimentalHStack as HStack,
|
|
17
18
|
BaseControl,
|
|
18
19
|
Tooltip,
|
|
19
20
|
VisuallyHidden,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
useRef,
|
|
29
30
|
useState,
|
|
30
31
|
} from '@wordpress/element';
|
|
31
|
-
import { __ } from '@wordpress/i18n';
|
|
32
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
32
33
|
import {
|
|
33
34
|
archive,
|
|
34
35
|
audio,
|
|
@@ -36,6 +37,10 @@ import {
|
|
|
36
37
|
file,
|
|
37
38
|
closeSmall,
|
|
38
39
|
error as errorIcon,
|
|
40
|
+
chevronUp,
|
|
41
|
+
chevronDown,
|
|
42
|
+
chevronLeft,
|
|
43
|
+
chevronRight,
|
|
39
44
|
} from '@wordpress/icons';
|
|
40
45
|
import {
|
|
41
46
|
MediaUpload,
|
|
@@ -49,9 +54,27 @@ import { store as noticesStore } from '@wordpress/notices';
|
|
|
49
54
|
*/
|
|
50
55
|
import { unlock } from '../../lock-unlock';
|
|
51
56
|
import type { MediaEditProps } from '../../types';
|
|
57
|
+
import useMovingAnimation from './use-moving-animation';
|
|
52
58
|
|
|
53
59
|
const { MediaUploadModal } = unlock( mediaUtilsPrivateApis );
|
|
54
60
|
|
|
61
|
+
function AnimatedMediaItem( {
|
|
62
|
+
children,
|
|
63
|
+
index,
|
|
64
|
+
className,
|
|
65
|
+
}: {
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
index: number;
|
|
68
|
+
className?: string;
|
|
69
|
+
} ) {
|
|
70
|
+
const ref = useMovingAnimation( index );
|
|
71
|
+
return (
|
|
72
|
+
<div ref={ ref } className={ className }>
|
|
73
|
+
{ children }
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
type BlobItem = {
|
|
56
79
|
id: string;
|
|
57
80
|
source_url: string;
|
|
@@ -59,6 +82,13 @@ type BlobItem = {
|
|
|
59
82
|
alt_text?: string;
|
|
60
83
|
};
|
|
61
84
|
|
|
85
|
+
function normalizeValue( value: number | number[] | undefined ): number[] {
|
|
86
|
+
if ( Array.isArray( value ) ) {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
return value ? [ value ] : [];
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
/**
|
|
63
93
|
* Conditional Media component that uses MediaUploadModal when experiment is enabled,
|
|
64
94
|
* otherwise falls back to media-utils MediaUpload.
|
|
@@ -122,7 +152,9 @@ function MediaPickerButton( {
|
|
|
122
152
|
const isBlob = attachment && isBlobURL( attachment.source_url );
|
|
123
153
|
const mediaPickerButton = (
|
|
124
154
|
<div
|
|
125
|
-
className=
|
|
155
|
+
className={ clsx( 'fields__media-edit-picker-button', {
|
|
156
|
+
'has-attachment': attachment,
|
|
157
|
+
} ) }
|
|
126
158
|
role="button"
|
|
127
159
|
tabIndex={ 0 }
|
|
128
160
|
onClick={ () => {
|
|
@@ -160,7 +192,11 @@ function MediaPickerButton( {
|
|
|
160
192
|
if ( ! showTooltip ) {
|
|
161
193
|
return mediaPickerButton;
|
|
162
194
|
}
|
|
163
|
-
return
|
|
195
|
+
return (
|
|
196
|
+
<Tooltip text={ label } placement="top">
|
|
197
|
+
{ mediaPickerButton }
|
|
198
|
+
</Tooltip>
|
|
199
|
+
);
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
const archiveMimeTypes = [
|
|
@@ -195,6 +231,54 @@ function MediaEditPlaceholder( props: {
|
|
|
195
231
|
);
|
|
196
232
|
}
|
|
197
233
|
|
|
234
|
+
function MoveButtons( {
|
|
235
|
+
itemId,
|
|
236
|
+
index,
|
|
237
|
+
totalItems,
|
|
238
|
+
isUploading,
|
|
239
|
+
moveItem,
|
|
240
|
+
orientation = 'vertical',
|
|
241
|
+
}: {
|
|
242
|
+
itemId: number;
|
|
243
|
+
index: number;
|
|
244
|
+
totalItems: number;
|
|
245
|
+
isUploading: boolean;
|
|
246
|
+
moveItem: ( id: number, direction: 'up' | 'down' ) => void;
|
|
247
|
+
orientation?: 'vertical' | 'horizontal';
|
|
248
|
+
} ) {
|
|
249
|
+
const isHorizontal = orientation === 'horizontal';
|
|
250
|
+
return (
|
|
251
|
+
<>
|
|
252
|
+
<Button
|
|
253
|
+
__next40pxDefaultSize
|
|
254
|
+
icon={ isHorizontal ? chevronLeft : chevronUp }
|
|
255
|
+
label={ isHorizontal ? __( 'Move left' ) : __( 'Move up' ) }
|
|
256
|
+
size="small"
|
|
257
|
+
disabled={ isUploading || index === 0 }
|
|
258
|
+
accessibleWhenDisabled
|
|
259
|
+
tooltipPosition="top"
|
|
260
|
+
onClick={ ( event: React.MouseEvent< HTMLButtonElement > ) => {
|
|
261
|
+
event.stopPropagation();
|
|
262
|
+
moveItem( itemId, 'up' );
|
|
263
|
+
} }
|
|
264
|
+
/>
|
|
265
|
+
<Button
|
|
266
|
+
__next40pxDefaultSize
|
|
267
|
+
icon={ isHorizontal ? chevronRight : chevronDown }
|
|
268
|
+
label={ isHorizontal ? __( 'Move right' ) : __( 'Move down' ) }
|
|
269
|
+
size="small"
|
|
270
|
+
disabled={ isUploading || index === totalItems - 1 }
|
|
271
|
+
accessibleWhenDisabled
|
|
272
|
+
tooltipPosition="top"
|
|
273
|
+
onClick={ ( event: React.MouseEvent< HTMLButtonElement > ) => {
|
|
274
|
+
event.stopPropagation();
|
|
275
|
+
moveItem( itemId, 'down' );
|
|
276
|
+
} }
|
|
277
|
+
/>
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
198
282
|
function MediaPreview( { attachment }: { attachment: MediaEditAttachment } ) {
|
|
199
283
|
const url = attachment.source_url;
|
|
200
284
|
const mimeType = attachment.mime_type || '';
|
|
@@ -222,9 +306,11 @@ interface MediaEditAttachmentsProps {
|
|
|
222
306
|
addButtonLabel: string;
|
|
223
307
|
multiple?: boolean;
|
|
224
308
|
removeItem: ( itemId: number ) => void;
|
|
309
|
+
moveItem: ( itemId: number, direction: 'up' | 'down' ) => void;
|
|
225
310
|
open: () => void;
|
|
226
311
|
onFilesDrop: ( files: File[], attachmentId?: number ) => void;
|
|
227
312
|
isUploading: boolean;
|
|
313
|
+
setTargetItemId: ( id?: number ) => void;
|
|
228
314
|
}
|
|
229
315
|
|
|
230
316
|
function ExpandedMediaEditAttachments( {
|
|
@@ -232,9 +318,11 @@ function ExpandedMediaEditAttachments( {
|
|
|
232
318
|
addButtonLabel,
|
|
233
319
|
multiple,
|
|
234
320
|
removeItem,
|
|
321
|
+
moveItem,
|
|
235
322
|
open,
|
|
236
323
|
onFilesDrop,
|
|
237
324
|
isUploading,
|
|
325
|
+
setTargetItemId,
|
|
238
326
|
}: MediaEditAttachmentsProps ) {
|
|
239
327
|
return (
|
|
240
328
|
<div
|
|
@@ -244,20 +332,35 @@ function ExpandedMediaEditAttachments( {
|
|
|
244
332
|
'is-empty': ! allItems?.length,
|
|
245
333
|
} ) }
|
|
246
334
|
>
|
|
247
|
-
{ allItems?.map( ( attachment ) => {
|
|
335
|
+
{ allItems?.map( ( attachment, index ) => {
|
|
248
336
|
const hasPreviewImage =
|
|
249
337
|
attachment.mime_type?.startsWith( 'image' );
|
|
250
338
|
const isBlob = isBlobURL( attachment.source_url );
|
|
339
|
+
const attachmentNumericId = attachment.id as number;
|
|
251
340
|
return (
|
|
252
|
-
<
|
|
341
|
+
<AnimatedMediaItem
|
|
253
342
|
key={ attachment.id }
|
|
343
|
+
index={ index }
|
|
254
344
|
className={ clsx( 'fields__media-edit-expanded-item', {
|
|
255
345
|
'has-preview-image': hasPreviewImage,
|
|
256
346
|
} ) }
|
|
257
347
|
>
|
|
258
348
|
<MediaPickerButton
|
|
259
|
-
open={
|
|
260
|
-
|
|
349
|
+
open={ () => {
|
|
350
|
+
setTargetItemId( attachmentNumericId );
|
|
351
|
+
open();
|
|
352
|
+
} }
|
|
353
|
+
label={
|
|
354
|
+
! isBlob
|
|
355
|
+
? sprintf(
|
|
356
|
+
/* translators: %s: The title of the media item. */
|
|
357
|
+
__( 'Replace %s' ),
|
|
358
|
+
(
|
|
359
|
+
attachment as Attachment< 'view' >
|
|
360
|
+
).title.rendered
|
|
361
|
+
)
|
|
362
|
+
: __( 'Replace' )
|
|
363
|
+
}
|
|
261
364
|
showTooltip
|
|
262
365
|
onFilesDrop={ onFilesDrop }
|
|
263
366
|
attachment={ attachment }
|
|
@@ -275,52 +378,54 @@ function ExpandedMediaEditAttachments( {
|
|
|
275
378
|
attachment={ attachment }
|
|
276
379
|
/>
|
|
277
380
|
) }
|
|
278
|
-
{ ! isBlob &&
|
|
279
|
-
( ! hasPreviewImage ? (
|
|
280
|
-
<MediaTitle
|
|
281
|
-
attachment={
|
|
282
|
-
attachment as Attachment< 'view' >
|
|
283
|
-
}
|
|
284
|
-
/>
|
|
285
|
-
) : (
|
|
286
|
-
<div className="fields__media-edit-expanded-overlay">
|
|
287
|
-
<div className="fields__media-edit-expanded-title">
|
|
288
|
-
<MediaTitle
|
|
289
|
-
attachment={
|
|
290
|
-
attachment as Attachment< 'view' >
|
|
291
|
-
}
|
|
292
|
-
/>
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
) ) }
|
|
296
381
|
</VStack>
|
|
297
382
|
</div>
|
|
298
383
|
</MediaPickerButton>
|
|
299
384
|
{ ! isBlob && (
|
|
300
385
|
<div className="fields__media-edit-expanded-overlay">
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
386
|
+
<HStack
|
|
387
|
+
className="fields__media-edit-expanded-actions"
|
|
388
|
+
spacing={ 0 }
|
|
389
|
+
alignment="flex-end"
|
|
390
|
+
expanded={ false }
|
|
391
|
+
>
|
|
392
|
+
{ multiple && allItems.length > 1 && (
|
|
393
|
+
<MoveButtons
|
|
394
|
+
itemId={ attachmentNumericId }
|
|
395
|
+
index={ index }
|
|
396
|
+
totalItems={ allItems.length }
|
|
397
|
+
isUploading={ isUploading }
|
|
398
|
+
moveItem={ moveItem }
|
|
399
|
+
orientation="horizontal"
|
|
400
|
+
/>
|
|
401
|
+
) }
|
|
402
|
+
<Button
|
|
403
|
+
__next40pxDefaultSize
|
|
404
|
+
icon={ closeSmall }
|
|
405
|
+
label={ __( 'Remove' ) }
|
|
406
|
+
size="small"
|
|
407
|
+
disabled={ isUploading }
|
|
408
|
+
accessibleWhenDisabled
|
|
409
|
+
tooltipPosition="top"
|
|
410
|
+
onClick={ (
|
|
411
|
+
event: React.MouseEvent< HTMLButtonElement >
|
|
412
|
+
) => {
|
|
413
|
+
event.stopPropagation();
|
|
414
|
+
removeItem( attachmentNumericId );
|
|
415
|
+
} }
|
|
416
|
+
/>
|
|
417
|
+
</HStack>
|
|
316
418
|
</div>
|
|
317
419
|
) }
|
|
318
|
-
</
|
|
420
|
+
</AnimatedMediaItem>
|
|
319
421
|
);
|
|
320
422
|
} ) }
|
|
321
423
|
{ ( multiple || ! allItems?.length ) && (
|
|
322
424
|
<MediaEditPlaceholder
|
|
323
|
-
open={
|
|
425
|
+
open={ () => {
|
|
426
|
+
setTargetItemId( undefined );
|
|
427
|
+
open();
|
|
428
|
+
} }
|
|
324
429
|
label={ addButtonLabel }
|
|
325
430
|
onFilesDrop={ onFilesDrop }
|
|
326
431
|
isUploading={ isUploading }
|
|
@@ -335,68 +440,110 @@ function CompactMediaEditAttachments( {
|
|
|
335
440
|
addButtonLabel,
|
|
336
441
|
multiple,
|
|
337
442
|
removeItem,
|
|
443
|
+
moveItem,
|
|
338
444
|
open,
|
|
339
445
|
onFilesDrop,
|
|
340
446
|
isUploading,
|
|
447
|
+
setTargetItemId,
|
|
341
448
|
}: MediaEditAttachmentsProps ) {
|
|
342
449
|
return (
|
|
343
450
|
<>
|
|
344
451
|
{ !! allItems?.length && (
|
|
345
|
-
<
|
|
346
|
-
{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
452
|
+
<div
|
|
453
|
+
className={ clsx( 'fields__media-edit-compact-group', {
|
|
454
|
+
'is-single': allItems.length === 1,
|
|
455
|
+
} ) }
|
|
456
|
+
>
|
|
457
|
+
<VStack spacing={ 0 }>
|
|
458
|
+
{ allItems.map( ( attachment, index ) => {
|
|
459
|
+
const isBlob = isBlobURL( attachment.source_url );
|
|
460
|
+
const showMoveButtons =
|
|
461
|
+
multiple && allItems.length > 1;
|
|
462
|
+
const attachmentNumericId = attachment.id as number;
|
|
463
|
+
return (
|
|
464
|
+
<AnimatedMediaItem
|
|
465
|
+
key={ attachment.id }
|
|
466
|
+
index={ index }
|
|
467
|
+
className="fields__media-edit-compact"
|
|
360
468
|
>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
469
|
+
<MediaPickerButton
|
|
470
|
+
open={ () => {
|
|
471
|
+
setTargetItemId(
|
|
472
|
+
attachmentNumericId
|
|
473
|
+
);
|
|
474
|
+
open();
|
|
475
|
+
} }
|
|
476
|
+
label={ __( 'Replace' ) }
|
|
477
|
+
showTooltip
|
|
478
|
+
onFilesDrop={ onFilesDrop }
|
|
479
|
+
attachment={ attachment }
|
|
480
|
+
isUploading={ isUploading }
|
|
481
|
+
>
|
|
482
|
+
<>
|
|
483
|
+
<MediaPreview
|
|
484
|
+
attachment={ attachment }
|
|
370
485
|
/>
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
486
|
+
{ ! isBlob && (
|
|
487
|
+
<MediaTitle
|
|
488
|
+
attachment={
|
|
489
|
+
attachment as Attachment< 'view' >
|
|
490
|
+
}
|
|
491
|
+
/>
|
|
492
|
+
) }
|
|
493
|
+
</>
|
|
494
|
+
</MediaPickerButton>
|
|
495
|
+
{ ! isBlob && (
|
|
496
|
+
<HStack
|
|
497
|
+
className="fields__media-edit-compact-movers"
|
|
498
|
+
spacing={ 0 }
|
|
499
|
+
alignment="flex-end"
|
|
500
|
+
expanded={ false }
|
|
501
|
+
>
|
|
502
|
+
{ showMoveButtons && (
|
|
503
|
+
<MoveButtons
|
|
504
|
+
itemId={
|
|
505
|
+
attachmentNumericId
|
|
506
|
+
}
|
|
507
|
+
index={ index }
|
|
508
|
+
totalItems={
|
|
509
|
+
allItems.length
|
|
510
|
+
}
|
|
511
|
+
isUploading={ isUploading }
|
|
512
|
+
moveItem={ moveItem }
|
|
513
|
+
orientation="vertical"
|
|
514
|
+
/>
|
|
515
|
+
) }
|
|
516
|
+
<Button
|
|
517
|
+
__next40pxDefaultSize
|
|
518
|
+
icon={ closeSmall }
|
|
519
|
+
label={ __( 'Remove' ) }
|
|
520
|
+
size="small"
|
|
521
|
+
disabled={ isUploading }
|
|
522
|
+
accessibleWhenDisabled
|
|
523
|
+
tooltipPosition="top"
|
|
524
|
+
onClick={ (
|
|
525
|
+
event: React.MouseEvent< HTMLButtonElement >
|
|
526
|
+
) => {
|
|
527
|
+
event.stopPropagation();
|
|
528
|
+
removeItem(
|
|
529
|
+
attachmentNumericId
|
|
530
|
+
);
|
|
531
|
+
} }
|
|
532
|
+
/>
|
|
533
|
+
</HStack>
|
|
534
|
+
) }
|
|
535
|
+
</AnimatedMediaItem>
|
|
536
|
+
);
|
|
537
|
+
} ) }
|
|
538
|
+
</VStack>
|
|
539
|
+
</div>
|
|
396
540
|
) }
|
|
397
541
|
{ ( multiple || ! allItems?.length ) && (
|
|
398
542
|
<MediaEditPlaceholder
|
|
399
|
-
open={
|
|
543
|
+
open={ () => {
|
|
544
|
+
setTargetItemId( undefined );
|
|
545
|
+
open();
|
|
546
|
+
} }
|
|
400
547
|
label={ addButtonLabel }
|
|
401
548
|
onFilesDrop={ onFilesDrop }
|
|
402
549
|
isUploading={ isUploading }
|
|
@@ -425,7 +572,7 @@ function CompactMediaEditAttachments( {
|
|
|
425
572
|
* @param {boolean} [props.hideLabelFromVision] - Whether the label should be hidden from vision.
|
|
426
573
|
* @param {boolean} [props.isExpanded] - Whether to render in an expanded form. Default `false`.
|
|
427
574
|
*
|
|
428
|
-
* @return {JSX.Element} The media edit control component.
|
|
575
|
+
* @return {React.JSX.Element} The media edit control component.
|
|
429
576
|
*
|
|
430
577
|
* @example
|
|
431
578
|
* ```tsx
|
|
@@ -477,18 +624,68 @@ export default function MediaEdit< Item >( {
|
|
|
477
624
|
if ( ! value ) {
|
|
478
625
|
return null;
|
|
479
626
|
}
|
|
480
|
-
const normalizedValue =
|
|
627
|
+
const normalizedValue = normalizeValue( value );
|
|
628
|
+
// Sorted IDs ensure stable cache key, avoiding
|
|
629
|
+
// unnecessary new requests on reorder.
|
|
630
|
+
const sortedIds = [ ...normalizedValue ].sort( ( a, b ) => a - b );
|
|
481
631
|
const { getEntityRecords } = select( coreStore );
|
|
482
632
|
return getEntityRecords( 'postType', 'attachment', {
|
|
483
|
-
include:
|
|
633
|
+
include: sortedIds,
|
|
484
634
|
} ) as Attachment< 'view' >[] | null;
|
|
485
635
|
},
|
|
486
636
|
[ value ]
|
|
487
637
|
);
|
|
638
|
+
// Keep previous attachments during null transitions. When value changes,
|
|
639
|
+
// useSelect briefly returns null while the new query resolves. For pure
|
|
640
|
+
// reorders (same IDs), we fall back to the cached list to avoid a visual
|
|
641
|
+
// flash in compact mode. For replacements/uploads (new IDs not in cache),
|
|
642
|
+
// we let attachments be null as normal.
|
|
643
|
+
const stableAttachmentsRef = useRef< Attachment< 'view' >[] | null >(
|
|
644
|
+
null
|
|
645
|
+
);
|
|
646
|
+
if ( attachments !== null ) {
|
|
647
|
+
stableAttachmentsRef.current = attachments;
|
|
648
|
+
}
|
|
649
|
+
let stableAttachments = attachments;
|
|
650
|
+
if ( attachments === null && stableAttachmentsRef.current && value ) {
|
|
651
|
+
const stableIds = new Set(
|
|
652
|
+
stableAttachmentsRef.current.map( ( a ) => a.id )
|
|
653
|
+
);
|
|
654
|
+
if ( normalizeValue( value ).every( ( id ) => stableIds.has( id ) ) ) {
|
|
655
|
+
stableAttachments = stableAttachmentsRef.current;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Reorder attachments to match value order.
|
|
659
|
+
const orderedAttachments = useMemo( () => {
|
|
660
|
+
if ( ! stableAttachments ) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
const normalizedValue = normalizeValue( value );
|
|
664
|
+
const attachmentMap = new Map(
|
|
665
|
+
stableAttachments.map( ( a ) => [ a.id, a ] )
|
|
666
|
+
);
|
|
667
|
+
return normalizedValue
|
|
668
|
+
.map( ( id ) => attachmentMap.get( id ) )
|
|
669
|
+
.filter( ( a ): a is Attachment< 'view' > => a !== undefined );
|
|
670
|
+
}, [ stableAttachments, value ] );
|
|
488
671
|
const { createErrorNotice } = useDispatch( noticesStore );
|
|
672
|
+
const { receiveEntityRecords } = useDispatch( coreStore );
|
|
489
673
|
// Support one upload action at a time for now.
|
|
490
|
-
const [
|
|
674
|
+
const [ targetItemId, setTargetItemId ] = useState< number >();
|
|
675
|
+
// Deferred open: the legacy class-based MediaUpload reads props
|
|
676
|
+
// imperatively when `open()` is called, so calling it in the same
|
|
677
|
+
// handler as `setTargetItemId()` would open the modal with stale
|
|
678
|
+
// `value`/`multiple` props. Setting a pending flag defers the open
|
|
679
|
+
// until after the next render when props are up to date.
|
|
680
|
+
const openModalRef = useRef< () => void >( undefined );
|
|
681
|
+
const [ pendingOpen, setPendingOpen ] = useState( false );
|
|
491
682
|
const [ blobs, setBlobs ] = useState< string[] >( [] );
|
|
683
|
+
useEffect( () => {
|
|
684
|
+
if ( pendingOpen ) {
|
|
685
|
+
setPendingOpen( false );
|
|
686
|
+
openModalRef.current?.();
|
|
687
|
+
}
|
|
688
|
+
}, [ pendingOpen ] );
|
|
492
689
|
const onChangeControl = useCallback(
|
|
493
690
|
( newValue: number | number[] | undefined ) =>
|
|
494
691
|
onChange( field.setValue( { item: data, value: newValue } ) ),
|
|
@@ -496,7 +693,7 @@ export default function MediaEdit< Item >( {
|
|
|
496
693
|
);
|
|
497
694
|
const removeItem = useCallback(
|
|
498
695
|
( itemId: number ) => {
|
|
499
|
-
const currentIds =
|
|
696
|
+
const currentIds = normalizeValue( value );
|
|
500
697
|
const newIds = currentIds.filter( ( id ) => id !== itemId );
|
|
501
698
|
// Mark as touched to immediately show any validation error.
|
|
502
699
|
setIsTouched( true );
|
|
@@ -504,65 +701,88 @@ export default function MediaEdit< Item >( {
|
|
|
504
701
|
},
|
|
505
702
|
[ value, onChangeControl ]
|
|
506
703
|
);
|
|
704
|
+
const moveItem = useCallback(
|
|
705
|
+
( itemId: number, direction: 'up' | 'down' ) => {
|
|
706
|
+
if ( ! orderedAttachments ) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const currentIds = orderedAttachments.map( ( a ) => a.id );
|
|
710
|
+
const index = currentIds.indexOf( itemId );
|
|
711
|
+
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
|
712
|
+
[ currentIds[ index ], currentIds[ newIndex ] ] = [
|
|
713
|
+
currentIds[ newIndex ],
|
|
714
|
+
currentIds[ index ],
|
|
715
|
+
];
|
|
716
|
+
onChangeControl( currentIds );
|
|
717
|
+
},
|
|
718
|
+
[ orderedAttachments, onChangeControl ]
|
|
719
|
+
);
|
|
507
720
|
const onFilesDrop = useCallback(
|
|
508
|
-
( files: File[],
|
|
721
|
+
( files: File[], _targetItemId?: number ) => {
|
|
722
|
+
setTargetItemId( _targetItemId );
|
|
509
723
|
uploadMedia( {
|
|
510
724
|
allowedTypes: allowedTypes?.length ? allowedTypes : undefined,
|
|
511
725
|
filesList: files,
|
|
512
726
|
onFileChange( uploadedMedia: any[] ) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
accumulator.uploadedItems.push( item.id );
|
|
520
|
-
}
|
|
521
|
-
return accumulator;
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
blobItems: [] as string[],
|
|
525
|
-
uploadedItems: [] as number[],
|
|
526
|
-
}
|
|
527
|
-
);
|
|
528
|
-
setBlobs( blobItems );
|
|
529
|
-
// If all uploads are complete reset the replacementId.
|
|
530
|
-
if ( uploadedItems.length === uploadedMedia.length ) {
|
|
531
|
-
setReplacementId( undefined );
|
|
532
|
-
}
|
|
533
|
-
if ( ! uploadedItems.length ) {
|
|
727
|
+
const blobUrls = uploadedMedia
|
|
728
|
+
.filter( ( item ) => isBlobURL( item.url ) )
|
|
729
|
+
.map( ( item ) => item.url );
|
|
730
|
+
setBlobs( blobUrls );
|
|
731
|
+
// Wait for all uploads to complete before updating value.
|
|
732
|
+
if ( !! blobUrls.length ) {
|
|
534
733
|
return;
|
|
535
734
|
}
|
|
735
|
+
// `uploadMedia` creates attachments via `apiFetch`
|
|
736
|
+
// outside the core-data store, so invalidate
|
|
737
|
+
// all attachment queries to keep them fresh for
|
|
738
|
+
// other components that rely on core-data.
|
|
739
|
+
receiveEntityRecords(
|
|
740
|
+
'postType',
|
|
741
|
+
'attachment',
|
|
742
|
+
[],
|
|
743
|
+
undefined,
|
|
744
|
+
true
|
|
745
|
+
);
|
|
746
|
+
const uploadedIds = uploadedMedia.map(
|
|
747
|
+
( item ) => item.id
|
|
748
|
+
);
|
|
536
749
|
if ( ! multiple ) {
|
|
537
|
-
onChangeControl(
|
|
750
|
+
onChangeControl( uploadedIds[ 0 ] );
|
|
751
|
+
setTargetItemId( undefined );
|
|
538
752
|
return;
|
|
539
753
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
754
|
+
const currentValue = normalizeValue( value );
|
|
755
|
+
// Dropped on placeholder: append new items.
|
|
756
|
+
if ( _targetItemId === undefined ) {
|
|
757
|
+
onChangeControl( [ ...currentValue, ...uploadedIds ] );
|
|
758
|
+
} else {
|
|
759
|
+
// Dropped on existing item: insert at that position.
|
|
760
|
+
const newValue = [ ...currentValue ];
|
|
761
|
+
newValue.splice(
|
|
762
|
+
currentValue.indexOf( _targetItemId ),
|
|
763
|
+
1,
|
|
764
|
+
...uploadedIds
|
|
765
|
+
);
|
|
766
|
+
onChangeControl( newValue );
|
|
543
767
|
}
|
|
544
|
-
|
|
545
|
-
? value
|
|
546
|
-
: [ value ];
|
|
547
|
-
const newIds = [
|
|
548
|
-
...( _replacementId
|
|
549
|
-
? normalizedValue.filter(
|
|
550
|
-
( id: any ) => id !== _replacementId
|
|
551
|
-
)
|
|
552
|
-
: normalizedValue ),
|
|
553
|
-
...uploadedItems,
|
|
554
|
-
];
|
|
555
|
-
onChangeControl( newIds );
|
|
768
|
+
setTargetItemId( undefined );
|
|
556
769
|
},
|
|
557
770
|
onError( error: Error ) {
|
|
558
|
-
|
|
771
|
+
setTargetItemId( undefined );
|
|
559
772
|
setBlobs( [] );
|
|
560
773
|
createErrorNotice( error.message, { type: 'snackbar' } );
|
|
561
774
|
},
|
|
562
775
|
multiple: !! multiple,
|
|
563
776
|
} );
|
|
564
777
|
},
|
|
565
|
-
[
|
|
778
|
+
[
|
|
779
|
+
allowedTypes,
|
|
780
|
+
value,
|
|
781
|
+
multiple,
|
|
782
|
+
createErrorNotice,
|
|
783
|
+
onChangeControl,
|
|
784
|
+
receiveEntityRecords,
|
|
785
|
+
]
|
|
566
786
|
);
|
|
567
787
|
const addButtonLabel =
|
|
568
788
|
field.placeholder ||
|
|
@@ -570,31 +790,27 @@ export default function MediaEdit< Item >( {
|
|
|
570
790
|
// Merge real attachments with any existing blob items that are being uploaded.
|
|
571
791
|
const allItems: Array< MediaEditAttachment > | null = useMemo( () => {
|
|
572
792
|
if ( ! blobs.length ) {
|
|
573
|
-
return
|
|
793
|
+
return orderedAttachments;
|
|
574
794
|
}
|
|
575
795
|
const items: Array< MediaEditAttachment > = [
|
|
576
|
-
...(
|
|
796
|
+
...( orderedAttachments || [] ),
|
|
577
797
|
];
|
|
578
798
|
const blobItems = blobs.map( ( url ) => ( {
|
|
579
799
|
id: url,
|
|
580
800
|
source_url: url,
|
|
581
801
|
mime_type: getBlobTypeByURL( url ),
|
|
582
802
|
} ) );
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
...blobItems,
|
|
592
|
-
...items.slice( replacementIndex + 1 ),
|
|
593
|
-
];
|
|
803
|
+
if ( targetItemId !== undefined ) {
|
|
804
|
+
// When files are dropped in existing media item, place the blobs at that item.
|
|
805
|
+
const targetIndex = items.findIndex(
|
|
806
|
+
( a ) => a.id === targetItemId
|
|
807
|
+
);
|
|
808
|
+
items.splice( targetIndex, 1, ...blobItems );
|
|
809
|
+
} else {
|
|
810
|
+
items.push( ...blobItems );
|
|
594
811
|
}
|
|
595
|
-
items.push( ...blobItems );
|
|
596
812
|
return items;
|
|
597
|
-
}, [
|
|
813
|
+
}, [ orderedAttachments, targetItemId, blobs ] );
|
|
598
814
|
useEffect( () => {
|
|
599
815
|
if ( ! isTouched ) {
|
|
600
816
|
return;
|
|
@@ -614,7 +830,7 @@ export default function MediaEdit< Item >( {
|
|
|
614
830
|
customValidityResult.message || __( 'Invalid' )
|
|
615
831
|
);
|
|
616
832
|
} else {
|
|
617
|
-
input.setCustomValidity( '' ); // Clear validity
|
|
833
|
+
input.setCustomValidity( '' ); // Clear validity.
|
|
618
834
|
}
|
|
619
835
|
} else {
|
|
620
836
|
// Clear any previous validation.
|
|
@@ -641,20 +857,56 @@ export default function MediaEdit< Item >( {
|
|
|
641
857
|
<fieldset className="fields__media-edit" data-field-id={ field.id }>
|
|
642
858
|
<ConditionalMediaUpload
|
|
643
859
|
onSelect={ ( selectedMedia: any ) => {
|
|
644
|
-
if ( multiple ) {
|
|
645
|
-
const newIds = Array.isArray( selectedMedia )
|
|
646
|
-
? selectedMedia.map( ( m: any ) => m.id )
|
|
647
|
-
: [ selectedMedia.id ];
|
|
648
|
-
onChangeControl( newIds );
|
|
649
|
-
} else {
|
|
860
|
+
if ( ! multiple ) {
|
|
650
861
|
onChangeControl( selectedMedia.id );
|
|
862
|
+
setTargetItemId( undefined );
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const newIds = Array.isArray( selectedMedia )
|
|
866
|
+
? selectedMedia.map( ( m: any ) => m.id )
|
|
867
|
+
: [ selectedMedia.id ];
|
|
868
|
+
const currentValue = normalizeValue( value );
|
|
869
|
+
if ( ! currentValue.length ) {
|
|
870
|
+
onChangeControl( newIds );
|
|
871
|
+
} else if ( targetItemId === undefined ) {
|
|
872
|
+
// Placeholder clicked: keep existing items that are
|
|
873
|
+
// still selected, then append newly selected items.
|
|
874
|
+
const existingItems = currentValue.filter( ( id ) =>
|
|
875
|
+
newIds.includes( id )
|
|
876
|
+
);
|
|
877
|
+
const newItems = newIds.filter(
|
|
878
|
+
( id ) => ! currentValue.includes( id )
|
|
879
|
+
);
|
|
880
|
+
onChangeControl( [
|
|
881
|
+
...existingItems,
|
|
882
|
+
...newItems,
|
|
883
|
+
] );
|
|
884
|
+
} else if ( selectedMedia.id !== targetItemId ) {
|
|
885
|
+
// Remove selected item from its old position, if it
|
|
886
|
+
// already exists in the value.
|
|
887
|
+
const filtered = currentValue.filter(
|
|
888
|
+
( id ) => id !== selectedMedia.id
|
|
889
|
+
);
|
|
890
|
+
// Replace the clicked item with the selected one.
|
|
891
|
+
onChangeControl(
|
|
892
|
+
filtered.map( ( id ) =>
|
|
893
|
+
id === targetItemId ? selectedMedia.id : id
|
|
894
|
+
)
|
|
895
|
+
);
|
|
651
896
|
}
|
|
897
|
+
setTargetItemId( undefined );
|
|
652
898
|
} }
|
|
899
|
+
onClose={ () => setTargetItemId( undefined ) }
|
|
653
900
|
allowedTypes={ allowedTypes }
|
|
654
|
-
|
|
655
|
-
|
|
901
|
+
// When replacing an existing item, pass only that item's ID
|
|
902
|
+
// and open in single-select mode so the user picks exactly
|
|
903
|
+
// one replacement, even if `multiple` is true.
|
|
904
|
+
value={ targetItemId !== undefined ? targetItemId : value }
|
|
905
|
+
multiple={ multiple && targetItemId === undefined }
|
|
656
906
|
title={ field.label }
|
|
657
907
|
render={ ( { open }: any ) => {
|
|
908
|
+
// Keep a ref to the latest `open` so the deferred effect can call it.
|
|
909
|
+
openModalRef.current = open;
|
|
658
910
|
const AttachmentsComponent = isExpanded
|
|
659
911
|
? ExpandedMediaEditAttachments
|
|
660
912
|
: CompactMediaEditAttachments;
|
|
@@ -666,7 +918,10 @@ export default function MediaEdit< Item >( {
|
|
|
666
918
|
{ field.label }
|
|
667
919
|
</VisuallyHidden>
|
|
668
920
|
) : (
|
|
669
|
-
<BaseControl.VisualLabel
|
|
921
|
+
<BaseControl.VisualLabel
|
|
922
|
+
as="legend"
|
|
923
|
+
style={ { marginBottom: 0 } }
|
|
924
|
+
>
|
|
670
925
|
{ field.label }
|
|
671
926
|
</BaseControl.VisualLabel>
|
|
672
927
|
) ) }
|
|
@@ -675,9 +930,11 @@ export default function MediaEdit< Item >( {
|
|
|
675
930
|
addButtonLabel={ addButtonLabel }
|
|
676
931
|
multiple={ multiple }
|
|
677
932
|
removeItem={ removeItem }
|
|
678
|
-
|
|
933
|
+
moveItem={ moveItem }
|
|
934
|
+
open={ () => setPendingOpen( true ) }
|
|
679
935
|
onFilesDrop={ onFilesDrop }
|
|
680
936
|
isUploading={ !! blobs.length }
|
|
937
|
+
setTargetItemId={ setTargetItemId }
|
|
681
938
|
/>
|
|
682
939
|
{ field.description && (
|
|
683
940
|
<Text variant="muted">
|