editorDom.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import * as dom from '../../base/browser/dom.js';
  6. import { GlobalMouseMoveMonitor } from '../../base/browser/globalMouseMoveMonitor.js';
  7. import { StandardMouseEvent } from '../../base/browser/mouseEvent.js';
  8. import { RunOnceScheduler } from '../../base/common/async.js';
  9. import { Disposable } from '../../base/common/lifecycle.js';
  10. import { asCssVariableName } from '../../platform/theme/common/colorRegistry.js';
  11. /**
  12. * Coordinates relative to the whole document (e.g. mouse event's pageX and pageY)
  13. */
  14. export class PageCoordinates {
  15. constructor(x, y) {
  16. this.x = x;
  17. this.y = y;
  18. this._pageCoordinatesBrand = undefined;
  19. }
  20. toClientCoordinates() {
  21. return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY);
  22. }
  23. }
  24. /**
  25. * Coordinates within the application's client area (i.e. origin is document's scroll position).
  26. *
  27. * For example, clicking in the top-left corner of the client area will
  28. * always result in a mouse event with a client.x value of 0, regardless
  29. * of whether the page is scrolled horizontally.
  30. */
  31. export class ClientCoordinates {
  32. constructor(clientX, clientY) {
  33. this.clientX = clientX;
  34. this.clientY = clientY;
  35. this._clientCoordinatesBrand = undefined;
  36. }
  37. toPageCoordinates() {
  38. return new PageCoordinates(this.clientX + dom.StandardWindow.scrollX, this.clientY + dom.StandardWindow.scrollY);
  39. }
  40. }
  41. /**
  42. * The position of the editor in the page.
  43. */
  44. export class EditorPagePosition {
  45. constructor(x, y, width, height) {
  46. this.x = x;
  47. this.y = y;
  48. this.width = width;
  49. this.height = height;
  50. this._editorPagePositionBrand = undefined;
  51. }
  52. }
  53. export function createEditorPagePosition(editorViewDomNode) {
  54. const editorPos = dom.getDomNodePagePosition(editorViewDomNode);
  55. return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height);
  56. }
  57. export class EditorMouseEvent extends StandardMouseEvent {
  58. constructor(e, editorViewDomNode) {
  59. super(e);
  60. this._editorMouseEventBrand = undefined;
  61. this.pos = new PageCoordinates(this.posx, this.posy);
  62. this.editorPos = createEditorPagePosition(editorViewDomNode);
  63. }
  64. }
  65. export class EditorMouseEventFactory {
  66. constructor(editorViewDomNode) {
  67. this._editorViewDomNode = editorViewDomNode;
  68. }
  69. _create(e) {
  70. return new EditorMouseEvent(e, this._editorViewDomNode);
  71. }
  72. onContextMenu(target, callback) {
  73. return dom.addDisposableListener(target, 'contextmenu', (e) => {
  74. callback(this._create(e));
  75. });
  76. }
  77. onMouseUp(target, callback) {
  78. return dom.addDisposableListener(target, 'mouseup', (e) => {
  79. callback(this._create(e));
  80. });
  81. }
  82. onMouseDown(target, callback) {
  83. return dom.addDisposableListener(target, 'mousedown', (e) => {
  84. callback(this._create(e));
  85. });
  86. }
  87. onMouseLeave(target, callback) {
  88. return dom.addDisposableNonBubblingMouseOutListener(target, (e) => {
  89. callback(this._create(e));
  90. });
  91. }
  92. onMouseMoveThrottled(target, callback, merger, minimumTimeMs) {
  93. const myMerger = (lastEvent, currentEvent) => {
  94. return merger(lastEvent, this._create(currentEvent));
  95. };
  96. return dom.addDisposableThrottledListener(target, 'mousemove', callback, myMerger, minimumTimeMs);
  97. }
  98. }
  99. export class EditorPointerEventFactory {
  100. constructor(editorViewDomNode) {
  101. this._editorViewDomNode = editorViewDomNode;
  102. }
  103. _create(e) {
  104. return new EditorMouseEvent(e, this._editorViewDomNode);
  105. }
  106. onPointerUp(target, callback) {
  107. return dom.addDisposableListener(target, 'pointerup', (e) => {
  108. callback(this._create(e));
  109. });
  110. }
  111. onPointerDown(target, callback) {
  112. return dom.addDisposableListener(target, 'pointerdown', (e) => {
  113. callback(this._create(e));
  114. });
  115. }
  116. onPointerLeave(target, callback) {
  117. return dom.addDisposableNonBubblingPointerOutListener(target, (e) => {
  118. callback(this._create(e));
  119. });
  120. }
  121. onPointerMoveThrottled(target, callback, merger, minimumTimeMs) {
  122. const myMerger = (lastEvent, currentEvent) => {
  123. return merger(lastEvent, this._create(currentEvent));
  124. };
  125. return dom.addDisposableThrottledListener(target, 'pointermove', callback, myMerger, minimumTimeMs);
  126. }
  127. }
  128. export class GlobalEditorMouseMoveMonitor extends Disposable {
  129. constructor(editorViewDomNode) {
  130. super();
  131. this._editorViewDomNode = editorViewDomNode;
  132. this._globalMouseMoveMonitor = this._register(new GlobalMouseMoveMonitor());
  133. this._keydownListener = null;
  134. }
  135. startMonitoring(initialElement, initialButtons, merger, mouseMoveCallback, onStopCallback) {
  136. // Add a <<capture>> keydown event listener that will cancel the monitoring
  137. // if something other than a modifier key is pressed
  138. this._keydownListener = dom.addStandardDisposableListener(document, 'keydown', (e) => {
  139. const kb = e.toKeybinding();
  140. if (kb.isModifierKey()) {
  141. // Allow modifier keys
  142. return;
  143. }
  144. this._globalMouseMoveMonitor.stopMonitoring(true, e.browserEvent);
  145. }, true);
  146. const myMerger = (lastEvent, currentEvent) => {
  147. return merger(lastEvent, new EditorMouseEvent(currentEvent, this._editorViewDomNode));
  148. };
  149. this._globalMouseMoveMonitor.startMonitoring(initialElement, initialButtons, myMerger, mouseMoveCallback, (e) => {
  150. this._keydownListener.dispose();
  151. onStopCallback(e);
  152. });
  153. }
  154. stopMonitoring() {
  155. this._globalMouseMoveMonitor.stopMonitoring(true);
  156. }
  157. }
  158. /**
  159. * A helper to create dynamic css rules, bound to a class name.
  160. * Rules are reused.
  161. * Reference counting and delayed garbage collection ensure that no rules leak.
  162. */
  163. export class DynamicCssRules {
  164. constructor(_editor) {
  165. this._editor = _editor;
  166. this._counter = 0;
  167. this._rules = new Map();
  168. // We delay garbage collection so that hanging rules can be reused.
  169. this._garbageCollectionScheduler = new RunOnceScheduler(() => this.garbageCollect(), 1000);
  170. }
  171. createClassNameRef(options) {
  172. const rule = this.getOrCreateRule(options);
  173. rule.increaseRefCount();
  174. return {
  175. className: rule.className,
  176. dispose: () => {
  177. rule.decreaseRefCount();
  178. this._garbageCollectionScheduler.schedule();
  179. }
  180. };
  181. }
  182. getOrCreateRule(properties) {
  183. const key = this.computeUniqueKey(properties);
  184. let existingRule = this._rules.get(key);
  185. if (!existingRule) {
  186. const counter = this._counter++;
  187. existingRule = new RefCountedCssRule(key, `dyn-rule-${counter}`, dom.isInShadowDOM(this._editor.getContainerDomNode())
  188. ? this._editor.getContainerDomNode()
  189. : undefined, properties);
  190. this._rules.set(key, existingRule);
  191. }
  192. return existingRule;
  193. }
  194. computeUniqueKey(properties) {
  195. return JSON.stringify(properties);
  196. }
  197. garbageCollect() {
  198. for (const rule of this._rules.values()) {
  199. if (!rule.hasReferences()) {
  200. this._rules.delete(rule.key);
  201. rule.dispose();
  202. }
  203. }
  204. }
  205. }
  206. class RefCountedCssRule {
  207. constructor(key, className, _containerElement, properties) {
  208. this.key = key;
  209. this.className = className;
  210. this.properties = properties;
  211. this._referenceCount = 0;
  212. this._styleElement = dom.createStyleSheet(_containerElement);
  213. this._styleElement.textContent = this.getCssText(this.className, this.properties);
  214. }
  215. getCssText(className, properties) {
  216. let str = `.${className} {`;
  217. for (const prop in properties) {
  218. const value = properties[prop];
  219. let cssValue;
  220. if (typeof value === 'object') {
  221. cssValue = `var(${asCssVariableName(value.id)})`;
  222. }
  223. else {
  224. cssValue = value;
  225. }
  226. const cssPropName = camelToDashes(prop);
  227. str += `\n\t${cssPropName}: ${cssValue};`;
  228. }
  229. str += `\n}`;
  230. return str;
  231. }
  232. dispose() {
  233. this._styleElement.remove();
  234. }
  235. increaseRefCount() {
  236. this._referenceCount++;
  237. }
  238. decreaseRefCount() {
  239. this._referenceCount--;
  240. }
  241. hasReferences() {
  242. return this._referenceCount > 0;
  243. }
  244. }
  245. function camelToDashes(str) {
  246. return str.replace(/(^[A-Z])/, ([first]) => first.toLowerCase())
  247. .replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`);
  248. }