UNPKG

monaco-editor-core

Version:

A browser based code editor

591 lines (590 loc) • 29.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; import './hover.css'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { Emitter } from '../../../../base/common/event.js'; import * as dom from '../../../../base/browser/dom.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js'; import { HoverAction, HoverWidget as BaseHoverWidget, getHoverAccessibleViewHint } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { MarkdownRenderer, openLinkFromMarkdown } from '../../widget/markdownRenderer/browser/markdownRenderer.js'; import { isMarkdownString } from '../../../../base/common/htmlContent.js'; import { localize } from '../../../../nls.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { status } from '../../../../base/browser/ui/aria/aria.js'; const $ = dom.$; let HoverWidget = class HoverWidget extends Widget { get _targetWindow() { return dom.getWindow(this._target.targetElements[0]); } get _targetDocumentElement() { return dom.getWindow(this._target.targetElements[0]).document.documentElement; } get isDisposed() { return this._isDisposed; } get isMouseIn() { return this._lockMouseTracker.isMouseIn; } get domNode() { return this._hover.containerDomNode; } get onDispose() { return this._onDispose.event; } get onRequestLayout() { return this._onRequestLayout.event; } get anchor() { return this._hoverPosition === 2 /* HoverPosition.BELOW */ ? 0 /* AnchorPosition.BELOW */ : 1 /* AnchorPosition.ABOVE */; } get x() { return this._x; } get y() { return this._y; } /** * Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not * hide and can be hovered regardless of whether the `hideOnHover` hover option is set. */ get isLocked() { return this._isLocked; } set isLocked(value) { if (this._isLocked === value) { return; } this._isLocked = value; this._hoverContainer.classList.toggle('locked', this._isLocked); } constructor(options, _keybindingService, _configurationService, _openerService, _instantiationService, _accessibilityService) { super(); this._keybindingService = _keybindingService; this._configurationService = _configurationService; this._openerService = _openerService; this._instantiationService = _instantiationService; this._accessibilityService = _accessibilityService; this._messageListeners = new DisposableStore(); this._isDisposed = false; this._forcePosition = false; this._x = 0; this._y = 0; this._isLocked = false; this._enableFocusTraps = false; this._addedFocusTrap = false; this._onDispose = this._register(new Emitter()); this._onRequestLayout = this._register(new Emitter()); this._linkHandler = options.linkHandler || (url => { return openLinkFromMarkdown(this._openerService, url, isMarkdownString(options.content) ? options.content.isTrusted : undefined); }); this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target); this._hoverPointer = options.appearance?.showPointer ? $('div.workbench-hover-pointer') : undefined; this._hover = this._register(new BaseHoverWidget()); this._hover.containerDomNode.classList.add('workbench-hover', 'fadeIn'); if (options.appearance?.compact) { this._hover.containerDomNode.classList.add('workbench-hover', 'compact'); } if (options.appearance?.skipFadeInAnimation) { this._hover.containerDomNode.classList.add('skip-fade-in'); } if (options.additionalClasses) { this._hover.containerDomNode.classList.add(...options.additionalClasses); } if (options.position?.forcePosition) { this._forcePosition = true; } if (options.trapFocus) { this._enableFocusTraps = true; } this._hoverPosition = options.position?.hoverPosition ?? 3 /* HoverPosition.ABOVE */; // Don't allow mousedown out of the widget, otherwise preventDefault will call and text will // not be selected. this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation()); // Hide hover on escape this.onkeydown(this._hover.containerDomNode, e => { if (e.equals(9 /* KeyCode.Escape */)) { this.dispose(); } }); // Hide when the window loses focus this._register(dom.addDisposableListener(this._targetWindow, 'blur', () => this.dispose())); const rowElement = $('div.hover-row.markdown-hover'); const contentsElement = $('div.hover-contents'); if (typeof options.content === 'string') { contentsElement.textContent = options.content; contentsElement.style.whiteSpace = 'pre-wrap'; } else if (dom.isHTMLElement(options.content)) { contentsElement.appendChild(options.content); contentsElement.classList.add('html-hover-contents'); } else { const markdown = options.content; const mdRenderer = this._instantiationService.createInstance(MarkdownRenderer, { codeBlockFontFamily: this._configurationService.getValue('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily }); const { element } = mdRenderer.render(markdown, { actionHandler: { callback: (content) => this._linkHandler(content), disposables: this._messageListeners }, asyncRenderCallback: () => { contentsElement.classList.add('code-hover-contents'); this.layout(); // This changes the dimensions of the hover so trigger a layout this._onRequestLayout.fire(); } }); contentsElement.appendChild(element); } rowElement.appendChild(contentsElement); this._hover.contentsDomNode.appendChild(rowElement); if (options.actions && options.actions.length > 0) { const statusBarElement = $('div.hover-row.status-bar'); const actionsElement = $('div.actions'); options.actions.forEach(action => { const keybinding = this._keybindingService.lookupKeybinding(action.commandId); const keybindingLabel = keybinding ? keybinding.getLabel() : null; HoverAction.render(actionsElement, { label: action.label, commandId: action.commandId, run: e => { action.run(e); this.dispose(); }, iconClass: action.iconClass }, keybindingLabel); }); statusBarElement.appendChild(actionsElement); this._hover.containerDomNode.appendChild(statusBarElement); } this._hoverContainer = $('div.workbench-hover-container'); if (this._hoverPointer) { this._hoverContainer.appendChild(this._hoverPointer); } this._hoverContainer.appendChild(this._hover.containerDomNode); // Determine whether to hide on hover let hideOnHover; if (options.actions && options.actions.length > 0) { // If there are actions, require hover so they can be accessed hideOnHover = false; } else { if (options.persistence?.hideOnHover === undefined) { // When unset, will default to true when it's a string or when it's markdown that // appears to have a link using a naive check for '](' and '</a>' hideOnHover = typeof options.content === 'string' || isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes('</a>'); } else { // It's set explicitly hideOnHover = options.persistence.hideOnHover; } } // Show the hover hint if needed if (options.appearance?.showHoverHint) { const statusBarElement = $('div.hover-row.status-bar'); const infoElement = $('div.info'); infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt'); statusBarElement.appendChild(infoElement); this._hover.containerDomNode.appendChild(statusBarElement); } const mouseTrackerTargets = [...this._target.targetElements]; if (!hideOnHover) { mouseTrackerTargets.push(this._hoverContainer); } const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets)); this._register(mouseTracker.onMouseOut(() => { if (!this._isLocked) { this.dispose(); } })); // Setup another mouse tracker when hideOnHover is set in order to track the hover as well // when it is locked. This ensures the hover will hide on mouseout after alt has been // released to unlock the element. if (hideOnHover) { const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer]; this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets)); this._register(this._lockMouseTracker.onMouseOut(() => { if (!this._isLocked) { this.dispose(); } })); } else { this._lockMouseTracker = mouseTracker; } } addFocusTrap() { if (!this._enableFocusTraps || this._addedFocusTrap) { return; } this._addedFocusTrap = true; // Add a hover tab loop if the hover has at least one element with a valid tabIndex const firstContainerFocusElement = this._hover.containerDomNode; const lastContainerFocusElement = this.findLastFocusableChild(this._hover.containerDomNode); if (lastContainerFocusElement) { const beforeContainerFocusElement = dom.prepend(this._hoverContainer, $('div')); const afterContainerFocusElement = dom.append(this._hoverContainer, $('div')); beforeContainerFocusElement.tabIndex = 0; afterContainerFocusElement.tabIndex = 0; this._register(dom.addDisposableListener(afterContainerFocusElement, 'focus', (e) => { firstContainerFocusElement.focus(); e.preventDefault(); })); this._register(dom.addDisposableListener(beforeContainerFocusElement, 'focus', (e) => { lastContainerFocusElement.focus(); e.preventDefault(); })); } } findLastFocusableChild(root) { if (root.hasChildNodes()) { for (let i = 0; i < root.childNodes.length; i++) { const node = root.childNodes.item(root.childNodes.length - i - 1); if (node.nodeType === node.ELEMENT_NODE) { const parsedNode = node; if (typeof parsedNode.tabIndex === 'number' && parsedNode.tabIndex >= 0) { return parsedNode; } } const recursivelyFoundElement = this.findLastFocusableChild(node); if (recursivelyFoundElement) { return recursivelyFoundElement; } } } return undefined; } render(container) { container.appendChild(this._hoverContainer); const hoverFocused = this._hoverContainer.contains(this._hoverContainer.ownerDocument.activeElement); const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel()); if (accessibleViewHint) { status(accessibleViewHint); } this.layout(); this.addFocusTrap(); } layout() { this._hover.containerDomNode.classList.remove('right-aligned'); this._hover.contentsDomNode.style.maxHeight = ''; const getZoomAccountedBoundingClientRect = (e) => { const zoom = dom.getDomNodeZoomLevel(e); const boundingRect = e.getBoundingClientRect(); return { top: boundingRect.top * zoom, bottom: boundingRect.bottom * zoom, right: boundingRect.right * zoom, left: boundingRect.left * zoom, }; }; const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e)); const { top, right, bottom, left } = targetBounds[0]; const width = right - left; const height = bottom - top; const targetRect = { top, right, bottom, left, width, height, center: { x: left + (width / 2), y: top + (height / 2) } }; // These calls adjust the position depending on spacing. this.adjustHorizontalHoverPosition(targetRect); this.adjustVerticalHoverPosition(targetRect); // This call limits the maximum height of the hover. this.adjustHoverMaxHeight(targetRect); // Offset the hover position if there is a pointer so it aligns with the target element this._hoverContainer.style.padding = ''; this._hoverContainer.style.margin = ''; if (this._hoverPointer) { switch (this._hoverPosition) { case 1 /* HoverPosition.RIGHT */: targetRect.left += 3 /* Constants.PointerSize */; targetRect.right += 3 /* Constants.PointerSize */; this._hoverContainer.style.paddingLeft = `${3 /* Constants.PointerSize */}px`; this._hoverContainer.style.marginLeft = `${-3 /* Constants.PointerSize */}px`; break; case 0 /* HoverPosition.LEFT */: targetRect.left -= 3 /* Constants.PointerSize */; targetRect.right -= 3 /* Constants.PointerSize */; this._hoverContainer.style.paddingRight = `${3 /* Constants.PointerSize */}px`; this._hoverContainer.style.marginRight = `${-3 /* Constants.PointerSize */}px`; break; case 2 /* HoverPosition.BELOW */: targetRect.top += 3 /* Constants.PointerSize */; targetRect.bottom += 3 /* Constants.PointerSize */; this._hoverContainer.style.paddingTop = `${3 /* Constants.PointerSize */}px`; this._hoverContainer.style.marginTop = `${-3 /* Constants.PointerSize */}px`; break; case 3 /* HoverPosition.ABOVE */: targetRect.top -= 3 /* Constants.PointerSize */; targetRect.bottom -= 3 /* Constants.PointerSize */; this._hoverContainer.style.paddingBottom = `${3 /* Constants.PointerSize */}px`; this._hoverContainer.style.marginBottom = `${-3 /* Constants.PointerSize */}px`; break; } targetRect.center.x = targetRect.left + (width / 2); targetRect.center.y = targetRect.top + (height / 2); } this.computeXCordinate(targetRect); this.computeYCordinate(targetRect); if (this._hoverPointer) { // reset this._hoverPointer.classList.remove('top'); this._hoverPointer.classList.remove('left'); this._hoverPointer.classList.remove('right'); this._hoverPointer.classList.remove('bottom'); this.setHoverPointerPosition(targetRect); } this._hover.onContentsChanged(); } computeXCordinate(target) { const hoverWidth = this._hover.containerDomNode.clientWidth + 2 /* Constants.HoverBorderWidth */; if (this._target.x !== undefined) { this._x = this._target.x; } else if (this._hoverPosition === 1 /* HoverPosition.RIGHT */) { this._x = target.right; } else if (this._hoverPosition === 0 /* HoverPosition.LEFT */) { this._x = target.left - hoverWidth; } else { if (this._hoverPointer) { this._x = target.center.x - (this._hover.containerDomNode.clientWidth / 2); } else { this._x = target.left; } // Hover is going beyond window towards right end if (this._x + hoverWidth >= this._targetDocumentElement.clientWidth) { this._hover.containerDomNode.classList.add('right-aligned'); this._x = Math.max(this._targetDocumentElement.clientWidth - hoverWidth - 2 /* Constants.HoverWindowEdgeMargin */, this._targetDocumentElement.clientLeft); } } // Hover is going beyond window towards left end if (this._x < this._targetDocumentElement.clientLeft) { this._x = target.left + 2 /* Constants.HoverWindowEdgeMargin */; } } computeYCordinate(target) { if (this._target.y !== undefined) { this._y = this._target.y; } else if (this._hoverPosition === 3 /* HoverPosition.ABOVE */) { this._y = target.top; } else if (this._hoverPosition === 2 /* HoverPosition.BELOW */) { this._y = target.bottom - 2; } else { if (this._hoverPointer) { this._y = target.center.y + (this._hover.containerDomNode.clientHeight / 2); } else { this._y = target.bottom; } } // Hover on bottom is going beyond window if (this._y > this._targetWindow.innerHeight) { this._y = target.bottom; } } adjustHorizontalHoverPosition(target) { // Do not adjust horizontal hover position if x cordiante is provided if (this._target.x !== undefined) { return; } const hoverPointerOffset = (this._hoverPointer ? 3 /* Constants.PointerSize */ : 0); // When force position is enabled, restrict max width if (this._forcePosition) { const padding = hoverPointerOffset + 2 /* Constants.HoverBorderWidth */; if (this._hoverPosition === 1 /* HoverPosition.RIGHT */) { this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`; } else if (this._hoverPosition === 0 /* HoverPosition.LEFT */) { this._hover.containerDomNode.style.maxWidth = `${target.left - padding}px`; } return; } // Position hover on right to target if (this._hoverPosition === 1 /* HoverPosition.RIGHT */) { const roomOnRight = this._targetDocumentElement.clientWidth - target.right; // Hover on the right is going beyond window. if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) { const roomOnLeft = target.left; // There's enough room on the left, flip the hover position if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) { this._hoverPosition = 0 /* HoverPosition.LEFT */; } // Hover on the left would go beyond window too else { this._hoverPosition = 2 /* HoverPosition.BELOW */; } } } // Position hover on left to target else if (this._hoverPosition === 0 /* HoverPosition.LEFT */) { const roomOnLeft = target.left; // Hover on the left is going beyond window. if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) { const roomOnRight = this._targetDocumentElement.clientWidth - target.right; // There's enough room on the right, flip the hover position if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) { this._hoverPosition = 1 /* HoverPosition.RIGHT */; } // Hover on the right would go beyond window too else { this._hoverPosition = 2 /* HoverPosition.BELOW */; } } // Hover on the left is going beyond window. if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) { this._hoverPosition = 1 /* HoverPosition.RIGHT */; } } } adjustVerticalHoverPosition(target) { // Do not adjust vertical hover position if the y coordinate is provided // or the position is forced if (this._target.y !== undefined || this._forcePosition) { return; } const hoverPointerOffset = (this._hoverPointer ? 3 /* Constants.PointerSize */ : 0); // Position hover on top of the target if (this._hoverPosition === 3 /* HoverPosition.ABOVE */) { // Hover on top is going beyond window if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) { this._hoverPosition = 2 /* HoverPosition.BELOW */; } } // Position hover below the target else if (this._hoverPosition === 2 /* HoverPosition.BELOW */) { // Hover on bottom is going beyond window if (target.bottom + this._hover.containerDomNode.clientHeight + hoverPointerOffset > this._targetWindow.innerHeight) { this._hoverPosition = 3 /* HoverPosition.ABOVE */; } } } adjustHoverMaxHeight(target) { let maxHeight = this._targetWindow.innerHeight / 2; // When force position is enabled, restrict max height if (this._forcePosition) { const padding = (this._hoverPointer ? 3 /* Constants.PointerSize */ : 0) + 2 /* Constants.HoverBorderWidth */; if (this._hoverPosition === 3 /* HoverPosition.ABOVE */) { maxHeight = Math.min(maxHeight, target.top - padding); } else if (this._hoverPosition === 2 /* HoverPosition.BELOW */) { maxHeight = Math.min(maxHeight, this._targetWindow.innerHeight - target.bottom - padding); } } this._hover.containerDomNode.style.maxHeight = `${maxHeight}px`; if (this._hover.contentsDomNode.clientHeight < this._hover.contentsDomNode.scrollHeight) { // Add padding for a vertical scrollbar const extraRightPadding = `${this._hover.scrollbar.options.verticalScrollbarSize}px`; if (this._hover.contentsDomNode.style.paddingRight !== extraRightPadding) { this._hover.contentsDomNode.style.paddingRight = extraRightPadding; } } } setHoverPointerPosition(target) { if (!this._hoverPointer) { return; } switch (this._hoverPosition) { case 0 /* HoverPosition.LEFT */: case 1 /* HoverPosition.RIGHT */: { this._hoverPointer.classList.add(this._hoverPosition === 0 /* HoverPosition.LEFT */ ? 'right' : 'left'); const hoverHeight = this._hover.containerDomNode.clientHeight; // If hover is taller than target, then show the pointer at the center of target if (hoverHeight > target.height) { this._hoverPointer.style.top = `${target.center.y - (this._y - hoverHeight) - 3 /* Constants.PointerSize */}px`; } // Otherwise show the pointer at the center of hover else { this._hoverPointer.style.top = `${Math.round((hoverHeight / 2)) - 3 /* Constants.PointerSize */}px`; } break; } case 3 /* HoverPosition.ABOVE */: case 2 /* HoverPosition.BELOW */: { this._hoverPointer.classList.add(this._hoverPosition === 3 /* HoverPosition.ABOVE */ ? 'bottom' : 'top'); const hoverWidth = this._hover.containerDomNode.clientWidth; // Position pointer at the center of the hover let pointerLeftPosition = Math.round((hoverWidth / 2)) - 3 /* Constants.PointerSize */; // If pointer goes beyond target then position it at the center of the target const pointerX = this._x + pointerLeftPosition; if (pointerX < target.left || pointerX > target.right) { pointerLeftPosition = target.center.x - this._x - 3 /* Constants.PointerSize */; } this._hoverPointer.style.left = `${pointerLeftPosition}px`; break; } } } focus() { this._hover.containerDomNode.focus(); } dispose() { if (!this._isDisposed) { this._onDispose.fire(); this._hoverContainer.remove(); this._messageListeners.dispose(); this._target.dispose(); super.dispose(); } this._isDisposed = true; } }; HoverWidget = __decorate([ __param(1, IKeybindingService), __param(2, IConfigurationService), __param(3, IOpenerService), __param(4, IInstantiationService), __param(5, IAccessibilityService) ], HoverWidget); export { HoverWidget }; class CompositeMouseTracker extends Widget { get onMouseOut() { return this._onMouseOut.event; } get isMouseIn() { return this._isMouseIn; } constructor(_elements) { super(); this._elements = _elements; this._isMouseIn = true; this._onMouseOut = this._register(new Emitter()); this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver(n))); this._elements.forEach(n => this.onmouseleave(n, () => this._onTargetMouseLeave(n))); } _onTargetMouseOver(target) { this._isMouseIn = true; this._clearEvaluateMouseStateTimeout(target); } _onTargetMouseLeave(target) { this._isMouseIn = false; this._evaluateMouseState(target); } _evaluateMouseState(target) { this._clearEvaluateMouseStateTimeout(target); // Evaluate whether the mouse is still outside asynchronously such that other mouse targets // have the opportunity to first their mouse in event. this._mouseTimeout = dom.getWindow(target).setTimeout(() => this._fireIfMouseOutside(), 0); } _clearEvaluateMouseStateTimeout(target) { if (this._mouseTimeout) { dom.getWindow(target).clearTimeout(this._mouseTimeout); this._mouseTimeout = undefined; } } _fireIfMouseOutside() { if (!this._isMouseIn) { this._onMouseOut.fire(); } } } class ElementHoverTarget { constructor(_element) { this._element = _element; this.targetElements = [this._element]; } dispose() { } }