UNPKG

monaco-editor-core

Version:

A browser based code editor

283 lines (282 loc) • 11.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from '../../base/browser/dom.js'; import { GlobalPointerMoveMonitor } from '../../base/browser/globalPointerMoveMonitor.js'; import { StandardMouseEvent } from '../../base/browser/mouseEvent.js'; import { RunOnceScheduler } from '../../base/common/async.js'; import { Disposable, DisposableStore } from '../../base/common/lifecycle.js'; import { asCssVariable } from '../../platform/theme/common/colorRegistry.js'; /** * Coordinates relative to the whole document (e.g. mouse event's pageX and pageY) */ export class PageCoordinates { constructor(x, y) { this.x = x; this.y = y; this._pageCoordinatesBrand = undefined; } toClientCoordinates(targetWindow) { return new ClientCoordinates(this.x - targetWindow.scrollX, this.y - targetWindow.scrollY); } } /** * Coordinates within the application's client area (i.e. origin is document's scroll position). * * For example, clicking in the top-left corner of the client area will * always result in a mouse event with a client.x value of 0, regardless * of whether the page is scrolled horizontally. */ export class ClientCoordinates { constructor(clientX, clientY) { this.clientX = clientX; this.clientY = clientY; this._clientCoordinatesBrand = undefined; } toPageCoordinates(targetWindow) { return new PageCoordinates(this.clientX + targetWindow.scrollX, this.clientY + targetWindow.scrollY); } } /** * The position of the editor in the page. */ export class EditorPagePosition { constructor(x, y, width, height) { this.x = x; this.y = y; this.width = width; this.height = height; this._editorPagePositionBrand = undefined; } } /** * Coordinates relative to the the (top;left) of the editor that can be used safely with other internal editor metrics. * **NOTE**: This position is obtained by taking page coordinates and transforming them relative to the * editor's (top;left) position in a way in which scale transformations are taken into account. * **NOTE**: These coordinates could be negative if the mouse position is outside the editor. */ export class CoordinatesRelativeToEditor { constructor(x, y) { this.x = x; this.y = y; this._positionRelativeToEditorBrand = undefined; } } export function createEditorPagePosition(editorViewDomNode) { const editorPos = dom.getDomNodePagePosition(editorViewDomNode); return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height); } export function createCoordinatesRelativeToEditor(editorViewDomNode, editorPagePosition, pos) { // The editor's page position is read from the DOM using getBoundingClientRect(). // // getBoundingClientRect() returns the actual dimensions, while offsetWidth and offsetHeight // reflect the unscaled size. We can use this difference to detect a transform:scale() // and we will apply the transformation in inverse to get mouse coordinates that make sense inside the editor. // // This could be expanded to cover rotation as well maybe by walking the DOM up from `editorViewDomNode` // and computing the effective transformation matrix using getComputedStyle(element).transform. // const scaleX = editorPagePosition.width / editorViewDomNode.offsetWidth; const scaleY = editorPagePosition.height / editorViewDomNode.offsetHeight; // Adjust mouse offsets if editor appears to be scaled via transforms const relativeX = (pos.x - editorPagePosition.x) / scaleX; const relativeY = (pos.y - editorPagePosition.y) / scaleY; return new CoordinatesRelativeToEditor(relativeX, relativeY); } export class EditorMouseEvent extends StandardMouseEvent { constructor(e, isFromPointerCapture, editorViewDomNode) { super(dom.getWindow(editorViewDomNode), e); this._editorMouseEventBrand = undefined; this.isFromPointerCapture = isFromPointerCapture; this.pos = new PageCoordinates(this.posx, this.posy); this.editorPos = createEditorPagePosition(editorViewDomNode); this.relativePos = createCoordinatesRelativeToEditor(editorViewDomNode, this.editorPos, this.pos); } } export class EditorMouseEventFactory { constructor(editorViewDomNode) { this._editorViewDomNode = editorViewDomNode; } _create(e) { return new EditorMouseEvent(e, false, this._editorViewDomNode); } onContextMenu(target, callback) { return dom.addDisposableListener(target, 'contextmenu', (e) => { callback(this._create(e)); }); } onMouseUp(target, callback) { return dom.addDisposableListener(target, 'mouseup', (e) => { callback(this._create(e)); }); } onMouseDown(target, callback) { return dom.addDisposableListener(target, dom.EventType.MOUSE_DOWN, (e) => { callback(this._create(e)); }); } onPointerDown(target, callback) { return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e) => { callback(this._create(e), e.pointerId); }); } onMouseLeave(target, callback) { return dom.addDisposableListener(target, dom.EventType.MOUSE_LEAVE, (e) => { callback(this._create(e)); }); } onMouseMove(target, callback) { return dom.addDisposableListener(target, 'mousemove', (e) => callback(this._create(e))); } } export class EditorPointerEventFactory { constructor(editorViewDomNode) { this._editorViewDomNode = editorViewDomNode; } _create(e) { return new EditorMouseEvent(e, false, this._editorViewDomNode); } onPointerUp(target, callback) { return dom.addDisposableListener(target, 'pointerup', (e) => { callback(this._create(e)); }); } onPointerDown(target, callback) { return dom.addDisposableListener(target, dom.EventType.POINTER_DOWN, (e) => { callback(this._create(e), e.pointerId); }); } onPointerLeave(target, callback) { return dom.addDisposableListener(target, dom.EventType.POINTER_LEAVE, (e) => { callback(this._create(e)); }); } onPointerMove(target, callback) { return dom.addDisposableListener(target, 'pointermove', (e) => callback(this._create(e))); } } export class GlobalEditorPointerMoveMonitor extends Disposable { constructor(editorViewDomNode) { super(); this._editorViewDomNode = editorViewDomNode; this._globalPointerMoveMonitor = this._register(new GlobalPointerMoveMonitor()); this._keydownListener = null; } startMonitoring(initialElement, pointerId, initialButtons, pointerMoveCallback, onStopCallback) { // Add a <<capture>> keydown event listener that will cancel the monitoring // if something other than a modifier key is pressed this._keydownListener = dom.addStandardDisposableListener(initialElement.ownerDocument, 'keydown', (e) => { const chord = e.toKeyCodeChord(); if (chord.isModifierKey()) { // Allow modifier keys return; } this._globalPointerMoveMonitor.stopMonitoring(true, e.browserEvent); }, true); this._globalPointerMoveMonitor.startMonitoring(initialElement, pointerId, initialButtons, (e) => { pointerMoveCallback(new EditorMouseEvent(e, true, this._editorViewDomNode)); }, (e) => { this._keydownListener.dispose(); onStopCallback(e); }); } stopMonitoring() { this._globalPointerMoveMonitor.stopMonitoring(true); } } /** * A helper to create dynamic css rules, bound to a class name. * Rules are reused. * Reference counting and delayed garbage collection ensure that no rules leak. */ export class DynamicCssRules { static { this._idPool = 0; } constructor(_editor) { this._editor = _editor; this._instanceId = ++DynamicCssRules._idPool; this._counter = 0; this._rules = new Map(); // We delay garbage collection so that hanging rules can be reused. this._garbageCollectionScheduler = new RunOnceScheduler(() => this.garbageCollect(), 1000); } createClassNameRef(options) { const rule = this.getOrCreateRule(options); rule.increaseRefCount(); return { className: rule.className, dispose: () => { rule.decreaseRefCount(); this._garbageCollectionScheduler.schedule(); } }; } getOrCreateRule(properties) { const key = this.computeUniqueKey(properties); let existingRule = this._rules.get(key); if (!existingRule) { const counter = this._counter++; existingRule = new RefCountedCssRule(key, `dyn-rule-${this._instanceId}-${counter}`, dom.isInShadowDOM(this._editor.getContainerDomNode()) ? this._editor.getContainerDomNode() : undefined, properties); this._rules.set(key, existingRule); } return existingRule; } computeUniqueKey(properties) { return JSON.stringify(properties); } garbageCollect() { for (const rule of this._rules.values()) { if (!rule.hasReferences()) { this._rules.delete(rule.key); rule.dispose(); } } } } class RefCountedCssRule { constructor(key, className, _containerElement, properties) { this.key = key; this.className = className; this.properties = properties; this._referenceCount = 0; this._styleElementDisposables = new DisposableStore(); this._styleElement = dom.createStyleSheet(_containerElement, undefined, this._styleElementDisposables); this._styleElement.textContent = this.getCssText(this.className, this.properties); } getCssText(className, properties) { let str = `.${className} {`; for (const prop in properties) { const value = properties[prop]; let cssValue; if (typeof value === 'object') { cssValue = asCssVariable(value.id); } else { cssValue = value; } const cssPropName = camelToDashes(prop); str += `\n\t${cssPropName}: ${cssValue};`; } str += `\n}`; return str; } dispose() { this._styleElementDisposables.dispose(); this._styleElement = undefined; } increaseRefCount() { this._referenceCount++; } decreaseRefCount() { this._referenceCount--; } hasReferences() { return this._referenceCount > 0; } } function camelToDashes(str) { return str.replace(/(^[A-Z])/, ([first]) => first.toLowerCase()) .replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`); }