diff options
author | Michał Gołębiowski-Owczarek <m.goleb@gmail.com> | 2025-03-21 00:03:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-21 00:03:17 +0100 |
commit | 44de3d325c1ac0c4a841deff0ec03265a0b670f7 (patch) | |
tree | 1e2d6714843aead988ec134ecace8a75e01cba24 | |
parent | 6843ced12e4051aefbee47cf87fa79794737eb8a (diff) | |
download | jquery-ui-44de3d325c1ac0c4a841deff0ec03265a0b670f7.tar.gz jquery-ui-44de3d325c1ac0c4a841deff0ec03265a0b670f7.zip |
As of gh-2338, if one has loaded the jQuery MouseWheel plugin, the `mousewheel`
handler would fire the `wheel` one, but the `wheel` one would also run in
response to the native `wheel` event, resulting in double the distance handled
by the spinner. To prevent the issue, only fire the `wheel` handler from inside
the `mousewheel` on if the event was triggered by jQuery - jQuery will not care
that the underlying event is `wheel` and will only fire handlers for
`mousewheel`.
Also, add an iframe test using jQuery MouseWheel to not affect all the other
tests.
Plus, migrate from `QUnit.reset` to `QUnit.done` (see qunitjs/qunit#354).
Closes gh-2342
Ref gh-2338
-rw-r--r-- | Gruntfile.js | 3 | ||||
-rw-r--r-- | bower.json | 1 | ||||
-rw-r--r-- | external/jquery-mousewheel/LICENSE.txt | 36 | ||||
-rw-r--r-- | external/jquery-mousewheel/jquery.mousewheel.js | 242 | ||||
-rw-r--r-- | tests/lib/helper.js | 59 | ||||
-rw-r--r-- | tests/lib/qunit.js | 23 | ||||
-rw-r--r-- | tests/lib/testIframe.js | 7 | ||||
-rw-r--r-- | tests/unit/spinner/core.js | 14 | ||||
-rw-r--r-- | tests/unit/spinner/mousewheel-wheel.html | 72 | ||||
-rw-r--r-- | ui/widgets/spinner.js | 7 |
10 files changed, 456 insertions, 8 deletions
diff --git a/Gruntfile.js b/Gruntfile.js index 4f7dcc73e..bbb71d33e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -247,6 +247,9 @@ grunt.initConfig( { "requirejs/require.js": "requirejs/require.js", + "jquery-mousewheel/jquery.mousewheel.js": "jquery-mousewheel/jquery.mousewheel.js", + "jquery-mousewheel/LICENSE.txt": "jquery-mousewheel/LICENSE.txt", + "jquery-simulate/jquery.simulate.js": "jquery-simulate/jquery.simulate.js", "jquery-simulate/LICENSE.txt": "jquery-simulate/LICENSE.txt", diff --git a/bower.json b/bower.json index 3ed76cee9..eb3187e0c 100644 --- a/bower.json +++ b/bower.json @@ -13,6 +13,7 @@ }, "devDependencies": { "jquery-color": "3.0.0", + "jquery-mousewheel": "3.2.2", "jquery-simulate": "1.1.1", "qunit": "2.19.4", "requirejs": "2.1.14", diff --git a/external/jquery-mousewheel/LICENSE.txt b/external/jquery-mousewheel/LICENSE.txt new file mode 100644 index 000000000..f56b79ae0 --- /dev/null +++ b/external/jquery-mousewheel/LICENSE.txt @@ -0,0 +1,36 @@ +Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-mousewheel + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. diff --git a/external/jquery-mousewheel/jquery.mousewheel.js b/external/jquery-mousewheel/jquery.mousewheel.js new file mode 100644 index 000000000..aec55baf8 --- /dev/null +++ b/external/jquery-mousewheel/jquery.mousewheel.js @@ -0,0 +1,242 @@ +/*! + * jQuery Mousewheel 3.2.2 + * Copyright OpenJS Foundation and other contributors + */ + +( function( factory ) { + "use strict"; + + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define( [ "jquery" ], factory ); + } else if ( typeof exports === "object" ) { + + // Node/CommonJS style for Browserify + module.exports = factory; + } else { + + // Browser globals + factory( jQuery ); + } +} )( function( $ ) { + "use strict"; + + var nullLowestDeltaTimeout, lowestDelta, + modernEvents = !!$.fn.on, + toFix = [ "wheel", "mousewheel", "DOMMouseScroll", "MozMousePixelScroll" ], + toBind = ( "onwheel" in window.document || window.document.documentMode >= 9 ) ? + [ "wheel" ] : [ "mousewheel", "DomMouseScroll", "MozMousePixelScroll" ], + slice = Array.prototype.slice; + + if ( $.event.fixHooks ) { + for ( var i = toFix.length; i; ) { + $.event.fixHooks[ toFix[ --i ] ] = $.event.mouseHooks; + } + } + + var special = $.event.special.mousewheel = { + version: "3.2.2", + + setup: function() { + if ( this.addEventListener ) { + for ( var i = toBind.length; i; ) { + this.addEventListener( toBind[ --i ], handler, false ); + } + } else { + this.onmousewheel = handler; + } + + // Store the line height and page height for this particular element + $.data( this, "mousewheel-line-height", special.getLineHeight( this ) ); + $.data( this, "mousewheel-page-height", special.getPageHeight( this ) ); + }, + + teardown: function() { + if ( this.removeEventListener ) { + for ( var i = toBind.length; i; ) { + this.removeEventListener( toBind[ --i ], handler, false ); + } + } else { + this.onmousewheel = null; + } + + // Clean up the data we added to the element + $.removeData( this, "mousewheel-line-height" ); + $.removeData( this, "mousewheel-page-height" ); + }, + + getLineHeight: function( elem ) { + var $elem = $( elem ), + $parent = $elem[ "offsetParent" in $.fn ? "offsetParent" : "parent" ](); + if ( !$parent.length ) { + $parent = $( "body" ); + } + return parseInt( $parent.css( "fontSize" ), 10 ) || + parseInt( $elem.css( "fontSize" ), 10 ) || 16; + }, + + getPageHeight: function( elem ) { + return $( elem ).height(); + }, + + settings: { + adjustOldDeltas: true, // see shouldAdjustOldDeltas() below + normalizeOffset: true // calls getBoundingClientRect for each event + } + }; + + $.fn.extend( { + mousewheel: function( fn ) { + return fn ? + this[ modernEvents ? "on" : "bind" ]( "mousewheel", fn ) : + this.trigger( "mousewheel" ); + }, + + unmousewheel: function( fn ) { + return this[ modernEvents ? "off" : "unbind" ]( "mousewheel", fn ); + } + } ); + + + function handler( event ) { + var orgEvent = event || window.event, + args = slice.call( arguments, 1 ), + delta = 0, + deltaX = 0, + deltaY = 0, + absDelta = 0; + event = $.event.fix( orgEvent ); + event.type = "mousewheel"; + + // Old school scrollwheel delta + if ( "detail" in orgEvent ) { + deltaY = orgEvent.detail * -1; + } + if ( "wheelDelta" in orgEvent ) { + deltaY = orgEvent.wheelDelta; + } + if ( "wheelDeltaY" in orgEvent ) { + deltaY = orgEvent.wheelDeltaY; + } + if ( "wheelDeltaX" in orgEvent ) { + deltaX = orgEvent.wheelDeltaX * -1; + } + + // Firefox < 17 horizontal scrolling related to DOMMouseScroll event + if ( "axis" in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { + deltaX = deltaY * -1; + deltaY = 0; + } + + // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatability + delta = deltaY === 0 ? deltaX : deltaY; + + // New school wheel delta (wheel event) + if ( "deltaY" in orgEvent ) { + deltaY = orgEvent.deltaY * -1; + delta = deltaY; + } + if ( "deltaX" in orgEvent ) { + deltaX = orgEvent.deltaX; + if ( deltaY === 0 ) { + delta = deltaX * -1; + } + } + + // No change actually happened, no reason to go any further + if ( deltaY === 0 && deltaX === 0 ) { + return; + } + + // Need to convert lines and pages to pixels if we aren't already in pixels + // There are three delta modes: + // * deltaMode 0 is by pixels, nothing to do + // * deltaMode 1 is by lines + // * deltaMode 2 is by pages + if ( orgEvent.deltaMode === 1 ) { + var lineHeight = $.data( this, "mousewheel-line-height" ); + delta *= lineHeight; + deltaY *= lineHeight; + deltaX *= lineHeight; + } else if ( orgEvent.deltaMode === 2 ) { + var pageHeight = $.data( this, "mousewheel-page-height" ); + delta *= pageHeight; + deltaY *= pageHeight; + deltaX *= pageHeight; + } + + // Store lowest absolute delta to normalize the delta values + absDelta = Math.max( Math.abs( deltaY ), Math.abs( deltaX ) ); + + if ( !lowestDelta || absDelta < lowestDelta ) { + lowestDelta = absDelta; + + // Adjust older deltas if necessary + if ( shouldAdjustOldDeltas( orgEvent, absDelta ) ) { + lowestDelta /= 40; + } + } + + // Adjust older deltas if necessary + if ( shouldAdjustOldDeltas( orgEvent, absDelta ) ) { + + // Divide all the things by 40! + delta /= 40; + deltaX /= 40; + deltaY /= 40; + } + + // Get a whole, normalized value for the deltas + delta = Math[ delta >= 1 ? "floor" : "ceil" ]( delta / lowestDelta ); + deltaX = Math[ deltaX >= 1 ? "floor" : "ceil" ]( deltaX / lowestDelta ); + deltaY = Math[ deltaY >= 1 ? "floor" : "ceil" ]( deltaY / lowestDelta ); + + // Normalise offsetX and offsetY properties + if ( special.settings.normalizeOffset && this.getBoundingClientRect ) { + var boundingRect = this.getBoundingClientRect(); + event.offsetX = event.clientX - boundingRect.left; + event.offsetY = event.clientY - boundingRect.top; + } + + // Add information to the event object + event.deltaX = deltaX; + event.deltaY = deltaY; + event.deltaFactor = lowestDelta; + + // Go ahead and set deltaMode to 0 since we converted to pixels + // Although this is a little odd since we overwrite the deltaX/Y + // properties with normalized deltas. + event.deltaMode = 0; + + // Add event and delta to the front of the arguments + args.unshift( event, delta, deltaX, deltaY ); + + // Clear out lowestDelta after sometime to better + // handle multiple device types that give different + // a different lowestDelta + // Ex: trackpad = 3 and mouse wheel = 120 + if ( nullLowestDeltaTimeout ) { + window.clearTimeout( nullLowestDeltaTimeout ); + } + nullLowestDeltaTimeout = window.setTimeout( function() { + lowestDelta = null; + }, 200 ); + + return ( $.event.dispatch || $.event.handle ).apply( this, args ); + } + + function shouldAdjustOldDeltas( orgEvent, absDelta ) { + + // If this is an older event and the delta is divisible by 120, + // then we are assuming that the browser is treating this as an + // older mouse wheel event and that we should divide the deltas + // by 40 to try and get a more usable deltaFactor. + // Side note, this actually impacts the reported scroll distance + // in older browsers and can cause scrolling to be slower than native. + // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false. + return special.settings.adjustOldDeltas && orgEvent.type === "mousewheel" && + absDelta % 120 === 0; + } + +} ); diff --git a/tests/lib/helper.js b/tests/lib/helper.js index 2315c5e19..2be4c48de 100644 --- a/tests/lib/helper.js +++ b/tests/lib/helper.js @@ -51,6 +51,65 @@ exports.moduleAfterEach = function( assert ) { } }; +exports.testIframe = function( title, fileName, func, wrapper, iframeStyles ) { + if ( !wrapper ) { + wrapper = QUnit.test; + } + wrapper.call( QUnit, title, function( assert ) { + var done = assert.async(), + $iframe = jQuery( "<iframe></iframe>" ) + .css( { + position: "absolute", + top: "0", + left: "-600px", + width: "500px", + zIndex: 1, + background: "white" + } ) + .attr( { id: "qunit-fixture-iframe", src: fileName } ); + + // Add other iframe styles + if ( iframeStyles ) { + $iframe.css( iframeStyles ); + } + + // Test iframes are expected to invoke this via startIframeTest + // (cf. iframeTest.js) + window.iframeCallback = function() { + var args = Array.prototype.slice.call( arguments ); + + args.unshift( assert ); + + setTimeout( function() { + var result; + + this.iframeCallback = undefined; + + result = func.apply( this, args ); + + function finish() { + func = function() {}; + $iframe.remove(); + done(); + } + + // Wait for promises returned by `func`. + if ( result && result.then ) { + result.then( finish ); + } else { + finish(); + } + } ); + }; + + // Attach iframe to the body for visibility-dependent code. + // It will be removed by either the above code, or the testDone + // callback in qunit.js. + $iframe.prependTo( document.body ); + } ); +}; +window.iframeCallback = undefined; + return exports; } ); diff --git a/tests/lib/qunit.js b/tests/lib/qunit.js index 6441019bd..c4c96ef58 100644 --- a/tests/lib/qunit.js +++ b/tests/lib/qunit.js @@ -7,6 +7,8 @@ define( [ ], function( QUnit, $ ) { "use strict"; +var ajaxSettings = $.ajaxSettings; + QUnit.config.autostart = false; QUnit.config.requireExpects = true; @@ -34,16 +36,21 @@ QUnit.config.urlConfig.push( { label: "Enable jquery-migrate" } ); -QUnit.reset = ( function( reset ) { - return function() { +QUnit.testDone( function() { + + // Ensure jQuery events and data on the fixture are properly removed + $( "#qunit-fixture" ).empty(); - // Ensure jQuery events and data on the fixture are properly removed - $( "#qunit-fixture" ).empty(); + // Remove the iframe fixture + $( "#qunit-fixture-iframe" ).remove(); - // Let QUnit reset the fixture - reset.apply( this, arguments ); - }; -} )( QUnit.reset ); + // Reset internal $ state + if ( ajaxSettings ) { + $.ajaxSettings = $.extend( true, {}, ajaxSettings ); + } else { + delete $.ajaxSettings; + } +} ); return QUnit; diff --git a/tests/lib/testIframe.js b/tests/lib/testIframe.js new file mode 100644 index 000000000..4db56833c --- /dev/null +++ b/tests/lib/testIframe.js @@ -0,0 +1,7 @@ +window.startIframeTest = function() { + var args = Array.prototype.slice.call( arguments ); + + // Note: jQuery may be undefined if page did not load it + args.unshift( window.jQuery, window, document ); + window.parent.iframeCallback.apply( null, args ); +}; diff --git a/tests/unit/spinner/core.js b/tests/unit/spinner/core.js index befe439f6..42bcc7bb5 100644 --- a/tests/unit/spinner/core.js +++ b/tests/unit/spinner/core.js @@ -239,6 +239,20 @@ QUnit.test( "mousewheel on input (DEPRECATED)", function( assert ) { } } ); +helper.testIframe( + "wheel & mousewheel conflicts", + "mousewheel-wheel.html", + function( assert, jQuery, window, document, values ) { + assert.expect( 5 ); + + assert.equal( values[ 0 ], 0, "wheel event without delta does not change value" ); + assert.equal( values[ 1 ], 2, "delta -1" ); + assert.equal( values[ 2 ], 0, "delta 0.2" ); + assert.equal( values[ 3 ], -2, "delta 15" ); + assert.equal( values[ 4 ], -2, "wheel when not focused" ); + } +); + QUnit.test( "reading HTML5 attributes", function( assert ) { assert.expect( 6 ); var markup = "<input type='number' min='-100' max='100' value='5' step='2'>", diff --git a/tests/unit/spinner/mousewheel-wheel.html b/tests/unit/spinner/mousewheel-wheel.html new file mode 100644 index 000000000..e512a36cc --- /dev/null +++ b/tests/unit/spinner/mousewheel-wheel.html @@ -0,0 +1,72 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>jQuery UI Spinner Test Suite</title> + + <script src="../../../external/requirejs/require.js"></script> + <script src="../../../external/jquery/jquery.js"></script> + <script src="../../lib/css.js" data-modules="core button spinner theme"></script> + <script src="../../lib/testIframe.js"></script> +</head> +<body> + +<input id="spin" class="foo"> + +<script> + function runTest() { + var values = [], + element = $( "#spin" ).val( 0 ).spinner( { + step: 2 + } ); + + element.focus(); + setTimeout( step1 ); + + function dispatchWheelEvent( elem, deltaY ) { + elem[ 0 ].dispatchEvent( new WheelEvent( "wheel", { + deltaY: deltaY + } ) ); + } + + function step1() { + dispatchWheelEvent( element ); + values.push( element.val() ); + + dispatchWheelEvent( element, -1 ); + values.push( element.val() ); + + dispatchWheelEvent( element, 0.2 ); + values.push( element.val() ); + + dispatchWheelEvent( element, 15 ); + values.push( element.val() ); + + element.blur(); + setTimeout( step2 ); + } + + function step2() { + dispatchWheelEvent( element, -1 ); + values.push( element.val() ); + + startIframeTest( values ); + } + } + + requirejs.config( { + paths: { + "jquery-mousewheel": "../../../external/jquery-mousewheel/jquery.mousewheel", + "ui": "../../../ui" + }, + } ); + + require( [ + "jquery-mousewheel", + "ui/widgets/spinner" + ], function() { + runTest(); + } ); +</script> +</body> +</html> diff --git a/ui/widgets/spinner.js b/ui/widgets/spinner.js index 4fb41d7bb..d4034b458 100644 --- a/ui/widgets/spinner.js +++ b/ui/widgets/spinner.js @@ -164,6 +164,13 @@ $.widget( "ui.spinner", { // event. The `delta` parameter is provided by the jQuery Mousewheel // plugin if one is loaded. mousewheel: function( event, delta ) { + if ( !event.isTrigger ) { + + // If this is not a trigger call, the `wheel` handler will + // fire as well, let's not duplicate it. + return; + } + var wheelEvent = $.Event( event ); wheelEvent.type = "wheel"; if ( delta ) { |