diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index dd49d15685724..2b40cafc74123 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -543,6 +543,15 @@ Show a block pattern. ([Source](https://github.com/WordPress/gutenberg/tree/trun
- **Supports:** interactivity (clientNavigation), ~~html~~, ~~inserter~~, ~~renaming~~
- **Attributes:** slug
+## Playlist
+
+Embed a simple playlist. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/playlist))
+
+- **Name:** core/playlist
+- **Category:** media
+- **Supports:** align, anchor, color (background, gradients, link, text), spacing (margin, padding)
+- **Attributes:** artists, ids, images, order, tracklist, tracknumbers, type
+
## Author
Display post author details such as name, avatar, and bio. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/post-author))
diff --git a/lib/blocks.php b/lib/blocks.php
index c3fdb26700c58..68d82cde0c16e 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -31,6 +31,7 @@ function gutenberg_reregister_core_block_types() {
'more',
'nextpage',
'paragraph',
+ 'playlist',
'preformatted',
'pullquote',
'quote',
@@ -88,6 +89,7 @@ function gutenberg_reregister_core_block_types() {
'post-author.php' => 'core/post-author',
'post-author-name.php' => 'core/post-author-name',
'post-author-biography.php' => 'core/post-author-biography',
+ 'playlist.php' => 'core/playlist',
'post-comment.php' => 'core/post-comment',
'post-comments-count.php' => 'core/post-comments-count',
'post-comments-form.php' => 'core/post-comments-form',
diff --git a/packages/block-library/package.json b/packages/block-library/package.json
index e9e76b8018e1d..12f16c0cd74a2 100644
--- a/packages/block-library/package.json
+++ b/packages/block-library/package.json
@@ -34,6 +34,7 @@
"./file/view": "./build-module/file/view.js",
"./image/view": "./build-module/image/view.js",
"./navigation/view": "./build-module/navigation/view.js",
+ "./playlist/view": "./build-module/playlist/view.js",
"./query/view": "./build-module/query/view.js",
"./search/view": "./build-module/search/view.js"
},
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 56365c87a268f..45917d91efbd3 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -74,6 +74,7 @@ import * as pattern from './pattern';
import * as pageList from './page-list';
import * as pageListItem from './page-list-item';
import * as paragraph from './paragraph';
+import * as playlist from './playlist';
import * as postAuthor from './post-author';
import * as postAuthorName from './post-author-name';
import * as postAuthorBiography from './post-author-biography';
@@ -239,6 +240,10 @@ const getAllBlocks = () => {
blocks.push( formSubmissionNotification );
}
+ if ( window?.__experimentalEnableBlockExperiments ) {
+ blocks.push( playlist );
+ }
+
// When in a WordPress context, conditionally
// add the classic block and TinyMCE editor
// under any of the following conditions:
diff --git a/packages/block-library/src/playlist/block.json b/packages/block-library/src/playlist/block.json
new file mode 100644
index 0000000000000..53c3b3e50969a
--- /dev/null
+++ b/packages/block-library/src/playlist/block.json
@@ -0,0 +1,70 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "core/playlist",
+ "title": "Playlist",
+ "category": "media",
+ "description": "Embed a simple playlist.",
+ "keywords": [ "music", "sound" ],
+ "textdomain": "default",
+ "attributes": {
+ "ids": {
+ "type": "array"
+ },
+ "type": {
+ "type": "string",
+ "default": "audio"
+ },
+ "order": {
+ "type": "string",
+ "default": "ASC"
+ },
+ "tracklist": {
+ "type": "boolean",
+ "default": true
+ },
+ "tracknumbers": {
+ "type": "boolean",
+ "default": true
+ },
+ "images": {
+ "type": "boolean",
+ "default": true
+ },
+ "artists": {
+ "type": "boolean",
+ "default": true
+ }
+ },
+ "supports": {
+ "anchor": true,
+ "align": true,
+ "color": {
+ "gradients": true,
+ "link": true,
+ "__experimentalDefaultControls": {
+ "background": true,
+ "text": true
+ }
+ },
+ "__experimentalBorder": {
+ "color": true,
+ "radius": true,
+ "style": true,
+ "width": true,
+ "__experimentalDefaultControls": {
+ "color": true,
+ "radius": true,
+ "style": true,
+ "width": true
+ }
+ },
+ "interactivity": true,
+ "spacing": {
+ "margin": true,
+ "padding": true
+ }
+ },
+ "editorStyle": "wp-block-playlist-editor",
+ "style": "wp-block-playlist"
+}
diff --git a/packages/block-library/src/playlist/edit.js b/packages/block-library/src/playlist/edit.js
new file mode 100644
index 0000000000000..f627601d059d6
--- /dev/null
+++ b/packages/block-library/src/playlist/edit.js
@@ -0,0 +1,349 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useCallback } from '@wordpress/element';
+import {
+ MediaPlaceholder,
+ MediaReplaceFlow,
+ BlockIcon,
+ useBlockProps,
+ BlockControls,
+ InspectorControls,
+} from '@wordpress/block-editor';
+import {
+ PanelBody,
+ ToggleControl,
+ Disabled,
+ SelectControl,
+ Button,
+} from '@wordpress/components';
+import { useDispatch } from '@wordpress/data';
+import { store as noticesStore } from '@wordpress/notices';
+import { __, _x, sprintf } from '@wordpress/i18n';
+import { audio as icon } from '@wordpress/icons';
+import { safeHTML, __unstableStripHTML as stripHTML } from '@wordpress/dom';
+
+const ALLOWED_MEDIA_TYPES = [ 'audio' ];
+
+const PlaylistEdit = ( { attributes, setAttributes, isSelected } ) => {
+ const {
+ ids,
+ order,
+ tracklist,
+ tracknumbers,
+ images,
+ artists,
+ tagName: TagName = tracknumbers ? 'ol' : 'ul',
+ } = attributes;
+ const [ trackListIndex, setTrackListIndex ] = useState( 0 );
+ const blockProps = useBlockProps();
+
+ const { createErrorNotice } = useDispatch( noticesStore );
+ function onUploadError( message ) {
+ createErrorNotice( message, { type: 'snackbar' } );
+ }
+
+ const onSelectTracks = useCallback(
+ ( media ) => {
+ if ( ! media ) {
+ return;
+ }
+
+ if ( ! Array.isArray( media ) ) {
+ const currentIds = [ ...ids ];
+ media = [ ...currentIds, media ];
+ }
+
+ const trackList = media.map( ( track ) => ( {
+ id: track.id ? track.id : track.url,
+ url: track.url,
+ title: track.title,
+ artist: track.artist,
+ album: track.album,
+ caption: track.caption,
+ length: track.fileLength,
+ image: track.image ? track.image : '',
+ } ) );
+ setAttributes( { ids: trackList } );
+ },
+ [ ids, setAttributes ]
+ );
+
+ const onChangeTrack = useCallback( ( index ) => {
+ setTrackListIndex( index );
+ }, [] );
+
+ const onTrackEnd = useCallback( () => {
+ /* If there are tracks left, play the next track */
+ if ( trackListIndex < ids.length - 1 ) {
+ setTrackListIndex( trackListIndex + 1 );
+ } else {
+ setTrackListIndex( 0 );
+ }
+ }, [ trackListIndex, ids ] );
+
+ const onChangeOrder = useCallback(
+ ( trackOrder ) => {
+ const sortedIds = [ ...ids ];
+ if ( 'ASC' === trackOrder ) {
+ sortedIds.sort( ( a, b ) => a.id - b.id );
+ } else {
+ sortedIds.sort( ( a, b ) => b.id - a.id );
+ }
+
+ setAttributes( { order: trackOrder, ids: sortedIds } );
+ },
+ [ ids, setAttributes ]
+ );
+
+ function toggleAttribute( attribute ) {
+ return ( newValue ) => {
+ setAttributes( { [ attribute ]: newValue } );
+ };
+ }
+
+ if ( ! ids ) {
+ return (
+
+ }
+ labels={ {
+ title: __( 'Playlist' ),
+ instructions: __(
+ 'Upload an audio file or pick one from your media library.'
+ ),
+ } }
+ onSelect={ onSelectTracks }
+ accept="audio/*"
+ multiple
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
+ value={ attributes }
+ onError={ onUploadError }
+ />
+
+ );
+ }
+
+ return (
+ <>
+
+ onSelectTracks( value ) }
+ accept="audio/*"
+ addToPlaylist
+ mediaIds={ ids
+ .filter( ( track ) => track.id )
+ .map( ( track ) => track.id ) }
+ multiple
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
+ value={ attributes }
+ onError={ onUploadError }
+ />
+
+
+
+
+ { tracklist && (
+ <>
+
+
+ >
+ ) }
+
+ onChangeOrder( value ) }
+ />
+
+
+
+ >
+ );
+};
+
+export default PlaylistEdit;
diff --git a/packages/block-library/src/playlist/index.js b/packages/block-library/src/playlist/index.js
new file mode 100644
index 0000000000000..cfaa381675ed0
--- /dev/null
+++ b/packages/block-library/src/playlist/index.js
@@ -0,0 +1,21 @@
+/**
+ * WordPress dependencies
+ */
+import { audio as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import initBlock from '../utils/init-block';
+import metadata from './block.json';
+import edit from './edit';
+
+const { name } = metadata;
+export { metadata, name };
+
+export const settings = {
+ icon,
+ edit,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/playlist/index.php b/packages/block-library/src/playlist/index.php
new file mode 100644
index 0000000000000..06e6027bed54a
--- /dev/null
+++ b/packages/block-library/src/playlist/index.php
@@ -0,0 +1,146 @@
+ $current_id,
+ 'currentURL' => $attributes['ids'][0]['url'],
+ 'currentTitle' => $current_title ,
+ 'currentAlbum ' => $current_album,
+ 'currentArtist' => $current_artist,
+ 'currentImage' => isset( $attributes['ids'][0]['image']['src'] ) ? $attributes['ids'][0]['image']['src'] : $placeholder_image,
+ 'ariaLabel' => $aria_label,
+ )
+ );
+
+ $html = '';
+
+ return $html;
+}
+
+/**
+ * Registers the `core/playlist` block on server.
+ *
+ * @since 6.7.0
+ */
+function register_block_core_playlist() {
+ register_block_type_from_metadata(
+ __DIR__ . '/playlist',
+ array(
+ 'render_callback' => 'render_block_core_playlist',
+ )
+ );
+}
+add_action( 'init', 'register_block_core_playlist' );
diff --git a/packages/block-library/src/playlist/init.js b/packages/block-library/src/playlist/init.js
new file mode 100644
index 0000000000000..79f0492c2cb2f
--- /dev/null
+++ b/packages/block-library/src/playlist/init.js
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import { init } from './';
+
+export default init();
diff --git a/packages/block-library/src/playlist/style.scss b/packages/block-library/src/playlist/style.scss
new file mode 100644
index 0000000000000..603add8dae413
--- /dev/null
+++ b/packages/block-library/src/playlist/style.scss
@@ -0,0 +1,60 @@
+.wp-block-playlist {
+
+ .wp-block-playlist__current-item {
+ audio {
+ width: 100%;
+ margin-top: 10px;
+ }
+ img {
+ float: left;
+ margin-right: 10px;
+ }
+ ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+ min-height: 70px;
+ li {
+ list-style: none;
+ }
+ }
+ .wp-block-playlist__item-album {
+ font-style: italic;
+ }
+ }
+
+ .wp-block-playlist__item {
+ border-bottom: 1px solid #ccc;
+ }
+
+ ol.wp-block-playlist__tracks {
+ padding-left: 20px;
+ }
+ ul.wp-block-playlist__tracks {
+ padding-left: 0;
+ li {
+ list-style: none;
+ }
+ }
+
+ .wp-block-playlist__tracks button {
+ display: flex;
+ min-width: 100%;
+ padding: 0;
+ font-size: 14px;
+ line-height: 1.5;
+ background-color: transparent;
+ color: inherit;
+ border: 0;
+
+ span {
+ margin-right: $grid-unit-10;
+ }
+ .wp-block-playlist__item-length {
+ margin-left: auto;
+ }
+ &[aria-current="true"] {
+ font-weight: bold;
+ }
+ }
+}
diff --git a/packages/block-library/src/playlist/view.js b/packages/block-library/src/playlist/view.js
new file mode 100644
index 0000000000000..095775255c7a2
--- /dev/null
+++ b/packages/block-library/src/playlist/view.js
@@ -0,0 +1,68 @@
+/**
+ * WordPress dependencies
+ */
+import { store, getContext, getElement } from '@wordpress/interactivity';
+
+const { state } = store(
+ 'core/playlist',
+ {
+ actions: {
+ changeTrack() {
+ const context = getContext();
+ const { ref } = getElement();
+ const id = context.trackID;
+ const src = context.trackURL;
+ const title = context.trackTitle;
+ const artist = context.trackArtist;
+ const album = context.trackAlbum;
+ const image = context.trackImageSrc;
+
+ /*
+ * Since we are changing the track, we need to remove aria-current from the buttons,
+ * and re-add it to the button that was clicked.
+ */
+ if ( ref ) {
+ const trackListButtons = ref
+ .closest( '.wp-block-playlist' )
+ .querySelectorAll( '.wp-block-playlist__item button' );
+
+ trackListButtons.forEach( ( buttons ) => {
+ buttons.removeAttribute( 'aria-current' );
+ } );
+
+ ref.setAttribute( 'aria-current', 'true' );
+ }
+
+ if ( id ) {
+ state.currentID = id;
+ }
+ if ( src ) {
+ state.currentURL = src;
+ }
+ if ( title ) {
+ state.currentTitle = title;
+ }
+ if ( artist ) {
+ state.currentArtist = artist;
+ }
+ if ( album ) {
+ state.currentAlbum = album;
+ }
+ if ( image ) {
+ state.currentImage = image;
+ }
+
+ /**
+ * Find the audio element and play the selected track.
+ */
+ const audio = ref
+ .closest( '.wp-block-playlist' )
+ .querySelector( 'audio' );
+ if ( audio ) {
+ audio.play();
+ }
+ },
+ },
+ },
+ { lock: true }
+);
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index a8819c2084dc2..725a32d194970 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -33,6 +33,7 @@
@import "./navigation-link/style.scss";
@import "./page-list/style.scss";
@import "./paragraph/style.scss";
+@import "./playlist/style.scss";
@import "./post-author/style.scss";
@import "./post-author-biography/style.scss";
@import "./post-comments-form/style.scss";