@wordpress/block-library 9.40.2-next.v.202602271551.0 → 9.41.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/button/block.json +11 -3
  3. package/build/button/deprecated.cjs +246 -13
  4. package/build/button/deprecated.cjs.map +2 -2
  5. package/build/button/edit.cjs +45 -58
  6. package/build/button/edit.cjs.map +3 -3
  7. package/build/button/save.cjs +3 -7
  8. package/build/button/save.cjs.map +2 -2
  9. package/build/button/utils.cjs +59 -0
  10. package/build/button/utils.cjs.map +7 -0
  11. package/build/image/image.cjs +1 -1
  12. package/build/image/image.cjs.map +2 -2
  13. package/build/navigation-link/shared/use-link-preview.cjs +2 -2
  14. package/build/navigation-link/shared/use-link-preview.cjs.map +2 -2
  15. package/build/playlist/edit.cjs +43 -136
  16. package/build/playlist/edit.cjs.map +3 -3
  17. package/build/playlist/view.cjs +56 -38
  18. package/build/playlist/view.cjs.map +2 -2
  19. package/build/playlist-track/edit.cjs +0 -1
  20. package/build/playlist-track/edit.cjs.map +2 -2
  21. package/build/post-title/block.json +3 -0
  22. package/build/post-title/edit.cjs +2 -2
  23. package/build/post-title/edit.cjs.map +2 -2
  24. package/build/utils/waveform-player.cjs +68 -0
  25. package/build/utils/waveform-player.cjs.map +7 -0
  26. package/build/utils/waveform-utils.cjs +171 -0
  27. package/build/utils/waveform-utils.cjs.map +7 -0
  28. package/build-module/button/block.json +11 -3
  29. package/build-module/button/deprecated.mjs +246 -13
  30. package/build-module/button/deprecated.mjs.map +2 -2
  31. package/build-module/button/edit.mjs +47 -63
  32. package/build-module/button/edit.mjs.map +2 -2
  33. package/build-module/button/save.mjs +3 -7
  34. package/build-module/button/save.mjs.map +2 -2
  35. package/build-module/button/utils.mjs +33 -0
  36. package/build-module/button/utils.mjs.map +7 -0
  37. package/build-module/image/image.mjs +1 -1
  38. package/build-module/image/image.mjs.map +2 -2
  39. package/build-module/navigation-link/shared/use-link-preview.mjs +2 -2
  40. package/build-module/navigation-link/shared/use-link-preview.mjs.map +2 -2
  41. package/build-module/playlist/edit.mjs +41 -139
  42. package/build-module/playlist/edit.mjs.map +2 -2
  43. package/build-module/playlist/view.mjs +56 -38
  44. package/build-module/playlist/view.mjs.map +2 -2
  45. package/build-module/playlist-track/edit.mjs +0 -1
  46. package/build-module/playlist-track/edit.mjs.map +2 -2
  47. package/build-module/post-title/block.json +3 -0
  48. package/build-module/post-title/edit.mjs +2 -2
  49. package/build-module/post-title/edit.mjs.map +2 -2
  50. package/build-module/utils/waveform-player.mjs +43 -0
  51. package/build-module/utils/waveform-player.mjs.map +7 -0
  52. package/build-module/utils/waveform-utils.mjs +131 -0
  53. package/build-module/utils/waveform-utils.mjs.map +7 -0
  54. package/build-style/button/style-rtl.css +6 -0
  55. package/build-style/button/style.css +6 -0
  56. package/build-style/editor-rtl.css +3 -3
  57. package/build-style/editor.css +3 -3
  58. package/build-style/playlist/editor-rtl.css +3 -3
  59. package/build-style/playlist/editor.css +3 -3
  60. package/build-style/playlist/style-rtl.css +351 -17
  61. package/build-style/playlist/style.css +351 -17
  62. package/build-style/style-rtl.css +357 -17
  63. package/build-style/style.css +357 -17
  64. package/package.json +39 -38
  65. package/src/button/block.json +11 -3
  66. package/src/button/deprecated.js +254 -16
  67. package/src/button/edit.js +50 -61
  68. package/src/button/index.php +68 -0
  69. package/src/button/save.js +2 -8
  70. package/src/button/style.scss +49 -7
  71. package/src/button/test/utils.js +84 -0
  72. package/src/button/utils.js +42 -0
  73. package/src/image/image.js +14 -15
  74. package/src/image/index.php +3 -1
  75. package/src/navigation-link/shared/test/use-link-preview.test.js +9 -0
  76. package/src/navigation-link/shared/use-link-preview.js +6 -9
  77. package/src/playlist/edit.js +60 -154
  78. package/src/playlist/editor.scss +3 -3
  79. package/src/playlist/index.php +15 -40
  80. package/src/playlist/style.scss +34 -27
  81. package/src/playlist/test/edit.js +137 -0
  82. package/src/playlist/view.js +97 -40
  83. package/src/playlist-track/edit.js +0 -1
  84. package/src/post-title/block.json +3 -0
  85. package/src/post-title/edit.js +4 -2
  86. package/src/search/index.php +1 -1
  87. package/src/utils/test/waveform-utils.js +328 -0
  88. package/src/utils/waveform-player.js +77 -0
  89. package/src/utils/waveform-utils.js +232 -0
@@ -55,16 +55,41 @@ $blocks-block__margin: 0.5em;
55
55
  }
56
56
  }
57
57
 
58
+ &[class*="wp-block-button__width"] {
59
+ width:
60
+ calc(var(--wp--block-button--width) * 1% -
61
+ (
62
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
63
+ ( 1 - var(--wp--block-button--width) / 100 )
64
+ ));
65
+ }
66
+
67
+ // Legacy width classes for backwards compatibility.
58
68
  &.wp-block-button__width-25 {
59
- width: calc(25% - (var(--wp--style--block-gap, #{$blocks-block__margin}) * 0.75));
69
+ width:
70
+ calc(25% -
71
+ (
72
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
73
+ 0.75
74
+ ));
60
75
  }
61
76
 
62
77
  &.wp-block-button__width-50 {
63
- width: calc(50% - (var(--wp--style--block-gap, #{$blocks-block__margin}) * 0.5));
78
+ width:
79
+ calc(50% -
80
+ (
81
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
82
+ 0.5
83
+ ));
64
84
  }
65
85
 
66
86
  &.wp-block-button__width-75 {
67
- width: calc(75% - (var(--wp--style--block-gap, #{$blocks-block__margin}) * 0.25));
87
+ width:
88
+ calc(75% -
89
+ (
90
+ var(--wp--style--block-gap, #{$blocks-block__margin}) *
91
+ 0.25
92
+ ));
68
93
  }
69
94
 
70
95
  &.wp-block-button__width-100 {
@@ -75,6 +100,11 @@ $blocks-block__margin: 0.5em;
75
100
 
76
101
  // For vertical buttons, gap is not factored into width calculations.
77
102
  .wp-block-buttons.is-vertical > .wp-block-button {
103
+ &[class*="wp-block-button__width"] {
104
+ width: calc(var(--wp--block-button--width) * 1%);
105
+ }
106
+
107
+ // Legacy width classes for backwards compatibility.
78
108
  &.wp-block-button__width-25 {
79
109
  width: 25%;
80
110
  }
@@ -110,13 +140,25 @@ $blocks-block__margin: 0.5em;
110
140
  padding: 0.667em 1.333em;
111
141
  }
112
142
 
113
- :where(.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-text-color)),
114
- :where(.wp-block-button .wp-block-button__link.is-style-outline:not(.has-text-color)) {
143
+ :where(
144
+ .wp-block-button.is-style-outline
145
+ > .wp-block-button__link:not(.has-text-color)
146
+ ),
147
+ :where(
148
+ .wp-block-button
149
+ .wp-block-button__link.is-style-outline:not(.has-text-color)
150
+ ) {
115
151
  color: currentColor;
116
152
  }
117
153
 
118
- :where(.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-background)),
119
- :where(.wp-block-button .wp-block-button__link.is-style-outline:not(.has-background)) {
154
+ :where(
155
+ .wp-block-button.is-style-outline
156
+ > .wp-block-button__link:not(.has-background)
157
+ ),
158
+ :where(
159
+ .wp-block-button
160
+ .wp-block-button__link.is-style-outline:not(.has-background)
161
+ ) {
120
162
  background-color: transparent;
121
163
  // background-image is required to overwrite a gradient background
122
164
  background-image: none;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { getWidthClasses, isPercentageWidth } from '../utils';
5
+
6
+ describe( 'isPercentageWidth', () => {
7
+ it( 'should return true for percentage values', () => {
8
+ expect( isPercentageWidth( '50%' ) ).toBe( true );
9
+ expect( isPercentageWidth( '100%' ) ).toBe( true );
10
+ expect( isPercentageWidth( '33.5%' ) ).toBe( true );
11
+ } );
12
+
13
+ it( 'should return false for non-percentage values', () => {
14
+ expect( isPercentageWidth( '200px' ) ).toBe( false );
15
+ expect( isPercentageWidth( '10em' ) ).toBe( false );
16
+ expect( isPercentageWidth( undefined ) ).toBe( false );
17
+ expect( isPercentageWidth( null ) ).toBe( false );
18
+ } );
19
+
20
+ it( 'should return false for preset strings', () => {
21
+ expect( isPercentageWidth( 'var:preset|dimension|custom-width' ) ).toBe(
22
+ false
23
+ );
24
+ } );
25
+ } );
26
+
27
+ describe( 'getWidthClasses', () => {
28
+ it( 'should return empty object when no width is provided', () => {
29
+ expect( getWidthClasses( undefined ) ).toEqual( {} );
30
+ expect( getWidthClasses( '' ) ).toEqual( {} );
31
+ expect( getWidthClasses( null ) ).toEqual( {} );
32
+ } );
33
+
34
+ it( 'should return percentage classes for standard percentage widths', () => {
35
+ expect( getWidthClasses( '25%' ) ).toEqual( {
36
+ 'has-custom-width': true,
37
+ 'wp-block-button__width': true,
38
+ 'wp-block-button__width-25': true,
39
+ } );
40
+
41
+ expect( getWidthClasses( '50%' ) ).toEqual( {
42
+ 'has-custom-width': true,
43
+ 'wp-block-button__width': true,
44
+ 'wp-block-button__width-50': true,
45
+ } );
46
+
47
+ expect( getWidthClasses( '75%' ) ).toEqual( {
48
+ 'has-custom-width': true,
49
+ 'wp-block-button__width': true,
50
+ 'wp-block-button__width-75': true,
51
+ } );
52
+
53
+ expect( getWidthClasses( '100%' ) ).toEqual( {
54
+ 'has-custom-width': true,
55
+ 'wp-block-button__width': true,
56
+ 'wp-block-button__width-100': true,
57
+ } );
58
+ } );
59
+
60
+ it( 'should return generic percentage classes for non-standard percentage widths', () => {
61
+ expect( getWidthClasses( '33%' ) ).toEqual( {
62
+ 'has-custom-width': true,
63
+ 'wp-block-button__width': true,
64
+ } );
65
+ } );
66
+
67
+ it( 'should return only has-custom-width for non-percentage values', () => {
68
+ expect( getWidthClasses( '200px' ) ).toEqual( {
69
+ 'has-custom-width': true,
70
+ } );
71
+
72
+ expect( getWidthClasses( '10em' ) ).toEqual( {
73
+ 'has-custom-width': true,
74
+ } );
75
+ } );
76
+
77
+ it( 'should return only has-custom-width for resolved non-percentage preset values', () => {
78
+ // When a preset resolves to a non-percentage value (e.g., 200px),
79
+ // the resolved value is passed to getWidthClasses, not the preset string.
80
+ expect( getWidthClasses( '300px' ) ).toEqual( {
81
+ 'has-custom-width': true,
82
+ } );
83
+ } );
84
+ } );
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Returns whether the given width value is a percentage.
3
+ *
4
+ * @param {string} width - The width value.
5
+ * @return {boolean} True if the width is a percentage value.
6
+ */
7
+ export function isPercentageWidth( width ) {
8
+ return typeof width === 'string' && width.endsWith( '%' );
9
+ }
10
+
11
+ /**
12
+ * Returns the width classes for the button based on the width attribute.
13
+ *
14
+ * @param {string} width - The width value (e.g., '25%', '50%', '75%', '100%', or custom value).
15
+ * @return {Object} Object with width-related class names as keys and true as values.
16
+ */
17
+ export function getWidthClasses( width ) {
18
+ if ( ! width ) {
19
+ return {};
20
+ }
21
+
22
+ if ( isPercentageWidth( width ) ) {
23
+ const legacyWidthClasses = {
24
+ '25%': 'wp-block-button__width-25',
25
+ '50%': 'wp-block-button__width-50',
26
+ '75%': 'wp-block-button__width-75',
27
+ '100%': 'wp-block-button__width-100',
28
+ };
29
+ return {
30
+ 'has-custom-width': true,
31
+ 'wp-block-button__width': true,
32
+ // Maintain legacy class for backwards compatibility.
33
+ ...( legacyWidthClasses[ width ] && {
34
+ [ legacyWidthClasses[ width ] ]: true,
35
+ } ),
36
+ };
37
+ }
38
+
39
+ return {
40
+ 'has-custom-width': true,
41
+ };
42
+ }
@@ -1241,21 +1241,20 @@ export default function Image( {
1241
1241
  } );
1242
1242
  };
1243
1243
 
1244
- const featuredImageControl = (
1245
- <BlockSettingsMenuControls>
1246
- { ( { selectedClientIds } ) =>
1247
- selectedClientIds.length === 1 &&
1248
- ! isDescendentOfQueryLoop &&
1249
- postId &&
1250
- id &&
1251
- clientId === selectedClientIds[ 0 ] && (
1252
- <MenuItem onClick={ setPostFeatureImage }>
1253
- { __( 'Set as featured image' ) }
1254
- </MenuItem>
1255
- )
1256
- }
1257
- </BlockSettingsMenuControls>
1258
- );
1244
+ const featuredImageControl =
1245
+ ! isDescendentOfQueryLoop && postId && id ? (
1246
+ <BlockSettingsMenuControls>
1247
+ { ( { canEdit, selectedClientIds } ) =>
1248
+ canEdit &&
1249
+ selectedClientIds.length === 1 &&
1250
+ clientId === selectedClientIds[ 0 ] && (
1251
+ <MenuItem onClick={ setPostFeatureImage }>
1252
+ { __( 'Set as featured image' ) }
1253
+ </MenuItem>
1254
+ )
1255
+ }
1256
+ </BlockSettingsMenuControls>
1257
+ ) : null;
1259
1258
 
1260
1259
  return (
1261
1260
  <>
@@ -218,7 +218,9 @@ function block_core_image_render_lightbox( $block_content, $block, $block_instan
218
218
  if ( isset( $block['attrs']['id'] ) ) {
219
219
  $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] );
220
220
  $img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] );
221
- $img_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'] );
221
+ $has_dimensions = ( $img_metadata['width'] ?? '' ) && ( $img_metadata['height'] ?? '' );
222
+ $srcset_size = $has_dimensions ? array( $img_metadata['width'], $img_metadata['height'] ) : 'large';
223
+ $img_srcset = wp_get_attachment_image_srcset( $block['attrs']['id'], $srcset_size );
222
224
  $img_width = $img_metadata['width'] ?? 'none';
223
225
  $img_height = $img_metadata['height'] ?? 'none';
224
226
  }
@@ -105,6 +105,15 @@ describe( 'computeDisplayUrl', () => {
105
105
  expect( result.isExternal ).toBe( false );
106
106
  expect( result.displayUrl ).toBe( '/my-page' );
107
107
  } );
108
+
109
+ it( 'should treat http and https to same host as internal (compare by host, not origin)', () => {
110
+ const result = computeDisplayUrl( {
111
+ linkUrl: 'http://example.com/my-page',
112
+ homeUrl: 'https://example.com',
113
+ } );
114
+ expect( result.isExternal ).toBe( false );
115
+ expect( result.displayUrl ).toBe( '/my-page' );
116
+ } );
108
117
  } );
109
118
 
110
119
  describe( 'special protocols and edge cases', () => {
@@ -31,7 +31,7 @@ function capitalize( str ) {
31
31
  *
32
32
  * @param {Object} options - Parameters object
33
33
  * @param {string} options.linkUrl - The URL to process
34
- * @param {string} options.homeUrl - The WordPress site URL (falls back to window.location.origin)
34
+ * @param {string} options.homeUrl - The WordPress site URL (required for internal/external detection)
35
35
  * @return {Object} Object with displayUrl and isExternal flag
36
36
  */
37
37
  export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) {
@@ -51,12 +51,10 @@ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) {
51
51
  // This must happen before trusting the type attribute
52
52
  try {
53
53
  const parsedUrl = new URL( linkUrl );
54
- // Use provided homeUrl or fall back to window.location.origin
55
- const siteDomain = homeUrl
56
- ? new URL( homeUrl ).origin
57
- : window.location.origin;
54
+ // Compare by host (not origin) so http/https to same site both count as internal
55
+ const siteHost = new URL( homeUrl ).host;
58
56
 
59
- if ( parsedUrl.origin === siteDomain ) {
57
+ if ( parsedUrl.host === siteHost ) {
60
58
  // Show only the pathname (and search/hash if present)
61
59
  let path = parsedUrl.pathname + parsedUrl.search + parsedUrl.hash;
62
60
  // Remove trailing slash
@@ -65,12 +63,11 @@ export function computeDisplayUrl( { linkUrl, homeUrl } = {} ) {
65
63
  }
66
64
  displayUrl = path;
67
65
  } else {
68
- // Different origin - this is an external link
66
+ // Different host - this is an external link
69
67
  isExternal = true;
70
68
  }
71
69
  } catch ( e ) {
72
- // URL parsing failed - this means it's likely a URL without a protocol (e.g., "www.example.com")
73
- // Since we already checked for relative paths and hash links above, treat as external
70
+ // URL parsing failed - treat as external (e.g. no homeUrl, or URL without protocol)
74
71
  isExternal = true;
75
72
  }
76
73
 
@@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid';
7
7
  /**
8
8
  * WordPress dependencies
9
9
  */
10
- import { useState, useCallback, useEffect } from '@wordpress/element';
10
+ import { useCallback, useEffect } from '@wordpress/element';
11
11
  import {
12
12
  store as blockEditorStore,
13
13
  MediaPlaceholder,
@@ -23,15 +23,13 @@ import {
23
23
  ToggleControl,
24
24
  Disabled,
25
25
  SelectControl,
26
- Spinner,
27
26
  __experimentalToolsPanel as ToolsPanel,
28
27
  __experimentalToolsPanelItem as ToolsPanelItem,
29
28
  } from '@wordpress/components';
30
29
  import { useSelect, useDispatch } from '@wordpress/data';
31
30
  import { store as noticesStore } from '@wordpress/notices';
32
- import { __, _x, sprintf } from '@wordpress/i18n';
31
+ import { __ } from '@wordpress/i18n';
33
32
  import { audio as icon } from '@wordpress/icons';
34
- import { safeHTML, __unstableStripHTML as stripHTML } from '@wordpress/dom';
35
33
  import { createBlock } from '@wordpress/blocks';
36
34
 
37
35
  /**
@@ -39,99 +37,42 @@ import { createBlock } from '@wordpress/blocks';
39
37
  */
40
38
  import { Caption } from '../utils/caption';
41
39
  import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
40
+ import { WaveformPlayer } from '../utils/waveform-player';
42
41
 
43
42
  const ALLOWED_MEDIA_TYPES = [ 'audio' ];
44
43
 
45
- const CurrentTrack = ( { track, showImages, onTrackEnd } ) => {
46
- /**
47
- * dangerouslySetInnerHTML and safeHTML are used because
48
- * the media library allows using some HTML tags in the title, artist, and album fields.
49
- */
50
- const trackTitle = {
51
- dangerouslySetInnerHTML: {
52
- __html: safeHTML( track?.title ? track.title : __( 'Untitled' ) ),
53
- },
54
- };
55
- const trackArtist = {
56
- dangerouslySetInnerHTML: {
57
- __html: safeHTML(
58
- track?.artist ? track.artist : __( 'Unknown artist' )
59
- ),
60
- },
61
- };
62
- const trackAlbum = {
63
- dangerouslySetInnerHTML: {
64
- __html: safeHTML(
65
- track?.album ? track.album : __( 'Unknown album' )
66
- ),
67
- },
44
+ /**
45
+ * Transform media library data into track block attributes.
46
+ *
47
+ * @param {Object} media - Media object from the media library.
48
+ * @return {Object} Track attributes for the playlist-track block.
49
+ */
50
+ function getTrackAttributes( media ) {
51
+ return {
52
+ id: media.id || media.url, // Attachment ID or URL.
53
+ uniqueId: uuid(), // Unique ID for the track.
54
+ src: media.url,
55
+ title: media.title,
56
+ artist:
57
+ media.artist ||
58
+ media?.meta?.artist ||
59
+ media?.media_details?.artist ||
60
+ __( 'Unknown artist' ),
61
+ album:
62
+ media.album ||
63
+ media?.meta?.album ||
64
+ media?.media_details?.album ||
65
+ __( 'Unknown album' ),
66
+ length: media?.fileLength || media?.media_details?.length_formatted,
67
+ // Prevent using the default media attachment icon as the track image.
68
+ // Note: Image is not available when a new track is uploaded.
69
+ image:
70
+ media?.image?.src &&
71
+ media?.image?.src.endsWith( '/images/media/audio.svg' )
72
+ ? ''
73
+ : media?.image?.src,
68
74
  };
69
-
70
- let ariaLabel;
71
- if ( track?.title && track?.artist && track?.album ) {
72
- ariaLabel = stripHTML(
73
- sprintf(
74
- /* translators: %1$s: track title, %2$s artist name, %3$s: album name. */
75
- _x(
76
- '%1$s by %2$s from the album %3$s',
77
- 'track title, artist name, album name'
78
- ),
79
- track?.title,
80
- track?.artist,
81
- track?.album
82
- )
83
- );
84
- } else if ( track?.title ) {
85
- ariaLabel = stripHTML( track.title );
86
- } else {
87
- ariaLabel = stripHTML( __( 'Untitled' ) );
88
- }
89
-
90
- return (
91
- <>
92
- <div className="wp-block-playlist__current-item">
93
- { showImages && track?.image && (
94
- <img
95
- className="wp-block-playlist__item-image"
96
- src={ track.image }
97
- alt=""
98
- width="70px"
99
- height="70px"
100
- />
101
- ) }
102
- <div>
103
- { ! track?.title ? (
104
- <span className="wp-block-playlist__item-title">
105
- <Spinner />
106
- </span>
107
- ) : (
108
- <span
109
- className="wp-block-playlist__item-title"
110
- { ...trackTitle }
111
- />
112
- ) }
113
- <div className="wp-block-playlist__current-item-artist-album">
114
- <span
115
- className="wp-block-playlist__item-artist"
116
- { ...trackArtist }
117
- />
118
- <span
119
- className="wp-block-playlist__item-album"
120
- { ...trackAlbum }
121
- />
122
- </div>
123
- </div>
124
- </div>
125
- <audio
126
- controls="controls"
127
- src={ track?.url ? track.url : '' }
128
- onEnded={ onTrackEnd }
129
- aria-label={ ariaLabel }
130
- tabIndex={ 0 }
131
- />
132
- </>
133
- );
134
- };
75
+ }
135
76
 
136
77
  const PlaylistEdit = ( {
137
78
  attributes,
@@ -148,7 +89,6 @@ const PlaylistEdit = ( {
148
89
  showArtists,
149
90
  currentTrack,
150
91
  } = attributes;
151
- const [ trackListIndex, setTrackListIndex ] = useState( 0 );
152
92
  const blockProps = useBlockProps();
153
93
  const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } =
154
94
  useDispatch( blockEditorStore );
@@ -235,33 +175,7 @@ const PlaylistEdit = ( {
235
175
  media = [ media ];
236
176
  }
237
177
 
238
- const trackAttributes = ( track ) => ( {
239
- id: track.id || track.url, // Attachment ID or URL.
240
- uniqueId: uuid(), // Unique ID for the track.
241
- src: track.url,
242
- title: track.title,
243
- artist:
244
- track.artist ||
245
- track?.meta?.artist ||
246
- track?.media_details?.artist ||
247
- __( 'Unknown artist' ),
248
- album:
249
- track.album ||
250
- track?.meta?.album ||
251
- track?.media_details?.album ||
252
- __( 'Unknown album' ),
253
- length:
254
- track?.fileLength || track?.media_details?.length_formatted,
255
- // Prevent using the default media attachment icon as the track image.
256
- // Note: Image is not available when a new track is uploaded.
257
- image:
258
- track?.image?.src &&
259
- track?.image?.src.endsWith( '/images/media/audio.svg' )
260
- ? ''
261
- : track?.image?.src,
262
- } );
263
-
264
- const trackList = media.map( trackAttributes );
178
+ const trackList = media.map( getTrackAttributes );
265
179
  __unstableMarkNextChangeAsNotPersistent();
266
180
  setAttributes( {
267
181
  currentTrack:
@@ -282,29 +196,21 @@ const PlaylistEdit = ( {
282
196
  ]
283
197
  );
284
198
 
285
- const onTrackEnd = useCallback( () => {
286
- /* If there are tracks left, play the next track */
287
- if ( trackListIndex < tracks.length - 1 ) {
288
- if ( tracks[ trackListIndex + 1 ]?.uniqueId ) {
289
- setTrackListIndex( trackListIndex + 1 );
290
- setAttributes( {
291
- currentTrack: tracks[ trackListIndex + 1 ].uniqueId,
292
- } );
293
- }
294
- } else {
295
- setTrackListIndex( 0 );
296
- if ( tracks[ 0 ].uniqueId ) {
297
- setAttributes( { currentTrack: tracks[ 0 ].uniqueId } );
298
- } else if ( tracks.length > 0 ) {
299
- const validTrack = tracks.find(
300
- ( track ) => track.uniqueId !== undefined
301
- );
302
- if ( validTrack ) {
303
- setAttributes( { currentTrack: validTrack.uniqueId } );
304
- }
305
- }
199
+ // Get current track data by finding the track with matching uniqueId.
200
+ const currentTrackData = tracks.find(
201
+ ( track ) => track.uniqueId === currentTrack
202
+ );
203
+
204
+ // Handle track end - advance to next track or loop to first.
205
+ const onTrackEnded = useCallback( () => {
206
+ const currentIndex = tracks.findIndex(
207
+ ( track ) => track.uniqueId === currentTrack
208
+ );
209
+ const nextTrack = tracks[ currentIndex + 1 ] || tracks[ 0 ];
210
+ if ( nextTrack?.uniqueId ) {
211
+ setAttributes( { currentTrack: nextTrack.uniqueId } );
306
212
  }
307
- }, [ setAttributes, trackListIndex, tracks ] );
213
+ }, [ currentTrack, tracks, setAttributes ] );
308
214
 
309
215
  const onChangeOrder = useCallback(
310
216
  ( trackOrder ) => {
@@ -317,16 +223,13 @@ const PlaylistEdit = ( {
317
223
  }
318
224
  return titleB.localeCompare( titleA );
319
225
  } );
320
- const sortedTracks = sortedBlocks.map(
321
- ( block ) => block.attributes
322
- );
226
+ const firstUniqueId = sortedBlocks[ 0 ]?.attributes?.uniqueId;
323
227
  replaceInnerBlocks( clientId, sortedBlocks );
324
228
  setAttributes( {
325
229
  order: trackOrder,
326
230
  currentTrack:
327
- sortedTracks.length > 0 &&
328
- sortedTracks[ 0 ].uniqueId !== currentTrack
329
- ? sortedTracks[ 0 ].uniqueId
231
+ firstUniqueId && firstUniqueId !== currentTrack
232
+ ? firstUniqueId
330
233
  : currentTrack,
331
234
  } );
332
235
  },
@@ -358,7 +261,7 @@ const PlaylistEdit = ( {
358
261
  renderAppender: hasAnySelected && InnerBlocks.ButtonBlockAppender,
359
262
  } );
360
263
 
361
- if ( ! tracks || ( Array.isArray( tracks ) && tracks.length === 0 ) ) {
264
+ if ( tracks.length === 0 ) {
362
265
  return (
363
266
  <div
364
267
  { ...blockProps }
@@ -498,10 +401,12 @@ const PlaylistEdit = ( {
498
401
  </InspectorControls>
499
402
  <figure { ...blockProps }>
500
403
  <Disabled isDisabled={ ! isSelected }>
501
- <CurrentTrack
502
- track={ tracks[ trackListIndex ] }
503
- showImages={ showImages }
504
- onTrackEnd={ onTrackEnd }
404
+ <WaveformPlayer
405
+ src={ currentTrackData?.src }
406
+ title={ currentTrackData?.title }
407
+ artist={ currentTrackData?.artist }
408
+ image={ currentTrackData?.image }
409
+ onEnded={ onTrackEnded }
505
410
  />
506
411
  </Disabled>
507
412
  { showTracklist && (
@@ -529,3 +434,4 @@ const PlaylistEdit = ( {
529
434
  };
530
435
 
531
436
  export default PlaylistEdit;
437
+ export { getTrackAttributes };
@@ -1,6 +1,6 @@
1
1
  .wp-block-playlist {
2
- &.is-placeholder {
3
- padding: 0;
4
- border: none;
2
+ li.block-list-appender.block-list-appender {
3
+ position: initial;
4
+ margin-top: var(--wp--preset--spacing--30, 1em);
5
5
  }
6
6
  }