diff --git a/core/util/cursor.js b/core/util/cursor.js index 63de7419..0dee4eb9 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -132,7 +132,8 @@ export default class Cursor { } _handleMouseLeave(event) { - this._hideCursor(); + // Check if we should show the cursor on the element we are leaving to + this._updateVisibility(event.relatedTarget); } _handleMouseMove(event) { @@ -150,6 +151,25 @@ export default class Cursor { // now and adjust visibility based on that. let target = document.elementFromPoint(event.clientX, event.clientY); this._updateVisibility(target); + + // Captures end with a mouseup but we can't know the event order of + // mouseup vs releaseCapture. + // + // In the cases when releaseCapture comes first, the code above is + // enough. + // + // In the cases when the mouseup comes first, we need wait for the + // browser to flush all events and then check again if the cursor + // should be visible. + if (this._captureIsActive()) { + window.setTimeout(() => { + // Refresh the target from elementFromPoint since queued events + // might have altered the DOM + target = document.elementFromPoint(event.clientX, + event.clientY); + this._updateVisibility(target); + }, 0); + } } _handleTouchStart(event) { @@ -189,6 +209,9 @@ export default class Cursor { // (i.e. are we over the target, or a child of the target without a // different cursor set) _shouldShowCursor(target) { + if (!target) { + return false; + } // Easy case if (target === this._target) { return true; @@ -207,6 +230,11 @@ export default class Cursor { } _updateVisibility(target) { + // When the cursor target has capture we want to show the cursor. + // So, if a capture is active - look at the captured element instead. + if (this._captureIsActive()) { + target = document.capturedElem; + } if (this._shouldShowCursor(target)) { this._showCursor(); } else { @@ -218,4 +246,9 @@ export default class Cursor { this._canvas.style.left = this._position.x + "px"; this._canvas.style.top = this._position.y + "px"; } + + _captureIsActive() { + return document.capturedElem && + document.documentElement.contains(document.capturedElem); + } } diff --git a/core/util/events.js b/core/util/events.js index f1222796..daaed828 100644 --- a/core/util/events.js +++ b/core/util/events.js @@ -21,7 +21,8 @@ export function stopEvent(e) { // Emulate Element.setCapture() when not supported let _captureRecursion = false; -let _captureElem = null; +let _elementForUnflushedEvents = null; +document.capturedElem = null; function _captureProxy(e) { // Recursion protection as we'll see our own event if (_captureRecursion) return; @@ -30,7 +31,11 @@ function _captureProxy(e) { const newEv = new e.constructor(e.type, e); _captureRecursion = true; - _captureElem.dispatchEvent(newEv); + if (document.capturedElem) { + document.capturedElem.dispatchEvent(newEv); + } else { + _elementForUnflushedEvents.dispatchEvent(newEv); + } _captureRecursion = false; // Avoid double events @@ -48,58 +53,56 @@ function _captureProxy(e) { } // Follow cursor style of target element -function _captureElemChanged() { - const captureElem = document.getElementById("noVNC_mouse_capture_elem"); - captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor; +function _capturedElemChanged() { + const proxyElem = document.getElementById("noVNC_mouse_capture_elem"); + proxyElem.style.cursor = window.getComputedStyle(document.capturedElem).cursor; } -const _captureObserver = new MutationObserver(_captureElemChanged); +const _captureObserver = new MutationObserver(_capturedElemChanged); -let _captureIndex = 0; +export function setCapture(target) { + if (target.setCapture) { -export function setCapture(elem) { - if (elem.setCapture) { - - elem.setCapture(); + target.setCapture(); + document.capturedElem = target; // IE releases capture on 'click' events which might not trigger - elem.addEventListener('mouseup', releaseCapture); + target.addEventListener('mouseup', releaseCapture); } else { // Release any existing capture in case this method is // called multiple times without coordination releaseCapture(); - let captureElem = document.getElementById("noVNC_mouse_capture_elem"); + let proxyElem = document.getElementById("noVNC_mouse_capture_elem"); - if (captureElem === null) { - captureElem = document.createElement("div"); - captureElem.id = "noVNC_mouse_capture_elem"; - captureElem.style.position = "fixed"; - captureElem.style.top = "0px"; - captureElem.style.left = "0px"; - captureElem.style.width = "100%"; - captureElem.style.height = "100%"; - captureElem.style.zIndex = 10000; - captureElem.style.display = "none"; - document.body.appendChild(captureElem); + if (proxyElem === null) { + proxyElem = document.createElement("div"); + proxyElem.id = "noVNC_mouse_capture_elem"; + proxyElem.style.position = "fixed"; + proxyElem.style.top = "0px"; + proxyElem.style.left = "0px"; + proxyElem.style.width = "100%"; + proxyElem.style.height = "100%"; + proxyElem.style.zIndex = 10000; + proxyElem.style.display = "none"; + document.body.appendChild(proxyElem); // This is to make sure callers don't get confused by having // our blocking element as the target - captureElem.addEventListener('contextmenu', _captureProxy); + proxyElem.addEventListener('contextmenu', _captureProxy); - captureElem.addEventListener('mousemove', _captureProxy); - captureElem.addEventListener('mouseup', _captureProxy); + proxyElem.addEventListener('mousemove', _captureProxy); + proxyElem.addEventListener('mouseup', _captureProxy); } - _captureElem = elem; - _captureIndex++; + document.capturedElem = target; // Track cursor and get initial cursor - _captureObserver.observe(elem, {attributes: true}); - _captureElemChanged(); + _captureObserver.observe(target, {attributes: true}); + _capturedElemChanged(); - captureElem.style.display = ""; + proxyElem.style.display = ""; // We listen to events on window in order to keep tracking if it // happens to leave the viewport @@ -112,26 +115,26 @@ export function releaseCapture() { if (document.releaseCapture) { document.releaseCapture(); + document.capturedElem = null; } else { - if (!_captureElem) { + if (!document.capturedElem) { return; } - // There might be events already queued, so we need to wait for - // them to flush. E.g. contextmenu in Microsoft Edge - window.setTimeout((expected) => { - // Only clear it if it's the expected grab (i.e. no one - // else has initiated a new grab) - if (_captureIndex === expected) { - _captureElem = null; - } - }, 0, _captureIndex); + // There might be events already queued. The event proxy needs + // access to the captured element for these queued events. + // E.g. contextmenu (right-click) in Microsoft Edge + // + // Before removing the capturedElem pointer we save it to a + // temporary variable that the unflushed events can use. + _elementForUnflushedEvents = document.capturedElem; + document.capturedElem = null; _captureObserver.disconnect(); - const captureElem = document.getElementById("noVNC_mouse_capture_elem"); - captureElem.style.display = "none"; + const proxyElem = document.getElementById("noVNC_mouse_capture_elem"); + proxyElem.style.display = "none"; window.removeEventListener('mousemove', _captureProxy); window.removeEventListener('mouseup', _captureProxy);