diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 34db859f71d1d..056add23addf0 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -543,6 +543,28 @@ 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 +- **Experimental:** true +- **Category:** media +- **Allowed Blocks:** core/playlist-track +- **Supports:** align, anchor, color (background, gradients, link, text), interactivity, spacing (margin, padding) +- **Attributes:** caption, currentTrack, order, showArtists, showImages, showNumbers, showTracklist, tracks, type + +## Playlist track + +Playlist track. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/playlist-track)) + +- **Name:** core/playlist-track +- **Experimental:** true +- **Category:** media +- **Parent:** core/playlist +- **Supports:** interactivity (clientNavigation), ~~html~~, ~~reusable~~ +- **Attributes:** blob, id, src + ## 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..c17a3617a278e 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -31,6 +31,8 @@ function gutenberg_reregister_core_block_types() { 'more', 'nextpage', 'paragraph', + 'playlist', + 'playlist-track', 'preformatted', 'pullquote', 'quote', @@ -88,6 +90,8 @@ 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', + 'playlist-track.php' => 'core/playlist-track', '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 e507ef3636726..923712d84d194 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -30,6 +30,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/editor.scss b/packages/block-library/src/editor.scss index a16d5a6c2c69c..375659da34e4b 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -31,6 +31,7 @@ @import "./nextpage/editor.scss"; @import "./page-list/editor.scss"; @import "./paragraph/editor.scss"; +@import "./playlist/editor.scss"; @import "./post-author/editor.scss"; @import "./post-excerpt/editor.scss"; @import "./pullquote/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 56365c87a268f..f5fce4b140dcb 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -74,6 +74,8 @@ 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 playlistTrack from './playlist-track'; import * as postAuthor from './post-author'; import * as postAuthorName from './post-author-name'; import * as postAuthorBiography from './post-author-biography'; @@ -239,6 +241,11 @@ const getAllBlocks = () => { blocks.push( formSubmissionNotification ); } + if ( window?.__experimentalEnableBlockExperiments ) { + blocks.push( playlist ); + blocks.push( playlistTrack ); + } + // 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-track/block.json b/packages/block-library/src/playlist-track/block.json new file mode 100755 index 0000000000000..51b8aa3714057 --- /dev/null +++ b/packages/block-library/src/playlist-track/block.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "__experimental": true, + "name": "core/playlist-track", + "title": "Playlist track", + "category": "media", + "parent": [ "core/playlist" ], + "description": "Playlist track.", + "keywords": [ "music", "sound" ], + "textdomain": "default", + "usesContext": [ "showArtists", "currentTrack" ], + "attributes": { + "blob": { + "type": "string", + "role": "local" + }, + "id": { + "type": "number" + }, + "src": { + "type": "string" + } + }, + "supports": { + "html": false, + "interactivity": { + "clientNavigation": true + }, + "reusable": false + }, + "editorStyle": "wp-block-playlist-track-editor", + "style": "wp-block-playlist-track" +} diff --git a/packages/block-library/src/playlist-track/edit.js b/packages/block-library/src/playlist-track/edit.js new file mode 100755 index 0000000000000..51ec06b87277f --- /dev/null +++ b/packages/block-library/src/playlist-track/edit.js @@ -0,0 +1,297 @@ +/** + * WordPress dependencies + */ +import { isBlobURL } from '@wordpress/blob'; +import { useRef, useState } from '@wordpress/element'; +import { + MediaPlaceholder, + MediaReplaceFlow, + MediaUpload, + MediaUploadCheck, + BlockIcon, + useBlockProps, + BlockControls, + InspectorControls, + RichText, +} from '@wordpress/block-editor'; +import { + Button, + PanelBody, + TextControl, + BaseControl, + Spinner, +} from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as coreStore, useEntityProp } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; +import { __ } from '@wordpress/i18n'; +import { audio as icon } from '@wordpress/icons'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; + +/** + * Internal dependencies + */ +import { useUploadMediaFromBlobURL } from '../utils/hooks'; + +const ALLOWED_MEDIA_TYPES = [ 'audio' ]; +const ALBUM_COVER_ALLOWED_MEDIA_TYPES = [ 'image' ]; + +const PlaylistTrackEdit = ( { attributes, setAttributes, context } ) => { + const { id, src } = attributes; + const [ temporaryURL, setTemporaryURL ] = useState( attributes.blob ); + const showArtists = context?.showArtists; + const currentTrack = context?.currentTrack; + const imageButton = useRef(); + const blockProps = useBlockProps(); + const { createErrorNotice } = useDispatch( noticesStore ); + function onUploadError( message ) { + createErrorNotice( message, { type: 'snackbar' } ); + } + + const track = useSelect( + ( select ) => { + const media = id && select( coreStore ).getMedia( id ); + const _isRequestingMedia = + !! id && + ! select( coreStore ).hasFinishedResolution( 'getMedia', [ + id, + { context: 'view' }, + ] ); + + return { + artist: + media?.artist || + media?.meta?.artist || + media?.media_details?.artist || + __( 'Unknown artist' ), + album: + media?.album || + media?.meta?.album || + media?.media_details?.album || + __( 'Unknown album' ), + // Prevent using the default media attachment icon as the track image. + image: + media?.image?.src && + media?.image?.src.endsWith( '/images/media/audio.svg' ) + ? '' + : media?.image?.src, + fileLength: + media?.fileLength || media?.media_details?.length_formatted, + // Important: This is not the media details title, but the title of the attachment. + title: media?.title.rendered, + url: media?.url, + isRequestingMedia: _isRequestingMedia, + }; + }, + [ id ] + ); + + useUploadMediaFromBlobURL( { + src: temporaryURL, + allowedTypes: ALLOWED_MEDIA_TYPES, + onChange: onSelectTrack, + onError: onUploadError, + } ); + + function onSelectTrack( media ) { + if ( ! media || ! media.url ) { + // In this case there was an error and we should continue in the editing state + // previous attributes should be removed because they may be temporary blob urls. + setAttributes( { + blob: undefined, + id: undefined, + } ); + setTemporaryURL(); + return; + } + + if ( isBlobURL( media.url ) ) { + setTemporaryURL( media.url ); + return; + } + + setAttributes( { + blob: undefined, + id: media.id, + src: media.url, + } ); + setTemporaryURL(); + } + + function onSelectAlbumCoverImage( coverImage ) { + setAttributes( { image: coverImage.url } ); + } + + function onRemoveAlbumCoverImage() { + setAttributes( { image: undefined } ); + + // Move focus back to the Media Upload button. + imageButton.current.focus(); + } + + const [ title, setTitle ] = useEntityProp( 'root', 'media', 'title', id ); + + if ( ! src && ! temporaryURL ) { + return ( +
+ } + labels={ { + title: __( 'Track' ), + instructions: __( + 'Upload an audio file or pick one from your media library.' + ), + } } + onSelect={ onSelectTrack } + accept="audio/*" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes } + onError={ onUploadError } + /> +
+ ); + } + + return ( + <> + + + + + + { + setAttributes( { artist: artistValue } ); + } } + /> + { + setAttributes( { album: albumValue } ); + } } + /> + + +
+ + { __( 'Album cover image' ) } + + { !! track.image && ( + { + ) } + ( + + ) } + /> + { !! track.image && ( + + ) } +
+
+
+
+
  • + { !! temporaryURL && } + +
  • + + ); +}; + +export default PlaylistTrackEdit; diff --git a/packages/block-library/src/playlist-track/index.js b/packages/block-library/src/playlist-track/index.js new file mode 100755 index 0000000000000..cfaa381675ed0 --- /dev/null +++ b/packages/block-library/src/playlist-track/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-track/index.php b/packages/block-library/src/playlist-track/index.php new file mode 100755 index 0000000000000..8217a57892bcc --- /dev/null +++ b/packages/block-library/src/playlist-track/index.php @@ -0,0 +1,104 @@ + $unique_id ) ); + + wp_interactivity_state( + 'core/playlist', + array( + 'tracks' => array( + $unique_id => array( + 'media_id' => $media_id, + 'url' => $url, + 'title' => $title, + 'artist' => $artist, + 'album' => $album, + 'image' => $image, + 'length' => $length, + 'ariaLabel' => $aria_label, + ), + ), + ) + ); + + $html = '
  • '; + $html .= ''; + $html .= '
  • '; + + return $html; +} + +/** + * Registers the `core/playlist-track` block on server. + * + * @since 6.8.0 + */ +function register_block_core_playlist_track() { + register_block_type_from_metadata( + __DIR__ . '/playlist-track', + array( + 'render_callback' => 'render_block_core_playlist_track', + ) + ); +} +add_action( 'init', 'register_block_core_playlist_track' ); diff --git a/packages/block-library/src/playlist-track/init.js b/packages/block-library/src/playlist-track/init.js new file mode 100755 index 0000000000000..79f0492c2cb2f --- /dev/null +++ b/packages/block-library/src/playlist-track/init.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import { init } from './'; + +export default init(); diff --git a/packages/block-library/src/playlist-track/style.scss b/packages/block-library/src/playlist-track/style.scss new file mode 100755 index 0000000000000..5745c30a1fa72 --- /dev/null +++ b/packages/block-library/src/playlist-track/style.scss @@ -0,0 +1,29 @@ +.wp-block-playlist-track { + border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent); + padding: $grid-unit-10; + + .wp-block-playlist-track__button { + display: flex; + flex-wrap: wrap; + height: auto; + min-width: 100%; + padding: 0; + font-size: 14px; + font-family: inherit; + text-align: left; // Override default button text-align. + line-height: 1.5; + background-color: transparent; + color: inherit; + border: 0; + + span { + margin-right: $grid-unit-10; + } + .wp-block-playlist-track__length { + margin-left: auto; + } + &[aria-current="true"] { + font-weight: 600; + } + } +} diff --git a/packages/block-library/src/playlist/block.json b/packages/block-library/src/playlist/block.json new file mode 100644 index 0000000000000..c5e2b5997a569 --- /dev/null +++ b/packages/block-library/src/playlist/block.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "__experimental": true, + "name": "core/playlist", + "title": "Playlist", + "category": "media", + "description": "Embed a simple playlist.", + "keywords": [ "music", "sound" ], + "textdomain": "default", + "allowedBlocks": [ "core/playlist-track" ], + "attributes": { + "tracks": { + "type": "array" + }, + "currentTrack": { + "type": "number" + }, + "type": { + "type": "string", + "default": "audio" + }, + "order": { + "type": "string", + "default": "ASC" + }, + "showTracklist": { + "type": "boolean", + "default": true + }, + "showImages": { + "type": "boolean", + "default": true + }, + "showArtists": { + "type": "boolean", + "default": true + }, + "showNumbers": { + "type": "boolean", + "default": true + }, + "caption": { + "type": "string" + } + }, + "providesContext": { + "showArtists": "showArtists", + "currentTrack": "currentTrack" + }, + "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..2203fb3192083 --- /dev/null +++ b/packages/block-library/src/playlist/edit.js @@ -0,0 +1,472 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { useState, useCallback, useEffect } from '@wordpress/element'; +import { + store as blockEditorStore, + MediaPlaceholder, + MediaReplaceFlow, + BlockIcon, + useBlockProps, + useInnerBlocksProps, + BlockControls, + InspectorControls, + InnerBlocks, +} from '@wordpress/block-editor'; +import { + PanelBody, + ToggleControl, + Disabled, + SelectControl, + Spinner, +} from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-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'; +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { Caption } from '../utils/caption'; + +const ALLOWED_MEDIA_TYPES = [ 'audio' ]; + +const CurrentTrack = ( { trackData, showImages, onTrackEnd } ) => { + const id = trackData?.id; + /** + * dangerouslySetInnerHTML and safeHTML are used because + * the media library allows using some HTML tags in the title, artist, and album fields. + */ + const track = useSelect( + ( select ) => { + const media = id && select( coreStore ).getMedia( id ); + return { + artist: + media?.artist || + media?.meta?.artist || + media?.media_details?.artist || + __( 'Unknown artist' ), + album: + media?.album || + media?.meta?.album || + media?.media_details?.album || + __( 'Unknown album' ), + // Prevent using the default media attachment icon as the track image. + image: + media?.image?.src && + media?.image?.src.endsWith( '/images/media/audio.svg' ) + ? '' + : media?.image?.src, + fileLength: + media?.fileLength || media?.media_details?.length_formatted, + // Important: This is not the media details title, but the title of the attachment. + title: media?.title.rendered, + url: media?.url, + }; + }, + [ id ] + ); + + let ariaLabel; + if ( track?.title && track?.artist && track?.album ) { + ariaLabel = stripHTML( + sprintf( + /* translators: %1$s: track title, %2$s artist name, %3$s: album name. */ + _x( + '%1$s by %2$s from the album %3$s', + 'track title, artist name, album name' + ), + track?.title, + track?.artist, + track?.album + ) + ); + } else if ( track?.title ) { + ariaLabel = stripHTML( track.title ); + } else { + ariaLabel = stripHTML( __( 'Untitled' ) ); + } + + return ( + <> +
    + { showImages && track?.image && ( + + ) } +
    + { ! track?.title ? ( + + + + ) : ( + + ) } +
    + + +
    +
    +
    +