/*
* Copyright 2016 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import classNames from "classnames";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { Classes } from "../../common";
import { Dialog, IDialogProps } from "../../components";
import { Hotkey, IHotkeyProps } from "./hotkey";
import { Hotkeys } from "./hotkeys";
export interface IHotkeysDialogProps extends IDialogProps {
/**
* This string displayed as the group name in the hotkeys dialog for all
* global hotkeys.
*/
globalHotkeysGroup?: string;
}
/**
* The delay before showing or hiding the dialog. Should be long enough to
* allow all registered hotkey listeners to execute first.
*/
const DELAY_IN_MS = 10;
class HotkeysDialog {
public componentProps = ({
globalHotkeysGroup: "Global hotkeys",
} as any) as IHotkeysDialogProps;
private container: HTMLElement;
private hotkeysQueue = [] as IHotkeyProps[][];
private isDialogShowing = false;
private showTimeoutToken: number;
private hideTimeoutToken: number;
public render() {
if (this.container == null) {
this.container = this.getContainer();
}
ReactDOM.render(this.renderComponent(), this.container);
}
public unmount() {
if (this.container != null) {
ReactDOM.unmountComponentAtNode(this.container);
this.container.remove();
delete this.container;
}
}
/**
* Because hotkeys can be registered globally and locally and because
* event ordering cannot be guaranteed, we use this debouncing method to
* allow all hotkey listeners to fire and add their hotkeys to the dialog.
*
* 10msec after the last listener adds their hotkeys, we render the dialog
* and clear the queue.
*/
public enqueueHotkeysForDisplay(hotkeys: IHotkeyProps[]) {
this.hotkeysQueue.push(hotkeys);
// reset timeout for debounce
window.clearTimeout(this.showTimeoutToken);
this.showTimeoutToken = window.setTimeout(this.show, DELAY_IN_MS);
}
public hideAfterDelay() {
window.clearTimeout(this.hideTimeoutToken);
this.hideTimeoutToken = window.setTimeout(this.hide, DELAY_IN_MS);
}
public show = () => {
this.isDialogShowing = true;
this.render();
};
public hide = () => {
this.isDialogShowing = false;
this.render();
};
public isShowing() {
return this.isDialogShowing;
}
private getContainer() {
if (this.container == null) {
this.container = document.createElement("div");
this.container.classList.add(Classes.PORTAL);
document.body.appendChild(this.container);
}
return this.container;
}
private renderComponent() {
return (
);
}
private renderHotkeys() {
const hotkeys = this.emptyHotkeyQueue();
const elements = hotkeys.map((hotkey, index) => {
const group =
hotkey.global === true && hotkey.group == null ? this.componentProps.globalHotkeysGroup : hotkey.group;
return ;
});
return {elements};
}
private emptyHotkeyQueue() {
// flatten then empty the hotkeys queue
const hotkeys = this.hotkeysQueue.reduce((arr, queued) => arr.concat(queued), []);
this.hotkeysQueue.length = 0;
return hotkeys;
}
}
// singleton instance
const HOTKEYS_DIALOG = new HotkeysDialog();
export function isHotkeysDialogShowing() {
return HOTKEYS_DIALOG.isShowing();
}
export function setHotkeysDialogProps(props: Partial) {
for (const key in props) {
if (props.hasOwnProperty(key)) {
(HOTKEYS_DIALOG.componentProps as any)[key] = (props as any)[key];
}
}
}
export function showHotkeysDialog(hotkeys: IHotkeyProps[]) {
HOTKEYS_DIALOG.enqueueHotkeysForDisplay(hotkeys);
}
export function hideHotkeysDialog() {
HOTKEYS_DIALOG.hide();
}
/**
* Use this function instead of `hideHotkeysDialog` if you need to ensure that all hotkey listeners
* have time to execute with the dialog in a consistent open state. This can avoid flickering the
* dialog between open and closed states as successive listeners fire.
*/
export function hideHotkeysDialogAfterDelay() {
HOTKEYS_DIALOG.hideAfterDelay();
}