From d84f28304b295e7cd8a932f996b94e6922d02775 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Wed, 24 Sep 2025 12:43:01 +0200 Subject: [PATCH 01/11] Create pointer.js A quick port of mouse.js to pointer events --- ui/widgets/pointer.js | 209 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 ui/widgets/pointer.js diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js new file mode 100644 index 0000000000..1854a37ef1 --- /dev/null +++ b/ui/widgets/pointer.js @@ -0,0 +1,209 @@ +/*! + * jQuery UI Pointer @VERSION + * https://jqueryui.com + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license + */ + +//>>label: Pointer +//>>group: Widgets +//>>description: Abstracts pointer-based interactions to assist in creating certain widgets. +//>>docs: https://api.jqueryui.com/pointer/ + +( function( factory ) { + "use strict"; + + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define( [ + "jquery", + "../version", + "../widget" + ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +} )( function( $ ) { +"use strict"; + +var pointerHandled = false; +$( document ).on( "pointerup", function() { + pointerHandled = false; +} ); + +return $.widget( "ui.mouse", { + version: "@VERSION", + options: { + cancel: "input, textarea, button, select, option", + distance: 1, + delay: 0 + }, + _pointerInit: function() { + var that = this; + + this.element + .on( "pointerdown." + this.widgetName, function( event ) { + return that._pointerDown( event ); + } ) + .on( "click." + this.widgetName, function( event ) { + if ( true === $.data( event.target, that.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, that.widgetName + ".preventClickEvent" ); + event.stopImmediatePropagation(); + return false; + } + } ); + + this.started = false; + }, + + _pointerDestroy: function() { + this.element.off( "." + this.widgetName ); + if ( this._pointerMoveDelegate ) { + this.document + .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) + .off( "pointerup." + this.widgetName, this._pointerUpDelegate ); + } + }, + + _pointerDown: function( event ) { + if ( pointerHandled ) { + return; + } + + this._pointerMoved = false; + + if ( this._pointerStarted ) { + this._pointerUp( event ); + } + + this._pointerDownEvent = event; + + var that = this, + btnIsLeft = event.button === 0, + elIsCancel = typeof this.options.cancel === "string" ? + $( event.target ).closest( this.options.cancel ).length : + false; + if ( !btnIsLeft || elIsCancel || !this._pointerCapture( event ) ) { + return true; + } + + this.pointerDelayMet = !this.options.delay; + if ( !this.pointerDelayMet ) { + this._pointerDelayTimer = setTimeout( function() { + that.pointerDelayMet = true; + }, this.options.delay ); + } + + if ( this._pointerDistanceMet( event ) && this._pointerDelayMet( event ) ) { + this._pointerStarted = ( this._pointerStart( event ) !== false ); + if ( !this._pointerStarted ) { + event.preventDefault(); + return true; + } + } + + if ( true === $.data( event.target, this.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, this.widgetName + ".preventClickEvent" ); + } + + this._pointerMoveDelegate = function( event ) { + return that._pointerMove( event ); + }; + this._pointerUpDelegate = function( event ) { + return that._pointerUp( event ); + }; + + this.document + .on( "pointermove." + this.widgetName, this._pointerMoveDelegate ) + .on( "pointerup." + this.widgetName, this._pointerUpDelegate ); + + event.preventDefault(); + + pointerHandled = true; + return true; + }, + + _pointerMove: function( event ) { + if ( this._pointerMoved && event.buttons === 0 ) { + if ( event.altKey || event.ctrlKey || + event.metaKey || event.shiftKey ) { + this.ignoreMissingButtons = true; + } else if ( !this.ignoreMissingButtons ) { + return this._pointerUp( event ); + } + } + + if ( event.buttons || event.button ) { + this._pointerMoved = true; + } + + if ( this._pointerStarted ) { + this._pointerDrag( event ); + return event.preventDefault(); + } + + if ( this._pointerDistanceMet( event ) && this._pointerDelayMet( event ) ) { + this._pointerStarted = + ( this._pointerStart( this._pointerDownEvent, event ) !== false ); + if ( this._pointerStarted ) { + this._pointerDrag( event ); + } else { + this._pointerUp( event ); + } + } + + return !this._pointerStarted; + }, + + _pointerUp: function( event ) { + this.document + .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) + .off( "pointerup." + this.widgetName, this._pointerUpDelegate ); + + if ( this._pointerStarted ) { + this._pointerStarted = false; + + if ( event.target === this._pointerDownEvent.target ) { + $.data( event.target, this.widgetName + ".preventClickEvent", true ); + } + + this._pointerStop( event ); + } + + if ( this._pointerDelayTimer ) { + clearTimeout( this._pointerDelayTimer ); + delete this._pointerDelayTimer; + } + + this.ignoreMissingButtons = false; + pointerHandled = false; + event.preventDefault(); + }, + + _pointerDistanceMet: function( event ) { + return ( Math.max( + Math.abs( this._pointerDownEvent.pageX - event.pageX ), + Math.abs( this._pointerDownEvent.pageY - event.pageY ) + ) >= this.options.distance + ); + }, + + _pointerDelayMet: function( /* event */ ) { + return this.pointerDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _pointerStart: function( /* event */ ) {}, + _pointerDrag: function( /* event */ ) {}, + _pointerStop: function( /* event */ ) {}, + _pointerCapture: function( /* event */ ) { + return true; + } +} ); + +} ); From ab6a305392175456a102a5023c029b80dbc2f13b Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Wed, 24 Sep 2025 12:46:04 +0200 Subject: [PATCH 02/11] Update pointer.js missed a mouse --- ui/widgets/pointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 1854a37ef1..51839c4821 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -36,7 +36,7 @@ $( document ).on( "pointerup", function() { pointerHandled = false; } ); -return $.widget( "ui.mouse", { +return $.widget( "ui.pointer", { version: "@VERSION", options: { cancel: "input, textarea, button, select, option", From e154738c4474320a47af312487101acb49b852b3 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Tue, 27 Jan 2026 10:07:56 +0100 Subject: [PATCH 03/11] Update pointer.js Remove "ignoreMissingButtons" boolean and change the if to remove the custom safari code --- ui/widgets/pointer.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 51839c4821..60f1f6a5da 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -130,12 +130,7 @@ return $.widget( "ui.pointer", { _pointerMove: function( event ) { if ( this._pointerMoved && event.buttons === 0 ) { - if ( event.altKey || event.ctrlKey || - event.metaKey || event.shiftKey ) { - this.ignoreMissingButtons = true; - } else if ( !this.ignoreMissingButtons ) { return this._pointerUp( event ); - } } if ( event.buttons || event.button ) { @@ -180,7 +175,6 @@ return $.widget( "ui.pointer", { delete this._pointerDelayTimer; } - this.ignoreMissingButtons = false; pointerHandled = false; event.preventDefault(); }, From 1a2106367a7c3737d5c110c7b44c82c531e236e1 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Mon, 9 Mar 2026 15:16:36 +0100 Subject: [PATCH 04/11] Update pointer.js In _pointerInit, the code used this.started = false, but everywhere else the code uses this._pointerStarted --- ui/widgets/pointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 60f1f6a5da..ce66c8df22 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -58,7 +58,7 @@ return $.widget( "ui.pointer", { } } ); - this.started = false; + this._pointerStarted = false; }, _pointerDestroy: function() { From 0778a2e9b9461f8591cc7cb672fe20360b9e7738 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Wed, 25 Mar 2026 09:11:48 +0100 Subject: [PATCH 05/11] Implement pointercancel event handling Added pointercancel event handling to pointer widget. --- ui/widgets/pointer.js | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index ce66c8df22..b43e4b16b2 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -32,7 +32,7 @@ "use strict"; var pointerHandled = false; -$( document ).on( "pointerup", function() { +$( document ).on( "pointerup pointercancel", function() { pointerHandled = false; } ); @@ -66,7 +66,8 @@ return $.widget( "ui.pointer", { if ( this._pointerMoveDelegate ) { this.document .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) - .off( "pointerup." + this.widgetName, this._pointerUpDelegate ); + .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) + .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); } }, @@ -117,10 +118,14 @@ return $.widget( "ui.pointer", { this._pointerUpDelegate = function( event ) { return that._pointerUp( event ); }; + this._pointerCancelDelegate = function( event ) { + return that._pointerCancel( event ); + }; this.document .on( "pointermove." + this.widgetName, this._pointerMoveDelegate ) - .on( "pointerup." + this.widgetName, this._pointerUpDelegate ); + .on( "pointerup." + this.widgetName, this._pointerUpDelegate ) + .on( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); event.preventDefault(); @@ -158,7 +163,8 @@ return $.widget( "ui.pointer", { _pointerUp: function( event ) { this.document .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) - .off( "pointerup." + this.widgetName, this._pointerUpDelegate ); + .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) + .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); if ( this._pointerStarted ) { this._pointerStarted = false; @@ -179,6 +185,29 @@ return $.widget( "ui.pointer", { event.preventDefault(); }, + // pointercancel fires when the browser takes over pointer control (e.g. scroll + // gesture, orientation change, stylus palm rejection). Unlike pointerup, it is + // not cancelable, so we skip preventDefault() and click-prevention data, but we + // still need to tear down all listeners and stop any active drag. + _pointerCancel: function( event ) { + this.document + .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) + .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) + .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); + + if ( this._pointerStarted ) { + this._pointerStarted = false; + this._pointerCancel( event ); + } + + if ( this._pointerDelayTimer ) { + clearTimeout( this._pointerDelayTimer ); + delete this._pointerDelayTimer; + } + + pointerHandled = false; + }, + _pointerDistanceMet: function( event ) { return ( Math.max( Math.abs( this._pointerDownEvent.pageX - event.pageX ), @@ -195,6 +224,10 @@ return $.widget( "ui.pointer", { _pointerStart: function( /* event */ ) {}, _pointerDrag: function( /* event */ ) {}, _pointerStop: function( /* event */ ) {}, + // _pointerStop by default so existing subwidgets need no changes. + _pointerCancel: function( event ) { + this._pointerStop( event ); + }, _pointerCapture: function( /* event */ ) { return true; } From fe3d8cbbb0b42f52c4d89ff43225b5826227e927 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Fri, 3 Apr 2026 13:34:32 +0200 Subject: [PATCH 06/11] Refactor pointer event handling and cleanup --- ui/widgets/pointer.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index b43e4b16b2..1347fcd156 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -69,6 +69,14 @@ return $.widget( "ui.pointer", { .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); } + + if ( this._pointerDelayTimer ) { + clearTimeout( this._pointerDelayTimer ); + delete this._pointerDelayTimer; + } + + this._pointerStarted = false; + pointerHandled = false; }, _pointerDown: function( event ) { @@ -82,8 +90,6 @@ return $.widget( "ui.pointer", { this._pointerUp( event ); } - this._pointerDownEvent = event; - var that = this, btnIsLeft = event.button === 0, elIsCancel = typeof this.options.cancel === "string" ? @@ -93,8 +99,13 @@ return $.widget( "ui.pointer", { return true; } + this._pointerDownEvent = event; + this.pointerDelayMet = !this.options.delay; if ( !this.pointerDelayMet ) { + if ( this._pointerDelayTimer ) { + clearTimeout( this._pointerDelayTimer ); + } this._pointerDelayTimer = setTimeout( function() { that.pointerDelayMet = true; }, this.options.delay ); @@ -144,7 +155,8 @@ return $.widget( "ui.pointer", { if ( this._pointerStarted ) { this._pointerDrag( event ); - return event.preventDefault(); + event.preventDefault(); + return false; } if ( this._pointerDistanceMet( event ) && this._pointerDelayMet( event ) ) { @@ -224,10 +236,6 @@ return $.widget( "ui.pointer", { _pointerStart: function( /* event */ ) {}, _pointerDrag: function( /* event */ ) {}, _pointerStop: function( /* event */ ) {}, - // _pointerStop by default so existing subwidgets need no changes. - _pointerCancel: function( event ) { - this._pointerStop( event ); - }, _pointerCapture: function( /* event */ ) { return true; } From dcad989983c03753b4537e70c551ed85ab49ebe2 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Fri, 3 Apr 2026 14:13:43 +0200 Subject: [PATCH 07/11] Rename pointer methods for consistency _handlePointerCancel is the private event handler _pointerCancel is the placeholder as the override point for subwidgets --- ui/widgets/pointer.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 1347fcd156..b0b8d962b9 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -112,7 +112,7 @@ return $.widget( "ui.pointer", { } if ( this._pointerDistanceMet( event ) && this._pointerDelayMet( event ) ) { - this._pointerStarted = ( this._pointerStart( event ) !== false ); + this._pointerStarted = ( this._pointerStart( this._pointerDownEvent, event ) !== false ); if ( !this._pointerStarted ) { event.preventDefault(); return true; @@ -130,7 +130,7 @@ return $.widget( "ui.pointer", { return that._pointerUp( event ); }; this._pointerCancelDelegate = function( event ) { - return that._pointerCancel( event ); + return that._handlePointerCancel( event ); }; this.document @@ -201,7 +201,7 @@ return $.widget( "ui.pointer", { // gesture, orientation change, stylus palm rejection). Unlike pointerup, it is // not cancelable, so we skip preventDefault() and click-prevention data, but we // still need to tear down all listeners and stop any active drag. - _pointerCancel: function( event ) { + _handlePointerCancel: function( event ) { this.document .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) @@ -236,6 +236,9 @@ return $.widget( "ui.pointer", { _pointerStart: function( /* event */ ) {}, _pointerDrag: function( /* event */ ) {}, _pointerStop: function( /* event */ ) {}, + _pointerCancel: function( event ) { + this._pointerStop( event ); + }, _pointerCapture: function( /* event */ ) { return true; } From e56e9545082c37ff52cef42d940fbebdcd4f34ee Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Sat, 4 Apr 2026 17:32:32 +0200 Subject: [PATCH 08/11] Using isPrimary to Filter Pointer Events Make sure only the first pointer will be handled --- ui/widgets/pointer.js | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index b0b8d962b9..9a23999061 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -31,10 +31,6 @@ } )( function( $ ) { "use strict"; -var pointerHandled = false; -$( document ).on( "pointerup pointercancel", function() { - pointerHandled = false; -} ); return $.widget( "ui.pointer", { version: "@VERSION", @@ -43,6 +39,7 @@ return $.widget( "ui.pointer", { distance: 1, delay: 0 }, + _pointerInit: function() { var that = this; @@ -76,11 +73,11 @@ return $.widget( "ui.pointer", { } this._pointerStarted = false; - pointerHandled = false; }, _pointerDown: function( event ) { - if ( pointerHandled ) { + // Ignore any pointer that isn't the primary one (e.g. extra fingers). + if ( !event.isPrimary ) { return; } @@ -139,14 +136,17 @@ return $.widget( "ui.pointer", { .on( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); event.preventDefault(); - - pointerHandled = true; return true; }, _pointerMove: function( event ) { + // Document-level listeners fire for all pointers; ignore non-primary ones. + if ( !event.isPrimary ) { + return; + } + if ( this._pointerMoved && event.buttons === 0 ) { - return this._pointerUp( event ); + return this._pointerUp( event ); } if ( event.buttons || event.button ) { @@ -173,6 +173,11 @@ return $.widget( "ui.pointer", { }, _pointerUp: function( event ) { + // Document-level listeners fire for all pointers; ignore non-primary ones. + if ( !event.isPrimary ) { + return; + } + this.document .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) @@ -193,15 +198,15 @@ return $.widget( "ui.pointer", { delete this._pointerDelayTimer; } - pointerHandled = false; event.preventDefault(); }, - // pointercancel fires when the browser takes over pointer control (e.g. scroll - // gesture, orientation change, stylus palm rejection). Unlike pointerup, it is - // not cancelable, so we skip preventDefault() and click-prevention data, but we - // still need to tear down all listeners and stop any active drag. _handlePointerCancel: function( event ) { + // Document-level listeners fire for all pointers; ignore non-primary ones. + if ( !event.isPrimary ) { + return; + } + this.document .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) @@ -216,8 +221,6 @@ return $.widget( "ui.pointer", { clearTimeout( this._pointerDelayTimer ); delete this._pointerDelayTimer; } - - pointerHandled = false; }, _pointerDistanceMet: function( event ) { From 90ea6052b432bb5bf6334f4085faee2fc39b3726 Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Tue, 7 Apr 2026 21:35:45 +0200 Subject: [PATCH 09/11] Fix jsLint errors Fix linting errors --- ui/widgets/pointer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 9a23999061..6c0a56d9e7 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -76,6 +76,7 @@ return $.widget( "ui.pointer", { }, _pointerDown: function( event ) { + // Ignore any pointer that isn't the primary one (e.g. extra fingers). if ( !event.isPrimary ) { return; @@ -109,7 +110,7 @@ return $.widget( "ui.pointer", { } if ( this._pointerDistanceMet( event ) && this._pointerDelayMet( event ) ) { - this._pointerStarted = ( this._pointerStart( this._pointerDownEvent, event ) !== false ); + this._pointerStarted = this._pointerStart( this._pointerDownEvent, event ) !== false; if ( !this._pointerStarted ) { event.preventDefault(); return true; @@ -140,6 +141,7 @@ return $.widget( "ui.pointer", { }, _pointerMove: function( event ) { + // Document-level listeners fire for all pointers; ignore non-primary ones. if ( !event.isPrimary ) { return; @@ -173,6 +175,7 @@ return $.widget( "ui.pointer", { }, _pointerUp: function( event ) { + // Document-level listeners fire for all pointers; ignore non-primary ones. if ( !event.isPrimary ) { return; @@ -202,6 +205,7 @@ return $.widget( "ui.pointer", { }, _handlePointerCancel: function( event ) { + // Document-level listeners fire for all pointers; ignore non-primary ones. if ( !event.isPrimary ) { return; From ab6b68b962633054b9cb48b57bdb8a6700861bcc Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Wed, 8 Apr 2026 13:43:20 +0200 Subject: [PATCH 10/11] Clean up pointer.js by removing blank lines Removed unnecessary blank lines in pointer.js. --- ui/widgets/pointer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 6c0a56d9e7..49090db339 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -76,7 +76,7 @@ return $.widget( "ui.pointer", { }, _pointerDown: function( event ) { - + // Ignore any pointer that isn't the primary one (e.g. extra fingers). if ( !event.isPrimary ) { return; @@ -141,7 +141,7 @@ return $.widget( "ui.pointer", { }, _pointerMove: function( event ) { - + // Document-level listeners fire for all pointers; ignore non-primary ones. if ( !event.isPrimary ) { return; @@ -175,7 +175,7 @@ return $.widget( "ui.pointer", { }, _pointerUp: function( event ) { - + // Document-level listeners fire for all pointers; ignore non-primary ones. if ( !event.isPrimary ) { return; @@ -205,7 +205,7 @@ return $.widget( "ui.pointer", { }, _handlePointerCancel: function( event ) { - + // Document-level listeners fire for all pointers; ignore non-primary ones. if ( !event.isPrimary ) { return; From 3e0e42ba87bc368477c9155d40f43ec7bf4b16fb Mon Sep 17 00:00:00 2001 From: mark van tilburg Date: Thu, 11 Jun 2026 10:04:50 +0200 Subject: [PATCH 11/11] Refactor pointer event handling and capture logic capture on the element and not on document --- ui/widgets/pointer.js | 46 ++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/ui/widgets/pointer.js b/ui/widgets/pointer.js index 49090db339..a975b4ce28 100644 --- a/ui/widgets/pointer.js +++ b/ui/widgets/pointer.js @@ -60,11 +60,9 @@ return $.widget( "ui.pointer", { _pointerDestroy: function() { this.element.off( "." + this.widgetName ); - if ( this._pointerMoveDelegate ) { - this.document - .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) - .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) - .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); + if ( this._pointerId !== undefined ) { + this._releasePointerCapture( this._pointerId ); + delete this._pointerId; } if ( this._pointerDelayTimer ) { @@ -131,7 +129,14 @@ return $.widget( "ui.pointer", { return that._handlePointerCancel( event ); }; - this.document + // Capture the pointer so that all subsequent events for it are + // retargeted to the element, even when the pointer leaves it. This + // mimics the touch model (events fire where the interaction started) + // and removes the need for document-level listeners. + this._pointerId = event.pointerId; + this.element[ 0 ].setPointerCapture( event.pointerId ); + + this.element .on( "pointermove." + this.widgetName, this._pointerMoveDelegate ) .on( "pointerup." + this.widgetName, this._pointerUpDelegate ) .on( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); @@ -142,7 +147,8 @@ return $.widget( "ui.pointer", { _pointerMove: function( event ) { - // Document-level listeners fire for all pointers; ignore non-primary ones. + // The element receives events for every pointer over it, not just + // the captured one; ignore non-primary pointers. if ( !event.isPrimary ) { return; } @@ -176,16 +182,20 @@ return $.widget( "ui.pointer", { _pointerUp: function( event ) { - // Document-level listeners fire for all pointers; ignore non-primary ones. + // The element receives events for every pointer over it, not just + // the captured one; ignore non-primary pointers. if ( !event.isPrimary ) { return; } - this.document + this.element .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); + this._releasePointerCapture( event.pointerId ); + delete this._pointerId; + if ( this._pointerStarted ) { this._pointerStarted = false; @@ -206,16 +216,20 @@ return $.widget( "ui.pointer", { _handlePointerCancel: function( event ) { - // Document-level listeners fire for all pointers; ignore non-primary ones. + // The element receives events for every pointer over it, not just + // the captured one; ignore non-primary pointers. if ( !event.isPrimary ) { return; } - this.document + this.element .off( "pointermove." + this.widgetName, this._pointerMoveDelegate ) .off( "pointerup." + this.widgetName, this._pointerUpDelegate ) .off( "pointercancel." + this.widgetName, this._pointerCancelDelegate ); + this._releasePointerCapture( event.pointerId ); + delete this._pointerId; + if ( this._pointerStarted ) { this._pointerStarted = false; this._pointerCancel( event ); @@ -239,6 +253,16 @@ return $.widget( "ui.pointer", { return this.pointerDelayMet; }, + _releasePointerCapture: function( pointerId ) { + var elem = this.element[ 0 ]; + + // The capture is released implicitly on pointerup/pointercancel, + // so only release it explicitly if it is still active. + if ( elem && elem.hasPointerCapture( pointerId ) ) { + elem.releasePointerCapture( pointerId ); + } + }, + // These are placeholder methods, to be overriden by extending plugin _pointerStart: function( /* event */ ) {}, _pointerDrag: function( /* event */ ) {},