From 20c17f7252f5124b6e4f42ab4843eb072fc8e500 Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Tue, 10 Dec 2019 01:06:27 +0000 Subject: [PATCH] Fix the admin toolbar js when jQuery is not present and replace the jQuery based hoverIntent.js with a native implementation. Introduces the "hoverintent" (no dependencies) package. Props dinhtungdu, audrasjb, azaozz. Merges [46872] to the 5.3 branch. Fixes #47069. git-svn-id: https://develop.svn.wordpress.org/branches/5.3@46873 602fd350-edb4-49c9-b593-d223f7449a82 --- Gruntfile.js | 3 + package-lock.json | 5 + package.json | 3 +- src/js/_enqueues/lib/admin-bar.js | 881 +++++++++++++----------------- src/wp-includes/script-loader.php | 5 +- 5 files changed, 382 insertions(+), 515 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index b31cb61fc7..4a5a42601f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -157,6 +157,9 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-includes/js/backbone.js' ]: [ './node_modules/backbone/backbone.js' ], [ WORKING_DIR + 'wp-includes/js/clipboard.js' ]: [ './node_modules/clipboard/dist/clipboard.js' ], [ WORKING_DIR + 'wp-includes/js/hoverIntent.js' ]: [ './node_modules/jquery-hoverintent/jquery.hoverIntent.js' ], + + // Renamed to avoid conflict with jQuery hoverIntent.min.js (after minifying) + [ WORKING_DIR + 'wp-includes/js/hoverintent-js.min.js' ]: [ './node_modules/hoverintent/dist/hoverintent.min.js' ], [ WORKING_DIR + 'wp-includes/js/imagesloaded.min.js' ]: [ './node_modules/imagesloaded/imagesloaded.pkgd.min.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery-migrate.js' ]: [ './node_modules/jquery-migrate/dist/jquery-migrate.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery-migrate.min.js' ]: [ './node_modules/jquery-migrate/dist/jquery-migrate.min.js' ], diff --git a/package-lock.json b/package-lock.json index 94abc3355b..7436a4eb77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13217,6 +13217,11 @@ "jquery": ">=1.7" } }, + "hoverintent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/hoverintent/-/hoverintent-2.2.1.tgz", + "integrity": "sha512-VyU54L1xW5rSqpsv/LJ6ecymGXsXXeGs9iVEKot4kKBCq5UodSAuy3DqX686LZxEpaMEfeCHPu4LndsMX5Q9eQ==" + }, "jquery-hoverintent": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/jquery-hoverintent/-/jquery-hoverintent-1.8.3.tgz", diff --git a/package.json b/package.json index 0531768f01..0858952158 100644 --- a/package.json +++ b/package.json @@ -120,8 +120,9 @@ "imagesloaded": "3.2.0", "jquery-color": "https://github.com/jquery/jquery-color/archive/2.1.2.tar.gz", "jquery-form": "4.2.1", - "jquery-hoverintent": "1.8.3", "jquery-ui": "https://github.com/jquery/jquery-ui/archive/1.11.4.tar.gz", + "jquery-hoverintent": "1.8.3", + "hoverintent": "2.2.1", "lodash": "4.17.15", "masonry-layout": "3.3.2", "moment": "2.22.2", diff --git a/src/js/_enqueues/lib/admin-bar.js b/src/js/_enqueues/lib/admin-bar.js index 6b3b8029a9..948de01fb3 100644 --- a/src/js/_enqueues/lib/admin-bar.js +++ b/src/js/_enqueues/lib/admin-bar.js @@ -1,550 +1,405 @@ /** * @output wp-includes/js/admin-bar.js */ - -/* jshint loopfunc: true */ -// use jQuery and hoverIntent if loaded -if ( typeof(jQuery) != 'undefined' ) { - if ( typeof(jQuery.fn.hoverIntent) == 'undefined' ) { - /* jshint ignore:start */ - // hoverIntent v1.8.1 - Copy of wp-includes/js/hoverIntent.min.js - !function(a){a.fn.hoverIntent=function(b,c,d){var e={interval:100,sensitivity:6,timeout:0};e="object"==typeof b?a.extend(e,b):a.isFunction(c)?a.extend(e,{over:b,out:c,selector:d}):a.extend(e,{over:b,out:b,selector:c});var f,g,h,i,j=function(a){f=a.pageX,g=a.pageY},k=function(b,c){return c.hoverIntent_t=clearTimeout(c.hoverIntent_t),Math.sqrt((h-f)*(h-f)+(i-g)*(i-g)) .ab-item').bind('keydown.adminbar', function(e){ - // Key code 13 is the enter key. - if ( e.which != 13 ) - return; + adminBar.addEventListener( 'click', scrollToTop ); - var target = $(e.target), - wrap = target.closest('.ab-sub-wrapper'), - parentHasHover = target.parent().hasClass('hover'); + for ( i = 0; i < topMenuItems.length; i++ ) { + /** + * Adds or removes the hover class based on the hover intent. + */ + hoverintent( + topMenuItems[i], + addHoverClass.bind( null, topMenuItems[i] ), + removeHoverClass.bind( null, topMenuItems[i] ) + ).options( { + timeout: 180, + } ); - e.stopPropagation(); - e.preventDefault(); - - if ( !wrap.length ) - wrap = $('#wpadminbar .quicklinks'); - - wrap.find('.menupop').removeClass('hover'); - - if ( ! parentHasHover ) { - target.parent().toggleClass('hover'); - } - - target.siblings('.ab-sub-wrapper').find('.ab-item').each(refresh); - }).each(refresh); + /** + * Toggle hover class if the enter key is pressed. + */ + topMenuItems[i].addEventListener( 'keydown', toggleHoverIfEnter ); + } /** - * Removes the hover class when the escape key is pressed. - * - * Makes sure the tab index is refreshed by refreshing each ab-item - * and its children. - * - * @param {Object} e The keydown event. - * - * @return {void} + * Remove hover class if the escape key is pressed. */ - $('#wpadminbar .ab-item').bind('keydown.adminbar', function(e){ - // Key code 27 is the escape key. - if ( e.which != 27 ) - return; + for ( i = 0; i < allMenuItems.length; i++ ) { + allMenuItems[i].addEventListener( 'keydown', removeHoverIfEscape ); + } - var target = $(e.target); + if ( adminBarSearchForm ) { + adminBarSearchInput = document.getElementById( 'adminbar-search' ); - e.stopPropagation(); - e.preventDefault(); - - target.closest('.hover').removeClass('hover').children('.ab-item').focus(); - target.siblings('.ab-sub-wrapper').find('.ab-item').each(refresh); - }); - - /** - * Scrolls to top of page by clicking the adminbar. - * - * @param {Object} e The click event. - * - * @return {void} - */ - adminbar.click( function(e) { - if ( e.target.id != 'wpadminbar' && e.target.id != 'wp-admin-bar-top-secondary' ) { - return; - } - - adminbar.find( 'li.menupop.hover' ).removeClass( 'hover' ); - $( 'html, body' ).animate( { scrollTop: 0 }, 'fast' ); - e.preventDefault(); - }); - - /** - * Sets the focus on an element with a href attribute. - * - * The timeout is used to fix a focus bug in WebKit. - * - * @param {Object} e The keydown event. - * - * @return {void} - */ - $('.screen-reader-shortcut').keydown( function(e) { - var id, ua; - - if ( 13 != e.which ) - return; - - id = $( this ).attr( 'href' ); - - ua = navigator.userAgent.toLowerCase(); - - if ( ua.indexOf('applewebkit') != -1 && id && id.charAt(0) == '#' ) { - setTimeout(function () { - $(id).focus(); - }, 100); - } - }); - - $( '#adminbar-search' ).on({ /** * Adds the adminbar-focused class on focus. - * - * @return {void} */ - focus: function() { - $( '#adminbarsearch' ).addClass( 'adminbar-focused' ); + adminBarSearchInput.addEventListener( 'focus', function() { + adminBarSearchForm.classList.add( 'adminbar-focused' ); + } ); + /** * Removes the adminbar-focused class on blur. - * - * @return {void} */ - }, blur: function() { - $( '#adminbarsearch' ).removeClass( 'adminbar-focused' ); - } - } ); - - if ( 'sessionStorage' in window ) { - /** - * Empties sessionStorage on logging out. - * - * @return {void} - */ - $('#wp-admin-bar-logout a').click( function() { - try { - for ( var key in sessionStorage ) { - if ( key.indexOf('wp-autosave-') != -1 ) - sessionStorage.removeItem(key); - } - } catch(e) {} - }); + adminBarSearchInput.addEventListener( 'blur', function() { + adminBarSearchForm.classList.remove( 'adminbar-focused' ); + } ); } + /** + * Focus the target of skip link after pressing Enter. + */ + skipLink.addEventListener( 'keydown', focusTargetAfterEnter ); + + if ( shortlink ) { + shortlink.addEventListener( 'click', clickShortlink ); + } + + /** + * Prevents the toolbar from covering up content when a hash is present + * in the URL. + */ + if ( window.location.hash ) { + window.scrollBy( 0, -32 ); + } + + /** + * Add no-font-face class to body if needed. + */ if ( navigator.userAgent && document.body.className.indexOf( 'no-font-face' ) === -1 && /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 ) ) { - document.body.className += ' no-font-face'; } - }); -} else { + + /** + * Clear sessionStorage on logging out. + */ + adminBarLogout.addEventListener( 'click', emptySessionStorage ); + } ); + /** - * Wrapper function for the adminbar that's used if jQuery isn't available. + * Remove hover class for top level menu item when escape is pressed. * - * @param {Object} d The document object. - * @param {Object} w The window object. + * @since 5.3.0 + * + * @param {Event} e The keydown event. + */ + function removeHoverIfEscape( e ) { + var wrapper; + + if ( e.which != 27 ) { + return; + } + + wrapper = getClosest( e.target, '.menupop' ); + + if ( ! wrapper ) { + return; + } + + wrapper.querySelector( '.menupop > .ab-item' ).focus(); + removeHoverClass( wrapper ); + } + + /** + * Toggle hover class for top level menu item when enter is pressed. + * + * @since 5.3.0 + * + * @param {Event} e The keydown event. + */ + function toggleHoverIfEnter( e ) { + var wrapper; + + if ( e.which != 13 ) { + return; + } + + if ( !! getClosest( e.target, '.ab-sub-wrapper' ) ) { + return; + } + + wrapper = getClosest( e.target, '.menupop' ); + + if ( ! wrapper ) { + return; + } + + e.preventDefault(); + if ( hasHoverClass( wrapper ) ) { + removeHoverClass( wrapper ); + } else { + addHoverClass( wrapper ); + } + } + + /** + * Focus the target of skip link after pressing Enter. + * + * @since 5.3.0 + * + * @param {Event} e The keydown event. + */ + function focusTargetAfterEnter( e ) { + var id, userAgent; + + if ( 13 !== e.which ) { + return; + } + + id = e.target.getAttribute( 'href' ); + userAgent = navigator.userAgent.toLowerCase(); + + if ( userAgent.indexOf( 'applewebkit' ) != -1 && id && id.charAt( 0 ) == '#' ) { + setTimeout( function() { + var target = document.getElementById( id.replace( '#', '' ) ); + + target.setAttribute( 'tabIndex', '0' ); + target.focus(); + }, 100 ); + } + } + + /** + * Toogle hover class for mobile devices. + * + * @since 5.3.0 + * + * @param {NodeList} topMenuItems All menu items. + * @param {Event} e The click event. + */ + function mobileHover( topMenuItems, e ) { + var wrapper; + + if ( !! getClosest( e.target, '.ab-sub-wrapper' ) ) { + return; + } + + e.preventDefault(); + + wrapper = getClosest( e.target, '.menupop' ); + + if ( ! wrapper ) { + return; + } + + if ( hasHoverClass( wrapper ) ) { + removeHoverClass( wrapper ); + } else { + removeAllHoverClass( topMenuItems ); + addHoverClass( wrapper ); + } + } + + /** + * Handles the click on the Shortlink link in the adminbar. + * + * @since 3.1.0 + * @since 5.3.0 Use querySelector to clean up the function. + * + * @param {Event} e The click event. + * + * @return {boolean} Returns false to prevent default click behavior. + */ + function clickShortlink( e ) { + var wrapper = e.target.parentNode, + input = wrapper.querySelector( '.shortlink-input' ); + + // IE doesn't support preventDefault, and does support returnValue + if ( e.preventDefault ) { + e.preventDefault(); + } + e.returnValue = false; + + wrapper.classList.add( 'selected' ); + input.focus(); + input.select(); + input.onblur = function() { + wrapper.classList.remove( 'selected' ); + }; + + return false; + } + + /** + * Clear sessionStorage on logging out. + * + * @since 5.3.0 + */ + function emptySessionStorage() { + if ( 'sessionStorage' in window ) { + try { + for ( var key in sessionStorage ) { + if ( key.indexOf( 'wp-autosave-' ) != -1 ) { + sessionStorage.removeItem( key ); + } + } + } catch ( e ) {} + } + } + + /** + * Check if menu item has hover class. + * + * @since 5.3.0 + * + * @param {HTMLElement} item Menu item Element. + */ + function hasHoverClass( item ) { + return item.classList.contains( 'hover' ); + } + + /** + * Add hover class for menu item. + * + * @since 5.3.0 + * + * @param {HTMLElement} item Menu item Element. + */ + function addHoverClass( item ) { + item.classList.add( 'hover' ); + } + + /** + * Remove hover class for menu item. + * + * @since 5.3.0 + * + * @param {HTMLElement} item Menu item Element. + */ + function removeHoverClass( item ) { + item.classList.remove( 'hover' ); + } + + /** + * Remove hover class for all menu items. + * + * @since 5.3.0 + * + * @param {NodeList} topMenuItems All menu items. + */ + function removeAllHoverClass( topMenuItems ) { + for ( var i = 0; i < topMenuItems.length; i++ ) { + if ( hasHoverClass( topMenuItems[i] ) ) { + removeHoverClass( topMenuItems[i] ); + } + } + } + + /** + * Scrolls to the top of the page. + * + * @since 3.4.0 + * + * @param {Event} e The Click event. * * @return {void} */ - (function(d, w) { - /** - * Adds an event listener to an object. - * - * @since 3.1.0 - * - * @param {Object} obj The object to add the event listener to. - * @param {string} type The type of event. - * @param {function} fn The function to bind to the event listener. - * - * @return {void} - */ - var addEvent = function( obj, type, fn ) { - if ( obj && typeof obj.addEventListener === 'function' ) { - obj.addEventListener( type, fn, false ); - } else if ( obj && typeof obj.attachEvent === 'function' ) { - obj.attachEvent( 'on' + type, function() { - return fn.call( obj, window.event ); - } ); + function scrollToTop( event ) { + // Only scroll when clicking on the wpadminbar, not on menus or submenus. + if ( + event.target && + event.target.id && + event.target.id != 'wpadminbar' && + event.target.id != 'wp-admin-bar-top-secondary' + ) { + return; + } + + try { + window.scrollTo( { + top: -32, + left: 0, + behavior: 'smooth' + } ); + } catch ( er ) { + window.scrollTo( 0, -32 ); + } + } + + /** + * Get closest Element. + * + * @since 5.3.0 + * + * @param {HTMLElement} el Element to get parent. + * @param {string} selector CSS selector to match. + */ + function getClosest( el, selector ) { + if ( ! Element.prototype.matches ) { + Element.prototype.matches = + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector || + function( s ) { + var matches = ( this.document || this.ownerDocument ).querySelectorAll( s ), + i = matches.length; + while ( --i >= 0 && matches.item( i ) !== this ) { } + return i > -1; + }; + } + + // Get the closest matching elent + for ( ; el && el !== document; el = el.parentNode ) { + if ( el.matches( selector ) ) { + return el; } - }, - - aB, hc = new RegExp('\\bhover\\b', 'g'), q = [], - rselected = new RegExp('\\bselected\\b', 'g'), - - /** - * Gets the timeout ID of the given element. - * - * @since 3.1.0 - * - * @param {HTMLElement} el The HTML element. - * - * @return {number|boolean} The ID value of the timer that is set or false. - */ - getTOID = function(el) { - var i = q.length; - while ( i-- ) { - if ( q[i] && el == q[i][1] ) - return q[i][0]; - } - return false; - }, - - /** - * Adds the hoverclass to menu items. - * - * @since 3.1.0 - * - * @param {HTMLElement} t The HTML element. - * - * @return {void} - */ - addHoverClass = function(t) { - var i, id, inA, hovering, ul, li, - ancestors = [], - ancestorLength = 0; - - // aB is adminbar. d is document. - while ( t && t != aB && t != d ) { - if ( 'LI' == t.nodeName.toUpperCase() ) { - ancestors[ ancestors.length ] = t; - id = getTOID(t); - if ( id ) - clearTimeout( id ); - t.className = t.className ? ( t.className.replace(hc, '') + ' hover' ) : 'hover'; - hovering = t; - } - t = t.parentNode; - } - - // Removes any selected classes. - if ( hovering && hovering.parentNode ) { - ul = hovering.parentNode; - if ( ul && 'UL' == ul.nodeName.toUpperCase() ) { - i = ul.childNodes.length; - while ( i-- ) { - li = ul.childNodes[i]; - if ( li != hovering ) - li.className = li.className ? li.className.replace( rselected, '' ) : ''; - } - } - } - - // Removes the hover class for any objects not in the immediate element's ancestry. - i = q.length; - while ( i-- ) { - inA = false; - ancestorLength = ancestors.length; - while( ancestorLength-- ) { - if ( ancestors[ ancestorLength ] == q[i][1] ) - inA = true; - } - - if ( ! inA ) - q[i][1].className = q[i][1].className ? q[i][1].className.replace(hc, '') : ''; - } - }, - - /** - * Removes the hoverclass from menu items. - * - * @since 3.1.0 - * - * @param {HTMLElement} t The HTML element. - * - * @return {void} - */ - removeHoverClass = function(t) { - while ( t && t != aB && t != d ) { - if ( 'LI' == t.nodeName.toUpperCase() ) { - (function(t) { - var to = setTimeout(function() { - t.className = t.className ? t.className.replace(hc, '') : ''; - }, 500); - q[q.length] = [to, t]; - })(t); - } - t = t.parentNode; - } - }, - - /** - * Handles the click on the Shortlink link in the adminbar. - * - * @since 3.1.0 - * - * @param {Object} e The click event. - * - * @return {boolean} Returns false to prevent default click behavior. - */ - clickShortlink = function(e) { - var i, l, node, - t = e.target || e.srcElement; - - // Make t the shortlink menu item, or return. - while ( true ) { - // Check if we've gone past the shortlink node, - // or if the user is clicking on the input. - if ( ! t || t == d || t == aB ) - return; - // Check if we've found the shortlink node. - if ( t.id && t.id == 'wp-admin-bar-get-shortlink' ) - break; - t = t.parentNode; - } - - // IE doesn't support preventDefault, and does support returnValue - if ( e.preventDefault ) - e.preventDefault(); - e.returnValue = false; - - if ( -1 == t.className.indexOf('selected') ) - t.className += ' selected'; - - for ( i = 0, l = t.childNodes.length; i < l; i++ ) { - node = t.childNodes[i]; - if ( node.className && -1 != node.className.indexOf('shortlink-input') ) { - node.focus(); - node.select(); - node.onblur = function() { - t.className = t.className ? t.className.replace( rselected, '' ) : ''; - }; - break; - } - } - return false; - }, - - /** - * Scrolls to the top of the page. - * - * @since 3.4.0 - * - * @param {HTMLElement} t The HTML element. - * - * @return {void} - */ - scrollToTop = function(t) { - var distance, speed, step, steps, timer, speed_step; - - // Ensure that the #wpadminbar was the target of the click. - if ( t.id != 'wpadminbar' && t.id != 'wp-admin-bar-top-secondary' ) - return; - - distance = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; - - if ( distance < 1 ) - return; - - speed_step = distance > 800 ? 130 : 100; - speed = Math.min( 12, Math.round( distance / speed_step ) ); - step = distance > 800 ? Math.round( distance / 30 ) : Math.round( distance / 20 ); - steps = []; - timer = 0; - - // Animate scrolling to the top of the page by generating steps to - // the top of the page and shifting to each step at a set interval. - while ( distance ) { - distance -= step; - if ( distance < 0 ) - distance = 0; - steps.push( distance ); - - setTimeout( function() { - window.scrollTo( 0, steps.shift() ); - }, timer * speed ); - - timer++; - } - }; - - addEvent(w, 'load', function() { - aB = d.getElementById('wpadminbar'); - - if ( d.body && aB ) { - d.body.appendChild( aB ); - - if ( aB.className ) - aB.className = aB.className.replace(/nojs/, ''); - - addEvent(aB, 'mouseover', function(e) { - addHoverClass( e.target || e.srcElement ); - }); - - addEvent(aB, 'mouseout', function(e) { - removeHoverClass( e.target || e.srcElement ); - }); - - addEvent(aB, 'click', clickShortlink ); - - addEvent(aB, 'click', function(e) { - scrollToTop( e.target || e.srcElement ); - }); - - addEvent( document.getElementById('wp-admin-bar-logout'), 'click', function() { - if ( 'sessionStorage' in window ) { - try { - for ( var key in sessionStorage ) { - if ( key.indexOf('wp-autosave-') != -1 ) - sessionStorage.removeItem(key); - } - } catch(e) {} - } - }); - } - - if ( w.location.hash ) - w.scrollBy(0,-32); - - if ( navigator.userAgent && document.body.className.indexOf( 'no-font-face' ) === -1 && - /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 ) ) { - - document.body.className += ' no-font-face'; - } - }); - })(document, window); - -} + } + return null; + } +} )( document, window, navigator ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 8f62ae0384..5e1d6eba26 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1490,7 +1490,7 @@ function wp_default_scripts( &$scripts ) { $scripts->add( 'user-suggest', "/wp-admin/js/user-suggest$suffix.js", array( 'jquery-ui-autocomplete' ), false, 1 ); - $scripts->add( 'admin-bar', "/wp-includes/js/admin-bar$suffix.js", array(), false, 1 ); + $scripts->add( 'admin-bar', "/wp-includes/js/admin-bar$suffix.js", array( 'hoverintent-js' ), false, 1 ); $scripts->add( 'wplink', "/wp-includes/js/wplink$suffix.js", array( 'jquery', 'wp-a11y' ), false, 1 ); did_action( 'init' ) && $scripts->localize( @@ -1515,6 +1515,9 @@ function wp_default_scripts( &$scripts ) { $scripts->add( 'hoverIntent', "/wp-includes/js/hoverIntent$suffix.js", array( 'jquery' ), '1.8.1', 1 ); + // JS-only version of hoverintent (no dependencies). + $scripts->add( 'hoverintent-js', "/wp-includes/js/hoverintent-js.min.js", array(), '2.2.1', 1 ); + $scripts->add( 'customize-base', "/wp-includes/js/customize-base$suffix.js", array( 'jquery', 'json2', 'underscore' ), false, 1 ); $scripts->add( 'customize-loader', "/wp-includes/js/customize-loader$suffix.js", array( 'customize-base' ), false, 1 ); $scripts->add( 'customize-preview', "/wp-includes/js/customize-preview$suffix.js", array( 'wp-a11y', 'customize-base' ), false, 1 );