WordPress.org

Make WordPress Core

Changeset 46883


Ignore:
Timestamp:
12/11/2019 06:54:55 PM (22 months ago)
Author:
azaozz
Message:

Fixes in admin-bar.js:

  • Silence errors when a node doesn't exist similarly to jQuery.
  • Add "feature testing" and fallbacks for old browsers as this may run on the front-end.
  • Improve inline docs.

Props dinhtungdu, azaozz.
Fixes #47069.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/js/_enqueues/lib/admin-bar.js

    r46874 r46883  
    44/**
    55 * Admin bar with Vanilla JS, no external dependencies.
     6 *
     7 * @since 5.3.1
    68 *
    79 * @param {Object} document  The document object.
     
    1113 * @return {void}
    1214 */
    13 /* global hoverintent */
    1415( function( document, window, navigator ) {
    1516    document.addEventListener( 'DOMContentLoaded', function() {
    1617        var adminBar = document.getElementById( 'wpadminbar' ),
    17             topMenuItems = adminBar.querySelectorAll( 'li.menupop' ),
    18             allMenuItems = adminBar.querySelectorAll( '.ab-item' ),
    19             adminBarLogout = document.getElementById( 'wp-admin-bar-logout' ),
    20             adminBarSearchForm = document.getElementById( 'adminbarsearch' ),
    21             shortlink = document.getElementById( 'wp-admin-bar-get-shortlink' ),
    22             skipLink = adminBar.querySelector( '.screen-reader-shortcut' ),
    23             mobileEvent = /Mobile\/.+Safari/.test( navigator.userAgent ) ? 'touchstart' : 'click',
     18            topMenuItems,
     19            allMenuItems,
     20            adminBarLogout,
     21            adminBarSearchForm,
     22            shortlink,
     23            skipLink,
     24            mobileEvent,
     25            fontFaceRegex,
    2426            adminBarSearchInput,
    2527            i;
    2628
    27         /**
    28          * Remove nojs class after the DOM is loaded.
    29          */
    30         adminBar.classList.remove( 'nojs' );
     29        if ( ! adminBar || ! ( 'querySelectorAll' in adminBar ) ) {
     30            return;
     31        }
     32
     33        topMenuItems = adminBar.querySelectorAll( 'li.menupop' );
     34        allMenuItems = adminBar.querySelectorAll( '.ab-item' );
     35        adminBarLogout = document.getElementById( 'wp-admin-bar-logout' );
     36        adminBarSearchForm = document.getElementById( 'adminbarsearch' );
     37        shortlink = document.getElementById( 'wp-admin-bar-get-shortlink' );
     38        skipLink = adminBar.querySelector( '.screen-reader-shortcut' );
     39        mobileEvent = /Mobile\/.+Safari/.test( navigator.userAgent ) ? 'touchstart' : 'click';
     40        fontFaceRegex = /Android (1.0|1.1|1.5|1.6|2.0|2.1)|Nokia|Opera Mini|w(eb)?OSBrowser|webOS|UCWEB|Windows Phone OS 7|XBLWP7|ZuneWP7|MSIE 7/;
     41
     42        // Remove nojs class after the DOM is loaded.
     43        removeClass( adminBar, 'nojs' );
    3144
    3245        if ( 'ontouchstart' in window ) {
    33             /**
    34              * Remove hover class when the user touches outside the menu items.
    35              */
     46            // Remove hover class when the user touches outside the menu items.
    3647            document.body.addEventListener( mobileEvent, function( e ) {
    3748                if ( ! getClosest( e.target, 'li.menupop' ) ) {
     
    4051            } );
    4152
    42             /**
    43              * Add listener for menu items to toggle hover class by touches.
    44              * Remove the callback later for better performance.
    45              */
     53            // Add listener for menu items to toggle hover class by touches.
     54            // Remove the callback later for better performance.
    4655            adminBar.addEventListener( 'touchstart', function bindMobileEvents() {
    4756                for ( var i = 0; i < topMenuItems.length; i++ ) {
     
    5362        }
    5463
    55         /**
    56          * Scroll page to top when clicking on the admin bar.
    57          */
     64        // Scroll page to top when clicking on the admin bar.
    5865        adminBar.addEventListener( 'click', scrollToTop );
    5966
    6067        for ( i = 0; i < topMenuItems.length; i++ ) {
    61             /**
    62              * Adds or removes the hover class based on the hover intent.
    63              */
    64             hoverintent(
     68            // Adds or removes the hover class based on the hover intent.
     69            window.hoverintent(
    6570                topMenuItems[i],
    66                 addHoverClass.bind( null, topMenuItems[i] ),
    67                 removeHoverClass.bind( null, topMenuItems[i] )
     71                addClass.bind( null, topMenuItems[i], 'hover' ),
     72                removeClass.bind( null, topMenuItems[i], 'hover' )
    6873            ).options( {
    6974                timeout: 180
    7075            } );
    7176
    72             /**
    73              * Toggle hover class if the enter key is pressed.
    74              */
     77            // Toggle hover class if the enter key is pressed.
    7578            topMenuItems[i].addEventListener( 'keydown', toggleHoverIfEnter );
    7679        }
    7780
    78         /**
    79          * Remove hover class if the escape key is pressed.
    80          */
     81        // Remove hover class if the escape key is pressed.
    8182        for ( i = 0; i < allMenuItems.length; i++ ) {
    8283            allMenuItems[i].addEventListener( 'keydown', removeHoverIfEscape );
     
    8687            adminBarSearchInput = document.getElementById( 'adminbar-search' );
    8788
    88             /**
    89              * Adds the adminbar-focused class on focus.
    90              */
     89            // Adds the adminbar-focused class on focus.
    9190            adminBarSearchInput.addEventListener( 'focus', function() {
    92                 adminBarSearchForm.classList.add( 'adminbar-focused' );
    93             } );
    94 
    95             /**
    96              * Removes the adminbar-focused class on blur.
    97              */
     91                addClass( adminBarSearchForm, 'adminbar-focused' );
     92            } );
     93
     94            // Removes the adminbar-focused class on blur.
    9895            adminBarSearchInput.addEventListener( 'blur', function() {
    99                 adminBarSearchForm.classList.remove( 'adminbar-focused' );
    100             } );
    101         }
    102 
    103         /**
    104          * Focus the target of skip link after pressing Enter.
    105          */
    106         skipLink.addEventListener( 'keydown', focusTargetAfterEnter );
     96                removeClass( adminBarSearchForm, 'adminbar-focused' );
     97            } );
     98        }
     99
     100        if ( skipLink ) {
     101            // Focus the target of skip link after pressing Enter.
     102            skipLink.addEventListener( 'keydown', focusTargetAfterEnter );
     103        }
    107104
    108105        if ( shortlink ) {
     
    110107        }
    111108
    112         /**
    113          * Prevents the toolbar from covering up content when a hash is present
    114          * in the URL.
    115          */
     109        // Prevents the toolbar from covering up content when a hash is present in the URL.
    116110        if ( window.location.hash ) {
    117111            window.scrollBy( 0, -32 );
    118112        }
    119113
    120         /**
    121          * Add no-font-face class to body if needed.
    122          */
    123         if ( navigator.userAgent && document.body.className.indexOf( 'no-font-face' ) === -1 &&
    124             /Android (1.0|1.1|1.5|1.6|2.0|2.1)|Nokia|Opera Mini|w(eb)?OSBrowser|webOS|UCWEB|Windows Phone OS 7|XBLWP7|ZuneWP7|MSIE 7/.test( navigator.userAgent ) ) {
    125             document.body.className += ' no-font-face';
    126         }
    127 
    128         /**
    129          * Clear sessionStorage on logging out.
    130          */
    131         adminBarLogout.addEventListener( 'click', emptySessionStorage );
     114        // Add no-font-face class to body if needed.
     115        if (
     116            navigator.userAgent &&
     117            fontFaceRegex.test( navigator.userAgent ) &&
     118            ! hasClass( document.body, 'no-font-face' )
     119        ) {
     120            addClass( document.body, 'no-font-face' );
     121        }
     122
     123        // Clear sessionStorage on logging out.
     124        if ( adminBarLogout ) {
     125            adminBarLogout.addEventListener( 'click', emptySessionStorage );
     126        }
    132127    } );
    133128
     
    135130     * Remove hover class for top level menu item when escape is pressed.
    136131     *
    137      * @since 5.3.0
    138      *
    139      * @param {Event} e The keydown event.
    140      */
    141     function removeHoverIfEscape( e ) {
     132     * @since 5.3.1
     133     *
     134     * @param {Event} event The keydown event.
     135     */
     136    function removeHoverIfEscape( event ) {
    142137        var wrapper;
    143138
    144         if ( e.which != 27 ) {
    145             return;
    146         }
    147 
    148         wrapper = getClosest( e.target, '.menupop' );
     139        if ( event.which !== 27 ) {
     140            return;
     141        }
     142
     143        wrapper = getClosest( event.target, '.menupop' );
    149144
    150145        if ( ! wrapper ) {
     
    153148
    154149        wrapper.querySelector( '.menupop > .ab-item' ).focus();
    155         removeHoverClass( wrapper );
     150        removeClass( wrapper, 'hover' );
    156151    }
    157152
     
    159154     * Toggle hover class for top level menu item when enter is pressed.
    160155     *
    161      * @since 5.3.0
    162      *
    163      * @param {Event} e The keydown event.
    164      */
    165     function toggleHoverIfEnter( e ) {
     156     * @since 5.3.1
     157     *
     158     * @param {Event} event The keydown event.
     159     */
     160    function toggleHoverIfEnter( event ) {
    166161        var wrapper;
    167162
    168         if ( e.which != 13 ) {
    169             return;
    170         }
    171 
    172         if ( !! getClosest( e.target, '.ab-sub-wrapper' ) ) {
    173             return;
    174         }
    175 
    176         wrapper = getClosest( e.target, '.menupop' );
     163        if ( event.which !== 13 ) {
     164            return;
     165        }
     166
     167        if ( !! getClosest( event.target, '.ab-sub-wrapper' ) ) {
     168            return;
     169        }
     170
     171        wrapper = getClosest( event.target, '.menupop' );
    177172
    178173        if ( ! wrapper ) {
     
    180175        }
    181176
    182         e.preventDefault();
    183         if ( hasHoverClass( wrapper ) ) {
    184             removeHoverClass( wrapper );
     177        event.preventDefault();
     178
     179        if ( hasClass( wrapper, 'hover' ) ) {
     180            removeClass( wrapper, 'hover' );
    185181        } else {
    186             addHoverClass( wrapper );
     182            addClass( wrapper, 'hover' );
    187183        }
    188184    }
     
    191187     * Focus the target of skip link after pressing Enter.
    192188     *
    193      * @since 5.3.0
    194      *
    195      * @param {Event} e The keydown event.
    196      */
    197     function focusTargetAfterEnter( e ) {
     189     * @since 5.3.1
     190     *
     191     * @param {Event} event The keydown event.
     192     */
     193    function focusTargetAfterEnter( event ) {
    198194        var id, userAgent;
    199195
    200         if ( 13 !== e.which ) {
    201             return;
    202         }
    203 
    204         id = e.target.getAttribute( 'href' );
     196        if ( event.which !== 13 ) {
     197            return;
     198        }
     199
     200        id = event.target.getAttribute( 'href' );
    205201        userAgent = navigator.userAgent.toLowerCase();
    206202
    207         if ( userAgent.indexOf( 'applewebkit' ) != -1 && id && id.charAt( 0 ) == '#' ) {
     203        if ( userAgent.indexOf( 'applewebkit' ) > -1 && id && id.charAt( 0 ) === '#' ) {
    208204            setTimeout( function() {
    209205                var target = document.getElementById( id.replace( '#', '' ) );
    210206
    211                 target.setAttribute( 'tabIndex', '0' );
    212                 target.focus();
     207                if ( target ) {
     208                    target.setAttribute( 'tabIndex', '0' );
     209                    target.focus();
     210                }
    213211            }, 100 );
    214212        }
     
    218216     * Toogle hover class for mobile devices.
    219217     *
    220      * @since 5.3.0
     218     * @since 5.3.1
    221219     *
    222220     * @param {NodeList} topMenuItems All menu items.
    223      * @param {Event} e The click event.
    224      */
    225     function mobileHover( topMenuItems, e ) {
     221     * @param {Event} event The click event.
     222     */
     223    function mobileHover( topMenuItems, event ) {
    226224        var wrapper;
    227225
    228         if ( !! getClosest( e.target, '.ab-sub-wrapper' ) ) {
    229             return;
    230         }
    231 
    232         e.preventDefault();
    233 
    234         wrapper = getClosest( e.target, '.menupop' );
     226        if ( !! getClosest( event.target, '.ab-sub-wrapper' ) ) {
     227            return;
     228        }
     229
     230        event.preventDefault();
     231
     232        wrapper = getClosest( event.target, '.menupop' );
    235233
    236234        if ( ! wrapper ) {
     
    238236        }
    239237
    240         if ( hasHoverClass( wrapper ) ) {
    241             removeHoverClass( wrapper );
     238        if ( hasClass( wrapper, 'hover' ) ) {
     239            removeClass( wrapper, 'hover' );
    242240        } else {
    243241            removeAllHoverClass( topMenuItems );
    244             addHoverClass( wrapper );
     242            addClass( wrapper, 'hover' );
    245243        }
    246244    }
     
    250248     *
    251249     * @since 3.1.0
    252      * @since 5.3.0 Use querySelector to clean up the function.
    253      *
    254      * @param {Event} e The click event.
    255      *
     250     * @since 5.3.1 Use querySelector to clean up the function.
     251     *
     252     * @param {Event} event The click event.
    256253     * @return {boolean} Returns false to prevent default click behavior.
    257254     */
    258     function clickShortlink( e ) {
    259         var wrapper = e.target.parentNode,
     255    function clickShortlink( event ) {
     256        var wrapper = event.target.parentNode,
     257            input;
     258
     259        if ( wrapper ) {
    260260            input = wrapper.querySelector( '.shortlink-input' );
    261 
    262         // IE doesn't support preventDefault, and does support returnValue
    263         if ( e.preventDefault ) {
    264             e.preventDefault();
    265         }
    266         e.returnValue = false;
    267 
    268         wrapper.classList.add( 'selected' );
     261        }
     262
     263        if ( ! input ) {
     264            return;
     265        }
     266
     267        // (Old) IE doesn't support preventDefault, and does support returnValue
     268        if ( event.preventDefault ) {
     269            event.preventDefault();
     270        }
     271
     272        event.returnValue = false;
     273
     274        addClass( wrapper, 'selected' );
     275
    269276        input.focus();
    270277        input.select();
    271278        input.onblur = function() {
    272             wrapper.classList.remove( 'selected' );
     279            removeClass( wrapper, 'selected' );
    273280        };
    274281
     
    279286     * Clear sessionStorage on logging out.
    280287     *
    281      * @since 5.3.0
     288     * @since 5.3.1
    282289     */
    283290    function emptySessionStorage() {
     
    285292            try {
    286293                for ( var key in sessionStorage ) {
    287                     if ( key.indexOf( 'wp-autosave-' ) != -1 ) {
     294                    if ( key.indexOf( 'wp-autosave-' ) > -1 ) {
    288295                        sessionStorage.removeItem( key );
    289296                    }
    290297                }
    291             } catch ( e ) {}
    292         }
    293     }
    294 
    295     /**
    296      * Check if menu item has hover class.
    297      *
    298      * @since 5.3.0
    299      *
    300      * @param {HTMLElement} item Menu item Element.
    301      */
    302     function hasHoverClass( item ) {
    303         return item.classList.contains( 'hover' );
    304     }
    305 
    306     /**
    307      * Add hover class for menu item.
    308      *
    309      * @since 5.3.0
    310      *
    311      * @param {HTMLElement} item Menu item Element.
    312      */
    313     function addHoverClass( item ) {
    314         item.classList.add( 'hover' );
    315     }
    316 
    317     /**
    318      * Remove hover class for menu item.
    319      *
    320      * @since 5.3.0
    321      *
    322      * @param {HTMLElement} item Menu item Element.
    323      */
    324     function removeHoverClass( item ) {
    325         item.classList.remove( 'hover' );
     298            } catch ( er ) {}
     299        }
     300    }
     301
     302    /**
     303     * Check if element has class.
     304     *
     305     * @since 5.3.1
     306     *
     307     * @param {HTMLElement} element The HTML element.
     308     * @param {String}      className The class name.
     309     * @return {bool} Whether the element has the className.
     310     */
     311    function hasClass( element, className ) {
     312        var classNames;
     313
     314        if ( ! element ) {
     315            return false;
     316        }
     317
     318        if ( element.classList && element.classList.contains ) {
     319            return element.classList.contains( className );
     320        } else if ( element.className ) {
     321            classNames = element.className.split( ' ' );
     322            return classNames.indexOf( className ) > -1;
     323        }
     324
     325        return false;
     326    }
     327
     328    /**
     329     * Add class to an element.
     330     *
     331     * @since 5.3.1
     332     *
     333     * @param {HTMLElement} element The HTML element.
     334     * @param {String}      className The class name.
     335     */
     336    function addClass( element, className ) {
     337        if ( ! element ) {
     338            return;
     339        }
     340
     341        if ( element.classList && element.classList.add ) {
     342            element.classList.add( className );
     343        } else if ( ! hasClass( element, className ) ) {
     344            if ( element.className ) {
     345                element.className += ' ';
     346            }
     347
     348            element.className += className;
     349        }
     350    }
     351
     352    /**
     353     * Remove class from an element.
     354     *
     355     * @since 5.3.1
     356     *
     357     * @param {HTMLElement} element The HTML element.
     358     * @param {String}      className The class name.
     359     */
     360    function removeClass( element, className ) {
     361        var testName,
     362            classes;
     363
     364        if ( ! element || ! hasClass( element, className ) ) {
     365            return;
     366        }
     367
     368        if ( element.classList && element.classList.remove ) {
     369            element.classList.remove( className );
     370        } else {
     371            testName = ' ' + className + ' ';
     372            classes = ' ' + element.className + ' ';
     373
     374            while ( classes.indexOf( testName ) > -1 ) {
     375                classes = classes.replace( testName, '' );
     376            }
     377
     378            element.className = classes.replace( /^[\s]+|[\s]+$/g, '' );
     379        }
    326380    }
    327381
     
    329383     * Remove hover class for all menu items.
    330384     *
    331      * @since 5.3.0
     385     * @since 5.3.1
    332386     *
    333387     * @param {NodeList} topMenuItems All menu items.
    334388     */
    335389    function removeAllHoverClass( topMenuItems ) {
    336         for ( var i = 0; i < topMenuItems.length; i++ ) {
    337             if ( hasHoverClass( topMenuItems[i] ) ) {
    338                 removeHoverClass( topMenuItems[i] );
     390        if ( topMenuItems && topMenuItems.length ) {
     391            for ( var i = 0; i < topMenuItems.length; i++ ) {
     392                removeClass( topMenuItems[i], 'hover' );
    339393            }
    340394        }
     
    346400     * @since 3.4.0
    347401     *
    348      * @param {Event} e The Click event.
     402     * @param {Event} event The Click event.
    349403     *
    350404     * @return {void}
     
    354408        if (
    355409            event.target &&
    356             event.target.id &&
    357             event.target.id != 'wpadminbar' &&
    358             event.target.id != 'wp-admin-bar-top-secondary'
     410            event.target.id !== 'wpadminbar' &&
     411            event.target.id !== 'wp-admin-bar-top-secondary'
    359412        ) {
    360413            return;
     
    375428     * Get closest Element.
    376429     *
    377      * @since 5.3.0
     430     * @since 5.3.1
    378431     *
    379432     * @param {HTMLElement} el Element to get parent.
     
    381434     */
    382435    function getClosest( el, selector ) {
    383         if ( ! Element.prototype.matches ) {
    384             Element.prototype.matches =
    385                 Element.prototype.matchesSelector ||
    386                 Element.prototype.mozMatchesSelector ||
    387                 Element.prototype.msMatchesSelector ||
    388                 Element.prototype.oMatchesSelector ||
    389                 Element.prototype.webkitMatchesSelector ||
     436        if ( ! window.Element.prototype.matches ) {
     437            // Polyfill from https://developer.mozilla.org/en-US/docs/Web/API/Element/matches.
     438            window.Element.prototype.matches =
     439                window.Element.prototype.matchesSelector ||
     440                window.Element.prototype.mozMatchesSelector ||
     441                window.Element.prototype.msMatchesSelector ||
     442                window.Element.prototype.oMatchesSelector ||
     443                window.Element.prototype.webkitMatchesSelector ||
    390444                function( s ) {
    391445                    var matches = ( this.document || this.ownerDocument ).querySelectorAll( s ),
    392446                        i = matches.length;
     447
    393448                    while ( --i >= 0 && matches.item( i ) !== this ) { }
     449
    394450                    return i > -1;
    395451                };
     
    402458            }
    403459        }
     460
    404461        return null;
    405462    }
     463
    406464} )( document, window, navigator );
Note: See TracChangeset for help on using the changeset viewer.