Make WordPress Core

Changeset 39272


Ignore:
Timestamp:
11/16/2016 11:25:28 PM (8 years ago)
Author:
joemcgill
Message:

Themes: Improve a11y and extendability of custom video headers.

This adds play/pause controls to video headers, along with voice
assistance, using wp.a11y.speak, to make custom video headers more
accessible. To make styling the play/pause button easier for themes,
CSS has been omitted from the default implementation.

This also includes a refactor of the wp.customHeader code to introduce
a BaseHandler class, which can be extended by plugins and themes to modify
or enhance the default video handlers.

Props davidakennedy, afercia, bradyvercher, joemcgill, adamsilverstein, rianrietveld.
Fixes #38678.

Location:
trunk/src/wp-includes
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-manager.php

    r39268 r39272  
    35973597            'theme_supports'    => array( 'custom-header', 'video' ),
    35983598            'transport'         => 'postMessage',
    3599             'sanitize_callback' => 'esc_url',
     3599            'sanitize_callback' => 'esc_url_raw',
    36003600            'validate_callback' => array( $this, '_validate_external_header_video' ),
    36013601        ) );
  • trunk/src/wp-includes/js/wp-custom-header.js

    r39102 r39272  
    1 (function( window, settings ) {
    2 
     1/* global YT */
     2( function( window, settings ) {
     3
     4    var NativeHandler, YouTubeHandler;
     5
     6    window.wp = window.wp || {};
     7
     8    // Fail gracefully in unsupported browsers.
    39    if ( ! ( 'addEventListener' in window ) ) {
    4         // Fail gracefully in unsupported browsers.
    510        return;
    611    }
    712
    8     function wpCustomHeader() {
    9         var handlers = {
    10             nativeVideo: {
    11                 test: function( settings ) {
    12                     var video = document.createElement( 'video' );
    13                     return video.canPlayType( settings.mimeType );
    14                 },
    15                 callback: nativeHandler
    16             },
    17             youtube: {
    18                 test: function( settings ) {
    19                     return 'video/x-youtube' === settings.mimeType;
    20                 },
    21                 callback: youtubeHandler
    22             }
     13    /**
     14     * Trigger an event.
     15     *
     16     * @param {Element} target HTML element to dispatch the event on.
     17     * @param {string} name Event name.
     18     */
     19    function trigger( target, name ) {
     20        var evt;
     21
     22        if ( 'function' === typeof window.Event ) {
     23            evt = new Event( name );
     24        } else {
     25            evt = document.createEvent( 'Event' );
     26            evt.initEvent( name, true, true );
     27        }
     28
     29        target.dispatchEvent( evt );
     30    }
     31
     32    /**
     33     * Create a custom header instance.
     34     *
     35     * @class CustomHeader
     36     */
     37    function CustomHeader() {
     38        this.handlers = {
     39            nativeVideo: new NativeHandler(),
     40            youtube: new YouTubeHandler()
    2341        };
    24 
    25         function initialize() {
    26             settings.container = document.getElementById( 'wp-custom-header' );
    27 
    28             if ( supportsVideo() ) {
    29                 for ( var id in handlers ) {
    30                     var handler = handlers[ id ];
    31 
    32                     if ( handlers.hasOwnProperty( id ) && handler.test( settings ) ) {
    33                         handler.callback( settings );
    34 
    35                         // Set up and dispatch custom event when the video is loaded.
    36                         if ( 'dispatchEvent' in window ) {
    37                             var videoLoaded = new Event( 'wp-custom-header-video-loaded' );
    38                             document.dispatchEvent( videoLoaded );
    39                         }
    40 
     42    }
     43
     44    CustomHeader.prototype = {
     45        /**
     46         * Initalize the custom header.
     47         *
     48         * If the environment supports video, loops through registered handlers
     49         * until one is found that can handle the video.
     50         */
     51        initialize: function() {
     52            if ( this.supportsVideo() ) {
     53                for ( var id in this.handlers ) {
     54                    var handler = this.handlers[ id ];
     55
     56                    if ( 'test' in handler && handler.test( settings ) ) {
     57                        this.activeHandler = handler.initialize.call( handler, settings );
     58
     59                        // Dispatch custom event when the video is loaded.
     60                        trigger( document, 'wp-custom-header-video-loaded' );
    4161                        break;
    4262                    }
    4363                }
    4464            }
    45         }
    46 
    47         function supportsVideo() {
     65        },
     66
     67        /**
     68         * Determines if the current environment supports video.
     69         *
     70         * Themes and plugins can override this method to change the criteria.
     71         *
     72         * @return {boolean}
     73         */
     74        supportsVideo: function() {
    4875            // Don't load video on small screens. @todo: consider bandwidth and other factors.
    49             if ( window.innerWidth < settings.minWidth  || window.innerHeight < settings.minHeight ) {
     76            if ( window.innerWidth < settings.minWidth || window.innerHeight < settings.minHeight ) {
    5077                return false;
    5178            }
    5279
    5380            return true;
    54         }
    55 
    56         return {
    57             handlers: handlers,
    58             initialize: initialize,
    59             supportsVideo: supportsVideo
    60         };
    61     }
    62 
    63     function nativeHandler( settings ) {
    64         var video = document.createElement( 'video' );
    65 
    66         video.id = 'wp-custom-header-video';
    67         video.autoplay = 'autoplay';
    68         video.loop = 'loop';
    69         video.muted = 'muted';
    70         video.width = settings.width;
    71         video.height = settings.height;
    72 
    73         video.addEventListener( 'click', function() {
    74             if ( video.paused ) {
    75                 video.play();
     81        },
     82
     83        /**
     84         * Base handler for custom handlers to extend.
     85         *
     86         * @type {BaseHandler}
     87         */
     88        BaseVideoHandler: BaseHandler
     89    };
     90
     91    /**
     92     * Create a video handler instance.
     93     *
     94     * @class BaseHandler
     95     */
     96    function BaseHandler() {}
     97
     98    BaseHandler.prototype = {
     99        /**
     100         * Initialize the video handler.
     101         *
     102         * @param {object} settings Video settings.
     103         */
     104        initialize: function( settings ) {
     105            var handler = this,
     106                button = document.createElement( 'button' );
     107
     108            this.settings = settings;
     109            this.container = document.getElementById( 'wp-custom-header' ),
     110            this.button = button;
     111
     112            button.setAttribute( 'type', 'button' );
     113            button.setAttribute( 'id', 'wp-custom-header-video-button' );
     114            button.setAttribute( 'class', 'wp-custom-header-video-button wp-custom-header-video-play' );
     115            button.innerHTML = settings.l10n.play;
     116
     117            // Toggle video playback when the button is clicked.
     118            button.addEventListener( 'click', function() {
     119                if ( handler.isPaused() ) {
     120                    handler.play();
     121                } else {
     122                    handler.pause();
     123                }
     124            });
     125
     126            // Update the button class and text when the video state changes.
     127            this.container.addEventListener( 'play', function() {
     128                button.className = 'wp-custom-header-video-button wp-custom-header-video-play';
     129                button.innerHTML = settings.l10n.pause;
     130                if ( 'a11y' in window.wp ) {
     131                    window.wp.a11y.speak( settings.l10n.playSpeak);
     132                }
     133            });
     134
     135            this.container.addEventListener( 'pause', function() {
     136                button.className = 'wp-custom-header-video-button wp-custom-header-video-pause';
     137                button.innerHTML = settings.l10n.play;
     138                if ( 'a11y' in window.wp ) {
     139                    window.wp.a11y.speak( settings.l10n.pauseSpeak);
     140                }
     141            });
     142
     143            this.ready();
     144        },
     145
     146        /**
     147         * Ready method called after a handler is initialized.
     148         *
     149         * @abstract
     150         */
     151        ready: function() {},
     152
     153        /**
     154         * Whether the video is paused.
     155         *
     156         * @abstract
     157         * @return {boolean}
     158         */
     159        isPaused: function() {},
     160
     161        /**
     162         * Pause the video.
     163         *
     164         * @abstract
     165         */
     166        pause: function() {},
     167
     168        /**
     169         * Play the video.
     170         *
     171         * @abstract
     172         */
     173        play: function() {},
     174
     175        /**
     176         * Append a video node to the header container.
     177         *
     178         * @param {Element} node HTML element.
     179         */
     180        setVideo: function( node ) {
     181            var editShortcutNode,
     182                editShortcut = this.container.getElementsByClassName( 'customize-partial-edit-shortcut' );
     183
     184            if ( editShortcut.length ) {
     185                editShortcutNode = this.container.removeChild( editShortcut[0] );
     186            }
     187
     188            this.container.innerHTML = '';
     189            this.container.appendChild( node );
     190
     191            if ( editShortcutNode ) {
     192                this.container.appendChild( editShortcutNode );
     193            }
     194        },
     195
     196        /**
     197         * Show the video controls.
     198         *
     199         * Appends a play/pause button to header container.
     200         */
     201        showControls: function() {
     202            if ( ! this.container.contains( this.button ) ) {
     203                this.container.appendChild( this.button );
     204            }
     205        },
     206
     207        /**
     208         * Whether the handler can process a video.
     209         *
     210         * @abstract
     211         * @param {object} settings Video settings.
     212         * @return {boolean}
     213         */
     214        test: function() {
     215            return false;
     216        },
     217
     218        /**
     219         * Trigger an event on the header container.
     220         *
     221         * @param {string} name Event name.
     222         */
     223        trigger: function( name ) {
     224            trigger( this.container, name );
     225        }
     226    };
     227
     228    /**
     229     * Create a custom handler.
     230     *
     231     * @param  {object} protoProps Properties to apply to the prototype.
     232     * @return CustomHandler The subclass.
     233     */
     234    BaseHandler.extend = function( protoProps ) {
     235        var prop;
     236
     237        function CustomHandler() {
     238            var result = BaseHandler.apply( this, arguments );
     239            return result;
     240        }
     241
     242        CustomHandler.prototype = Object.create( BaseHandler.prototype );
     243        CustomHandler.prototype.constructor = CustomHandler;
     244
     245        for ( prop in protoProps ) {
     246            CustomHandler.prototype[ prop ] = protoProps[ prop ];
     247        }
     248
     249        return CustomHandler;
     250    };
     251
     252    /**
     253     * Native video handler.
     254     *
     255     * @class NativeHandler
     256     */
     257    NativeHandler = BaseHandler.extend({
     258        /**
     259         * Whether the native handler supports a video.
     260         *
     261         * @param {object} settings Video settings.
     262         * @return {boolean}
     263         */
     264        test: function( settings ) {
     265            var video = document.createElement( 'video' );
     266            return video.canPlayType( settings.mimeType );
     267        },
     268
     269        /**
     270         * Set up a native video element.
     271         */
     272        ready: function() {
     273            var handler = this,
     274                video = document.createElement( 'video' );
     275
     276            video.id = 'wp-custom-header-video';
     277            video.autoplay = 'autoplay';
     278            video.loop = 'loop';
     279            video.muted = 'muted';
     280            video.width = this.settings.width;
     281            video.height = this.settings.height;
     282
     283            video.addEventListener( 'play', function() {
     284                handler.trigger( 'play' );
     285            });
     286
     287            video.addEventListener( 'pause', function() {
     288                handler.trigger( 'pause' );
     289            });
     290
     291            video.addEventListener( 'canplay', function() {
     292                handler.showControls();
     293            });
     294
     295            this.video = video;
     296            handler.setVideo( video );
     297            video.src = this.settings.videoUrl;
     298        },
     299
     300        /**
     301         * Whether the video is paused.
     302         *
     303         * @return {boolean}
     304         */
     305        isPaused: function() {
     306            return this.video.paused;
     307        },
     308
     309        /**
     310         * Pause the video.
     311         */
     312        pause: function() {
     313            this.video.pause();
     314        },
     315
     316        /**
     317         * Play the video.
     318         */
     319        play: function() {
     320            this.video.play();
     321        }
     322    });
     323
     324    /**
     325     * YouTube video handler.
     326     *
     327     * @class YouTubeHandler
     328     */
     329    YouTubeHandler = BaseHandler.extend({
     330        /**
     331         * Whether the handler supports a video.
     332         *
     333         * @param {object} settings Video settings.
     334         * @return {boolean}
     335         */
     336        test: function( settings ) {
     337            return 'video/x-youtube' === settings.mimeType;
     338        },
     339
     340        /**
     341         * Set up a YouTube iframe.
     342         *
     343         * Loads the YouTube IFrame API if the 'YT' global doesn't exist.
     344         */
     345        ready: function() {
     346            var handler = this;
     347
     348            if ( 'YT' in window ) {
     349                YT.ready( handler.loadVideo.bind( handler ) );
    76350            } else {
    77                 video.pause();
    78             }
    79         });
    80 
    81         settings.container.innerHTML = '';
    82         settings.container.appendChild( video );
    83         video.src = settings.videoUrl;
    84     }
    85 
    86     function youtubeHandler( settings ) {
    87         // @link http://stackoverflow.com/a/27728417
    88         var VIDEO_ID_REGEX = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/,
    89             videoId = settings.videoUrl.match( VIDEO_ID_REGEX )[1];
    90 
    91         function loadVideo() {
    92             var YT = window.YT || {};
    93 
    94             YT.ready(function() {
    95                 var video = document.createElement( 'div' );
    96                 video.id = 'wp-custom-header-video';
    97                 settings.container.innerHTML = '';
    98                 settings.container.appendChild( video );
    99 
    100                 new YT.Player( video, {
    101                     height: settings.height,
    102                     width: settings.width,
    103                     videoId: videoId,
    104                     events: {
    105                         onReady: function( e ) {
    106                             e.target.mute();
    107                         },
    108                         onStateChange: function( e ) {
    109                             if ( YT.PlayerState.ENDED === e.data ) {
    110                                 e.target.playVideo();
    111                             }
     351                var tag = document.createElement( 'script' );
     352                tag.src = 'https://www.youtube.com/iframe_api';
     353                tag.onload = function () {
     354                    YT.ready( handler.loadVideo.bind( handler ) );
     355                };
     356
     357                document.getElementsByTagName( 'head' )[0].appendChild( tag );
     358            }
     359        },
     360
     361        /**
     362         * Load a YouTube video.
     363         */
     364        loadVideo: function() {
     365            var handler = this,
     366                video = document.createElement( 'div' ),
     367                // @link http://stackoverflow.com/a/27728417
     368                VIDEO_ID_REGEX = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
     369
     370            video.id = 'wp-custom-header-video';
     371            handler.setVideo( video );
     372
     373            handler.player = new YT.Player( video, {
     374                height: this.settings.height,
     375                width: this.settings.width,
     376                videoId: this.settings.videoUrl.match( VIDEO_ID_REGEX )[1],
     377                events: {
     378                    onReady: function( e ) {
     379                        e.target.mute();
     380                        handler.showControls();
     381                    },
     382                    onStateChange: function( e ) {
     383                        if ( YT.PlayerState.PLAYING === e.data ) {
     384                            handler.trigger( 'play' );
     385                        } else if ( YT.PlayerState.PAUSED === e.data ) {
     386                            handler.trigger( 'pause' );
     387                        } else if ( YT.PlayerState.ENDED === e.data ) {
     388                            e.target.playVideo();
    112389                        }
    113                     },
    114                     playerVars: {
    115                         autoplay: 1,
    116                         controls: 0,
    117                         disablekb: 1,
    118                         fs: 0,
    119                         iv_load_policy: 3,
    120                         loop: 1,
    121                         modestbranding: 1,
    122                         //origin: '',
    123                         playsinline: 1,
    124                         rel: 0,
    125                         showinfo: 0
    126390                    }
    127                 });
    128             });
    129         }
    130 
    131         if ( 'YT' in window ) {
    132             loadVideo();
    133         } else {
    134             var tag = document.createElement( 'script' );
    135             tag.src = 'https://www.youtube.com/player_api';
    136             tag.onload = function () { loadVideo(); };
    137             document.getElementsByTagName( 'head' )[0].appendChild( tag );
    138         }
    139     }
    140 
    141     window.wp = window.wp || {};
    142     window.wp.customHeader = new wpCustomHeader();
    143     document.addEventListener( 'DOMContentLoaded', window.wp.customHeader.initialize, false );
    144 
     391                },
     392                playerVars: {
     393                    autoplay: 1,
     394                    controls: 0,
     395                    disablekb: 1,
     396                    fs: 0,
     397                    iv_load_policy: 3,
     398                    loop: 1,
     399                    modestbranding: 1,
     400                    playsinline: 1,
     401                    rel: 0,
     402                    showinfo: 0
     403                }
     404            });
     405        },
     406
     407        /**
     408         * Whether the video is paused.
     409         *
     410         * @return {boolean}
     411         */
     412        isPaused: function() {
     413            return YT.PlayerState.PAUSED === this.player.getPlayerState();
     414        },
     415
     416        /**
     417         * Pause the video.
     418         */
     419        pause: function() {
     420            this.player.pauseVideo();
     421        },
     422
     423        /**
     424         * Play the video.
     425         */
     426        play: function() {
     427            this.player.playVideo();
     428        }
     429    });
     430
     431    // Initialize the custom header when the DOM is ready.
     432    window.wp.customHeader = new CustomHeader();
     433    document.addEventListener( 'DOMContentLoaded', window.wp.customHeader.initialize.bind( window.wp.customHeader ), false );
     434
     435    // Selective refresh support in the Customizer.
    145436    if ( 'customize' in window.wp ) {
    146         wp.customize.selectiveRefresh.bind( 'render-partials-response', function( response ) {
     437        window.wp.customize.selectiveRefresh.bind( 'render-partials-response', function( response ) {
    147438            if ( 'custom_header_settings' in response ) {
    148439                settings = response.custom_header_settings;
     
    150441        });
    151442
    152         wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
     443        window.wp.customize.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
    153444            if ( 'custom_header' === placement.partial.id ) {
    154445                window.wp.customHeader.initialize();
  • trunk/src/wp-includes/script-loader.php

    r39214 r39272  
    482482    $scripts->add( 'customize-preview-nav-menus', "/wp-includes/js/customize-preview-nav-menus$suffix.js", array( 'jquery', 'wp-util', 'customize-preview', 'customize-selective-refresh' ), false, 1 );
    483483
    484     $scripts->add( 'wp-custom-header', "/wp-includes/js/wp-custom-header$suffix.js", array(), false, 1 );
     484    $scripts->add( 'wp-custom-header', "/wp-includes/js/wp-custom-header$suffix.js", array( 'wp-a11y' ), false, 1 );
    485485
    486486    $scripts->add( 'accordion', "/wp-admin/js/accordion$suffix.js", array( 'jquery' ), false, 1 );
  • trunk/src/wp-includes/theme.php

    r39261 r39272  
    13821382        'minWidth'  => 900,
    13831383        'minHeight' => 500,
     1384        'l10n'      => array(
     1385            'pause'      => __( 'Pause' ),
     1386            'play'       => __( 'Play' ),
     1387            'pauseSpeak' => __( 'Video is paused.'),
     1388            'playSpeak'  => __( 'Video is playing.'),
     1389        ),
    13841390    );
    13851391
Note: See TracChangeset for help on using the changeset viewer.