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 ) } + /> + + +
+ + { !! ids[ trackListIndex ]?.id && ( +
+ { images && ( + + ) } +
    + { ids[ trackListIndex ]?.title && ( +
  • + ) } + { ids[ trackListIndex ]?.album && ( +
  • + ) } + { ids[ trackListIndex ]?.artist && ( +
  • + ) } +
+
+ ) } + +
+
+ + ); +}; + +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 = '
'; + $html .= '
'; + $html .= ' width='; + $html .= ''; + $html .= ''; + $html .= '
'; // End of current track information. + + if ( $tracklist ) { + $html .= '<' . $tagname . ' class="wp-block-playlist__tracks">'; + foreach ( $attributes['ids'] as $key => $value ) { + $id = isset( $attributes['ids'][ $key ]['id'] ) ? $attributes['ids'][ $key ]['id'] : ''; + $url = isset( $attributes['ids'][ $key ]['url'] ) ? $attributes['ids'][ $key ]['url'] : ''; + $title = isset( $attributes['ids'][ $key ]['title'] ) ? $attributes['ids'][ $key ]['title'] : ''; + $artist = isset( $attributes['ids'][ $key ]['artist'] ) ? $attributes['ids'][ $key ]['artist'] : ''; + $album = isset( $attributes['ids'][ $key ]['album'] ) ? $attributes['ids'][ $key ]['album'] : ''; + $image = isset( $attributes['ids'][ $key ]['image']['src'] ) ? $attributes['ids'][ $key ]['image']['src'] : $placeholder_image; + $length = isset( $attributes['ids'][ $key ]['length'] ) ? $attributes['ids'][ $key ]['length'] : ''; + + $contexts = wp_interactivity_data_wp_context( + array( + 'trackID' => $id, + 'trackURL' => $url, + 'trackTitle' => $title, + 'trackArtist' => $artist, + 'trackAlbum' => $album, + 'trackImageSrc' => $image, + ), + ); + + $html .= '
  • '; + $html .= ''; + $html .= '
  • '; + } + $html .= ''; + } + + $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";