@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.
- package/CHANGELOG.md +2 -0
- package/build/button/block.json +11 -3
- package/build/button/deprecated.cjs +246 -13
- package/build/button/deprecated.cjs.map +2 -2
- package/build/button/edit.cjs +45 -58
- package/build/button/edit.cjs.map +3 -3
- package/build/button/save.cjs +3 -7
- package/build/button/save.cjs.map +2 -2
- package/build/button/utils.cjs +59 -0
- package/build/button/utils.cjs.map +7 -0
- package/build/image/image.cjs +1 -1
- package/build/image/image.cjs.map +2 -2
- package/build/navigation-link/shared/use-link-preview.cjs +2 -2
- package/build/navigation-link/shared/use-link-preview.cjs.map +2 -2
- package/build/playlist/edit.cjs +43 -136
- package/build/playlist/edit.cjs.map +3 -3
- package/build/playlist/view.cjs +56 -38
- package/build/playlist/view.cjs.map +2 -2
- package/build/playlist-track/edit.cjs +0 -1
- package/build/playlist-track/edit.cjs.map +2 -2
- package/build/post-title/block.json +3 -0
- package/build/post-title/edit.cjs +2 -2
- package/build/post-title/edit.cjs.map +2 -2
- package/build/utils/waveform-player.cjs +68 -0
- package/build/utils/waveform-player.cjs.map +7 -0
- package/build/utils/waveform-utils.cjs +171 -0
- package/build/utils/waveform-utils.cjs.map +7 -0
- package/build-module/button/block.json +11 -3
- package/build-module/button/deprecated.mjs +246 -13
- package/build-module/button/deprecated.mjs.map +2 -2
- package/build-module/button/edit.mjs +47 -63
- package/build-module/button/edit.mjs.map +2 -2
- package/build-module/button/save.mjs +3 -7
- package/build-module/button/save.mjs.map +2 -2
- package/build-module/button/utils.mjs +33 -0
- package/build-module/button/utils.mjs.map +7 -0
- package/build-module/image/image.mjs +1 -1
- package/build-module/image/image.mjs.map +2 -2
- package/build-module/navigation-link/shared/use-link-preview.mjs +2 -2
- package/build-module/navigation-link/shared/use-link-preview.mjs.map +2 -2
- package/build-module/playlist/edit.mjs +41 -139
- package/build-module/playlist/edit.mjs.map +2 -2
- package/build-module/playlist/view.mjs +56 -38
- package/build-module/playlist/view.mjs.map +2 -2
- package/build-module/playlist-track/edit.mjs +0 -1
- package/build-module/playlist-track/edit.mjs.map +2 -2
- package/build-module/post-title/block.json +3 -0
- package/build-module/post-title/edit.mjs +2 -2
- package/build-module/post-title/edit.mjs.map +2 -2
- package/build-module/utils/waveform-player.mjs +43 -0
- package/build-module/utils/waveform-player.mjs.map +7 -0
- package/build-module/utils/waveform-utils.mjs +131 -0
- package/build-module/utils/waveform-utils.mjs.map +7 -0
- package/build-style/button/style-rtl.css +6 -0
- package/build-style/button/style.css +6 -0
- package/build-style/editor-rtl.css +3 -3
- package/build-style/editor.css +3 -3
- package/build-style/playlist/editor-rtl.css +3 -3
- package/build-style/playlist/editor.css +3 -3
- package/build-style/playlist/style-rtl.css +351 -17
- package/build-style/playlist/style.css +351 -17
- package/build-style/style-rtl.css +357 -17
- package/build-style/style.css +357 -17
- package/package.json +39 -38
- package/src/button/block.json +11 -3
- package/src/button/deprecated.js +254 -16
- package/src/button/edit.js +50 -61
- package/src/button/index.php +68 -0
- package/src/button/save.js +2 -8
- package/src/button/style.scss +49 -7
- package/src/button/test/utils.js +84 -0
- package/src/button/utils.js +42 -0
- package/src/image/image.js +14 -15
- package/src/image/index.php +3 -1
- package/src/navigation-link/shared/test/use-link-preview.test.js +9 -0
- package/src/navigation-link/shared/use-link-preview.js +6 -9
- package/src/playlist/edit.js +60 -154
- package/src/playlist/editor.scss +3 -3
- package/src/playlist/index.php +15 -40
- package/src/playlist/style.scss +34 -27
- package/src/playlist/test/edit.js +137 -0
- package/src/playlist/view.js +97 -40
- package/src/playlist-track/edit.js +0 -1
- package/src/post-title/block.json +3 -0
- package/src/post-title/edit.js +4 -2
- package/src/search/index.php +1 -1
- package/src/utils/test/waveform-utils.js +328 -0
- package/src/utils/waveform-player.js +77 -0
- package/src/utils/waveform-utils.js +232 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* External dependencies
|
|
7
|
+
*/
|
|
8
|
+
import '@testing-library/jest-dom';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Internal dependencies
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
createWaveformContainer,
|
|
15
|
+
styleSvgIcons,
|
|
16
|
+
setupPlayButtonAccessibility,
|
|
17
|
+
logPlayError,
|
|
18
|
+
} from '../waveform-utils';
|
|
19
|
+
|
|
20
|
+
// Base player data used across tests
|
|
21
|
+
const basePlayerData = {
|
|
22
|
+
url: 'https://example.com/song.mp3',
|
|
23
|
+
waveformColor: 'rgba(0, 0, 0, 0.3)',
|
|
24
|
+
progressColor: 'rgba(0, 0, 0, 0.6)',
|
|
25
|
+
buttonColor: '#000000',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe( 'Waveform utilities', () => {
|
|
29
|
+
describe( 'createWaveformContainer', () => {
|
|
30
|
+
it( 'should create a container with required data attributes', () => {
|
|
31
|
+
const container = createWaveformContainer( basePlayerData );
|
|
32
|
+
|
|
33
|
+
expect( container.tagName ).toBe( 'DIV' );
|
|
34
|
+
expect( container ).toHaveAttribute( 'data-waveform-player', '' );
|
|
35
|
+
expect( container ).toHaveAttribute(
|
|
36
|
+
'data-url',
|
|
37
|
+
'https://example.com/song.mp3'
|
|
38
|
+
);
|
|
39
|
+
expect( container ).toHaveAttribute( 'data-height', '100' );
|
|
40
|
+
expect( container ).toHaveAttribute(
|
|
41
|
+
'data-waveform-style',
|
|
42
|
+
'bars'
|
|
43
|
+
);
|
|
44
|
+
expect( container ).toHaveAttribute(
|
|
45
|
+
'data-waveform-color',
|
|
46
|
+
'rgba(0, 0, 0, 0.3)'
|
|
47
|
+
);
|
|
48
|
+
expect( container ).toHaveAttribute(
|
|
49
|
+
'data-progress-color',
|
|
50
|
+
'rgba(0, 0, 0, 0.6)'
|
|
51
|
+
);
|
|
52
|
+
expect( container ).toHaveAttribute(
|
|
53
|
+
'data-button-color',
|
|
54
|
+
'#000000'
|
|
55
|
+
);
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'should set optional attributes when provided', () => {
|
|
59
|
+
const container = createWaveformContainer( {
|
|
60
|
+
...basePlayerData,
|
|
61
|
+
title: 'My Song',
|
|
62
|
+
artist: 'The Artist',
|
|
63
|
+
artwork: 'https://example.com/cover.jpg',
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
expect( container ).toHaveAttribute( 'data-title', 'My Song' );
|
|
67
|
+
expect( container ).toHaveAttribute(
|
|
68
|
+
'data-subtitle',
|
|
69
|
+
'The Artist'
|
|
70
|
+
);
|
|
71
|
+
expect( container ).toHaveAttribute(
|
|
72
|
+
'data-artwork',
|
|
73
|
+
'https://example.com/cover.jpg'
|
|
74
|
+
);
|
|
75
|
+
} );
|
|
76
|
+
|
|
77
|
+
it( 'should not set optional attributes when not provided', () => {
|
|
78
|
+
const container = createWaveformContainer( basePlayerData );
|
|
79
|
+
|
|
80
|
+
expect( container ).not.toHaveAttribute( 'data-title' );
|
|
81
|
+
expect( container ).not.toHaveAttribute( 'data-subtitle' );
|
|
82
|
+
expect( container ).not.toHaveAttribute( 'data-artwork' );
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
it( 'should use custom height when provided', () => {
|
|
86
|
+
const container = createWaveformContainer( {
|
|
87
|
+
...basePlayerData,
|
|
88
|
+
height: 150,
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
expect( container ).toHaveAttribute( 'data-height', '150' );
|
|
92
|
+
} );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
describe( 'styleSvgIcons', () => {
|
|
96
|
+
it( 'should set white fill for dark button colors', () => {
|
|
97
|
+
const container = document.createElement( 'div' );
|
|
98
|
+
const svg = document.createElementNS(
|
|
99
|
+
'http://www.w3.org/2000/svg',
|
|
100
|
+
'svg'
|
|
101
|
+
);
|
|
102
|
+
const path = document.createElementNS(
|
|
103
|
+
'http://www.w3.org/2000/svg',
|
|
104
|
+
'path'
|
|
105
|
+
);
|
|
106
|
+
svg.appendChild( path );
|
|
107
|
+
container.appendChild( svg );
|
|
108
|
+
|
|
109
|
+
styleSvgIcons( container, '#000000' );
|
|
110
|
+
|
|
111
|
+
expect( path ).toHaveStyle( { fill: '#ffffff' } );
|
|
112
|
+
} );
|
|
113
|
+
|
|
114
|
+
it( 'should set black fill for light button colors', () => {
|
|
115
|
+
const container = document.createElement( 'div' );
|
|
116
|
+
const svg = document.createElementNS(
|
|
117
|
+
'http://www.w3.org/2000/svg',
|
|
118
|
+
'svg'
|
|
119
|
+
);
|
|
120
|
+
const path = document.createElementNS(
|
|
121
|
+
'http://www.w3.org/2000/svg',
|
|
122
|
+
'path'
|
|
123
|
+
);
|
|
124
|
+
svg.appendChild( path );
|
|
125
|
+
container.appendChild( svg );
|
|
126
|
+
|
|
127
|
+
styleSvgIcons( container, '#ffffff' );
|
|
128
|
+
|
|
129
|
+
expect( path ).toHaveStyle( { fill: '#000000' } );
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'should style multiple SVG paths', () => {
|
|
133
|
+
const container = document.createElement( 'div' );
|
|
134
|
+
const svg = document.createElementNS(
|
|
135
|
+
'http://www.w3.org/2000/svg',
|
|
136
|
+
'svg'
|
|
137
|
+
);
|
|
138
|
+
const path1 = document.createElementNS(
|
|
139
|
+
'http://www.w3.org/2000/svg',
|
|
140
|
+
'path'
|
|
141
|
+
);
|
|
142
|
+
const path2 = document.createElementNS(
|
|
143
|
+
'http://www.w3.org/2000/svg',
|
|
144
|
+
'path'
|
|
145
|
+
);
|
|
146
|
+
svg.appendChild( path1 );
|
|
147
|
+
svg.appendChild( path2 );
|
|
148
|
+
container.appendChild( svg );
|
|
149
|
+
|
|
150
|
+
styleSvgIcons( container, '#000000' );
|
|
151
|
+
|
|
152
|
+
expect( path1 ).toHaveStyle( { fill: '#ffffff' } );
|
|
153
|
+
expect( path2 ).toHaveStyle( { fill: '#ffffff' } );
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
it( 'should handle container with no SVG paths', () => {
|
|
157
|
+
const container = document.createElement( 'div' );
|
|
158
|
+
|
|
159
|
+
// Should not throw.
|
|
160
|
+
expect( () => {
|
|
161
|
+
styleSvgIcons( container, '#000000' );
|
|
162
|
+
} ).not.toThrow();
|
|
163
|
+
} );
|
|
164
|
+
|
|
165
|
+
it( 'should use white for dark colors', () => {
|
|
166
|
+
const container = document.createElement( 'div' );
|
|
167
|
+
const svg = document.createElementNS(
|
|
168
|
+
'http://www.w3.org/2000/svg',
|
|
169
|
+
'svg'
|
|
170
|
+
);
|
|
171
|
+
const path = document.createElementNS(
|
|
172
|
+
'http://www.w3.org/2000/svg',
|
|
173
|
+
'path'
|
|
174
|
+
);
|
|
175
|
+
svg.appendChild( path );
|
|
176
|
+
container.appendChild( svg );
|
|
177
|
+
|
|
178
|
+
// A dark blue color.
|
|
179
|
+
styleSvgIcons( container, '#000080' );
|
|
180
|
+
|
|
181
|
+
expect( path ).toHaveStyle( { fill: '#ffffff' } );
|
|
182
|
+
} );
|
|
183
|
+
|
|
184
|
+
it( 'should use black for mid-light colors', () => {
|
|
185
|
+
const container = document.createElement( 'div' );
|
|
186
|
+
const svg = document.createElementNS(
|
|
187
|
+
'http://www.w3.org/2000/svg',
|
|
188
|
+
'svg'
|
|
189
|
+
);
|
|
190
|
+
const path = document.createElementNS(
|
|
191
|
+
'http://www.w3.org/2000/svg',
|
|
192
|
+
'path'
|
|
193
|
+
);
|
|
194
|
+
svg.appendChild( path );
|
|
195
|
+
container.appendChild( svg );
|
|
196
|
+
|
|
197
|
+
// A light yellow color.
|
|
198
|
+
styleSvgIcons( container, '#ffff00' );
|
|
199
|
+
|
|
200
|
+
expect( path ).toHaveStyle( { fill: '#000000' } );
|
|
201
|
+
} );
|
|
202
|
+
} );
|
|
203
|
+
|
|
204
|
+
describe( 'setupPlayButtonAccessibility', () => {
|
|
205
|
+
it( 'should set aria-label to Play initially', () => {
|
|
206
|
+
const container = document.createElement( 'div' );
|
|
207
|
+
const playBtn = document.createElement( 'button' );
|
|
208
|
+
playBtn.className = 'waveform-btn';
|
|
209
|
+
container.appendChild( playBtn );
|
|
210
|
+
|
|
211
|
+
setupPlayButtonAccessibility( container );
|
|
212
|
+
|
|
213
|
+
expect( playBtn ).toHaveAttribute( 'aria-label', 'Play' );
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
it( 'should change aria-label to Pause on play event', () => {
|
|
217
|
+
const container = document.createElement( 'div' );
|
|
218
|
+
const playBtn = document.createElement( 'button' );
|
|
219
|
+
playBtn.className = 'waveform-btn';
|
|
220
|
+
container.appendChild( playBtn );
|
|
221
|
+
|
|
222
|
+
setupPlayButtonAccessibility( container );
|
|
223
|
+
container.dispatchEvent( new CustomEvent( 'waveformplayer:play' ) );
|
|
224
|
+
|
|
225
|
+
expect( playBtn ).toHaveAttribute( 'aria-label', 'Pause' );
|
|
226
|
+
} );
|
|
227
|
+
|
|
228
|
+
it( 'should change aria-label back to Play on pause event', () => {
|
|
229
|
+
const container = document.createElement( 'div' );
|
|
230
|
+
const playBtn = document.createElement( 'button' );
|
|
231
|
+
playBtn.className = 'waveform-btn';
|
|
232
|
+
container.appendChild( playBtn );
|
|
233
|
+
|
|
234
|
+
setupPlayButtonAccessibility( container );
|
|
235
|
+
container.dispatchEvent( new CustomEvent( 'waveformplayer:play' ) );
|
|
236
|
+
container.dispatchEvent(
|
|
237
|
+
new CustomEvent( 'waveformplayer:pause' )
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect( playBtn ).toHaveAttribute( 'aria-label', 'Play' );
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
it( 'should change aria-label back to Play on ended event', () => {
|
|
244
|
+
const container = document.createElement( 'div' );
|
|
245
|
+
const playBtn = document.createElement( 'button' );
|
|
246
|
+
playBtn.className = 'waveform-btn';
|
|
247
|
+
container.appendChild( playBtn );
|
|
248
|
+
|
|
249
|
+
setupPlayButtonAccessibility( container );
|
|
250
|
+
container.dispatchEvent( new CustomEvent( 'waveformplayer:play' ) );
|
|
251
|
+
container.dispatchEvent(
|
|
252
|
+
new CustomEvent( 'waveformplayer:ended' )
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect( playBtn ).toHaveAttribute( 'aria-label', 'Play' );
|
|
256
|
+
} );
|
|
257
|
+
|
|
258
|
+
it( 'should return cleanup function that removes listeners', () => {
|
|
259
|
+
const container = document.createElement( 'div' );
|
|
260
|
+
const playBtn = document.createElement( 'button' );
|
|
261
|
+
playBtn.className = 'waveform-btn';
|
|
262
|
+
container.appendChild( playBtn );
|
|
263
|
+
|
|
264
|
+
const cleanup = setupPlayButtonAccessibility( container );
|
|
265
|
+
cleanup();
|
|
266
|
+
|
|
267
|
+
// After cleanup, events should not change the label.
|
|
268
|
+
container.dispatchEvent( new CustomEvent( 'waveformplayer:play' ) );
|
|
269
|
+
expect( playBtn ).toHaveAttribute( 'aria-label', 'Play' );
|
|
270
|
+
} );
|
|
271
|
+
|
|
272
|
+
it( 'should do nothing when play button not found', () => {
|
|
273
|
+
const container = document.createElement( 'div' );
|
|
274
|
+
|
|
275
|
+
// Should not throw.
|
|
276
|
+
expect( () =>
|
|
277
|
+
setupPlayButtonAccessibility( container )
|
|
278
|
+
).not.toThrow();
|
|
279
|
+
} );
|
|
280
|
+
} );
|
|
281
|
+
|
|
282
|
+
describe( 'logPlayError', () => {
|
|
283
|
+
let consoleErrorSpy;
|
|
284
|
+
|
|
285
|
+
beforeEach( () => {
|
|
286
|
+
consoleErrorSpy = jest
|
|
287
|
+
.spyOn( console, 'error' )
|
|
288
|
+
.mockImplementation( () => {} );
|
|
289
|
+
} );
|
|
290
|
+
|
|
291
|
+
afterEach( () => {
|
|
292
|
+
consoleErrorSpy.mockRestore();
|
|
293
|
+
} );
|
|
294
|
+
|
|
295
|
+
it( 'should not log AbortError', () => {
|
|
296
|
+
const abortError = new DOMException( 'Aborted', 'AbortError' );
|
|
297
|
+
|
|
298
|
+
logPlayError( abortError );
|
|
299
|
+
|
|
300
|
+
expect( consoleErrorSpy ).not.toHaveBeenCalled();
|
|
301
|
+
} );
|
|
302
|
+
|
|
303
|
+
it( 'should log other errors', () => {
|
|
304
|
+
const otherError = new Error( 'Some other error' );
|
|
305
|
+
|
|
306
|
+
logPlayError( otherError );
|
|
307
|
+
|
|
308
|
+
expect( consoleErrorSpy ).toHaveBeenCalledWith(
|
|
309
|
+
'Playlist play error:',
|
|
310
|
+
otherError
|
|
311
|
+
);
|
|
312
|
+
} );
|
|
313
|
+
|
|
314
|
+
it( 'should log NotAllowedError', () => {
|
|
315
|
+
const notAllowedError = new DOMException(
|
|
316
|
+
'Not allowed',
|
|
317
|
+
'NotAllowedError'
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
logPlayError( notAllowedError );
|
|
321
|
+
|
|
322
|
+
expect( consoleErrorSpy ).toHaveBeenCalledWith(
|
|
323
|
+
'Playlist play error:',
|
|
324
|
+
notAllowedError
|
|
325
|
+
);
|
|
326
|
+
} );
|
|
327
|
+
} );
|
|
328
|
+
} );
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useRef } from '@wordpress/element';
|
|
5
|
+
import { useRefEffect } from '@wordpress/compose';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies
|
|
9
|
+
*/
|
|
10
|
+
import { initWaveformPlayer } from './waveform-utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A reusable WaveformPlayer component for the block editor.
|
|
14
|
+
*
|
|
15
|
+
* Renders an audio waveform visualization with play/pause controls.
|
|
16
|
+
* Automatically inherits colors from the parent block's text color.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} props - Component props.
|
|
19
|
+
* @param {string} props.src - The audio file URL.
|
|
20
|
+
* @param {string} props.title - The track title.
|
|
21
|
+
* @param {string} props.artist - The artist name.
|
|
22
|
+
* @param {string} props.image - The artwork image URL.
|
|
23
|
+
* @param {Function} props.onEnded - Callback when the track finishes playing.
|
|
24
|
+
* @return {Element} The WaveformPlayer element.
|
|
25
|
+
*/
|
|
26
|
+
export function WaveformPlayer( { src, title, artist, image, onEnded } ) {
|
|
27
|
+
// Store onEnded in a ref so it doesn't need to be a useRefEffect dependency.
|
|
28
|
+
// The callback changes reference on every render (its dependency chain
|
|
29
|
+
// includes an unstable array), which would cause useRefEffect to destroy
|
|
30
|
+
// and recreate the entire player on every re-render, making it disappear
|
|
31
|
+
// during editor resizes.
|
|
32
|
+
const onEndedRef = useRef( onEnded );
|
|
33
|
+
onEndedRef.current = onEnded;
|
|
34
|
+
|
|
35
|
+
const ref = useRefEffect(
|
|
36
|
+
( element ) => {
|
|
37
|
+
if ( ! src ) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
let playerDestroy;
|
|
43
|
+
|
|
44
|
+
function init() {
|
|
45
|
+
if ( cancelled ) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { destroy } = initWaveformPlayer( element, {
|
|
49
|
+
src,
|
|
50
|
+
title,
|
|
51
|
+
artist,
|
|
52
|
+
image,
|
|
53
|
+
onEnded: () => onEndedRef.current?.(),
|
|
54
|
+
} );
|
|
55
|
+
playerDestroy = destroy;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Defer initialization so the element inherits the correct
|
|
59
|
+
// text color, which is used to derive waveform colors. In the
|
|
60
|
+
// editor iframe, theme styles (CSS custom properties) are
|
|
61
|
+
// injected dynamically, so getComputedStyle may return the
|
|
62
|
+
// default black on first render.
|
|
63
|
+
// Using a requestAnimationFrame loop isn't sufficient to solve the issue.
|
|
64
|
+
// TODO - find a better option than a setTimeout, so we're not relying on an arbitrary number.
|
|
65
|
+
const timeoutId = setTimeout( init, 100 );
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
clearTimeout( timeoutId );
|
|
70
|
+
playerDestroy?.();
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
[ src, title, artist, image ]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return <div ref={ ref } className="wp-block-playlist__waveform-player" />;
|
|
77
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for waveform audio player functionality.
|
|
3
|
+
* Used by both the WaveformPlayer component (editor) and view.js (frontend).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* External dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { colord } from 'colord';
|
|
10
|
+
import WaveformPlayerLib from '@arraypress/waveform-player';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration constants.
|
|
14
|
+
* Note: DEFAULT_WAVEFORM_HEIGHT should match $waveform-player-height in style.scss.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_WAVEFORM_HEIGHT = 100;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get computed style for an element, using ownerDocument for iframe compatibility.
|
|
20
|
+
*
|
|
21
|
+
* @param {Element} element - The element to get styles from.
|
|
22
|
+
* @return {CSSStyleDeclaration} The computed style.
|
|
23
|
+
*/
|
|
24
|
+
function getComputedStyle( element ) {
|
|
25
|
+
return element.ownerDocument.defaultView.getComputedStyle( element );
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get all colors needed for the waveform player based on the element's styles.
|
|
30
|
+
*
|
|
31
|
+
* @param {Element} element - The element to derive colors from.
|
|
32
|
+
* @return {Object} Object containing textColor, waveformColor, progressColor.
|
|
33
|
+
*/
|
|
34
|
+
export function getWaveformColors( element ) {
|
|
35
|
+
const textColor = getComputedStyle( element ).color;
|
|
36
|
+
const waveformColor = colord( textColor ).alpha( 0.3 ).toRgbString();
|
|
37
|
+
const progressColor = colord( textColor ).alpha( 0.6 ).toRgbString();
|
|
38
|
+
|
|
39
|
+
return { textColor, waveformColor, progressColor };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a waveform container element with the specified attributes.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} options - The options for the container.
|
|
46
|
+
* @param {string} options.url - The audio URL.
|
|
47
|
+
* @param {string} options.title - The track title.
|
|
48
|
+
* @param {string} options.artist - The track artist.
|
|
49
|
+
* @param {string} options.artwork - The album artwork URL.
|
|
50
|
+
* @param {string} options.waveformColor - The waveform bar color.
|
|
51
|
+
* @param {string} options.progressColor - The progress indicator color.
|
|
52
|
+
* @param {string} options.buttonColor - The play button color.
|
|
53
|
+
* @param {number} options.height - The waveform height in pixels.
|
|
54
|
+
* @return {Element} The configured container element.
|
|
55
|
+
*/
|
|
56
|
+
export function createWaveformContainer( {
|
|
57
|
+
url,
|
|
58
|
+
title,
|
|
59
|
+
artist,
|
|
60
|
+
artwork,
|
|
61
|
+
waveformColor,
|
|
62
|
+
progressColor,
|
|
63
|
+
buttonColor,
|
|
64
|
+
height = DEFAULT_WAVEFORM_HEIGHT,
|
|
65
|
+
} ) {
|
|
66
|
+
const container = document.createElement( 'div' );
|
|
67
|
+
container.setAttribute( 'data-waveform-player', '' );
|
|
68
|
+
container.setAttribute( 'data-url', url );
|
|
69
|
+
container.setAttribute( 'data-height', String( height ) );
|
|
70
|
+
container.setAttribute( 'data-waveform-style', 'bars' );
|
|
71
|
+
container.setAttribute( 'data-waveform-color', waveformColor );
|
|
72
|
+
container.setAttribute( 'data-progress-color', progressColor );
|
|
73
|
+
container.setAttribute( 'data-button-color', buttonColor );
|
|
74
|
+
container.setAttribute( 'data-text-color', buttonColor );
|
|
75
|
+
container.setAttribute( 'data-text-secondary-color', buttonColor );
|
|
76
|
+
if ( title ) {
|
|
77
|
+
container.setAttribute( 'data-title', title );
|
|
78
|
+
}
|
|
79
|
+
if ( artist ) {
|
|
80
|
+
container.setAttribute( 'data-subtitle', artist );
|
|
81
|
+
}
|
|
82
|
+
if ( artwork ) {
|
|
83
|
+
container.setAttribute( 'data-artwork', artwork );
|
|
84
|
+
}
|
|
85
|
+
return container;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Apply contrasting color to SVG icon paths for visibility.
|
|
90
|
+
* The icons should contrast with the button background (which uses textColor).
|
|
91
|
+
*
|
|
92
|
+
* @param {Element} container - The waveform container element.
|
|
93
|
+
* @param {string} buttonColor - The button background color (textColor).
|
|
94
|
+
*/
|
|
95
|
+
export function styleSvgIcons( container, buttonColor ) {
|
|
96
|
+
// Compute a contrasting color for the icons based on button brightness.
|
|
97
|
+
const isButtonDark = colord( buttonColor ).isDark();
|
|
98
|
+
const iconColor = isButtonDark ? '#ffffff' : '#000000';
|
|
99
|
+
|
|
100
|
+
const svgPaths = container.querySelectorAll( 'svg path' );
|
|
101
|
+
svgPaths.forEach( ( path ) => {
|
|
102
|
+
path.style.fill = iconColor;
|
|
103
|
+
} );
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set up play button accessibility: aria-label that toggles on play/pause.
|
|
108
|
+
*
|
|
109
|
+
* @param {Element} container - The waveform container element.
|
|
110
|
+
* @param {Object} labels - Button labels.
|
|
111
|
+
* @param {string} labels.play - Label for the play state.
|
|
112
|
+
* @param {string} labels.pause - Label for the pause state.
|
|
113
|
+
*/
|
|
114
|
+
export function setupPlayButtonAccessibility(
|
|
115
|
+
container,
|
|
116
|
+
{ play: playLabel = 'Play', pause: pauseLabel = 'Pause' } = {}
|
|
117
|
+
) {
|
|
118
|
+
const playBtn = container.querySelector( '.waveform-btn' );
|
|
119
|
+
if ( ! playBtn ) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
playBtn.setAttribute( 'aria-label', playLabel );
|
|
124
|
+
|
|
125
|
+
const onPlay = () => playBtn.setAttribute( 'aria-label', pauseLabel );
|
|
126
|
+
const onPause = () => playBtn.setAttribute( 'aria-label', playLabel );
|
|
127
|
+
|
|
128
|
+
container.addEventListener( 'waveformplayer:play', onPlay );
|
|
129
|
+
container.addEventListener( 'waveformplayer:pause', onPause );
|
|
130
|
+
container.addEventListener( 'waveformplayer:ended', onPause );
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
container.removeEventListener( 'waveformplayer:play', onPlay );
|
|
134
|
+
container.removeEventListener( 'waveformplayer:pause', onPause );
|
|
135
|
+
container.removeEventListener( 'waveformplayer:ended', onPause );
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Log play errors, filtering out expected AbortError.
|
|
141
|
+
*
|
|
142
|
+
* @param {Error} error - The error from play().
|
|
143
|
+
*/
|
|
144
|
+
export function logPlayError( error ) {
|
|
145
|
+
// The browser throws AbortError when a play() promise is interrupted
|
|
146
|
+
// by a subsequent pause() or a new audio source load (track change).
|
|
147
|
+
// This is normal during rapid user interaction and safe to ignore.
|
|
148
|
+
if ( error.name === 'AbortError' ) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.error( 'Playlist play error:', error );
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Initialize a WaveformPlayer instance on an element.
|
|
157
|
+
*
|
|
158
|
+
* This is the shared core logic used by both the React component (editor)
|
|
159
|
+
* and the Interactivity API (frontend).
|
|
160
|
+
*
|
|
161
|
+
* @param {Element} element - The container element (must be in DOM).
|
|
162
|
+
* @param {Object} options - Configuration options.
|
|
163
|
+
* @param {string} options.src - The audio file URL.
|
|
164
|
+
* @param {string} options.title - The track title.
|
|
165
|
+
* @param {string} options.artist - The artist name.
|
|
166
|
+
* @param {string} options.image - The artwork image URL.
|
|
167
|
+
* @param {boolean} options.autoPlay - Whether to auto-play when ready.
|
|
168
|
+
* @param {Function} options.onEnded - Callback when track ends.
|
|
169
|
+
* @param {Object} options.labels - Translated button labels.
|
|
170
|
+
* @return {Object} Object with instance, container, and destroy function.
|
|
171
|
+
*/
|
|
172
|
+
export function initWaveformPlayer(
|
|
173
|
+
element,
|
|
174
|
+
{ src, title, artist, image, autoPlay, onEnded, labels }
|
|
175
|
+
) {
|
|
176
|
+
// Get colors from computed styles.
|
|
177
|
+
const { textColor, waveformColor, progressColor } =
|
|
178
|
+
getWaveformColors( element );
|
|
179
|
+
|
|
180
|
+
// Create the waveform container.
|
|
181
|
+
const container = createWaveformContainer( {
|
|
182
|
+
url: src,
|
|
183
|
+
title,
|
|
184
|
+
artist,
|
|
185
|
+
artwork: image,
|
|
186
|
+
waveformColor,
|
|
187
|
+
progressColor,
|
|
188
|
+
buttonColor: textColor,
|
|
189
|
+
} );
|
|
190
|
+
element.appendChild( container );
|
|
191
|
+
|
|
192
|
+
// Initialize the WaveformPlayer library.
|
|
193
|
+
const instance = new WaveformPlayerLib( container );
|
|
194
|
+
|
|
195
|
+
// Set up event handlers.
|
|
196
|
+
let cleanupAccessibility;
|
|
197
|
+
const handlers = {
|
|
198
|
+
ready: () => {
|
|
199
|
+
styleSvgIcons( container, textColor );
|
|
200
|
+
cleanupAccessibility = setupPlayButtonAccessibility(
|
|
201
|
+
container,
|
|
202
|
+
labels
|
|
203
|
+
);
|
|
204
|
+
if ( autoPlay ) {
|
|
205
|
+
instance.play()?.catch( logPlayError );
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
ended: () => onEnded?.(),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
container.addEventListener( 'waveformplayer:ready', handlers.ready );
|
|
212
|
+
container.addEventListener( 'waveformplayer:ended', handlers.ended );
|
|
213
|
+
|
|
214
|
+
// Return instance, container, and cleanup function.
|
|
215
|
+
return {
|
|
216
|
+
instance,
|
|
217
|
+
container,
|
|
218
|
+
destroy: () => {
|
|
219
|
+
cleanupAccessibility?.();
|
|
220
|
+
container.removeEventListener(
|
|
221
|
+
'waveformplayer:ready',
|
|
222
|
+
handlers.ready
|
|
223
|
+
);
|
|
224
|
+
container.removeEventListener(
|
|
225
|
+
'waveformplayer:ended',
|
|
226
|
+
handlers.ended
|
|
227
|
+
);
|
|
228
|
+
instance.destroy();
|
|
229
|
+
container.remove();
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|