@wordpress/block-library
Version:
Block library for the WordPress editor.
299 lines (283 loc) • 7.86 kB
JSX
import clsx from 'clsx';
import { isBlobURL } from '@wordpress/blob';
import {
Spinner,
Placeholder,
__experimentalToolsPanel as ToolsPanel,
} from '@wordpress/components';
import {
BlockControls,
BlockIcon,
InspectorControls,
MediaPlaceholder,
MediaReplaceFlow,
useBlockProps,
useBlockEditingMode,
__experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles,
} from '@wordpress/block-editor';
import { useRef, useEffect, useState } from '@wordpress/element';
import { useReducedMotion } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import { inertValue } from '@wordpress/react-inert-value';
import { useDispatch } from '@wordpress/data';
import { video as icon } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { prependHTTPS } from '@wordpress/url';
import { createUpgradedEmbedBlock } from '../embed/util';
import {
useUploadMediaFromBlobURL,
useToolsPanelDropdownMenuProps,
} from '../utils/hooks';
import VideoCommonSettings from './edit-common-settings';
import TracksEditor from './tracks-editor';
import Tracks from './tracks';
import { Caption } from '../utils/caption';
import PosterImage from '../utils/poster-image';
const ALLOWED_MEDIA_TYPES = [ 'video' ];
function VideoEdit( {
isSelected: isSingleSelected,
attributes,
className,
setAttributes,
insertBlocksAfter,
onReplace,
} ) {
const videoPlayer = useRef();
const {
id,
controls,
poster,
src,
tracks,
width,
height,
autoplay,
loop,
muted,
playsInline,
} = attributes;
// Give the <video> an explicit (non-`auto`) aspect ratio derived from the
// stored dimensions. The width/height attributes alone only yield
// `aspect-ratio: auto W/H`, whose `auto` keyword defers to the element's
// natural ratio while the poster/metadata load - during which Chrome briefly
// computes a runaway height (tens of thousands of pixels) before settling.
// That spike is what reads as a duplicated image during the GIF-to-video
// swap. A non-`auto` ratio governs the box height throughout the load.
const aspectRatio =
width && height ? `${ width } / ${ height }` : undefined;
const videoStyle = {
...( aspectRatio && { aspectRatio } ),
...getShadowClassesAndStyles( attributes ).style,
};
const [ temporaryURL, setTemporaryURL ] = useState( attributes.blob );
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
const blockEditingMode = useBlockEditingMode();
const hasNonContentControls = blockEditingMode === 'default';
const prefersReducedMotion = useReducedMotion();
useUploadMediaFromBlobURL( {
url: temporaryURL,
allowedTypes: ALLOWED_MEDIA_TYPES,
onChange: onSelectVideo,
onError: onUploadError,
} );
useEffect( () => {
// Placeholder may be rendered.
if ( videoPlayer.current ) {
videoPlayer.current.load();
}
}, [ poster ] );
// `autoPlay` only applies when the video loads, so pause a video that is
// already playing when the user turns on reduced motion.
useEffect( () => {
if ( prefersReducedMotion ) {
videoPlayer.current?.pause();
}
}, [ prefersReducedMotion ] );
// TODO: Whether the video was obtained from the media library or was provided by URL, obtain the `videoWidth` and `videoHeight` of the video once its metadata has loaded and persist in the block attributes.
function onSelectVideo( media ) {
if ( ! media || ! media.url ) {
// In this case there was an error
// previous attributes should be removed
// because they may be temporary blob urls.
setAttributes( {
src: undefined,
id: undefined,
poster: undefined,
caption: undefined,
blob: undefined,
} );
setTemporaryURL();
return;
}
if ( isBlobURL( media.url ) ) {
setTemporaryURL( media.url );
return;
}
// Sets the block's attribute and updates the edit component from the
// selected media.
setAttributes( {
blob: undefined,
src: media.url,
id: media.id,
poster:
media.image?.src !== media.icon ? media.image?.src : undefined,
caption: media.caption,
} );
setTemporaryURL();
}
function onSelectURL( newSrc ) {
if ( newSrc !== src ) {
const url = prependHTTPS( newSrc );
// Check if there's an embed block that handles this URL.
const embedBlock = createUpgradedEmbedBlock( {
attributes: { url },
} );
if ( undefined !== embedBlock && onReplace ) {
onReplace( embedBlock );
return;
}
setAttributes( {
blob: undefined,
src: url,
id: undefined,
poster: undefined,
} );
setTemporaryURL();
}
}
const { createErrorNotice } = useDispatch( noticesStore );
function onUploadError( message ) {
createErrorNotice( message, { type: 'snackbar' } );
}
// Much of this description is duplicated from MediaPlaceholder.
const placeholder = ( content ) => {
return (
<Placeholder
className="block-editor-media-placeholder"
withIllustration={ ! isSingleSelected }
icon={ icon }
label={ __( 'Video' ) }
instructions={ __(
'Drag and drop a video, upload, or choose from your library.'
) }
>
{ content }
</Placeholder>
);
};
const classes = clsx( className, {
'is-transient': !! temporaryURL,
} );
const blockProps = useBlockProps( {
className: classes,
} );
if ( ! src && ! temporaryURL ) {
return (
<div { ...blockProps }>
<MediaPlaceholder
icon={ <BlockIcon icon={ icon } /> }
onSelect={ onSelectVideo }
onSelectURL={ onSelectURL }
accept="video/*"
allowedTypes={ ALLOWED_MEDIA_TYPES }
value={ attributes }
onError={ onUploadError }
placeholder={ placeholder }
/>
</div>
);
}
return (
<>
{ isSingleSelected && (
<>
<BlockControls>
<TracksEditor
tracks={ tracks }
onChange={ ( newTracks ) => {
setAttributes( { tracks: newTracks } );
} }
/>
</BlockControls>
<BlockControls group="other">
<MediaReplaceFlow
mediaId={ id }
mediaURL={ src }
allowedTypes={ ALLOWED_MEDIA_TYPES }
accept="video/*"
onSelect={ onSelectVideo }
onSelectURL={ onSelectURL }
onError={ onUploadError }
onReset={ () => onSelectVideo( undefined ) }
variant="toolbar"
/>
</BlockControls>
</>
) }
<InspectorControls>
<ToolsPanel
label={ __( 'Settings' ) }
resetAll={ () => {
setAttributes( {
autoplay: false,
controls: true,
loop: false,
muted: false,
playsInline: false,
preload: 'metadata',
poster: undefined,
} );
} }
dropdownMenuProps={ dropdownMenuProps }
>
<VideoCommonSettings
setAttributes={ setAttributes }
attributes={ attributes }
/>
<PosterImage
poster={ poster }
onChange={ ( posterImage ) =>
setAttributes( {
poster: posterImage?.url,
} )
}
/>
</ToolsPanel>
</InspectorControls>
<figure { ...blockProps }>
<video
controls={ controls }
inert={ inertValue( ! isSingleSelected ) }
poster={ poster }
src={ src || temporaryURL }
ref={ videoPlayer }
autoPlay={ autoplay && ! prefersReducedMotion }
loop={ loop }
muted={ muted }
playsInline={ playsInline }
width={ width }
height={ height }
style={
Object.keys( videoStyle ).length
? videoStyle
: undefined
}
>
<Tracks tracks={ tracks } />
</video>
{ !! temporaryURL && <Spinner /> }
<Caption
attributes={ attributes }
setAttributes={ setAttributes }
isSelected={ isSingleSelected }
insertBlocksAfter={ insertBlocksAfter }
label={ __( 'Video caption text' ) }
showToolbarButton={
isSingleSelected && hasNonContentControls
}
/>
</figure>
</>
);
}
export default VideoEdit;