From 57ed23f5e9e672d7c59324ab529b5284d619a253 Mon Sep 17 00:00:00 2001 From: Miguel Torres Date: Wed, 7 Aug 2019 14:42:00 +0900 Subject: [PATCH] VideoPress: Add video settings to the VideoPress-enhanced video block (#13134) Add the video settings allowing to customize several options for the VideoPress player rendered by the video block (extended by our VideoPress extension for Gutenberg). Now, after inserting a VideoPress-enhanced video block, a user can set the following settings: - Autoplay: whether to start playing as soon as the player renders (note that some browsers can prevent a video from being autoplayed). - Loop: whether to loop the video. - Muted: whether the video should start in a muted state. - Playback controls: whether show the controls to allow the user to control video playback. - Preload: what content is loaded before the video is played: - None: Indicates that the video should not be preloaded. - Metadata: Indicates that only video metadata (e.g. length) is fetched. - Auto: Indicates that the whole video file can be downloaded, even if the user is not expected to use it. - Poster image: image to be shown while the video is downloading. Given that these settings modifies the URL saved in the block content, any existing post containing a VideoPress-enhanced video block would result in a "invalid content" error. In order to support those posts, a deprecated version of the block has been added. These changes also introduces a new `playsInline` attribute (added in core in https://github.com/WordPress/gutenberg/pull/14500), but it's currently unmodifiable, since VideoPress doesn't support it yet. --- .../blocks/videopress/deprecated/v1/index.js | 48 +++++ .../blocks/videopress/deprecated/v1/save.js | 25 +++ extensions/blocks/videopress/edit.js | 201 +++++++++++++++--- extensions/blocks/videopress/editor.js | 26 ++- extensions/blocks/videopress/save.js | 18 +- extensions/blocks/videopress/url.js | 27 +++ 6 files changed, 311 insertions(+), 34 deletions(-) create mode 100644 extensions/blocks/videopress/deprecated/v1/index.js create mode 100644 extensions/blocks/videopress/deprecated/v1/save.js create mode 100644 extensions/blocks/videopress/url.js diff --git a/extensions/blocks/videopress/deprecated/v1/index.js b/extensions/blocks/videopress/deprecated/v1/index.js new file mode 100644 index 0000000000000..ed4f99685b291 --- /dev/null +++ b/extensions/blocks/videopress/deprecated/v1/index.js @@ -0,0 +1,48 @@ +/** + * Internal dependencies + */ +import save from './save'; + +export default { + attributes: { + autoplay: { + type: 'boolean', + }, + caption: { + type: 'string', + source: 'html', + selector: 'figcaption', + }, + controls: { + type: 'boolean', + default: true, + }, + guid: { + type: 'string', + }, + id: { + type: 'number', + }, + loop: { + type: 'boolean', + }, + muted: { + type: 'boolean', + }, + poster: { + type: 'string', + }, + preload: { + type: 'string', + default: 'metadata', + }, + src: { + type: 'string', + }, + }, + support: { + reusable: false, + }, + save, + isDeprecation: true, +}; diff --git a/extensions/blocks/videopress/deprecated/v1/save.js b/extensions/blocks/videopress/deprecated/v1/save.js new file mode 100644 index 0000000000000..fec3480c3fa30 --- /dev/null +++ b/extensions/blocks/videopress/deprecated/v1/save.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { RichText } from '@wordpress/editor'; + +export default function VideoPressSave( { attributes } ) { + const { caption, guid } = attributes; + + if ( ! guid ) { + return null; + } + + const url = `https://videopress.com/v/${ guid }`; + + return ( +
+
+ { `\n${ url }\n` /* URL needs to be on its own line. */ } +
+ { ! RichText.isEmpty( caption ) && ( + + ) } +
+ ); +} diff --git a/extensions/blocks/videopress/edit.js b/extensions/blocks/videopress/edit.js index cd73e5642dec8..903d950d5f03b 100644 --- a/extensions/blocks/videopress/edit.js +++ b/extensions/blocks/videopress/edit.js @@ -2,20 +2,39 @@ * External dependencies */ import apiFetch from '@wordpress/api-fetch'; -import classnames from 'classnames'; -import { __ } from '@wordpress/i18n'; -import { BlockControls, RichText } from '@wordpress/editor'; +import { isBlobURL } from '@wordpress/blob'; +import { + BaseControl, + Button, + Disabled, + IconButton, + PanelBody, + SandBox, + SelectControl, + ToggleControl, + Toolbar, +} from '@wordpress/components'; +import { compose, createHigherOrderComponent, withInstanceId } from '@wordpress/compose'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { + BlockControls, + InspectorControls, + MediaUpload, + MediaUploadCheck, + RichText, +} from '@wordpress/editor'; import { Component, createRef, Fragment } from '@wordpress/element'; -import { compose, createHigherOrderComponent } from '@wordpress/compose'; -import { Disabled, IconButton, SandBox, Toolbar } from '@wordpress/components'; +import { __, _x, sprintf } from '@wordpress/i18n'; +import classnames from 'classnames'; import { get } from 'lodash'; -import { isBlobURL } from '@wordpress/blob'; -import { withSelect } from '@wordpress/data'; /** * Internal dependencies */ import Loading from './loading'; +import { getVideoPressUrl } from './url'; + +const VIDEO_POSTER_ALLOWED_MEDIA_TYPES = [ 'image' ]; const VideoPressEdit = CoreVideoEdit => class extends Component { @@ -37,11 +56,19 @@ const VideoPressEdit = CoreVideoEdit => } componentDidUpdate( prevProps ) { - const { attributes } = this.props; + const { attributes, invalidateCachedEmbedPreview, url } = this.props; if ( attributes.id !== prevProps.attributes.id ) { this.setGuid(); } + + if ( url && url !== prevProps.url ) { + // Due to a current bug in Gutenberg (https://github.com/WordPress/gutenberg/issues/16831), the + // `SandBox` component is not rendered again when the injected `html` prop changes. To work around that, + // we invalidate the cached preview of the embed VideoPress player in order to force the rendering of a + // new instance of the `SandBox` component that ensures the injected `html` will be rendered. + invalidateCachedEmbedPreview( url ); + } } fallbackToCore = () => { @@ -90,17 +117,36 @@ const VideoPressEdit = CoreVideoEdit => } ); }; + onSelectPoster = image => { + const { setAttributes } = this.props; + setAttributes( { poster: image.url } ); + }; + onRemovePoster = () => { - this.props.setAttributes( { poster: '' } ); + const { setAttributes } = this.props; + setAttributes( { poster: '' } ); // Move focus back to the Media Upload button. this.posterImageButton.current.focus(); }; + toggleAttribute = attribute => { + return newValue => { + this.props.setAttributes( { [ attribute ]: newValue } ); + }; + }; + + getAutoplayHelp = checked => { + return checked + ? __( 'Note: Autoplaying videos may cause usability issues for some visitors.', 'jetpack' ) + : null; + }; + render() { const { attributes, className, + instanceId, isFetchingPreview, isSelected, isUploading, @@ -108,13 +154,113 @@ const VideoPressEdit = CoreVideoEdit => setAttributes, } = this.props; const { fallback, isFetchingMedia } = this.state; + const { autoplay, caption, controls, loop, muted, poster, preload } = attributes; + + const videoPosterDescription = `video-block__poster-image-description-${ instanceId }`; + + const blockSettings = ( + + + + + + + + + + + + + setAttributes( { preload: value } ) } + options={ [ + { value: 'auto', label: _x( 'Auto', 'VideoPress preload setting', 'jetpack' ) }, + { + value: 'metadata', + label: _x( 'Metadata', 'VideoPress preload setting', 'jetpack' ), + }, + { value: 'none', label: _x( 'None', 'VideoPress preload setting', 'jetpack' ) }, + ] } + /> + + + ( + + ) } + /> + + { !! poster && ( + + ) } + + + + + + ); if ( isUploading ) { - return ; + return ( + + { blockSettings } + + + ); } if ( isFetchingMedia || isFetchingPreview ) { - return ; + return ( + + { blockSettings } + + + ); } if ( fallback || ! preview ) { @@ -122,20 +268,10 @@ const VideoPressEdit = CoreVideoEdit => } const { html, scripts } = preview; - const { caption } = attributes; return ( - - - - - + { blockSettings }
{ /* Disable the video player so the user clicking on it won't play the @@ -164,10 +300,17 @@ const VideoPressEdit = CoreVideoEdit => export default createHigherOrderComponent( compose( [ withSelect( ( select, ownProps ) => { - const { guid, src } = ownProps.attributes; + const { autoplay, controls, guid, loop, muted, poster, preload, src } = ownProps.attributes; const { getEmbedPreview, isRequestingEmbedPreview } = select( 'core' ); - const url = !! guid && `https://videopress.com/v/${ guid }`; + const url = getVideoPressUrl( guid, { + autoplay, + controls, + loop, + muted, + poster, + preload, + } ); const preview = !! url && getEmbedPreview( url ); const isFetchingEmbedPreview = !! url && isRequestingEmbedPreview( url ); @@ -177,8 +320,18 @@ export default createHigherOrderComponent( isFetchingPreview: isFetchingEmbedPreview, isUploading, preview, + url, + }; + } ), + withDispatch( dispatch => { + const invalidateCachedEmbedPreview = url => { + dispatch( 'core/data' ).invalidateResolution( 'core', 'getEmbedPreview', [ url ] ); + }; + return { + invalidateCachedEmbedPreview, }; } ), + withInstanceId, VideoPressEdit, ] ), 'withVideoPressEdit' diff --git a/extensions/blocks/videopress/editor.js b/extensions/blocks/videopress/editor.js index a39a2ac9ae95b..3b27431796d3e 100644 --- a/extensions/blocks/videopress/editor.js +++ b/extensions/blocks/videopress/editor.js @@ -13,12 +13,16 @@ import { every } from 'lodash'; import withVideoPressEdit from './edit'; import withVideoPressSave from './save'; import getJetpackExtensionAvailability from '../../shared/get-jetpack-extension-availability'; +import deprecatedV1 from './deprecated/v1'; const addVideoPressSupport = ( settings, name ) => { - if ( 'core/video' !== name ) { + // Bail if this is not the video block or if the hook has been triggered by a deprecation. + if ( 'core/video' !== name || settings.isDeprecation ) { return settings; } + const { attributes, deprecated, edit, save, supports, transforms } = settings; + const { available, unavailableReason } = getJetpackExtensionAvailability( 'videopress' ); // We customize the video block even if VideoPress it not available so we can support videos that were uploaded to @@ -52,6 +56,9 @@ const addVideoPressSupport = ( settings, name ) => { muted: { type: 'boolean', }, + playsInline: { + type: 'boolean', + }, poster: { type: 'string', }, @@ -65,7 +72,7 @@ const addVideoPressSupport = ( settings, name ) => { }, transforms: { - ...settings.transforms, + ...transforms, from: [ { type: 'files', @@ -95,21 +102,24 @@ const addVideoPressSupport = ( settings, name ) => { }, supports: { - ...settings.supports, + ...supports, reusable: false, }, - edit: withVideoPressEdit( settings.edit ), + edit: withVideoPressEdit( edit ), - save: withVideoPressSave( settings.save ), + save: withVideoPressSave( save ), deprecated: [ + ...( deprecated || [] ), { - attributes: settings.attributes, - save: settings.save, + attributes, isEligible: attrs => ! attrs.guid, + save, + supports, + isDeprecation: true, }, - ...( Array.isArray( settings.deprecated ) ? settings.deprecated : [] ), + deprecatedV1, ], }; } diff --git a/extensions/blocks/videopress/save.js b/extensions/blocks/videopress/save.js index 52790480769af..09555f9183627 100644 --- a/extensions/blocks/videopress/save.js +++ b/extensions/blocks/videopress/save.js @@ -4,8 +4,15 @@ import { createHigherOrderComponent } from '@wordpress/compose'; import { RichText } from '@wordpress/editor'; +/** + * Internal dependencies + */ +import { getVideoPressUrl } from './url'; + const VideoPressSave = CoreVideoSave => props => { - const { attributes: { caption, guid } = {} } = props; + const { + attributes: { autoplay, caption, controls, guid, loop, muted, poster, preload } = {}, + } = props; if ( ! guid ) { /** @@ -20,7 +27,14 @@ const VideoPressSave = CoreVideoSave => props => { return CoreVideoSave( props ); } - const url = `https://videopress.com/v/${ guid }`; + const url = getVideoPressUrl( guid, { + autoplay, + controls, + loop, + muted, + poster, + preload, + } ); return (
diff --git a/extensions/blocks/videopress/url.js b/extensions/blocks/videopress/url.js new file mode 100644 index 0000000000000..27372dd2c9c75 --- /dev/null +++ b/extensions/blocks/videopress/url.js @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { addQueryArgs } from '@wordpress/url'; + +export const getVideoPressUrl = ( guid, { autoplay, controls, loop, muted, poster, preload } ) => { + if ( ! guid ) { + return null; + } + + // In order to have a cleaner URL, we only set the options differing from the default VideoPress player settings: + // - Autoplay: Turned off by default. + // - Controls: Turned on by default. + // - Loop: Turned off by default. + // - Muted: Turned off by default. + // - Poster: No image by default. + // - Preload: None by default. + const options = { + ...( autoplay && { autoPlay: true } ), + ...( ! controls && { controls: false } ), + ...( loop && { loop: true } ), + ...( muted && { muted: true, persistVolume: false } ), + ...( poster && { posterUrl: poster } ), + ...( preload !== 'none' && { preloadContent: preload } ), + }; + return addQueryArgs( `https://videopress.com/v/${ guid }`, options ); +};