chrome-devtools-frontend
Version:
Chrome DevTools UI
341 lines (292 loc) • 12.1 kB
text/typescript
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
import type {Size} from './Geometry.js';
import glassPaneStyles from './glassPane.css.js';
import {deepElementFromEvent, measuredScrollbarWidth} from './UIUtils.js';
import {Widget} from './Widget.js';
export class GlassPane {
private readonly widgetInternal = new Widget(true);
element: typeof Widget.prototype.element;
contentElement: typeof Widget.prototype.contentElement;
private readonly onMouseDownBound: (event: Event) => void;
private onClickOutsideCallback: ((arg0: Event) => void)|null = null;
private maxSize: Size|null = null;
private positionX: number|null = null;
private positionY: number|null = null;
private anchorBox: AnchorBox|null = null;
private anchorBehavior = AnchorBehavior.PREFER_TOP;
private sizeBehavior = SizeBehavior.SET_EXACT_SIZE;
private marginBehavior = MarginBehavior.DEFAULT_MARGIN;
#ignoreLeftMargin = false;
constructor(jslog?: string) {
this.widgetInternal.markAsRoot();
this.element = this.widgetInternal.element;
this.contentElement = this.widgetInternal.contentElement;
if (jslog) {
this.contentElement.setAttribute('jslog', jslog);
}
this.registerRequiredCSS(glassPaneStyles);
this.setPointerEventsBehavior(PointerEventsBehavior.PIERCE_GLASS_PANE);
this.onMouseDownBound = this.onMouseDown.bind(this);
}
setJsLog(jslog: string): void {
this.contentElement.setAttribute('jslog', jslog);
}
isShowing(): boolean {
return this.widgetInternal.isShowing();
}
registerRequiredCSS(...cssFiles: Array<string&{_tag: 'CSS-in-JS'}>): void {
this.widgetInternal.registerRequiredCSS(...cssFiles);
}
setDefaultFocusedElement(element: Element|null): void {
this.widgetInternal.setDefaultFocusedElement(element);
}
setDimmed(dimmed: boolean): void {
this.element.classList.toggle('dimmed-pane', dimmed);
}
setPointerEventsBehavior(pointerEventsBehavior: PointerEventsBehavior): void {
this.element.classList.toggle(
'no-pointer-events', pointerEventsBehavior !== PointerEventsBehavior.BLOCKED_BY_GLASS_PANE);
this.contentElement.classList.toggle(
'no-pointer-events', pointerEventsBehavior === PointerEventsBehavior.PIERCE_CONTENTS);
}
setOutsideClickCallback(callback: ((arg0: Event) => void)|null): void {
this.onClickOutsideCallback = callback;
}
setMaxContentSize(size: Size|null): void {
this.maxSize = size;
this.positionContent();
}
setSizeBehavior(sizeBehavior: SizeBehavior): void {
this.sizeBehavior = sizeBehavior;
this.positionContent();
}
setContentPosition(x: number|null, y: number|null): void {
this.positionX = x;
this.positionY = y;
this.positionContent();
}
setContentAnchorBox(anchorBox: AnchorBox|null): void {
this.anchorBox = anchorBox;
this.positionContent();
}
setAnchorBehavior(behavior: AnchorBehavior): void {
this.anchorBehavior = behavior;
}
setMarginBehavior(behavior: MarginBehavior): void {
this.marginBehavior = behavior;
}
setIgnoreLeftMargin(ignore: boolean): void {
this.#ignoreLeftMargin = ignore;
}
show(document: Document): void {
if (this.isShowing()) {
return;
}
// TODO(crbug.com/1006759): Extract the magic number
// Deliberately starts with 3000 to hide other z-indexed elements below.
this.element.style.zIndex = `${3000 + 1000 * panes.size}`;
this.element.setAttribute('data-devtools-glass-pane', '');
document.body.addEventListener('mousedown', this.onMouseDownBound, true);
document.body.addEventListener('pointerdown', this.onMouseDownBound, true);
this.widgetInternal.show(document.body);
panes.add(this);
this.positionContent();
}
hide(): void {
if (!this.isShowing()) {
return;
}
panes.delete(this);
this.element.ownerDocument.body.removeEventListener('mousedown', this.onMouseDownBound, true);
this.element.ownerDocument.body.removeEventListener('pointerdown', this.onMouseDownBound, true);
this.widgetInternal.detach();
}
private onMouseDown(event: Event): void {
if (!this.onClickOutsideCallback) {
return;
}
const node = deepElementFromEvent(event);
if (!node || this.contentElement.isSelfOrAncestor(node)) {
return;
}
this.onClickOutsideCallback.call(null, event);
}
positionContent(): void {
if (!this.isShowing()) {
return;
}
const gutterSize = this.marginBehavior === MarginBehavior.NO_MARGIN ? 0 : 3;
const scrollbarSize = measuredScrollbarWidth(this.element.ownerDocument);
const offsetSize = 10;
const container = (containers.get((this.element.ownerDocument))) as HTMLElement;
if (this.sizeBehavior === SizeBehavior.MEASURE_CONTENT) {
this.contentElement.positionAt(0, 0);
this.contentElement.style.width = '';
this.contentElement.style.maxWidth = '';
this.contentElement.style.height = '';
this.contentElement.style.maxHeight = '';
}
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
let width = containerWidth - gutterSize * 2;
let height = containerHeight - gutterSize * 2;
let positionX = gutterSize;
let positionY = gutterSize;
if (this.maxSize) {
width = Math.min(width, this.maxSize.width);
height = Math.min(height, this.maxSize.height);
}
if (this.sizeBehavior === SizeBehavior.MEASURE_CONTENT) {
const measuredRect = this.contentElement.getBoundingClientRect();
const widthOverflow = height < measuredRect.height ? scrollbarSize : 0;
const heightOverflow = width < measuredRect.width ? scrollbarSize : 0;
width = Math.min(width, measuredRect.width + widthOverflow);
height = Math.min(height, measuredRect.height + heightOverflow);
}
if (this.anchorBox) {
const anchorBox = this.anchorBox.relativeToElement(container);
let behavior: AnchorBehavior.PREFER_BOTTOM|AnchorBehavior.PREFER_TOP|AnchorBehavior.PREFER_RIGHT|
AnchorBehavior.PREFER_LEFT|AnchorBehavior = this.anchorBehavior;
if (behavior === AnchorBehavior.PREFER_TOP || behavior === AnchorBehavior.PREFER_BOTTOM) {
const top = anchorBox.y - 2 * gutterSize;
const bottom = containerHeight - anchorBox.y - anchorBox.height - 2 * gutterSize;
if (behavior === AnchorBehavior.PREFER_TOP && top < height && bottom > top) {
behavior = AnchorBehavior.PREFER_BOTTOM;
}
if (behavior === AnchorBehavior.PREFER_BOTTOM && bottom < height && top > bottom) {
behavior = AnchorBehavior.PREFER_TOP;
}
let enoughHeight = true;
if (behavior === AnchorBehavior.PREFER_TOP) {
positionY = Math.max(gutterSize, anchorBox.y - height - gutterSize);
const spaceTop = anchorBox.y - positionY - gutterSize;
if (this.sizeBehavior === SizeBehavior.MEASURE_CONTENT) {
if (height > spaceTop) {
enoughHeight = false;
}
} else {
height = Math.min(height, spaceTop);
}
} else {
positionY = anchorBox.y + anchorBox.height + gutterSize;
const spaceBottom = containerHeight - positionY - gutterSize;
if (this.sizeBehavior === SizeBehavior.MEASURE_CONTENT) {
if (height > spaceBottom) {
positionY = containerHeight - gutterSize - height;
enoughHeight = false;
}
} else {
height = Math.min(height, spaceBottom);
}
}
const naturalPositionX = Math.min(anchorBox.x, containerWidth - width - gutterSize);
positionX = Math.max(gutterSize, naturalPositionX);
if (this.#ignoreLeftMargin && gutterSize > naturalPositionX) {
positionX = 0;
}
if (!enoughHeight) {
positionX = Math.min(positionX + offsetSize, containerWidth - width - gutterSize);
} else if (positionX - offsetSize >= gutterSize) {
positionX -= offsetSize;
}
width = Math.min(width, containerWidth - positionX - gutterSize);
} else {
const left = anchorBox.x - 2 * gutterSize;
const right = containerWidth - anchorBox.x - anchorBox.width - 2 * gutterSize;
if (behavior === AnchorBehavior.PREFER_LEFT && left < width && right > left) {
behavior = AnchorBehavior.PREFER_RIGHT;
}
if (behavior === AnchorBehavior.PREFER_RIGHT && right < width && left > right) {
behavior = AnchorBehavior.PREFER_LEFT;
}
let enoughWidth = true;
if (behavior === AnchorBehavior.PREFER_LEFT) {
positionX = Math.max(gutterSize, anchorBox.x - width - gutterSize);
const spaceLeft = anchorBox.x - positionX - gutterSize;
if (this.sizeBehavior === SizeBehavior.MEASURE_CONTENT) {
if (width > spaceLeft) {
enoughWidth = false;
}
} else {
width = Math.min(width, spaceLeft);
}
} else {
positionX = anchorBox.x + anchorBox.width + gutterSize;
const spaceRight = containerWidth - positionX - gutterSize;
if (this.sizeBehavior === SizeBehavior.MEASURE_CONTENT) {
if (width > spaceRight) {
positionX = containerWidth - gutterSize - width;
enoughWidth = false;
}
} else {
width = Math.min(width, spaceRight);
}
}
positionY = Math.max(gutterSize, Math.min(anchorBox.y, containerHeight - height - gutterSize));
if (!enoughWidth) {
positionY = Math.min(positionY + offsetSize, containerHeight - height - gutterSize);
} else if (positionY - offsetSize >= gutterSize) {
positionY -= offsetSize;
}
height = Math.min(height, containerHeight - positionY - gutterSize);
}
} else {
positionX = this.positionX !== null ? this.positionX : (containerWidth - width) / 2;
positionY = this.positionY !== null ? this.positionY : (containerHeight - height) / 2;
width = Math.min(width, containerWidth - positionX - gutterSize);
height = Math.min(height, containerHeight - positionY - gutterSize);
}
this.contentElement.style.width = width + 'px';
if (this.sizeBehavior === SizeBehavior.SET_EXACT_WIDTH_MAX_HEIGHT) {
this.contentElement.style.maxHeight = height + 'px';
} else {
this.contentElement.style.height = height + 'px';
}
this.contentElement.positionAt(positionX, positionY, container);
this.widgetInternal.doResize();
}
widget(): Widget {
return this.widgetInternal;
}
static setContainer(element: Element): void {
containers.set((element.ownerDocument), element);
GlassPane.containerMoved(element);
}
static container(document: Document): Element {
return containers.get(document) as Element;
}
static containerMoved(element: Element): void {
for (const pane of panes) {
if (pane.isShowing() && pane.element.ownerDocument === element.ownerDocument) {
pane.positionContent();
}
}
}
}
export const enum PointerEventsBehavior {
BLOCKED_BY_GLASS_PANE = 'BlockedByGlassPane',
PIERCE_GLASS_PANE = 'PierceGlassPane',
PIERCE_CONTENTS = 'PierceContents',
}
export const enum AnchorBehavior {
PREFER_TOP = 'PreferTop',
PREFER_BOTTOM = 'PreferBottom',
PREFER_LEFT = 'PreferLeft',
PREFER_RIGHT = 'PreferRight',
}
export const enum SizeBehavior {
SET_EXACT_SIZE = 'SetExactSize',
SET_EXACT_WIDTH_MAX_HEIGHT = 'SetExactWidthMaxHeight',
MEASURE_CONTENT = 'MeasureContent',
}
export const enum MarginBehavior {
DEFAULT_MARGIN = 'DefaultMargin',
NO_MARGIN = 'NoMargin',
}
const containers = new Map<Document, Element>();
const panes = new Set<GlassPane>();
// Exported for layout tests.
export const GlassPanePanes = panes;