From 8b44789a159c89a174b80d660e00871f697c34ab Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Thu, 11 May 2023 11:08:57 +0200 Subject: [PATCH 01/22] Basic playlist block --- docs/reference-guides/core-blocks.md | 9 + lib/blocks.php | 2 + lib/experimental/editor-settings.php | 3 + lib/experiments-page.php | 12 + packages/block-library/src/index.js | 4 + .../block-library/src/playlist/block.json | 58 ++++ packages/block-library/src/playlist/edit.js | 254 ++++++++++++++++++ packages/block-library/src/playlist/index.js | 23 ++ packages/block-library/src/playlist/init.js | 6 + packages/block-library/src/playlist/save.js | 102 +++++++ .../block-library/src/playlist/style.scss | 67 +++++ packages/block-library/src/playlist/view.js | 93 +++++++ packages/block-library/src/style.scss | 1 + 13 files changed, 634 insertions(+) create mode 100644 packages/block-library/src/playlist/block.json create mode 100644 packages/block-library/src/playlist/edit.js create mode 100644 packages/block-library/src/playlist/index.js create mode 100644 packages/block-library/src/playlist/init.js create mode 100644 packages/block-library/src/playlist/save.js create mode 100644 packages/block-library/src/playlist/style.scss create mode 100644 packages/block-library/src/playlist/view.js diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index e5d20bb7b1edd5..25790ba3ab4e92 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -467,6 +467,15 @@ Show a block pattern. ([Source](https://github.com/WordPress/gutenberg/tree/trun - **Supports:** ~~html~~, ~~inserter~~ - **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, spacing (margin, padding) +- **Attributes:** artists, ids, images, order, tracklist, tracknumbers, type + ## Post 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 bbee108b71c5f5..29abddd4b6753f 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -32,6 +32,7 @@ function gutenberg_reregister_core_block_types() { 'more', 'nextpage', 'paragraph', + 'playlist', 'preformatted', 'pullquote', 'quote', @@ -81,6 +82,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/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index c7dd5850a505c3..fc83f1e8e333b9 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -98,6 +98,9 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-interactivity-api-navigation-block', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableNavigationBlockInteractivity = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-playlist-block', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnablePlaylistBlock = true', 'before' ); + } if ( $gutenberg_experiments && array_key_exists( 'gutenberg-theme-previews', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableThemePreviews = true', 'before' ); } diff --git a/lib/experiments-page.php b/lib/experiments-page.php index ee51f5bea49f96..e670548943bade 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -125,6 +125,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-playlist-block', + __( 'Playlist block', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test the Playlist block', 'gutenberg' ), + 'id' => 'gutenberg-playlist-block', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index a0c7b75eac19b8..61024c6c0dfb23 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -70,6 +70,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'; @@ -229,6 +230,9 @@ const getAllBlocks = () => { if ( window?.__experimentalEnableDetailsBlocks ) { blocks.push( details ); } + if ( window?.__experimentalEnablePlaylistBlock ) { + blocks.push( playlist ); + } return blocks.filter( Boolean ); }; diff --git a/packages/block-library/src/playlist/block.json b/packages/block-library/src/playlist/block.json new file mode 100644 index 00000000000000..a8680dba3a0a56 --- /dev/null +++ b/packages/block-library/src/playlist/block.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "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, + "spacing": { + "margin": true, + "padding": true + } + }, + "styles": [ + { + "name": "light", + "label": "Light", + "isDefault": true + }, + { "name": "dark", "label": "Dark" } + ], + "viewScript": "file:./view.min.js", + "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 00000000000000..2fa48e802c871c --- /dev/null +++ b/packages/block-library/src/playlist/edit.js @@ -0,0 +1,254 @@ +/** + * 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 { __ } from '@wordpress/i18n'; +import { audio as icon } from '@wordpress/icons'; + +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; + } + const trackList = media.map( ( track ) => ( { + id: track.id, + url: track.url, + title: track.title, + artist: track.artist, + album: track.album, + caption: track.caption, + length: track.fileLength, + image: track.image, + } ) ); + setAttributes( { ids: trackList } ); + }, + [ 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' ), + } } + onSelect={ onSelectTracks } + accept="audio/*" + addToPlaylist={ true } + multiple={ true } + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes } + onError={ onUploadError } + /> +
+ ); + } + + return ( + <> + + track.id ) + .map( ( track ) => track.id ) } + addToPlaylist={ true } + onSelect={ onSelectTracks } + value={ attributes } + onError={ onUploadError } + /> + + + + + + + + onChangeOrder( value ) } + /> + + +
+ + { !! ids[ trackListIndex ].id && ( +
+ { images && ( + + ) } +
    +
  • + { '\u201c' + + ids[ trackListIndex ]?.title + + '\u201d' } +
  • +
  • + { 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 00000000000000..6a6d898a036ccf --- /dev/null +++ b/packages/block-library/src/playlist/index.js @@ -0,0 +1,23 @@ +/** + * 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'; +import save from './save'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + icon, + edit, + save, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/playlist/init.js b/packages/block-library/src/playlist/init.js new file mode 100644 index 00000000000000..79f0492c2cb2f8 --- /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/save.js b/packages/block-library/src/playlist/save.js new file mode 100644 index 00000000000000..256bc4b6da0bac --- /dev/null +++ b/packages/block-library/src/playlist/save.js @@ -0,0 +1,102 @@ +/** + * WordPress dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + const { + ids, + order, + tracklist, + tracknumbers, + images, + artists, + tagName: TagName = tracknumbers ? 'ol' : 'ul', + } = attributes; + + if ( ! ids ) { + return; + } + + const sortedIds = ids; + if ( 'ASC' === order ) { + sortedIds.sort( ( a, b ) => a.id - b.id ); + } else { + sortedIds.sort( ( a, b ) => b.id - a.id ); + } + + const currentTrack = sortedIds[ 0 ]; + + return ( + <> +
+
+ { images && ( + + ) } +
    +
  • + { '\u201c' + currentTrack?.title + '\u201d' } +
  • +
  • + { currentTrack?.album } +
  • +
  • + { currentTrack?.artist } +
  • +
+
+ +
+ + ); +} diff --git a/packages/block-library/src/playlist/style.scss b/packages/block-library/src/playlist/style.scss new file mode 100644 index 00000000000000..63a24bbd906dbf --- /dev/null +++ b/packages/block-library/src/playlist/style.scss @@ -0,0 +1,67 @@ +.wp-block-playlist { + border: 1px solid #ccc; + padding: 10px; + font-size: 14px; + + .wp-block-playlist__current-item { + margin-bottom: 10px; + audio { + width: 100%; + } + img { + float: left; + margin-right: 10px; + } + ul { + display: block; + margin-bottom: 10px; + padding: 0; + 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; + border: 0; + + span { + margin-right: $grid-unit-10; + } + .wp-block-playlist__item-length { + margin-left: auto; + } + &[aria-current="true"] { + font-weight: bold; + } + } + + // Variation, WIP + &.is-style-dark, + .is-style-dark { + background-color: #111; + } +} diff --git a/packages/block-library/src/playlist/view.js b/packages/block-library/src/playlist/view.js new file mode 100644 index 00000000000000..5c03a687247161 --- /dev/null +++ b/packages/block-library/src/playlist/view.js @@ -0,0 +1,93 @@ +window.addEventListener( 'load', () => { + /* + * There may be multiple playlists on the page, so we need to find them all + * and make sure the click events trigger on the correct playlist. + */ + const playlists = document.querySelectorAll( '.wp-block-playlist' ); + + function changeTrack( event ) { + // Find the closest button element, to make sure we are not targeting the span inside the button. + const button = event.target.closest( 'button' ); + if ( ! button ) { + return; + } + // Find the playlist block that is closest to the clicked button. + const playlist = button.closest( '.wp-block-playlist' ); + if ( ! playlist ) { + return; + } + + // Find the audio element inside the playlist. + const audio = playlist.querySelector( 'audio' ); + // Get the url from the button data attribute and change the current track. + audio.src = button.getAttribute( 'data-playlist-track-url' ); + + /* + * 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. + */ + const trackListButtons = playlist.querySelectorAll( + '.wp-block-playlist__item button' + ); + trackListButtons.forEach( ( buttons ) => { + buttons.removeAttribute( 'aria-current' ); + } ); + button.setAttribute( 'aria-current', 'true' ); + + const image = playlist.querySelector( 'img' ); + /* The image is optional, check if it exists before changing it. */ + if ( image ) { + image.src = button.getAttribute( 'data-playlist-track-image-src' ); + } + + const title = playlist.querySelector( + '.wp-block-playlist__item-title' + ); + title.innerHTML = button.getAttribute( 'data-playlist-track-title' ); + + const artist = playlist.querySelector( + '.wp-block-playlist__item-artist' + ); + artist.innerHTML = button.getAttribute( 'data-playlist-track-artist' ); + + const album = playlist.querySelector( + '.wp-block-playlist__item-album' + ); + album.innerHTML = button.getAttribute( 'data-playlist-track-album' ); + + // Finally, play the selected track. + audio.play(); + } + + function onTrackEnd( audio ) { + // Find the playlist block that is closest to the audio element. + const playlist = audio.closest( '.wp-block-playlist' ); + if ( ! playlist ) { + return; + } + + // Find the next track button. + const nextTrackButton = playlist + .querySelector( + '.wp-block-playlist__item button[aria-current="true"]' + ) + .closest( '.wp-block-playlist__item' ).nextElementSibling; + + // If there is a next track button, click it. + if ( nextTrackButton ) { + nextTrackButton.querySelector( 'button' ).click(); + } + } + + playlists.forEach( ( playlist ) => { + const trackListButtons = playlist.querySelectorAll( + '.wp-block-playlist__item button' + ); + const audio = playlist.querySelector( 'audio' ); + audio.addEventListener( 'ended', () => onTrackEnd( audio ) ); + + trackListButtons.forEach( function ( button ) { + button.addEventListener( 'click', changeTrack ); + } ); + } ); +} ); diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 8fe8b904c23f58..f86bb9df512eb3 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -27,6 +27,7 @@ @import "./page-list/style.scss"; @import "./paragraph/style.scss"; @import "./post-author/style.scss"; +@import "./playlist/style.scss"; @import "./post-comments-form/style.scss"; @import "./post-date/style.scss"; @import "./post-excerpt/style.scss"; From 4198e219f1138b81a09a0f50e42ad3eab3590e2a Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Fri, 12 May 2023 13:25:20 +0200 Subject: [PATCH 02/22] try to make some accessibility improvements --- packages/block-library/src/playlist/edit.js | 19 +++++++++++++++++++ packages/block-library/src/playlist/save.js | 19 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/playlist/edit.js b/packages/block-library/src/playlist/edit.js index 2fa48e802c871c..7ff0029e06bbd9 100644 --- a/packages/block-library/src/playlist/edit.js +++ b/packages/block-library/src/playlist/edit.js @@ -199,6 +199,14 @@ const PlaylistEdit = ( { attributes, setAttributes, isSelected } ) => { controls="controls" src={ ids[ trackListIndex ].url } onEnded={ onTrackEnd } + aria-label={ + ids[ trackListIndex ]?.title + + ', ' + + ids[ trackListIndex ]?.album + + ', ' + + ids[ trackListIndex ]?.artist + } + tabIndex={ 0 } /> ) } @@ -239,8 +247,19 @@ const PlaylistEdit = ( { attributes, setAttributes, isSelected } ) => { ) } + { track?.length && ( + + { + /* translators: %s: track length in "minutes:seconds" format */ + __( 'Length:' ) + } + + ) } { track?.length } + + { __( 'Select to play this track' ) } + ) ) } diff --git a/packages/block-library/src/playlist/save.js b/packages/block-library/src/playlist/save.js index 256bc4b6da0bac..690262acd16486 100644 --- a/packages/block-library/src/playlist/save.js +++ b/packages/block-library/src/playlist/save.js @@ -53,7 +53,13 @@ export default function save( { attributes } ) {