diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 05ce3b0ef317e6..896d9056d29d7c 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -588,6 +588,15 @@ Show a block pattern. ([Source](https://github.com/WordPress/gutenberg/tree/trun - **Supports:** interactivity (clientNavigation), ~~html~~, ~~inserter~~, ~~renaming~~ - **Attributes:** slug +## Playlist + +Display an audio playlist. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/playlist)) + +- **Name:** core/playlist +- **Category:** media +- **Supports:** align, anchor, interactivity (clientNavigation), spacing (margin, padding) +- **Attributes:** autoplay, items, loop, showItemList + ## 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 7d56a4a5df8819..fb502eba47de92 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -88,6 +88,7 @@ function gutenberg_reregister_core_block_types() { 'page-list.php' => 'core/page-list', 'page-list-item.php' => 'core/page-list-item', 'pattern.php' => 'core/pattern', + 'playlist.php' => 'core/playlist', 'post-author.php' => 'core/post-author', 'post-author-name.php' => 'core/post-author-name', 'post-author-biography.php' => 'core/post-author-biography', diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 1e2c9f1ff66362..a70ae7bdba054f 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -32,6 +32,7 @@ "./form/view": "./build-module/form/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 feea74b786a4a3..7c11f1ebca01a0 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-excerpt/editor.scss"; @import "./pullquote/editor.scss"; @import "./rss/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index f82df7d9a23ce0..233d3d3133ef20 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -108,6 +108,7 @@ import * as queryPaginationPrevious from './query-pagination-previous'; import * as queryTitle from './query-title'; import * as queryTotal from './query-total'; import * as quote from './quote'; +import * as playlist from './playlist'; import * as reusableBlock from './block'; import * as readMore from './read-more'; import * as rss from './rss'; @@ -152,6 +153,7 @@ const getAllBlocks = () => { // Register all remaining core blocks. archives, audio, + playlist, button, buttons, calendar, diff --git a/packages/block-library/src/playlist/block.json b/packages/block-library/src/playlist/block.json new file mode 100644 index 00000000000000..f6f89cc1abc014 --- /dev/null +++ b/packages/block-library/src/playlist/block.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "core/playlist", + "title": "Playlist", + "category": "media", + "description": "Display an audio playlist.", + "keywords": [ "audio", "music", "sound", "tracks" ], + "textdomain": "default", + "attributes": { + "items": { + "type": "array", + "default": [], + "items": { + "type": "number" + } + }, + "autoplay": { + "type": "boolean", + "default": false + }, + "loop": { + "type": "boolean", + "default": false + }, + "showItemList": { + "type": "boolean", + "default": true + } + }, + "supports": { + "anchor": true, + "align": true, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": true, + "radius": false, + "style": true, + "width": true + } + }, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": true + } + }, + "interactivity": { + "clientNavigation": true + } + }, + "editorStyle": "wp-block-playlist-editor", + "style": "wp-block-playlist", + "viewScriptModule": "@wordpress/block-library/playlist/view" +} diff --git a/packages/block-library/src/playlist/edit.js b/packages/block-library/src/playlist/edit.js new file mode 100644 index 00000000000000..7f1e14b37c21f5 --- /dev/null +++ b/packages/block-library/src/playlist/edit.js @@ -0,0 +1,417 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { + ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalHStack as HStack, + Spinner, + Button, +} from '@wordpress/components'; +import { + BlockIcon, + InspectorControls, + MediaPlaceholder, + MediaReplaceFlow, + useBlockProps, + BlockControls, +} from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { __, sprintf } from '@wordpress/i18n'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + audio, + chevronUp, + chevronDown, + tableRowDelete, +} from '@wordpress/icons'; +import { store as noticesStore } from '@wordpress/notices'; +import { useMemo, useState } from '@wordpress/element'; +import { isBlobURL } from '@wordpress/blob'; + +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + +const ALLOWED_MEDIA_TYPES = [ 'audio' ]; + +function PlaylistEdit( { + attributes: { items, autoplay, loop, showItemList }, + setAttributes, +} ) { + const [ currentItem, setCurrentItem ] = useState( 0 ); + const { createErrorNotice } = useDispatch( noticesStore ); + const itemsData = useSelect( + ( select ) => { + if ( ! items?.length ) { + return; + } + // We always create a set with the ids and sort them to avoid + // unnecessary requests when items change. + const uniqueItems = Array.from( new Set( items ) ).sort( + ( a, b ) => a - b + ); + // TODO: how can we avoid this request if we already have the data? + // when removing an item for example. + return select( coreStore ).getEntityRecords( + 'postType', + 'attachment', + { + include: uniqueItems.join( ',' ), + per_page: -1, + orderby: 'include', + } + ); + }, + [ items ] + ); + // Build items array because a playlist can contain the same item multiple times. + // Additionally we preserve the order of items that might be different in case of + // duplicates. + const orderedItemData = useMemo( () => { + if ( ! items?.length || ! itemsData?.length ) { + return; + } + return items + .map( ( itemId ) => itemsData.find( ( { id } ) => id === itemId ) ) + .filter( Boolean ); + }, [ items, itemsData ] ); + const currentItemData = orderedItemData?.[ currentItem ]; + function onSelectAudio( media ) { + if ( ! media ) { + return; + } + + const mediaArray = Array.isArray( media ) ? media : [ media ]; + + // Skip intermediate calls with blob URLs (upload in progress) + const hasBlobURLs = mediaArray.some( + ( file ) => file.url && isBlobURL( file.url ) + ); + if ( hasBlobURLs ) { + return; + } + + // Filter out invalid entries (missing url or id) + // MediaPlaceholder with allowedTypes already filters to audio files only. + const validMedia = mediaArray.filter( ( file ) => file.url && file.id ); + + if ( validMedia.length === 0 ) { + createErrorNotice( __( 'Please select valid audio files.' ), { + id: 'playlist-upload-invalid-file', + type: 'snackbar', + } ); + return; + } + + // Extract IDs from valid media + const newItemIds = validMedia.map( ( file ) => file.id ); + + setAttributes( { + items: [ ...( items || [] ), ...newItemIds ], + } ); + } + function onUploadError( message ) { + createErrorNotice( message, { type: 'snackbar' } ); + } + function removeItem( index ) { + const newItems = items.toSpliced( index, 1 ); + setAttributes( { + items: newItems, + } ); + if ( currentItem >= newItems.length ) { + setCurrentItem( Math.max( 0, newItems.length - 1 ) ); + } + } + function moveItemUp( index ) { + const newItems = [ ...items ]; + [ newItems[ index - 1 ], newItems[ index ] ] = [ + newItems[ index ], + newItems[ index - 1 ], + ]; + // Update current item only if we moved it. + if ( currentItem === index ) { + setCurrentItem( index - 1 ); + } + setAttributes( { items: newItems } ); + } + function moveItemDown( index ) { + const newItems = [ ...items ]; + [ newItems[ index ], newItems[ index + 1 ] ] = [ + newItems[ index + 1 ], + newItems[ index ], + ]; + // Update current item only if we moved it. + if ( currentItem === index ) { + setCurrentItem( index + 1 ); + } + setAttributes( { items: newItems } ); + } + const blockProps = useBlockProps(); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + if ( items?.length && ! orderedItemData ) { + return ( +
+
+ +
+
+ ); + } + // TODO: check if it has items but there are no records in database.. + if ( ! items?.length ) { + return ( +
+ } + labels={ { + title: __( 'Playlist' ), + instructions: __( + 'Drag and drop audio files, upload, or choose from your library.' + ), + } } + onSelect={ onSelectAudio } + accept="audio/*" + allowedTypes={ ALLOWED_MEDIA_TYPES } + multiple + onError={ onUploadError } + /> +
+ ); + } + return ( + <> + + { /* TODO: probably only allow single replace.. */ } + + + + { + setAttributes( { + autoplay: false, + loop: false, + showItemList: true, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + autoplay !== false } + label={ __( 'Autoplay' ) } + onDeselect={ () => + setAttributes( { autoplay: false } ) + } + isShownByDefault + > + + setAttributes( { autoplay: value } ) + } + help={ + autoplay + ? __( + 'Autoplay may cause usability issues for some users.' + ) + : null + } + /> + + loop !== false } + label={ __( 'Loop' ) } + onDeselect={ () => setAttributes( { loop: false } ) } + isShownByDefault + > + + setAttributes( { loop: value } ) + } + /> + + showItemList !== true } + label={ __( 'Show item list' ) } + onDeselect={ () => + setAttributes( { showItemList: true } ) + } + isShownByDefault + > + + setAttributes( { showItemList: value } ) + } + /> + + + +
+
+ { currentItemData && ( +
+ { currentItemData.media_details?.sizes?.thumbnail + ?.source_url && ( + + ) } +
+
+ { currentItemData.title?.rendered || + currentItemData.title || + __( 'Untitled' ) } +
+ { currentItemData.media_details?.artist && ( +
+ { sprintf( + // translators: %s is the artist name. + __( 'by %s' ), + currentItemData.media_details.artist + ) } +
+ ) } +
+
+ ) } + { currentItemData?.source_url && ( +
+ { showItemList && !! orderedItemData?.length && ( +
    + { orderedItemData.map( ( item, index ) => ( + // TODO: check this below.. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions, jsx-a11y/no-noninteractive-element-interactions +
  1. { + if ( currentItem !== index ) { + setCurrentItem( index ); + } + } } + > + + + { index + 1 }. + +
    +
    + { item.title?.rendered || + item.title || + __( 'Untitled' ) } +
    + { item.media_details?.artist && ( +
    + { sprintf( + // translators: %s is the artist name. + __( 'by %s' ), + item.media_details.artist + ) } +
    + ) } +
    + + { orderedItemData.length !== 1 && ( + <> +
  2. + ) ) } +
+ ) } +
+ + ); +} + +export default PlaylistEdit; diff --git a/packages/block-library/src/playlist/editor.scss b/packages/block-library/src/playlist/editor.scss new file mode 100644 index 00000000000000..99ba7a981f843f --- /dev/null +++ b/packages/block-library/src/playlist/editor.scss @@ -0,0 +1,5 @@ +// TODO: better way to handle this.. +.wp-block-playlist__item-controls { + flex-shrink: 0; + margin-left: auto; +} diff --git a/packages/block-library/src/playlist/index.js b/packages/block-library/src/playlist/index.js new file mode 100644 index 00000000000000..37edb43bc0ff84 --- /dev/null +++ b/packages/block-library/src/playlist/index.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { audio as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import edit from './edit'; +import metadata from './block.json'; +import save from './save'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + icon, + example: { + attributes: {}, + }, + edit, + save, +}; + +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 00000000000000..9a17cff3a1e4e7 --- /dev/null +++ b/packages/block-library/src/playlist/index.php @@ -0,0 +1,235 @@ + 'attachment', + 'post__in' => $items, + 'posts_per_page' => -1, + ) + ); + + // Index attachments by ID for quick lookup. + $attachments_by_id = array(); + foreach ( $attachments as $attachment ) { + $attachments_by_id[ $attachment->ID ] = $attachment; + } + + // Build items data maintaining the order from $items array. + $items_data = array(); + foreach ( $items as $item_id ) { + if ( ! isset( $attachments_by_id[ $item_id ] ) ) { + continue; + } + + $attachment = $attachments_by_id[ $item_id ]; + + $item_info = array( + 'id' => $item_id, + 'url' => wp_get_attachment_url( $item_id ), + 'title' => get_the_title( $item_id ), + 'image' => '', + 'artist' => '', + ); + + // Get thumbnail if available. + // TODO: This should probably be a custom field on the attachment.. + $thumbnail_id = get_post_thumbnail_id( $item_id ); + if ( $thumbnail_id ) { + $thumbnail = wp_get_attachment_image_src( $thumbnail_id, 'thumbnail' ); + if ( $thumbnail ) { + $item_info['image'] = $thumbnail[0]; + } + } + + // Get audio metadata (artist, etc). + $metadata = wp_get_attachment_metadata( $item_id ); + if ( ! empty( $metadata['audio']['artist'] ) ) { + $item_info['artist'] = $metadata['audio']['artist']; + } elseif ( ! empty( $metadata['artist'] ) ) { + $item_info['artist'] = $metadata['artist']; + } + + $items_data[] = $item_info; + } + + if ( empty( $items_data ) ) { + return ''; + } + + // Current item is always the first item. + $current_item_data = $items_data[0]; + + // Build the context for Interactivity API. + $context = array( + 'items' => $items, + 'itemsData' => $items_data, + 'currentItem' => $current_item, + 'isPlaying' => false, + 'autoplay' => $autoplay, + 'loop' => $loop, + ); + + // Build header image. + $header_image = ''; + if ( ! empty( $current_item_data['image'] ) ) { + $img = new WP_HTML_Tag_Processor( + sprintf( + '', + esc_url( $current_item_data['image'] ) + ) + ); + if ( $img->next_tag() ) { + $img->add_class( 'wp-block-playlist__header-image' ); + $img->set_attribute( 'data-wp-bind--src', 'state.currentItemImage' ); + } + $header_image = $img->get_updated_html(); + } + + // Build header artist. + $header_artist = ''; + if ( ! empty( $current_item_data['artist'] ) ) { + $artist_text = sprintf( + /* translators: %s is the artist name. */ + __( 'by %s' ), + esc_html( $current_item_data['artist'] ) + ); + $artist = new WP_HTML_Tag_Processor( sprintf( '
%s
', $artist_text ) ); + if ( $artist->next_tag() ) { + $artist->set_attribute( 'data-wp-text', 'state.currentItemArtist' ); + } + $header_artist = $artist->get_updated_html(); + } + + // Build header title. + $title = new WP_HTML_Tag_Processor( sprintf( '
%s
', esc_html( $current_item_data['title'] ) ) ); + if ( $title->next_tag() ) { + $title->set_attribute( 'data-wp-text', 'state.currentItemTitle' ); + } + $header_title = $title->get_updated_html(); + + // Build header. + $header = sprintf( + '
%s
%s%s
', + $header_image, + $header_title, + $header_artist + ); + + // Build audio element. + $audio = new WP_HTML_Tag_Processor( + sprintf( + '', + esc_url( $current_item_data['url'] ), + $autoplay ? 'autoplay' : '' + ) + ); + if ( $audio->next_tag() ) { + $audio->add_class( 'wp-block-playlist__audio' ); + $audio->set_attribute( 'data-wp-bind--src', 'state.currentItemSrc' ); + $audio->set_attribute( 'data-wp-on--ended', 'actions.onItemEnded' ); + $audio->set_attribute( 'data-wp-on--play', 'actions.onPlay' ); + $audio->set_attribute( 'data-wp-on--pause', 'actions.onPause' ); + } + + // Build player. + $player = sprintf( + '
%s%s
', + $header, + $audio->get_updated_html() + ); + + // Build items list. + $items_list = ''; + if ( $show_items ) { + $list_items = ''; + foreach ( $items_data as $index => $item ) { + $item_classes = array( 'wp-block-playlist__item' ); + if ( $index === 0 ) { + $item_classes[] = 'is-active'; + } + + $item_context = wp_json_encode( array( 'itemIndex' => $index ) ); + $item_number = sprintf( + '%d.', + (int) $index + 1 + ); + $item_artist = ''; + if ( ! empty( $item['artist'] ) ) { + $item_artist = sprintf( + '
%s
', + sprintf( + /* translators: %s is the artist name. */ + __( 'by %s' ), + esc_html( $item['artist'] ) + ) + ); + } + + $item_title = sprintf( + '
%s
%s
', + esc_html( $item['title'] ), + $item_artist + ); + + $list_items .= sprintf( + '
  • %s%s
  • ', + esc_attr( implode( ' ', $item_classes ) ), + $item_context, + $item_number, + $item_title + ); + } + $items_list = sprintf( '
      %s
    ', $list_items ); + } + + // Build wrapper with interactivity directives. + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => 'wp-block-playlist' ) ); + $interactivity_context = wp_interactivity_data_wp_context( $context ); + + return sprintf( + '
    %s%s
    ', + $wrapper_attributes, + $interactivity_context, + $player, + $items_list + ); +} + +/** + * Registers the `core/playlist` block on the server. + */ +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/save.js b/packages/block-library/src/playlist/save.js new file mode 100644 index 00000000000000..05bfd25df9f8b0 --- /dev/null +++ b/packages/block-library/src/playlist/save.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return attributes.items?.length > 0 &&
    ; +} diff --git a/packages/block-library/src/playlist/style.scss b/packages/block-library/src/playlist/style.scss new file mode 100644 index 00000000000000..e5db07528acb22 --- /dev/null +++ b/packages/block-library/src/playlist/style.scss @@ -0,0 +1,111 @@ +.wp-block-playlist { + display: flex; + flex-direction: column; + gap: $grid-unit-15; + justify-content: center; + + .wp-block-playlist__player { + margin: 0; + display: flex; + flex-direction: column; + gap: $grid-unit-15; + + .wp-block-playlist__header { + display: flex; + flex-direction: row; + gap: $grid-unit-05; + align-items: flex-start; + + .wp-block-playlist__header-image { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 4px; + flex-shrink: 0; + } + + .wp-block-playlist__header-info { + display: flex; + flex-direction: column; + gap: $grid-unit-05; + min-width: 0; + + .wp-block-playlist__header-title { + font-weight: $font-weight-medium; + font-size: $font-size-large; + } + + .wp-block-playlist__header-subtitle { + font-size: $font-size-medium; + color: $gray-700; + } + } + } + + .wp-block-playlist__audio { + width: 100%; + } + } + + .wp-block-playlist__items { + list-style: none; + padding: 0; + margin: 0; + + .wp-block-playlist__item { + display: flex; + flex-direction: row; + gap: $grid-unit-20; + width: 100%; + align-items: center; + justify-content: flex-start; + padding: $grid-unit-10; + background: transparent; + transition: background-color 0.2s ease; + cursor: pointer; + box-sizing: border-box; + + & + .wp-block-playlist__item { + border-top: 1px solid $gray-300; + } + + &:hover { + background: $gray-100; + } + + &.is-active { + background: $gray-100; + + .wp-block-playlist__item-title { + font-weight: $font-weight-medium; + } + } + + .wp-block-playlist__item-number { + font-size: $font-size-medium; + flex-shrink: 0; + } + + .wp-block-playlist__item-info { + display: flex; + flex-shrink: 0; + flex-direction: column; + justify-content: center; + gap: $grid-unit-05; + + .wp-block-playlist__item-title { + font-size: $font-size-large; + line-height: 1.3; + text-wrap: balance; /* Fallback for Safari. */ + text-wrap: pretty; + flex-shrink: 0; + } + + .wp-block-playlist__item-artist { + font-size: $font-size-medium; + color: $gray-700; + } + } + } + } +} diff --git a/packages/block-library/src/playlist/view.js b/packages/block-library/src/playlist/view.js new file mode 100644 index 00000000000000..d12e2148fc54da --- /dev/null +++ b/packages/block-library/src/playlist/view.js @@ -0,0 +1,120 @@ +/** + * WordPress dependencies + */ +import { store, getContext, getElement } from '@wordpress/interactivity'; + +const { state } = store( 'core/playlist', { + state: { + get currentItemData() { + const context = getContext(); + const { itemsData, currentItem } = context; + + if ( ! itemsData || ! itemsData[ currentItem ] ) { + return null; + } + + return itemsData[ currentItem ]; + }, + get currentItemSrc() { + const itemData = state.currentItemData; + return itemData?.url || ''; + }, + get currentItemTitle() { + const itemData = state.currentItemData; + return itemData?.title || ''; + }, + get currentItemArtist() { + const itemData = state.currentItemData; + if ( ! itemData?.artist ) { + return ''; + } + // translators: %s is the artist name. + return `by ${ itemData.artist }`; + }, + get currentItemImage() { + const itemData = state.currentItemData; + return itemData?.image || ''; + }, + get isItemActive() { + const context = getContext(); + const parentContext = getContext( 'core/playlist' ); + const { itemIndex } = context; + return itemIndex === parentContext.currentItem; + }, + }, + actions: { + selectItem: () => { + const context = getContext(); + const { itemIndex } = context; + + // Get the parent context and update current item + const parentContext = getContext( 'core/playlist' ); + const wasPlaying = parentContext.isPlaying; + parentContext.currentItem = itemIndex; + + // Get the audio element and play the new item if needed + const { ref } = getElement(); + const container = ref.closest( + '[data-wp-interactive="core/playlist"]' + ); + if ( container ) { + const audio = container.querySelector( + '.wp-block-playlist__audio' + ); + if ( audio ) { + audio.load(); + if ( wasPlaying ) { + audio.play(); + } + } + } + }, + onItemEnded: () => { + const context = getContext(); + const { itemsData, currentItem, loop } = context; + + // If not the last item, move to next. + if ( currentItem < itemsData.length - 1 ) { + context.currentItem = currentItem + 1; + const { ref } = getElement(); + if ( ref ) { + ref.load(); + ref.play(); + } + } else if ( loop ) { + // Last item and loop enabled - go back to first item. + context.currentItem = 0; + const { ref } = getElement(); + if ( ref ) { + ref.load(); + ref.play(); + } + } + // If last item and no loop - do nothing (stop playing). + }, + onPlay: () => { + const context = getContext(); + context.isPlaying = true; + }, + onPause: () => { + const context = getContext(); + context.isPlaying = false; + }, + }, + callbacks: { + updateAudio: () => { + const context = getContext(); + const { autoplay, currentItem } = context; + + const { ref } = getElement(); + if ( ! ref ) { + return; + } + + const audio = ref.querySelector( '.wp-block-playlist__audio' ); + if ( audio && autoplay && currentItem === 0 ) { + audio.play(); + } + }, + }, +} ); diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 1ea2158cc38692..41c0555da24798 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -36,6 +36,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"; diff --git a/test/integration/fixtures/blocks/core__playlist.html b/test/integration/fixtures/blocks/core__playlist.html new file mode 100644 index 00000000000000..5ed41fb1baaab9 --- /dev/null +++ b/test/integration/fixtures/blocks/core__playlist.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__playlist.json b/test/integration/fixtures/blocks/core__playlist.json new file mode 100644 index 00000000000000..d4bfa0b5f30be9 --- /dev/null +++ b/test/integration/fixtures/blocks/core__playlist.json @@ -0,0 +1,13 @@ +[ + { + "name": "core/playlist", + "isValid": true, + "attributes": { + "items": [], + "autoplay": false, + "loop": false, + "showItemList": true + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__playlist.parsed.json b/test/integration/fixtures/blocks/core__playlist.parsed.json new file mode 100644 index 00000000000000..fd095323d526ab --- /dev/null +++ b/test/integration/fixtures/blocks/core__playlist.parsed.json @@ -0,0 +1,9 @@ +[ + { + "blockName": "core/playlist", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__playlist.serialized.html b/test/integration/fixtures/blocks/core__playlist.serialized.html new file mode 100644 index 00000000000000..5ed41fb1baaab9 --- /dev/null +++ b/test/integration/fixtures/blocks/core__playlist.serialized.html @@ -0,0 +1 @@ +