mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-11 09:09:46 +01:00
408 lines
14 KiB
TypeScript
408 lines
14 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import 'vs/css!./media/scrollbars';
|
|
|
|
import * as DomUtils from 'vs/base/browser/dom';
|
|
import * as Platform from 'vs/base/common/platform';
|
|
import {StandardMouseWheelEvent, IMouseEvent} from 'vs/base/browser/mouseEvent';
|
|
import {DomNodeScrollable} from 'vs/base/browser/ui/scrollbar/domNodeScrollable';
|
|
import {HorizontalScrollbar} from 'vs/base/browser/ui/scrollbar/horizontalScrollbar';
|
|
import {VerticalScrollbar} from 'vs/base/browser/ui/scrollbar/verticalScrollbar';
|
|
import {
|
|
IScrollableElementOptions, IScrollbar, IDimensions, IMouseWheelEvent, visibilityFromString,
|
|
IScrollableElement, IScrollableElementCreationOptions, IOverviewRulerLayoutInfo
|
|
} from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
|
import {IDisposable, disposeAll} from 'vs/base/common/lifecycle';
|
|
import {IScrollable} from 'vs/base/common/scrollable';
|
|
import {Widget} from 'vs/base/browser/ui/widget';
|
|
import {TimeoutTimer} from 'vs/base/common/async';
|
|
|
|
const HIDE_TIMEOUT = 500;
|
|
const SCROLL_WHEEL_SENSITIVITY = 50;
|
|
|
|
export class ScrollableElement extends Widget implements IScrollableElement {
|
|
|
|
private _originalElement: HTMLElement;
|
|
private _options: IScrollableElementOptions;
|
|
private _scrollable: IScrollable;
|
|
public verticalScrollbarWidth: number;
|
|
public horizontalScrollbarHeight: number;
|
|
private _verticalScrollbar: IScrollbar;
|
|
private _horizontalScrollbar: IScrollbar;
|
|
private _domNode: HTMLElement;
|
|
|
|
private _leftShadowDomNode: HTMLElement;
|
|
private _topShadowDomNode: HTMLElement;
|
|
private _topLeftShadowDomNode: HTMLElement;
|
|
private _listenOnDomNode: HTMLElement;
|
|
|
|
private _mouseWheelToDispose: IDisposable[];
|
|
|
|
private _onElementDimensionsTimeout: TimeoutTimer;
|
|
private _onElementInternalDimensionsTimeout: TimeoutTimer;
|
|
private _isDragging: boolean;
|
|
private _mouseIsOver: boolean;
|
|
|
|
private _dimensions: IDimensions;
|
|
private _hideTimeout: TimeoutTimer;
|
|
|
|
constructor(element: HTMLElement, options: IScrollableElementCreationOptions, dimensions: IDimensions = null) {
|
|
super();
|
|
this._originalElement = element;
|
|
this._originalElement.style.overflow = 'hidden';
|
|
this._options = this._createOptions(options);
|
|
|
|
if (this._options.scrollable) {
|
|
this._scrollable = this._options.scrollable;
|
|
} else {
|
|
this._scrollable = this._register(new DomNodeScrollable(this._originalElement));
|
|
}
|
|
|
|
this.verticalScrollbarWidth = this._options.verticalScrollbarSize;
|
|
this.horizontalScrollbarHeight = this._options.horizontalScrollbarSize;
|
|
|
|
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this, this._options));
|
|
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this, this._options));
|
|
|
|
this._domNode = document.createElement('div');
|
|
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
|
|
this._domNode.setAttribute('role', 'presentation');
|
|
this._domNode.style.position = 'relative';
|
|
this._domNode.style.overflow = 'hidden';
|
|
this._domNode.appendChild(this._originalElement);
|
|
this._domNode.appendChild(this._horizontalScrollbar.domNode);
|
|
this._domNode.appendChild(this._verticalScrollbar.domNode);
|
|
|
|
if (this._options.useShadows) {
|
|
this._leftShadowDomNode = document.createElement('div');
|
|
this._leftShadowDomNode.className = 'shadow';
|
|
this._domNode.appendChild(this._leftShadowDomNode);
|
|
}
|
|
|
|
if (this._options.useShadows) {
|
|
this._topShadowDomNode = document.createElement('div');
|
|
this._topShadowDomNode.className = 'shadow';
|
|
this._domNode.appendChild(this._topShadowDomNode);
|
|
|
|
this._topLeftShadowDomNode = document.createElement('div');
|
|
this._topLeftShadowDomNode.className = 'shadow top-left-corner';
|
|
this._domNode.appendChild(this._topLeftShadowDomNode);
|
|
}
|
|
|
|
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
|
|
|
|
|
|
this._register(this._scrollable.addScrollListener(() => this._onScroll()));
|
|
|
|
this._mouseWheelToDispose = [];
|
|
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
|
|
|
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
|
|
this.onnonbubblingmouseout(this._listenOnDomNode, (e) => this._onMouseOut(e));
|
|
|
|
this._onElementDimensionsTimeout = this._register(new TimeoutTimer());
|
|
this._onElementInternalDimensionsTimeout = this._register(new TimeoutTimer());
|
|
this._hideTimeout = this._register(new TimeoutTimer());
|
|
this._isDragging = false;
|
|
this._mouseIsOver = false;
|
|
|
|
this.onElementDimensions(dimensions, true);
|
|
this.onElementInternalDimensions(true);
|
|
}
|
|
|
|
public dispose(): void {
|
|
this._mouseWheelToDispose = disposeAll(this._mouseWheelToDispose);
|
|
super.dispose();
|
|
}
|
|
|
|
public getDomNode(): HTMLElement {
|
|
return this._domNode;
|
|
}
|
|
|
|
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
|
|
return {
|
|
parent: this._domNode,
|
|
insertBefore: this._verticalScrollbar.domNode,
|
|
};
|
|
}
|
|
|
|
public delegateVerticalScrollbarMouseDown(browserEvent: MouseEvent): void {
|
|
this._verticalScrollbar.delegateMouseDown(browserEvent);
|
|
}
|
|
|
|
public onElementDimensions(dimensions: IDimensions = null, synchronous: boolean = false): void {
|
|
if (synchronous) {
|
|
this._actualElementDimensions(dimensions);
|
|
this._onElementDimensionsTimeout.cancel();
|
|
} else {
|
|
this._onElementDimensionsTimeout.cancelAndSet(() => this._actualElementDimensions(dimensions), 0);
|
|
}
|
|
}
|
|
|
|
private _actualElementDimensions(dimensions: IDimensions = null): void {
|
|
if (!dimensions) {
|
|
dimensions = {
|
|
width: this._domNode.clientWidth,
|
|
height: this._domNode.clientHeight
|
|
};
|
|
}
|
|
this._dimensions = this._computeDimensions(dimensions.width, dimensions.height);
|
|
this._verticalScrollbar.onElementSize(this._dimensions.height);
|
|
this._horizontalScrollbar.onElementSize(this._dimensions.width);
|
|
}
|
|
|
|
public onElementInternalDimensions(synchronous: boolean = false): void {
|
|
if (synchronous) {
|
|
this._actualElementInternalDimensions();
|
|
this._onElementInternalDimensionsTimeout.cancel();
|
|
} else {
|
|
this._onElementInternalDimensionsTimeout.setIfNotSet(() => this._actualElementInternalDimensions(), 0);
|
|
}
|
|
}
|
|
|
|
private _actualElementInternalDimensions(): void {
|
|
this._horizontalScrollbar.onElementScrollSize(this._scrollable.getScrollWidth());
|
|
this._verticalScrollbar.onElementScrollSize(this._scrollable.getScrollHeight());
|
|
}
|
|
|
|
public updateClassName(newClassName: string): void {
|
|
this._options.className = newClassName;
|
|
// Defaults are different on Macs
|
|
if (Platform.isMacintosh) {
|
|
this._options.className += ' mac';
|
|
}
|
|
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
|
|
}
|
|
|
|
public updateOptions(newOptions: IScrollableElementCreationOptions): void {
|
|
// only support handleMouseWheel changes for now
|
|
let massagedOptions = this._createOptions(newOptions);
|
|
this._options.handleMouseWheel = massagedOptions.handleMouseWheel;
|
|
this._options.mouseWheelScrollSensitivity = massagedOptions.mouseWheelScrollSensitivity;
|
|
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
|
}
|
|
|
|
// -------------------- mouse wheel scrolling --------------------
|
|
|
|
private _setListeningToMouseWheel(shouldListen: boolean): void {
|
|
let isListening = (this._mouseWheelToDispose.length > 0);
|
|
|
|
if (isListening === shouldListen) {
|
|
// No change
|
|
return;
|
|
}
|
|
|
|
// Stop listening (if necessary)
|
|
this._mouseWheelToDispose = disposeAll(this._mouseWheelToDispose);
|
|
|
|
// Start listening (if necessary)
|
|
if (shouldListen) {
|
|
let onMouseWheel = (browserEvent: MouseWheelEvent) => {
|
|
let e = new StandardMouseWheelEvent(browserEvent);
|
|
this.onMouseWheel(e);
|
|
};
|
|
|
|
this._mouseWheelToDispose.push(DomUtils.addDisposableListener(this._listenOnDomNode, 'mousewheel', onMouseWheel));
|
|
this._mouseWheelToDispose.push(DomUtils.addDisposableListener(this._listenOnDomNode, 'DOMMouseScroll', onMouseWheel));
|
|
}
|
|
}
|
|
|
|
public onMouseWheel(e: IMouseWheelEvent): void {
|
|
if (Platform.isMacintosh && e.browserEvent && this._options.saveLastScrollTimeOnClassName) {
|
|
// Mark dom node with timestamp of wheel event
|
|
let target = <HTMLElement>e.browserEvent.target;
|
|
if (target && target.nodeType === 1) {
|
|
let r = DomUtils.findParentWithClass(target, this._options.saveLastScrollTimeOnClassName);
|
|
if (r) {
|
|
r.setAttribute('last-scroll-time', String(new Date().getTime()));
|
|
}
|
|
}
|
|
}
|
|
|
|
let desiredScrollTop = -1;
|
|
let desiredScrollLeft = -1;
|
|
|
|
if (e.deltaY || e.deltaX) {
|
|
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
|
|
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
|
|
|
|
if (this._options.flipAxes) {
|
|
deltaY = e.deltaX;
|
|
deltaX = e.deltaY;
|
|
}
|
|
|
|
if (Platform.isMacintosh) {
|
|
// Give preference to vertical scrolling
|
|
if (deltaY && Math.abs(deltaX) < 0.2) {
|
|
deltaX = 0;
|
|
}
|
|
if (Math.abs(deltaY) > Math.abs(deltaX) * 0.5) {
|
|
deltaX = 0;
|
|
}
|
|
}
|
|
|
|
if (deltaY) {
|
|
let currentScrollTop = this._scrollable.getScrollTop();
|
|
desiredScrollTop = this._verticalScrollbar.validateScrollPosition((desiredScrollTop !== -1 ? desiredScrollTop : currentScrollTop) - SCROLL_WHEEL_SENSITIVITY * deltaY);
|
|
if (desiredScrollTop === currentScrollTop) {
|
|
desiredScrollTop = -1;
|
|
}
|
|
}
|
|
if (deltaX) {
|
|
let currentScrollLeft = this._scrollable.getScrollLeft();
|
|
desiredScrollLeft = this._horizontalScrollbar.validateScrollPosition((desiredScrollLeft !== -1 ? desiredScrollLeft : currentScrollLeft) - SCROLL_WHEEL_SENSITIVITY * deltaX);
|
|
if (desiredScrollLeft === currentScrollLeft) {
|
|
desiredScrollLeft = -1;
|
|
}
|
|
}
|
|
|
|
if (desiredScrollTop !== -1 || desiredScrollLeft !== -1) {
|
|
if (desiredScrollTop !== -1) {
|
|
this._verticalScrollbar.setDesiredScrollPosition(desiredScrollTop);
|
|
desiredScrollTop = -1;
|
|
}
|
|
if (desiredScrollLeft !== -1) {
|
|
this._horizontalScrollbar.setDesiredScrollPosition(desiredScrollLeft);
|
|
desiredScrollLeft = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
private _onScroll(): void {
|
|
let scrollHeight = this._scrollable.getScrollHeight();
|
|
let scrollTop = this._scrollable.getScrollTop();
|
|
let scrollWidth = this._scrollable.getScrollWidth();
|
|
let scrollLeft = this._scrollable.getScrollLeft();
|
|
|
|
this._verticalScrollbar.onElementScrollPosition(scrollTop);
|
|
this._horizontalScrollbar.onElementScrollPosition(scrollLeft);
|
|
|
|
if (this._options.useShadows) {
|
|
let enableTop = scrollHeight > 0 && scrollTop > 0;
|
|
let enableLeft = this._options.useShadows && scrollWidth > 0 && scrollLeft > 0;
|
|
|
|
if (this._topShadowDomNode) {
|
|
DomUtils.toggleClass(this._topShadowDomNode, 'top', enableTop);
|
|
}
|
|
|
|
if (this._topLeftShadowDomNode) {
|
|
DomUtils.toggleClass(this._topLeftShadowDomNode, 'top', enableTop);
|
|
}
|
|
|
|
if (this._leftShadowDomNode) {
|
|
DomUtils.toggleClass(this._leftShadowDomNode, 'left', enableLeft);
|
|
}
|
|
|
|
if (this._topLeftShadowDomNode) {
|
|
DomUtils.toggleClass(this._topLeftShadowDomNode, 'left', enableLeft);
|
|
}
|
|
}
|
|
|
|
this._reveal();
|
|
}
|
|
|
|
// -------------------- fade in / fade out --------------------
|
|
|
|
public onDragStart(): void {
|
|
this._isDragging = true;
|
|
this._reveal();
|
|
}
|
|
|
|
public onDragEnd(): void {
|
|
this._isDragging = false;
|
|
this._hide();
|
|
}
|
|
|
|
private _onMouseOut(e: IMouseEvent): void {
|
|
this._mouseIsOver = false;
|
|
this._hide();
|
|
}
|
|
|
|
private _onMouseOver(e: IMouseEvent): void {
|
|
this._mouseIsOver = true;
|
|
this._reveal();
|
|
}
|
|
|
|
private _reveal(): void {
|
|
this._verticalScrollbar.beginReveal();
|
|
this._horizontalScrollbar.beginReveal();
|
|
this._scheduleHide();
|
|
}
|
|
|
|
private _hide(): void {
|
|
if (!this._mouseIsOver && !this._isDragging) {
|
|
this._verticalScrollbar.beginHide();
|
|
this._horizontalScrollbar.beginHide();
|
|
}
|
|
}
|
|
|
|
private _scheduleHide(): void {
|
|
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
|
|
}
|
|
|
|
// -------------------- size & layout --------------------
|
|
|
|
private _computeDimensions(clientWidth: number, clientHeight: number): IDimensions {
|
|
let width = clientWidth;
|
|
let height = clientHeight;
|
|
|
|
return {
|
|
width: width,
|
|
height: height
|
|
};
|
|
}
|
|
|
|
private _createOptions(options: IScrollableElementCreationOptions): IScrollableElementOptions {
|
|
|
|
function ensureValue<V>(source: any, prop: string, value: V) {
|
|
if (source.hasOwnProperty(prop)) {
|
|
return <V>source[prop];
|
|
}
|
|
return value;
|
|
}
|
|
|
|
let result: IScrollableElementOptions = {
|
|
forbidTranslate3dUse: ensureValue(options, 'forbidTranslate3dUse', false),
|
|
className: ensureValue(options, 'className', ''),
|
|
useShadows: ensureValue(options, 'useShadows', true),
|
|
handleMouseWheel: ensureValue(options, 'handleMouseWheel', true),
|
|
flipAxes: ensureValue(options, 'flipAxes', false),
|
|
mouseWheelScrollSensitivity: ensureValue(options, 'mouseWheelScrollSensitivity', 1),
|
|
arrowSize: ensureValue(options, 'arrowSize', 11),
|
|
|
|
scrollable: ensureValue<IScrollable>(options, 'scrollable', null),
|
|
listenOnDomNode: ensureValue<HTMLElement>(options, 'listenOnDomNode', null),
|
|
|
|
horizontal: visibilityFromString(ensureValue(options, 'horizontal', 'auto')),
|
|
horizontalScrollbarSize: ensureValue(options, 'horizontalScrollbarSize', 10),
|
|
horizontalSliderSize: 0,
|
|
horizontalHasArrows: ensureValue(options, 'horizontalHasArrows', false),
|
|
|
|
vertical: visibilityFromString(ensureValue(options, 'vertical', 'auto')),
|
|
verticalScrollbarSize: ensureValue(options, 'verticalScrollbarSize', 10),
|
|
verticalHasArrows: ensureValue(options, 'verticalHasArrows', false),
|
|
verticalSliderSize: 0,
|
|
|
|
saveLastScrollTimeOnClassName: ensureValue(options, 'saveLastScrollTimeOnClassName', null)
|
|
};
|
|
|
|
result.horizontalSliderSize = ensureValue(options, 'horizontalSliderSize', result.horizontalScrollbarSize);
|
|
result.verticalSliderSize = ensureValue(options, 'verticalSliderSize', result.verticalScrollbarSize);
|
|
|
|
// Defaults are different on Macs
|
|
if (Platform.isMacintosh) {
|
|
result.className += ' mac';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|