@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
@@ -58,13 +58,16 @@ function render_block_core_playlist( $attributes, $content, $block ) {
58
58
  );
59
59
  }
60
60
 
61
+ // Data is passed to wp_interactivity_state() which JSON-encodes it,
62
+ // so we use wp_strip_all_tags() instead of esc_html() to prevent
63
+ // HTML injection without double-encoding. URLs still use esc_url().
61
64
  $tracks_data[ $unique_id ] = array(
62
65
  'url' => esc_url( $url ),
63
- 'title' => esc_html( $title ),
64
- 'artist' => esc_html( $artist ),
65
- 'album' => esc_html( $album ),
66
+ 'title' => wp_strip_all_tags( $title ),
67
+ 'artist' => wp_strip_all_tags( $artist ),
68
+ 'album' => wp_strip_all_tags( $album ),
66
69
  'image' => esc_url( $image ),
67
- 'ariaLabel' => esc_attr( $aria_label ),
70
+ 'ariaLabel' => wp_strip_all_tags( $aria_label ),
68
71
  );
69
72
 
70
73
  if ( $unique_id === $current_media_id ) {
@@ -96,41 +99,14 @@ function render_block_core_playlist( $attributes, $content, $block ) {
96
99
  )
97
100
  );
98
101
 
99
- // Create the HTML for the current track which shows above the tracklist.
100
- $html = '<div class="wp-block-playlist__current-item">';
101
-
102
- // The alt attribute is intentionally left empty, as the image is decorative.
103
- if ( $attributes['showImages'] ?? false ) {
104
- $html .=
105
- '<img
106
- class="wp-block-playlist__item-image"
107
- alt=""
108
- width="70px"
109
- height="70px"
110
- data-wp-bind--src="state.currentTrack.image"
111
- data-wp-bind--hidden="!state.currentTrack.image"
112
- />';
113
- }
114
-
115
- $html .= '
116
- <div>
117
- <span class="wp-block-playlist__item-title" data-wp-text="state.currentTrack.title"></span>
118
- <div class="wp-block-playlist__current-item-artist-album">
119
- <span class="wp-block-playlist__item-artist" data-wp-text="state.currentTrack.artist"></span>
120
- <span class="wp-block-playlist__item-album" data-wp-text="state.currentTrack.album"></span>
121
- </div>
122
- </div>
123
- </div>
124
- <audio
125
- controls="controls"
126
- data-wp-on--ended="actions.nextSong"
127
- data-wp-on--play="actions.isPlaying"
128
- data-wp-on--pause="actions.isPaused"
129
- data-wp-bind--src="state.currentTrack.url"
130
- data-wp-bind--aria-label="state.currentTrack.ariaLabel"
131
- data-wp-watch="callbacks.autoPlay"
132
- ></audio>
133
- ';
102
+ // Add waveform player container with translated button labels.
103
+ $label_play = esc_attr__( 'Play' );
104
+ $label_pause = esc_attr__( 'Pause' );
105
+ $html = '<div class="wp-block-playlist__waveform-player"
106
+ data-wp-watch="callbacks.initWaveformPlayer"
107
+ data-label-play="' . $label_play . '"
108
+ data-label-pause="' . $label_pause . '"
109
+ ></div>';
134
110
 
135
111
  // Add the HTML for the current track inside the figure.
136
112
  $figure = null;
@@ -149,7 +125,6 @@ function render_block_core_playlist( $attributes, $content, $block ) {
149
125
  'playlistId' => $playlist_id,
150
126
  'currentId' => $current_unique_id,
151
127
  'tracks' => $playlist_tracks,
152
- 'isPlaying' => false,
153
128
  )
154
129
  )
155
130
  );
@@ -1,32 +1,44 @@
1
+ @use "@arraypress/waveform-player/dist/waveform-player";
2
+
3
+ // Waveform player dimensions.
4
+ $waveform-player-height: 100px;
5
+
1
6
  .wp-block-playlist {
2
- .wp-block-playlist__current-item {
3
- display: flex;
4
- align-items: center;
5
- gap: var(--wp--preset--spacing--40, 1.5em);
6
- align-self: stretch;
7
- padding-bottom: var(--wp--preset--spacing--30, 1em);
8
- margin-bottom: var(--wp--preset--spacing--30, 1em);
9
-
10
- div {
11
- display: flex;
12
- flex-direction: column;
13
- align-items: flex-start;
14
- gap: var(--wp--preset--spacing--20, 0.5em);
15
- }
7
+ // Main waveform player container.
8
+ .wp-block-playlist__waveform-player {
9
+ width: 100%;
10
+ margin: var(--wp--preset--spacing--20, 0.625em) 0;
11
+ position: relative;
12
+ }
16
13
 
17
- & .wp-block-playlist__current-item-artist-album {
18
- flex-direction: row;
19
- }
14
+ // Set the waveform track height and remove gap between button and waveform.
15
+ .waveform-track {
16
+ height: $waveform-player-height;
17
+ gap: 0;
18
+ }
20
19
 
21
- .wp-block-playlist__item-title {
22
- word-break: break-all;
20
+ // WaveformPlayer button styling.
21
+ .waveform-btn {
22
+ border-radius: 0;
23
+ border-end-start-radius: 0.125rem;
24
+ border-start-start-radius: 0.125rem;
25
+ width: $waveform-player-height;
26
+ height: $waveform-player-height;
27
+ min-width: $waveform-player-height;
28
+ background: currentColor;
29
+ margin: 0;
30
+
31
+ &:hover:not(:disabled) {
32
+ transform: none;
23
33
  }
34
+ }
24
35
 
36
+ .waveform-track.waveform-align-bottom .waveform-btn {
37
+ margin-bottom: 0;
25
38
  }
26
39
 
27
- audio {
28
- width: 100%;
29
- margin-top: var(--wp--preset--spacing--20, 0.625em);
40
+ .waveform-subtitle {
41
+ opacity: 0.7;
30
42
  }
31
43
 
32
44
  .wp-block-playlist__tracklist {
@@ -49,9 +61,4 @@
49
61
  counter-reset: playlist-track;
50
62
  }
51
63
  }
52
-
53
- li.block-list-appender.block-list-appender {
54
- position: initial;
55
- margin-top: var(--wp--preset--spacing--30, 1em);
56
- }
57
64
  }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ /**
6
+ * Internal dependencies
7
+ */
8
+ import { getTrackAttributes } from '../edit';
9
+
10
+ // Mock uuid to return predictable values.
11
+ jest.mock( 'uuid', () => ( {
12
+ v4: jest.fn( () => 'mock-uuid-1234' ),
13
+ } ) );
14
+
15
+ describe( 'Playlist block edit utilities', () => {
16
+ describe( 'getTrackAttributes', () => {
17
+ it( 'should transform media object to track attributes', () => {
18
+ const media = {
19
+ id: 123,
20
+ url: 'https://example.com/song.mp3',
21
+ title: 'My Song',
22
+ artist: 'The Artist',
23
+ album: 'Great Album',
24
+ fileLength: '3:45',
25
+ image: { src: 'https://example.com/cover.jpg' },
26
+ };
27
+
28
+ const result = getTrackAttributes( media );
29
+
30
+ expect( result ).toEqual( {
31
+ id: 123,
32
+ uniqueId: 'mock-uuid-1234',
33
+ src: 'https://example.com/song.mp3',
34
+ title: 'My Song',
35
+ artist: 'The Artist',
36
+ album: 'Great Album',
37
+ length: '3:45',
38
+ image: 'https://example.com/cover.jpg',
39
+ } );
40
+ } );
41
+
42
+ it( 'should use URL as id when attachment id is not available', () => {
43
+ const media = {
44
+ url: 'https://example.com/song.mp3',
45
+ title: 'My Song',
46
+ };
47
+
48
+ const result = getTrackAttributes( media );
49
+
50
+ expect( result.id ).toBe( 'https://example.com/song.mp3' );
51
+ } );
52
+
53
+ it( 'should fall back to meta.artist when artist is not available', () => {
54
+ const media = {
55
+ url: 'https://example.com/song.mp3',
56
+ title: 'My Song',
57
+ meta: { artist: 'Meta Artist' },
58
+ };
59
+
60
+ const result = getTrackAttributes( media );
61
+
62
+ expect( result.artist ).toBe( 'Meta Artist' );
63
+ } );
64
+
65
+ it( 'should fall back to media_details.artist when artist and meta.artist are not available', () => {
66
+ const media = {
67
+ url: 'https://example.com/song.mp3',
68
+ title: 'My Song',
69
+ media_details: { artist: 'Media Details Artist' },
70
+ };
71
+
72
+ const result = getTrackAttributes( media );
73
+
74
+ expect( result.artist ).toBe( 'Media Details Artist' );
75
+ } );
76
+
77
+ it( 'should use "Unknown artist" when no artist is available', () => {
78
+ const media = {
79
+ url: 'https://example.com/song.mp3',
80
+ title: 'My Song',
81
+ };
82
+
83
+ const result = getTrackAttributes( media );
84
+
85
+ expect( result.artist ).toBe( 'Unknown artist' );
86
+ } );
87
+
88
+ it( 'should use "Unknown album" when no album is available', () => {
89
+ const media = {
90
+ url: 'https://example.com/song.mp3',
91
+ title: 'My Song',
92
+ };
93
+
94
+ const result = getTrackAttributes( media );
95
+
96
+ expect( result.album ).toBe( 'Unknown album' );
97
+ } );
98
+
99
+ it( 'should use media_details.length_formatted when fileLength is not available', () => {
100
+ const media = {
101
+ url: 'https://example.com/song.mp3',
102
+ title: 'My Song',
103
+ media_details: { length_formatted: '4:30' },
104
+ };
105
+
106
+ const result = getTrackAttributes( media );
107
+
108
+ expect( result.length ).toBe( '4:30' );
109
+ } );
110
+
111
+ it( 'should exclude default audio icon from image', () => {
112
+ const media = {
113
+ url: 'https://example.com/song.mp3',
114
+ title: 'My Song',
115
+ image: {
116
+ src: 'https://example.com/wp-includes/images/media/audio.svg',
117
+ },
118
+ };
119
+
120
+ const result = getTrackAttributes( media );
121
+
122
+ expect( result.image ).toBe( '' );
123
+ } );
124
+
125
+ it( 'should include image URLs', () => {
126
+ const media = {
127
+ url: 'https://example.com/song.mp3',
128
+ title: 'My Song',
129
+ image: { src: 'https://example.com/cover.jpg' },
130
+ };
131
+
132
+ const result = getTrackAttributes( media );
133
+
134
+ expect( result.image ).toBe( 'https://example.com/cover.jpg' );
135
+ } );
136
+ } );
137
+ } );
@@ -3,22 +3,21 @@
3
3
  */
4
4
  import { store, getContext, getElement } from '@wordpress/interactivity';
5
5
 
6
- store(
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { initWaveformPlayer, logPlayError } from '../utils/waveform-utils';
10
+
11
+ /**
12
+ * Store player state for each element.
13
+ */
14
+ const playerState = new WeakMap();
15
+
16
+ const { state } = store(
7
17
  'core/playlist',
8
18
  {
9
19
  state: {
10
20
  playlists: {},
11
- get currentTrack() {
12
- const { currentId, playlistId } = getContext();
13
- if ( ! currentId || ! playlistId ) {
14
- return {};
15
- }
16
- const playlist = this.playlists[ playlistId ];
17
- if ( ! playlist ) {
18
- return {};
19
- }
20
- return playlist.tracks[ currentId ] || {};
21
- },
22
21
  get isCurrentTrack() {
23
22
  const { currentId, uniqueId } = getContext();
24
23
  return currentId === uniqueId;
@@ -28,42 +27,100 @@ store(
28
27
  changeTrack() {
29
28
  const context = getContext();
30
29
  context.currentId = context.uniqueId;
31
- context.isPlaying = true;
32
- },
33
- isPlaying() {
34
- const context = getContext();
35
- context.isPlaying = true;
36
- },
37
- isPaused() {
38
- const context = getContext();
39
- context.isPlaying = false;
40
- },
41
- nextSong() {
42
- const context = getContext();
43
- const currentIndex = context.tracks.findIndex(
44
- ( uniqueId ) => uniqueId === context.currentId
45
- );
46
- const nextTrack = context.tracks[ currentIndex + 1 ];
47
- if ( nextTrack ) {
48
- context.currentId = nextTrack;
49
- const { ref } = getElement();
50
- // Waits a moment before changing the track, since
51
- // immediately changing the track can be jarring.
52
- setTimeout( () => {
53
- ref.play();
54
- }, 1000 );
55
- }
56
30
  },
57
31
  },
58
32
  callbacks: {
59
- autoPlay() {
33
+ initWaveformPlayer() {
60
34
  const context = getContext();
61
35
  const { ref } = getElement();
62
- if ( context.currentId && context.isPlaying ) {
63
- ref.play();
36
+
37
+ if ( ! context.currentId || ! ref ) {
38
+ return;
64
39
  }
40
+
41
+ const track =
42
+ state.playlists[ context.playlistId ]?.tracks[
43
+ context.currentId
44
+ ];
45
+ if ( ! track?.url ) {
46
+ return;
47
+ }
48
+
49
+ const existing = playerState.get( ref );
50
+
51
+ // Skip if we already initialized with this exact URL.
52
+ if ( existing?.url === track.url ) {
53
+ return;
54
+ }
55
+
56
+ // Autoplay if we're switching from a different track (user action),
57
+ // but not on initial page load (when existing has no URL).
58
+ const shouldAutoPlay = !! existing?.url;
59
+
60
+ initPlayer( ref, track, shouldAutoPlay, context );
65
61
  },
66
62
  },
67
63
  },
68
64
  { lock: true }
69
65
  );
66
+
67
+ /**
68
+ * Initialize the waveform player for a given element.
69
+ *
70
+ * @param {Element} ref - The container element.
71
+ * @param {Object} track - The track data.
72
+ * @param {boolean} shouldAutoPlay - Whether to auto-play after initialization.
73
+ * @param {Object} context - The Interactivity API context.
74
+ */
75
+ function initPlayer( ref, track, shouldAutoPlay, context ) {
76
+ const existing = playerState.get( ref );
77
+
78
+ // If a player already exists, load the new track without recreating.
79
+ if ( existing?.instance ) {
80
+ existing.instance
81
+ .loadTrack( track.url, track.title, track.artist, {
82
+ artwork: track.image,
83
+ } )
84
+ .then( () => {
85
+ existing.url = track.url;
86
+ if ( shouldAutoPlay ) {
87
+ existing.instance.play()?.catch( logPlayError );
88
+ }
89
+ } )
90
+ .catch( logPlayError );
91
+ return;
92
+ }
93
+
94
+ // Read translated labels from server-rendered data attributes.
95
+ const labels = {
96
+ play: ref.dataset.labelPlay,
97
+ pause: ref.dataset.labelPause,
98
+ };
99
+
100
+ // Initialize using the shared core.
101
+ const player = initWaveformPlayer( ref, {
102
+ src: track.url,
103
+ title: track.title,
104
+ artist: track.artist,
105
+ image: track.image,
106
+ autoPlay: shouldAutoPlay,
107
+ labels,
108
+ onEnded: () => {
109
+ // Advance to next track (autoPlay handles playback).
110
+ const currentIndex = context.tracks.findIndex(
111
+ ( uniqueId ) => uniqueId === context.currentId
112
+ );
113
+ const nextTrack = context.tracks[ currentIndex + 1 ];
114
+ if ( nextTrack ) {
115
+ context.currentId = nextTrack;
116
+ }
117
+ },
118
+ } );
119
+
120
+ // Store state for cleanup, including instance for loadTrack reuse.
121
+ playerState.set( ref, {
122
+ url: track.url,
123
+ instance: player.instance,
124
+ destroy: player.destroy,
125
+ } );
126
+ }
@@ -180,7 +180,6 @@ const PlaylistTrackEdit = ( { attributes, setAttributes, context } ) => {
180
180
  __next40pxDefaultSize
181
181
  label={ __( 'Title' ) }
182
182
  value={ title ? stripHTML( title ) : '' }
183
- placeholder={ title ? stripHTML( title ) : '' }
184
183
  onChange={ ( titleValue ) => {
185
184
  setAttributes( { title: titleValue } );
186
185
  } }
@@ -30,6 +30,9 @@
30
30
  "type": "string",
31
31
  "default": "_self",
32
32
  "role": "content"
33
+ },
34
+ "placeholder": {
35
+ "type": "string"
33
36
  }
34
37
  },
35
38
  "example": {
@@ -28,7 +28,7 @@ import { createInterpolateElement } from '@wordpress/element';
28
28
  import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
29
29
 
30
30
  export default function PostTitleEdit( {
31
- attributes: { level, levelOptions, isLink, rel, linkTarget },
31
+ attributes: { level, levelOptions, isLink, rel, linkTarget, placeholder },
32
32
  setAttributes,
33
33
  context: { postType, postId, queryId },
34
34
  insertBlocksAfter,
@@ -70,7 +70,9 @@ export default function PostTitleEdit( {
70
70
  const blockEditingMode = useBlockEditingMode();
71
71
  const dropdownMenuProps = useToolsPanelDropdownMenuProps();
72
72
 
73
- let titleElement = <TagName { ...blockProps }>{ __( 'Title' ) }</TagName>;
73
+ let titleElement = (
74
+ <TagName { ...blockProps }>{ placeholder || __( 'Title' ) }</TagName>
75
+ );
74
76
 
75
77
  if ( postType && postId ) {
76
78
  titleElement = userCanEdit ? (
@@ -72,7 +72,7 @@ function render_block_core_search( $attributes ) {
72
72
  if ( $input->next_tag() ) {
73
73
  $input->add_class( implode( ' ', $input_classes ) );
74
74
  $input->set_attribute( 'id', $input_id );
75
- $input->set_attribute( 'value', get_search_query() );
75
+ $input->set_attribute( 'value', get_search_query( false ) );
76
76
  $input->set_attribute( 'placeholder', $attributes['placeholder'] );
77
77
 
78
78
  // If it's interactive, enqueue the script module and add the directives.