chrome-devtools-frontend
Version:
Chrome DevTools UI
747 lines (643 loc) • 29.9 kB
text/typescript
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* eslint-disable rulesdir/no-imperative-dom-api */
import * as Common from '../../../../core/common/common.js';
import * as i18n from '../../../../core/i18n/i18n.js';
import * as Platform from '../../../../core/platform/platform.js';
import * as IconButton from '../../../components/icon_button/icon_button.js';
import * as VisualLogging from '../../../visual_logging/visual_logging.js';
import * as UI from '../../legacy.js';
import overviewGridStyles from './overviewGrid.css.js';
import {type Calculator, TimelineGrid} from './TimelineGrid.js';
const UIStrings = {
/**
*@description Label for the window for Overview grids
*/
overviewGridWindow: 'Overview grid window',
/**
*@description Label for left window resizer for Overview grids
*/
leftResizer: 'Left Resizer',
/**
*@description Label for right window resizer for Overview grids
*/
rightResizer: 'Right Resizer',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/perf_ui/OverviewGrid.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class OverviewGrid {
element: HTMLDivElement;
private readonly grid: TimelineGrid;
// The |window| will manage the html element of resizers, the left/right blue-colour curtain, and handle the resizing,
// zooming, and breadcrumb creation.
private readonly window: Window;
constructor(prefix: string, calculator?: Calculator) {
this.element = document.createElement('div');
this.element.id = prefix + '-overview-container';
this.grid = new TimelineGrid();
this.grid.element.id = prefix + '-overview-grid';
this.grid.setScrollTop(0);
this.element.appendChild(this.grid.element);
this.window = new Window(this.element, this.grid.dividersLabelBarElement, calculator);
}
enableCreateBreadcrumbsButton(): HTMLElement {
return this.window.enableCreateBreadcrumbsButton();
}
set showingScreenshots(isShowing: boolean) {
this.window.showingScreenshots = isShowing;
}
clientWidth(): number {
return this.element.clientWidth;
}
updateDividers(calculator: Calculator): void {
this.grid.updateDividers(calculator);
}
addEventDividers(dividers: Element[]): void {
this.grid.addEventDividers(dividers);
}
removeEventDividers(): void {
this.grid.removeEventDividers();
}
reset(): void {
this.window.reset();
}
// The ratio of the left slider position compare to the whole overview grid.
// It should be a number between 0 and 1.
windowLeftRatio(): number {
return this.window.windowLeftRatio || 0;
}
// The ratio of the right slider position compare to the whole overview grid.
// It should be a number between 0 and 1.
windowRightRatio(): number {
return this.window.windowRightRatio || 0;
}
/**
* This function will return the raw value of the slider window.
* Since the OverviewGrid is used in Performance panel or Memory panel, the raw value can be in milliseconds or bytes.
*
* @returns the pair of start/end value of the slider window in milliseconds or bytes
*/
calculateWindowValue(): {rawStartValue: number, rawEndValue: number} {
return this.window.calculateWindowValue();
}
setWindowRatio(leftRatio: number, rightRatio: number): void {
this.window.setWindowRatio(leftRatio, rightRatio);
}
addEventListener<T extends keyof EventTypes>(
eventType: T, listener: Common.EventTarget.EventListener<EventTypes, T>,
thisObject?: Object): Common.EventTarget.EventDescriptor {
return this.window.addEventListener(eventType, listener, thisObject);
}
setClickHandler(clickHandler: ((arg0: Event) => boolean)|null): void {
this.window.setClickHandler(clickHandler);
}
zoom(zoomFactor: number, referencePoint: number): void {
this.window.zoom(zoomFactor, referencePoint);
}
setResizeEnabled(enabled: boolean): void {
this.window.setResizeEnabled(enabled);
}
}
const MinSelectableSize = 14;
const WindowScrollSpeedFactor = .3;
const ResizerOffset = 5;
const OffsetFromWindowEnds = 10;
export class Window extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
private parentElement: Element;
private calculator: Calculator|undefined;
private leftResizeElement: HTMLElement;
private rightResizeElement: HTMLElement;
private leftCurtainElement: HTMLElement;
private rightCurtainElement: HTMLElement;
private breadcrumbButtonContainerElement: HTMLElement;
private createBreadcrumbButton: HTMLElement;
private curtainsRange?: HTMLElement;
private breadcrumbZoomIcon?: IconButton.Icon.Icon;
private overviewWindowSelector!: WindowSelector|undefined;
private offsetLeft!: number;
private dragStartPointPixel!: number;
private dragStartLeftRatio!: number;
private dragStartRightRatio!: number;
// The ratio of the left/right resizer position compare to the whole overview grid.
// They should be a number between 0 and 1.
windowLeftRatio = 0;
windowRightRatio = 1;
private resizeEnabled?: boolean;
private clickHandler?: ((arg0: Event) => boolean)|null;
private resizerParentOffsetLeft?: number;
#breadcrumbsEnabled = false;
#mouseOverGridOverview = false;
constructor(parentElement: HTMLElement, dividersLabelBarElement?: Element, calculator?: Calculator) {
super();
this.parentElement = parentElement;
this.parentElement.classList.add('parent-element');
UI.ARIAUtils.markAsGroup(this.parentElement);
this.calculator = calculator;
UI.ARIAUtils.setLabel(this.parentElement, i18nString(UIStrings.overviewGridWindow));
UI.UIUtils.installDragHandle(
this.parentElement, this.startWindowSelectorDragging.bind(this), this.windowSelectorDragging.bind(this),
this.endWindowSelectorDragging.bind(this), 'text', null);
if (dividersLabelBarElement) {
UI.UIUtils.installDragHandle(
dividersLabelBarElement, this.startWindowDragging.bind(this), this.windowDragging.bind(this), null,
'-webkit-grabbing', '-webkit-grab');
}
this.parentElement.addEventListener('wheel', this.onMouseWheel.bind(this), true);
this.parentElement.addEventListener('dblclick', this.resizeWindowMaximum.bind(this), true);
Platform.DOMUtilities.appendStyle(this.parentElement, overviewGridStyles);
this.leftResizeElement = parentElement.createChild('div', 'overview-grid-window-resizer');
UI.UIUtils.installDragHandle(
this.leftResizeElement, this.resizerElementStartDragging.bind(this), this.leftResizeElementDragging.bind(this),
null, 'ew-resize');
this.rightResizeElement = parentElement.createChild('div', 'overview-grid-window-resizer');
UI.UIUtils.installDragHandle(
this.rightResizeElement, this.resizerElementStartDragging.bind(this),
this.rightResizeElementDragging.bind(this), null, 'ew-resize');
UI.ARIAUtils.setLabel(this.leftResizeElement, i18nString(UIStrings.leftResizer));
UI.ARIAUtils.markAsSlider(this.leftResizeElement);
const leftKeyDown = (event: Event): void => this.handleKeyboardResizing(event, false);
this.leftResizeElement.addEventListener('keydown', leftKeyDown);
this.leftResizeElement.addEventListener('click', this.onResizerClicked);
UI.ARIAUtils.setLabel(this.rightResizeElement, i18nString(UIStrings.rightResizer));
UI.ARIAUtils.markAsSlider(this.rightResizeElement);
const rightKeyDown = (event: Event): void => this.handleKeyboardResizing(event, true);
this.rightResizeElement.addEventListener('keydown', rightKeyDown);
this.rightResizeElement.addEventListener('focus', this.onRightResizeElementFocused.bind(this));
this.rightResizeElement.addEventListener('click', this.onResizerClicked);
this.leftCurtainElement = parentElement.createChild('div', 'window-curtain-left');
this.rightCurtainElement = parentElement.createChild('div', 'window-curtain-right');
this.breadcrumbButtonContainerElement = parentElement.createChild('div', 'create-breadcrumb-button-container');
this.createBreadcrumbButton = this.breadcrumbButtonContainerElement.createChild('div', 'create-breadcrumb-button');
this.createBreadcrumbButton.setAttribute(
'jslog', `${VisualLogging.action('timeline.create-breadcrumb').track({click: true})}`);
this.reset();
}
enableCreateBreadcrumbsButton(): HTMLElement {
this.curtainsRange = this.createBreadcrumbButton.createChild('div');
this.breadcrumbZoomIcon = IconButton.Icon.create('zoom-in');
this.createBreadcrumbButton.appendChild(this.breadcrumbZoomIcon);
this.createBreadcrumbButton.addEventListener('click', () => {
this.#createBreadcrumb();
});
this.#breadcrumbsEnabled = true;
this.#changeBreadcrumbButtonVisibilityOnInteraction(this.parentElement);
this.#changeBreadcrumbButtonVisibilityOnInteraction(this.rightResizeElement);
this.#changeBreadcrumbButtonVisibilityOnInteraction(this.leftResizeElement);
return this.breadcrumbButtonContainerElement;
}
set showingScreenshots(isShowing: boolean) {
this.breadcrumbButtonContainerElement.classList.toggle('with-screenshots', isShowing);
}
#changeBreadcrumbButtonVisibilityOnInteraction(element: Element): void {
if (!this.#breadcrumbsEnabled) {
return;
}
element.addEventListener('mouseover', () => {
if (this.windowLeftRatio <= 0 && this.windowRightRatio >= 1) {
this.breadcrumbButtonContainerElement.classList.toggle('is-breadcrumb-button-visible', false);
this.#mouseOverGridOverview = false;
} else {
this.breadcrumbButtonContainerElement.classList.toggle('is-breadcrumb-button-visible', true);
this.#mouseOverGridOverview = true;
}
});
element.addEventListener('mouseout', () => {
this.breadcrumbButtonContainerElement.classList.toggle('is-breadcrumb-button-visible', false);
this.#mouseOverGridOverview = false;
});
}
private onResizerClicked(event: Event): void {
if (event.target) {
(event.target as HTMLElement).focus();
}
}
private onRightResizeElementFocused(): void {
// To prevent browser focus from scrolling the element into view and shifting the contents of the strip
this.parentElement.scrollLeft = 0;
}
reset(): void {
this.windowLeftRatio = 0;
this.windowRightRatio = 1;
this.setResizeEnabled(true);
this.updateCurtains();
}
setResizeEnabled(resizeEnabled: boolean): void {
this.resizeEnabled = resizeEnabled;
this.rightResizeElement.tabIndex = resizeEnabled ? 0 : -1;
this.leftResizeElement.tabIndex = resizeEnabled ? 0 : -1;
}
setClickHandler(clickHandler: ((arg0: Event) => boolean)|null): void {
this.clickHandler = clickHandler;
}
private resizerElementStartDragging(event: Event): boolean {
const mouseEvent = (event as MouseEvent);
const target = (event.target as HTMLElement);
if (!this.resizeEnabled) {
return false;
}
this.resizerParentOffsetLeft = mouseEvent.pageX - mouseEvent.offsetX - target.offsetLeft;
event.stopPropagation();
return true;
}
private leftResizeElementDragging(event: Event): void {
const mouseEvent = (event as MouseEvent);
this.resizeWindowLeft(mouseEvent.pageX - (this.resizerParentOffsetLeft || 0));
event.preventDefault();
}
private rightResizeElementDragging(event: Event): void {
const mouseEvent = (event as MouseEvent);
this.resizeWindowRight(mouseEvent.pageX - (this.resizerParentOffsetLeft || 0));
event.preventDefault();
}
private handleKeyboardResizing(event: Event, moveRightResizer?: boolean): void {
const keyboardEvent = (event as KeyboardEvent);
const target = (event.target as HTMLElement);
let increment = false;
if (keyboardEvent.key === 'ArrowLeft' || keyboardEvent.key === 'ArrowRight') {
if (keyboardEvent.key === 'ArrowRight') {
increment = true;
}
const newPos = this.getNewResizerPosition(target.offsetLeft, increment, keyboardEvent.ctrlKey);
if (moveRightResizer) {
this.resizeWindowRight(newPos);
} else {
this.resizeWindowLeft(newPos);
}
event.consume(true);
}
}
private getNewResizerPosition(offset: number, increment?: boolean, ctrlPressed?: boolean): number {
let newPos;
// We shift by 10px if the ctrlKey is pressed and 2 otherwise. 1px shifts result in noOp due to rounding in updateCurtains
let pixelsToShift: number|(2 | 10) = ctrlPressed ? 10 : 2;
pixelsToShift = increment ? pixelsToShift : -Math.abs(pixelsToShift);
const offsetLeft = offset + ResizerOffset;
newPos = offsetLeft + pixelsToShift;
if (increment && newPos < OffsetFromWindowEnds) {
// When incrementing, snap to the window offset value (10px) if the new position is between 0px and 10px
newPos = OffsetFromWindowEnds;
} else if (!increment && newPos > this.parentElement.clientWidth - OffsetFromWindowEnds) {
// When decrementing, snap to the window offset value (10px) from the rightmost side if the new position is within 10px from the end.
newPos = this.parentElement.clientWidth - OffsetFromWindowEnds;
}
return newPos;
}
private startWindowSelectorDragging(event: Event): boolean {
if (!this.resizeEnabled) {
return false;
}
const mouseEvent = (event as MouseEvent);
this.offsetLeft = this.parentElement.getBoundingClientRect().left;
const position = mouseEvent.x - this.offsetLeft;
this.overviewWindowSelector = new WindowSelector(this.parentElement, position);
return true;
}
private windowSelectorDragging(event: Event): void {
this.#mouseOverGridOverview = true;
if (!this.overviewWindowSelector) {
return;
}
const mouseEvent = (event as MouseEvent);
this.overviewWindowSelector.updatePosition(mouseEvent.x - this.offsetLeft);
event.preventDefault();
}
private endWindowSelectorDragging(event: Event): void {
if (!this.overviewWindowSelector) {
return;
}
const mouseEvent = (event as MouseEvent);
const window = this.overviewWindowSelector.close(mouseEvent.x - this.offsetLeft);
// prevent selecting a window on clicking the minimap if breadcrumbs are enabled
if (this.#breadcrumbsEnabled && window.start === window.end) {
return;
}
delete this.overviewWindowSelector;
const clickThreshold = 3;
if (window.end - window.start < clickThreshold) {
if (this.clickHandler?.call(null, event)) {
return;
}
const middle = window.end;
window.start = Math.max(0, middle - MinSelectableSize / 2);
window.end = Math.min(this.parentElement.clientWidth, middle + MinSelectableSize / 2);
} else if (window.end - window.start < MinSelectableSize) {
if (this.parentElement.clientWidth - window.end > MinSelectableSize) {
window.end = window.start + MinSelectableSize;
} else {
window.start = window.end - MinSelectableSize;
}
}
this.setWindowPosition(window.start, window.end);
}
private startWindowDragging(event: Event): boolean {
const mouseEvent = (event as MouseEvent);
this.dragStartPointPixel = mouseEvent.pageX;
this.dragStartLeftRatio = this.windowLeftRatio;
this.dragStartRightRatio = this.windowRightRatio;
event.stopPropagation();
return true;
}
private windowDragging(event: Event): void {
this.#mouseOverGridOverview = true;
if (this.#breadcrumbsEnabled) {
this.breadcrumbButtonContainerElement.classList.toggle('is-breadcrumb-button-visible', true);
}
const mouseEvent = (event as MouseEvent);
mouseEvent.preventDefault();
let delta: number = (mouseEvent.pageX - this.dragStartPointPixel) / this.parentElement.clientWidth;
if (this.dragStartLeftRatio + delta < 0) {
delta = -this.dragStartLeftRatio;
}
if (this.dragStartRightRatio + delta > 1) {
delta = 1 - this.dragStartRightRatio;
}
this.setWindowRatio(this.dragStartLeftRatio + delta, this.dragStartRightRatio + delta);
}
private resizeWindowLeft(start: number): void {
this.#mouseOverGridOverview = true;
// Glue to edge.
if (start < OffsetFromWindowEnds) {
start = 0;
} else if (start > this.rightResizeElement.offsetLeft - 4) {
start = this.rightResizeElement.offsetLeft - 4;
}
this.setWindowPosition(start, null);
}
private resizeWindowRight(end: number): void {
this.#mouseOverGridOverview = true;
// Glue to edge.
if (end > this.parentElement.clientWidth - OffsetFromWindowEnds) {
end = this.parentElement.clientWidth;
} else if (end < this.leftResizeElement.offsetLeft + MinSelectableSize) {
end = this.leftResizeElement.offsetLeft + MinSelectableSize;
}
this.setWindowPosition(null, end);
}
private resizeWindowMaximum(): void {
this.setWindowPosition(0, this.parentElement.clientWidth);
}
/**
* This function will return the raw value of the give slider.
* Since the OverviewGrid is used in Performance panel or Memory panel, the raw value can be in milliseconds or bytes.
* @param leftSlider if this slider is the left one
* @returns the value in milliseconds or bytes
*/
private getRawSliderValue(leftSlider?: boolean): number {
if (!this.calculator) {
throw new Error('No calculator to calculate boundaries');
}
const minimumValue = this.calculator.minimumBoundary();
const valueSpan = this.calculator.maximumBoundary() - minimumValue;
if (leftSlider) {
return minimumValue + valueSpan * this.windowLeftRatio;
}
return minimumValue + valueSpan * this.windowRightRatio;
}
private updateResizeElementAriaValue(leftPercentValue: number, rightPercentValue: number): void {
const roundedLeftValue = leftPercentValue.toFixed(2);
const roundedRightValue = rightPercentValue.toFixed(2);
UI.ARIAUtils.setAriaValueNow(this.leftResizeElement, roundedLeftValue);
UI.ARIAUtils.setAriaValueNow(this.rightResizeElement, roundedRightValue);
// Left and right sliders cannot be within 0.5% of each other (Range of AriaValueMin/Max/Now is from 0-100).
const leftResizeCeiling = Number(roundedRightValue) - 0.5;
const rightResizeFloor = Number(roundedLeftValue) + 0.5;
UI.ARIAUtils.setAriaValueMinMax(this.leftResizeElement, '0', leftResizeCeiling.toString());
UI.ARIAUtils.setAriaValueMinMax(this.rightResizeElement, rightResizeFloor.toString(), '100');
}
private updateResizeElementPositionLabels(): void {
if (!this.calculator) {
return;
}
const startValue = this.calculator.formatValue(this.getRawSliderValue(/* leftSlider */ true));
const endValue = this.calculator.formatValue(this.getRawSliderValue(/* leftSlider */ false));
UI.ARIAUtils.setAriaValueText(this.leftResizeElement, String(startValue));
UI.ARIAUtils.setAriaValueText(this.rightResizeElement, String(endValue));
}
private updateResizeElementPercentageLabels(leftValue: string, rightValue: string): void {
UI.ARIAUtils.setAriaValueText(this.leftResizeElement, leftValue);
UI.ARIAUtils.setAriaValueText(this.rightResizeElement, rightValue);
}
/**
* This function will return the raw value of the slider window.
* Since the OverviewGrid is used in Performance panel or Memory panel, the raw value can be in milliseconds or bytes.
*
* @returns the pair of start/end value of the slider window in milliseconds or bytes
*/
calculateWindowValue(): {
rawStartValue: number,
rawEndValue: number,
} {
return {
rawStartValue: this.getRawSliderValue(/* leftSlider */ true),
rawEndValue: this.getRawSliderValue(/* leftSlider */ false),
};
}
setWindowRatio(windowLeftRatio: number, windowRightRatio: number): void {
this.windowLeftRatio = windowLeftRatio;
this.windowRightRatio = windowRightRatio;
this.updateCurtains();
if (this.calculator) {
this.dispatchEventToListeners(Events.WINDOW_CHANGED_WITH_POSITION, this.calculateWindowValue());
}
this.dispatchEventToListeners(Events.WINDOW_CHANGED);
this.#changeBreadcrumbButtonVisibility(windowLeftRatio, windowRightRatio);
}
// "Create breadcrumb" button is only visible when the window is set to
// something other than the full range and mouse is hovering over the MiniMap
#changeBreadcrumbButtonVisibility(windowLeftRatio: number, windowRightRatio: number): void {
if (!this.#breadcrumbsEnabled) {
return;
}
if ((windowRightRatio >= 1 && windowLeftRatio <= 0) || !this.#mouseOverGridOverview) {
this.breadcrumbButtonContainerElement.classList.toggle('is-breadcrumb-button-visible', false);
} else {
this.breadcrumbButtonContainerElement.classList.toggle('is-breadcrumb-button-visible', true);
}
}
#createBreadcrumb(): void {
this.dispatchEventToListeners(Events.BREADCRUMB_ADDED, this.calculateWindowValue());
}
private updateCurtains(): void {
const windowLeftRatio = this.windowLeftRatio;
const windowRightRatio = this.windowRightRatio;
let leftRatio = windowLeftRatio;
let rightRatio = windowRightRatio;
const widthRatio = rightRatio - leftRatio;
// OverviewGrids that are instantiated before the parentElement is shown will have a parent element client width of 0 which throws off the 'factor' calculation
if (this.parentElement.clientWidth !== 0) {
// We allow actual time window to be arbitrarily small but don't want the UI window to be too small.
const widthInPixels = widthRatio * this.parentElement.clientWidth;
const minWidthInPixels = MinSelectableSize / 2;
if (widthInPixels < minWidthInPixels) {
const factor = minWidthInPixels / widthInPixels;
leftRatio = ((windowRightRatio + windowLeftRatio) - widthRatio * factor) / 2;
rightRatio = ((windowRightRatio + windowLeftRatio) + widthRatio * factor) / 2;
}
}
const leftResizerPercLeftOffset = (100 * leftRatio);
const rightResizerPercLeftOffset = (100 * rightRatio);
const rightResizerPercRightOffset = (100 - (100 * rightRatio));
const leftResizerPercLeftOffsetString = leftResizerPercLeftOffset + '%';
const rightResizerPercLeftOffsetString = rightResizerPercLeftOffset + '%';
this.leftResizeElement.style.left = leftResizerPercLeftOffsetString;
this.rightResizeElement.style.left = rightResizerPercLeftOffsetString;
this.leftCurtainElement.style.width = leftResizerPercLeftOffsetString;
this.rightCurtainElement.style.width = rightResizerPercRightOffset + '%';
this.breadcrumbButtonContainerElement.style.marginLeft =
(leftResizerPercLeftOffset > 0) ? leftResizerPercLeftOffset + '%' : '0%';
this.breadcrumbButtonContainerElement.style.marginRight =
(rightResizerPercRightOffset > 0) ? rightResizerPercRightOffset + '%' : '0%';
if (this.curtainsRange) {
this.curtainsRange.textContent = this.getWindowRange().toFixed(0) + ' ms';
}
this.updateResizeElementAriaValue(leftResizerPercLeftOffset, rightResizerPercLeftOffset);
if (this.calculator) {
this.updateResizeElementPositionLabels();
} else {
this.updateResizeElementPercentageLabels(leftResizerPercLeftOffsetString, rightResizerPercLeftOffsetString);
}
this.toggleBreadcrumbZoomButtonDisplay();
}
private toggleBreadcrumbZoomButtonDisplay(): void {
if (this.breadcrumbZoomIcon) {
// disable button that creates breadcrumbs and hide the zoom icon
// when the selected window is smaller than 4.5 ms
// 4.5 is rounded to 5 in the UI
if (this.getWindowRange() < 4.5) {
this.breadcrumbZoomIcon.style.display = 'none';
this.breadcrumbButtonContainerElement.style.pointerEvents = 'none';
} else {
this.breadcrumbZoomIcon.style.display = 'flex';
this.breadcrumbButtonContainerElement.style.pointerEvents = 'auto';
}
}
}
private getWindowRange(): number {
if (!this.calculator) {
throw new Error('No calculator to calculate window range');
}
const leftRatio = this.windowLeftRatio > 0 ? this.windowLeftRatio : 0;
const rightRatio = this.windowRightRatio < 1 ? this.windowRightRatio : 1;
return (this.calculator.boundarySpan() * (rightRatio - leftRatio));
}
private setWindowPosition(startPixel: number|null, endPixel: number|null): void {
const clientWidth = this.parentElement.clientWidth;
const windowLeft = typeof startPixel === 'number' ? startPixel / clientWidth : this.windowLeftRatio;
const windowRight = typeof endPixel === 'number' ? endPixel / clientWidth : this.windowRightRatio;
this.setWindowRatio(windowLeft || 0, windowRight || 0);
}
private onMouseWheel(event: Event): void {
const wheelEvent = (event as WheelEvent);
if (!this.resizeEnabled) {
return;
}
if (wheelEvent.deltaY) {
const zoomFactor = 1.1;
const wheelZoomSpeed = 1 / 53;
const reference = wheelEvent.offsetX / this.parentElement.clientWidth;
this.zoom(Math.pow(zoomFactor, wheelEvent.deltaY * wheelZoomSpeed), reference);
}
if (wheelEvent.deltaX) {
let offset = Math.round(wheelEvent.deltaX * WindowScrollSpeedFactor);
const windowLeftPixel = this.leftResizeElement.offsetLeft + ResizerOffset;
const windowRightPixel = this.rightResizeElement.offsetLeft + ResizerOffset;
if (windowLeftPixel - offset < 0) {
offset = windowLeftPixel;
}
if (windowRightPixel - offset > this.parentElement.clientWidth) {
offset = windowRightPixel - this.parentElement.clientWidth;
}
this.setWindowPosition(windowLeftPixel - offset, windowRightPixel - offset);
wheelEvent.preventDefault();
}
}
zoom(factor: number, reference: number): void {
let leftRatio: number = this.windowLeftRatio || 0;
let rightRatio: number = this.windowRightRatio || 0;
const windowSizeRatio = rightRatio - leftRatio;
let newWindowSizeRatio: 1|number = factor * windowSizeRatio;
if (newWindowSizeRatio > 1) {
newWindowSizeRatio = 1;
factor = newWindowSizeRatio / windowSizeRatio;
}
leftRatio = reference + (leftRatio - reference) * factor;
leftRatio = Platform.NumberUtilities.clamp(leftRatio, 0, 1 - newWindowSizeRatio);
rightRatio = reference + (rightRatio - reference) * factor;
rightRatio = Platform.NumberUtilities.clamp(rightRatio, newWindowSizeRatio, 1);
this.setWindowRatio(leftRatio, rightRatio);
}
}
export const enum Events {
WINDOW_CHANGED = 'WindowChanged',
WINDOW_CHANGED_WITH_POSITION = 'WindowChangedWithPosition',
BREADCRUMB_ADDED = 'BreadcrumbAdded',
}
export interface WindowChangedWithPositionEvent {
rawStartValue: number;
rawEndValue: number;
}
export interface EventTypes {
[Events.WINDOW_CHANGED]: void;
[Events.BREADCRUMB_ADDED]: WindowChangedWithPositionEvent;
[Events.WINDOW_CHANGED_WITH_POSITION]: WindowChangedWithPositionEvent;
}
export class WindowSelector {
private startPosition: number;
private width: number;
private windowSelector: HTMLDivElement;
constructor(parent: Element, position: number) {
this.startPosition = position;
this.width = (parent as HTMLElement).offsetWidth;
this.windowSelector = document.createElement('div');
this.windowSelector.className = 'overview-grid-window-selector';
this.windowSelector.style.left = this.startPosition + 'px';
this.windowSelector.style.right = this.width - this.startPosition + 'px';
parent.appendChild(this.windowSelector);
}
close(position: number): {
start: number,
end: number,
} {
position = Math.max(0, Math.min(position, this.width));
this.windowSelector.remove();
return this.startPosition < position ? {start: this.startPosition, end: position} :
{start: position, end: this.startPosition};
}
updatePosition(position: number): void {
position = Math.max(0, Math.min(position, this.width));
if (position < this.startPosition) {
this.windowSelector.style.left = position + 'px';
this.windowSelector.style.right = this.width - this.startPosition + 'px';
} else {
this.windowSelector.style.left = this.startPosition + 'px';
this.windowSelector.style.right = this.width - position + 'px';
}
}
}