UNPKG

5.45 kBTypeScriptView Raw
1/*
2 * Copyright 2016 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import classNames from "classnames";
18import * as React from "react";
19import * as ReactDOM from "react-dom";
20
21import { Classes } from "../../common";
22import { Dialog, DialogProps } from "../../components";
23import { Hotkey, IHotkeyProps } from "./hotkey";
24import { Hotkeys } from "./hotkeys";
25
26export interface IHotkeysDialogProps extends DialogProps {
27 /**
28 * This string displayed as the group name in the hotkeys dialog for all
29 * global hotkeys.
30 */
31 globalHotkeysGroup?: string;
32}
33
34/**
35 * The delay before showing or hiding the dialog. Should be long enough to
36 * allow all registered hotkey listeners to execute first.
37 */
38const DELAY_IN_MS = 10;
39
40class HotkeysDialog {
41 public componentProps = ({
42 globalHotkeysGroup: "Global hotkeys",
43 } as any) as IHotkeysDialogProps;
44
45 private container: HTMLElement | null = null;
46
47 private hotkeysQueue = [] as IHotkeyProps[][];
48
49 private isDialogShowing = false;
50
51 private showTimeoutToken?: number;
52
53 private hideTimeoutToken?: number;
54
55 public render() {
56 if (this.container == null) {
57 this.container = this.getContainer();
58 }
59 ReactDOM.render(this.renderComponent(), this.container);
60 }
61
62 public unmount() {
63 if (this.container != null) {
64 ReactDOM.unmountComponentAtNode(this.container);
65 this.container.remove();
66 this.container = null;
67 }
68 }
69
70 /**
71 * Because hotkeys can be registered globally and locally and because
72 * event ordering cannot be guaranteed, we use this debouncing method to
73 * allow all hotkey listeners to fire and add their hotkeys to the dialog.
74 *
75 * 10msec after the last listener adds their hotkeys, we render the dialog
76 * and clear the queue.
77 */
78 public enqueueHotkeysForDisplay(hotkeys: IHotkeyProps[]) {
79 this.hotkeysQueue.push(hotkeys);
80
81 // reset timeout for debounce
82 window.clearTimeout(this.showTimeoutToken);
83 this.showTimeoutToken = window.setTimeout(this.show, DELAY_IN_MS);
84 }
85
86 public hideAfterDelay() {
87 window.clearTimeout(this.hideTimeoutToken);
88 this.hideTimeoutToken = window.setTimeout(this.hide, DELAY_IN_MS);
89 }
90
91 public show = () => {
92 this.isDialogShowing = true;
93 this.render();
94 };
95
96 public hide = () => {
97 this.isDialogShowing = false;
98 this.render();
99 };
100
101 public isShowing() {
102 return this.isDialogShowing;
103 }
104
105 private getContainer() {
106 if (this.container == null) {
107 this.container = document.createElement("div");
108 this.container.classList.add(Classes.PORTAL);
109 document.body.appendChild(this.container);
110 }
111 return this.container;
112 }
113
114 private renderComponent() {
115 return (
116 <Dialog
117 {...this.componentProps}
118 className={classNames(Classes.HOTKEY_DIALOG, this.componentProps.className)}
119 isOpen={this.isDialogShowing}
120 onClose={this.hide}
121 >
122 <div className={Classes.DIALOG_BODY}>{this.renderHotkeys()}</div>
123 </Dialog>
124 );
125 }
126
127 private renderHotkeys() {
128 const hotkeys = this.emptyHotkeyQueue();
129 const elements = hotkeys.map((hotkey, index) => {
130 const group =
131 hotkey.global === true && hotkey.group == null ? this.componentProps.globalHotkeysGroup : hotkey.group;
132
133 return <Hotkey key={index} {...hotkey} group={group} />;
134 });
135
136 return <Hotkeys>{elements}</Hotkeys>;
137 }
138
139 private emptyHotkeyQueue() {
140 // flatten then empty the hotkeys queue
141 const hotkeys = this.hotkeysQueue.reduce((arr, queued) => arr.concat(queued), []);
142 this.hotkeysQueue.length = 0;
143 return hotkeys;
144 }
145}
146
147// singleton instance
148const HOTKEYS_DIALOG = new HotkeysDialog();
149
150export function isHotkeysDialogShowing() {
151 return HOTKEYS_DIALOG.isShowing();
152}
153
154export function setHotkeysDialogProps(props: Partial<IHotkeysDialogProps>) {
155 for (const key in props) {
156 if (props.hasOwnProperty(key)) {
157 (HOTKEYS_DIALOG.componentProps as any)[key] = (props as any)[key];
158 }
159 }
160}
161
162export function showHotkeysDialog(hotkeys: IHotkeyProps[]) {
163 HOTKEYS_DIALOG.enqueueHotkeysForDisplay(hotkeys);
164}
165
166export function hideHotkeysDialog() {
167 HOTKEYS_DIALOG.hide();
168}
169
170/**
171 * Use this function instead of `hideHotkeysDialog` if you need to ensure that all hotkey listeners
172 * have time to execute with the dialog in a consistent open state. This can avoid flickering the
173 * dialog between open and closed states as successive listeners fire.
174 */
175export function hideHotkeysDialogAfterDelay() {
176 HOTKEYS_DIALOG.hideAfterDelay();
177}