WordPress.org

Make WordPress Core

Changeset 39272


Ignore:
Timestamp:
11/16/16 23:25:28 (6 months 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.