[go: nahoru, domu]

blob: f9d8ca7339e5f4670c9d9d57675bdf834c7af985 [file] [log] [blame]
/*
* Copyright (C) 2012 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.
*/
import * as ARIAUtils from './ARIAUtils.js';
import {GlassPane, PointerEventsBehavior} from './GlassPane.js';
import {InspectorView} from './InspectorView.js';
import {KeyboardShortcut, Keys} from './KeyboardShortcut.js';
import type {SplitWidget} from './SplitWidget.js';
import type {DevToolsCloseButton} from './UIUtils.js';
import type {WidgetElement} from './Widget.js';
import {WidgetFocusRestorer} from './Widget.js';
export class Dialog extends GlassPane {
private tabIndexBehavior: OutsideTabIndexBehavior;
private tabIndexMap: Map<HTMLElement, number>;
private focusRestorer: WidgetFocusRestorer|null;
private closeOnEscape: boolean;
private targetDocument!: Document|null;
private readonly targetDocumentKeyDownHandler: (event: Event) => void;
private escapeKeyCallback: ((arg0: Event) => void)|null;
constructor() {
super();
this.registerRequiredCSS('ui/legacy/dialog.css');
this.contentElement.tabIndex = 0;
this.contentElement.addEventListener('focus', () => this.widget().focus(), false);
this.widget().setDefaultFocusedElement(this.contentElement);
this.setPointerEventsBehavior(PointerEventsBehavior.BlockedByGlassPane);
this.setOutsideClickCallback(event => {
this.hide();
event.consume(true);
});
ARIAUtils.markAsModalDialog(this.contentElement);
this.tabIndexBehavior = OutsideTabIndexBehavior.DisableAllOutsideTabIndex;
this.tabIndexMap = new Map();
this.focusRestorer = null;
this.closeOnEscape = true;
this.targetDocumentKeyDownHandler = this.onKeyDown.bind(this);
this.escapeKeyCallback = null;
}
static hasInstance(): boolean {
return Boolean(Dialog.instance);
}
show(where?: Document | Element): void {
const document = (where instanceof Document ? where : (where || InspectorView.instance().element).ownerDocument as Document);
this.targetDocument = document;
this.targetDocument.addEventListener('keydown', this.targetDocumentKeyDownHandler, true);
if (Dialog.instance) {
Dialog.instance.hide();
}
Dialog.instance = this;
this.disableTabIndexOnElements(document);
super.show(document);
this.focusRestorer = new WidgetFocusRestorer(this.widget());
}
hide(): void {
if (this.focusRestorer) {
this.focusRestorer.restore();
}
super.hide();
if (this.targetDocument) {
this.targetDocument.removeEventListener('keydown', this.targetDocumentKeyDownHandler, true);
}
this.restoreTabIndexOnElements();
this.dispatchEventToListeners('hidden');
Dialog.instance = null;
}
setCloseOnEscape(close: boolean): void {
this.closeOnEscape = close;
}
setEscapeKeyCallback(callback: (arg0: Event) => void): void {
this.escapeKeyCallback = callback;
}
addCloseButton(): void {
const closeButton =
(this.contentElement.createChild('div', 'dialog-close-button', 'dt-close-button') as DevToolsCloseButton);
closeButton.gray = true;
closeButton.addEventListener('click', () => this.hide(), false);
}
setOutsideTabIndexBehavior(tabIndexBehavior: OutsideTabIndexBehavior): void {
this.tabIndexBehavior = tabIndexBehavior;
}
private disableTabIndexOnElements(document: Document): void {
if (this.tabIndexBehavior === OutsideTabIndexBehavior.PreserveTabIndex) {
return;
}
let exclusionSet: Set<HTMLElement>|(Set<HTMLElement>| null) = (null as Set<HTMLElement>| null);
if (this.tabIndexBehavior === OutsideTabIndexBehavior.PreserveMainViewTabIndex) {
exclusionSet = this.getMainWidgetTabIndexElements(InspectorView.instance().ownerSplit());
}
this.tabIndexMap.clear();
let node: (Node|null)|Document = document;
for (; node; node = node.traverseNextNode(document)) {
if (node instanceof HTMLElement) {
const element = (node as HTMLElement);
const tabIndex = element.tabIndex;
if (tabIndex >= 0 && (!exclusionSet || !exclusionSet.has(element))) {
this.tabIndexMap.set(element, tabIndex);
element.tabIndex = -1;
}
}
}
}
private getMainWidgetTabIndexElements(splitWidget: SplitWidget|null): Set<HTMLElement> {
const elementSet = (new Set() as Set<HTMLElement>);
if (!splitWidget) {
return elementSet;
}
const mainWidget = splitWidget.mainWidget();
if (!mainWidget || !mainWidget.element) {
return elementSet;
}
let node: Node|null|WidgetElement = mainWidget.element;
for (; node; node = node.traverseNextNode(mainWidget.element)) {
if (!(node instanceof HTMLElement)) {
continue;
}
const element = (node as HTMLElement);
const tabIndex = element.tabIndex;
if (tabIndex < 0) {
continue;
}
elementSet.add(element);
}
return elementSet;
}
private restoreTabIndexOnElements(): void {
for (const element of this.tabIndexMap.keys()) {
element.tabIndex = (this.tabIndexMap.get(element) as number);
}
this.tabIndexMap.clear();
}
private onKeyDown(event: Event): void {
const keyboardEvent = (event as KeyboardEvent);
if (keyboardEvent.keyCode === Keys.Esc.code && KeyboardShortcut.hasNoModifiers(event)) {
if (this.escapeKeyCallback) {
this.escapeKeyCallback(event);
}
if (event.handled) {
return;
}
if (this.closeOnEscape) {
event.consume(true);
this.hide();
}
}
}
private static instance: Dialog|null = null;
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum OutsideTabIndexBehavior {
DisableAllOutsideTabIndex = 'DisableAllTabIndex',
PreserveMainViewTabIndex = 'PreserveMainViewTabIndex',
PreserveTabIndex = 'PreserveTabIndex',
}