cursor.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. /*
  2. * noVNC: HTML5 VNC client
  3. * Copyright (C) 2019 The noVNC Authors
  4. * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
  5. */
  6. import { supportsCursorURIs, isTouchDevice } from './browser.js';
  7. const useFallback = !supportsCursorURIs || isTouchDevice;
  8. export default class Cursor {
  9. constructor() {
  10. this._target = null;
  11. this._canvas = document.createElement('canvas');
  12. if (useFallback) {
  13. this._canvas.style.position = 'fixed';
  14. this._canvas.style.zIndex = '65535';
  15. this._canvas.style.pointerEvents = 'none';
  16. // Safari on iOS can select the cursor image
  17. // https://bugs.webkit.org/show_bug.cgi?id=249223
  18. this._canvas.style.userSelect = 'none';
  19. this._canvas.style.WebkitUserSelect = 'none';
  20. // Can't use "display" because of Firefox bug #1445997
  21. this._canvas.style.visibility = 'hidden';
  22. }
  23. this._position = { x: 0, y: 0 };
  24. this._hotSpot = { x: 0, y: 0 };
  25. this._eventHandlers = {
  26. 'mouseover': this._handleMouseOver.bind(this),
  27. 'mouseleave': this._handleMouseLeave.bind(this),
  28. 'mousemove': this._handleMouseMove.bind(this),
  29. 'mouseup': this._handleMouseUp.bind(this),
  30. };
  31. }
  32. attach(target) {
  33. if (this._target) {
  34. this.detach();
  35. }
  36. this._target = target;
  37. if (useFallback) {
  38. document.body.appendChild(this._canvas);
  39. const options = { capture: true, passive: true };
  40. this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
  41. this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
  42. this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
  43. this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
  44. }
  45. this.clear();
  46. }
  47. detach() {
  48. if (!this._target) {
  49. return;
  50. }
  51. if (useFallback) {
  52. const options = { capture: true, passive: true };
  53. this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
  54. this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
  55. this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
  56. this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
  57. if (document.contains(this._canvas)) {
  58. document.body.removeChild(this._canvas);
  59. }
  60. }
  61. this._target = null;
  62. }
  63. change(rgba, hotx, hoty, w, h) {
  64. if ((w === 0) || (h === 0)) {
  65. this.clear();
  66. return;
  67. }
  68. this._position.x = this._position.x + this._hotSpot.x - hotx;
  69. this._position.y = this._position.y + this._hotSpot.y - hoty;
  70. this._hotSpot.x = hotx;
  71. this._hotSpot.y = hoty;
  72. let ctx = this._canvas.getContext('2d');
  73. this._canvas.width = w;
  74. this._canvas.height = h;
  75. let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
  76. ctx.clearRect(0, 0, w, h);
  77. ctx.putImageData(img, 0, 0);
  78. if (useFallback) {
  79. this._updatePosition();
  80. } else {
  81. let url = this._canvas.toDataURL();
  82. this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
  83. }
  84. }
  85. clear() {
  86. this._target.style.cursor = 'none';
  87. this._canvas.width = 0;
  88. this._canvas.height = 0;
  89. this._position.x = this._position.x + this._hotSpot.x;
  90. this._position.y = this._position.y + this._hotSpot.y;
  91. this._hotSpot.x = 0;
  92. this._hotSpot.y = 0;
  93. }
  94. // Mouse events might be emulated, this allows
  95. // moving the cursor in such cases
  96. move(clientX, clientY) {
  97. if (!useFallback) {
  98. return;
  99. }
  100. // clientX/clientY are relative the _visual viewport_,
  101. // but our position is relative the _layout viewport_,
  102. // so try to compensate when we can
  103. if (window.visualViewport) {
  104. this._position.x = clientX + window.visualViewport.offsetLeft;
  105. this._position.y = clientY + window.visualViewport.offsetTop;
  106. } else {
  107. this._position.x = clientX;
  108. this._position.y = clientY;
  109. }
  110. this._updatePosition();
  111. let target = document.elementFromPoint(clientX, clientY);
  112. this._updateVisibility(target);
  113. }
  114. _handleMouseOver(event) {
  115. // This event could be because we're entering the target, or
  116. // moving around amongst its sub elements. Let the move handler
  117. // sort things out.
  118. this._handleMouseMove(event);
  119. }
  120. _handleMouseLeave(event) {
  121. // Check if we should show the cursor on the element we are leaving to
  122. this._updateVisibility(event.relatedTarget);
  123. }
  124. _handleMouseMove(event) {
  125. this._updateVisibility(event.target);
  126. this._position.x = event.clientX - this._hotSpot.x;
  127. this._position.y = event.clientY - this._hotSpot.y;
  128. this._updatePosition();
  129. }
  130. _handleMouseUp(event) {
  131. // We might get this event because of a drag operation that
  132. // moved outside of the target. Check what's under the cursor
  133. // now and adjust visibility based on that.
  134. let target = document.elementFromPoint(event.clientX, event.clientY);
  135. this._updateVisibility(target);
  136. // Captures end with a mouseup but we can't know the event order of
  137. // mouseup vs releaseCapture.
  138. //
  139. // In the cases when releaseCapture comes first, the code above is
  140. // enough.
  141. //
  142. // In the cases when the mouseup comes first, we need wait for the
  143. // browser to flush all events and then check again if the cursor
  144. // should be visible.
  145. if (this._captureIsActive()) {
  146. window.setTimeout(() => {
  147. // We might have detached at this point
  148. if (!this._target) {
  149. return;
  150. }
  151. // Refresh the target from elementFromPoint since queued events
  152. // might have altered the DOM
  153. target = document.elementFromPoint(event.clientX,
  154. event.clientY);
  155. this._updateVisibility(target);
  156. }, 0);
  157. }
  158. }
  159. _showCursor() {
  160. if (this._canvas.style.visibility === 'hidden') {
  161. this._canvas.style.visibility = '';
  162. }
  163. }
  164. _hideCursor() {
  165. if (this._canvas.style.visibility !== 'hidden') {
  166. this._canvas.style.visibility = 'hidden';
  167. }
  168. }
  169. // Should we currently display the cursor?
  170. // (i.e. are we over the target, or a child of the target without a
  171. // different cursor set)
  172. _shouldShowCursor(target) {
  173. if (!target) {
  174. return false;
  175. }
  176. // Easy case
  177. if (target === this._target) {
  178. return true;
  179. }
  180. // Other part of the DOM?
  181. if (!this._target.contains(target)) {
  182. return false;
  183. }
  184. // Has the child its own cursor?
  185. // FIXME: How can we tell that a sub element has an
  186. // explicit "cursor: none;"?
  187. if (window.getComputedStyle(target).cursor !== 'none') {
  188. return false;
  189. }
  190. return true;
  191. }
  192. _updateVisibility(target) {
  193. // When the cursor target has capture we want to show the cursor.
  194. // So, if a capture is active - look at the captured element instead.
  195. if (this._captureIsActive()) {
  196. target = document.captureElement;
  197. }
  198. if (this._shouldShowCursor(target)) {
  199. this._showCursor();
  200. } else {
  201. this._hideCursor();
  202. }
  203. }
  204. _updatePosition() {
  205. this._canvas.style.left = this._position.x + "px";
  206. this._canvas.style.top = this._position.y + "px";
  207. }
  208. _captureIsActive() {
  209. return document.captureElement &&
  210. document.documentElement.contains(document.captureElement);
  211. }
  212. }