-
Notifications
You must be signed in to change notification settings - Fork 208
/
KeyinField.ts
243 lines (210 loc) · 7.98 KB
/
KeyinField.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Widgets
*/
import { IModelApp, MessageBoxIconType, MessageBoxType, ParseAndRunResult } from "@itwin/core-frontend";
import { createButton } from "../ui/Button";
import { appendDataListEntries, createDataList, DataList, DataListEntry } from "../ui/DataList";
import { createTextBox, TextBox } from "../ui/TextBox";
function keyinsToDataListEntries(keyins: string[]): DataListEntry[] {
const entries: DataListEntry[] = [];
for (const keyin of keyins) {
entries.push({ value: keyin });
}
return entries;
}
/** Controls whether localized and/or non-localized key-in strings appear in a KeyinField's auto-completion list.
* @beta
*/
export enum KeyinFieldLocalization {
/** Include only non-localized key-in strings. */
NonLocalized,
/** Include only localized key-in strings. */
Localized,
/** Include localized and non-localized strings for each key-in. */
Both,
}
/** Properties controlling how a KeyinField is created.
* @beta
*/
export interface KeyinFieldProps {
/** If supplied, the keyin field's elements will be added as children of this parent element. */
parent?: HTMLElement;
/** Required, unique ID prefix used to produce unique IDs for child elements. */
baseId: string;
/** Default: false. */
wantButton?: boolean;
/** Default: false. */
wantLabel?: boolean;
/** The maximum number of submitted key-ins to store in the history.
* If greater than zero, pressing up/down while the KeyinField has focus will move backwards/forwards through the history.
* Default: zero;
*/
historyLength?: number;
/** Controls whether localized and/or non-localized keyin strings appear in the autocompletion list.
* Note: the KeyinField will still accept either localized or non-localized strings; this option only controls what is displayed in the auto-completion list.
* Default: non-localized
*/
localization?: KeyinFieldLocalization;
}
/** A textbox allowing input of key-ins (localized tool names) combined with a drop-down that lists all registered key-ins, filtered by substring match on the current input.
* Press `enter` or click the Enter button to run the key-in.
* @beta
*/
export class KeyinField {
/** @alpha */
public readonly autoCompleteList: DataList;
public readonly textBox: TextBox;
public readonly keyins: string[];
private _historyIndex?: number;
private _historyLength = 0;
private readonly _history: string[] | undefined;
private readonly _localization: KeyinFieldLocalization;
public constructor(props: KeyinFieldProps) {
this._localization = props.localization ?? KeyinFieldLocalization.NonLocalized;
this.keyins = this.findKeyins();
const autoCompleteListId = `${props.baseId}_autoComplete`;
this.autoCompleteList = createDataList({
parent: props.parent,
entries: keyinsToDataListEntries(this.keyins),
id: autoCompleteListId,
inline: true,
});
this.textBox = createTextBox({
label: props.wantLabel ? "Key-in: " : undefined,
id: `${props.baseId}_textBox`,
parent: props.parent,
handler: () => this.selectAll(),
keypresshandler: async (_tb, ev) => { await this.handleKeyPress(ev); },
focushandler: (_tb) => { this.respondToKeyinFocus(); },
tooltip: "Type the key-in text here",
inline: true,
list: autoCompleteListId,
});
if (props.wantButton) {
createButton({
handler: async (_bt) => { await this.submitKeyin(); },
parent: props.parent,
value: "Enter",
inline: true,
tooltip: "Click here to execute the key-in",
});
}
if (undefined !== props.historyLength && props.historyLength > 0) {
this.textBox.textbox.onkeydown = (ev) => this.handleKeyDown(ev); // eslint-disable-line @typescript-eslint/promise-function-async
this._historyLength = props.historyLength;
this._history = [];
}
}
public focus() { this.textBox.textbox.focus(); }
public loseFocus() { this.textBox.textbox.blur(); }
public selectAll(): void {
this.textBox.textbox.setSelectionRange(0, this.textBox.textbox.value.length);
}
private async handleKeyPress(ev: KeyboardEvent): Promise<void> {
ev.stopPropagation();
if ("Enter" === ev.key)
await this.submitKeyin();
}
private async handleKeyDown(ev: KeyboardEvent): Promise<void> {
ev.stopPropagation();
if (undefined === this._history || 0 === this._history.length)
return;
// NB: History list is ordered by most to least recent so moving "backwards" means incrementing the index.
const direction = ev.key === "ArrowDown" ? 1 : (ev.key === "ArrowUp" ? 1 : 0);
if (0 === direction)
return;
ev.preventDefault();
ev.stopPropagation();
if (this._historyIndex === undefined) {
if (direction < 0)
return;
else
this._historyIndex = -1;
}
const newIndex = this._historyIndex + direction;
if (newIndex >= 0 && newIndex < this._history.length) {
this._historyIndex = newIndex;
if (this._historyIndex >= 0)
this.textBox.textbox.value = this._history[newIndex];
}
}
private resetHistoryIndex(): void {
this._historyIndex = undefined;
}
private pushHistory(keyin: string): void {
if (undefined === this._history)
return;
this.textBox.textbox.value = "";
this.resetHistoryIndex();
if (this._history.length === 0 || keyin.toLowerCase() !== this._history[0].toLowerCase()) {
this._history.unshift(keyin);
if (this._history.length > this._historyLength)
this._history.pop();
}
}
private async submitKeyin(): Promise<void> {
this.selectAll();
const textBox = this.textBox.textbox;
const input = textBox.value;
this.pushHistory(input);
let message: string | undefined;
try {
switch (await IModelApp.tools.parseAndRun(input)) {
case ParseAndRunResult.ToolNotFound:
message = `Cannot find a key-in that matches: ${input}`;
break;
case ParseAndRunResult.BadArgumentCount:
message = "Incorrect number of arguments";
break;
case ParseAndRunResult.FailedToRun:
message = "Key-in failed to run";
break;
}
} catch (ex) {
message = `Key-in produced exception: ${ex}`;
}
if (undefined !== message)
await IModelApp.notifications.openMessageBox(MessageBoxType.MediumAlert, message, MessageBoxIconType.Warning);
}
private respondToKeyinFocus() {
this.resetHistoryIndex();
// Handle case in which new tools were registered since we last populated the auto-complete list.
// This can occur e.g. as a result of loading a extension, or deferred initialization of a package like markup.
const keyins = this.findKeyins();
if (keyins.length > this.keyins.length) {
const newKeyins: string[] = [];
for (const keyin of keyins)
if (!this.keyins.includes(keyin)) {
newKeyins.push(keyin);
this.keyins.push(keyin);
}
if (newKeyins.length > 0)
appendDataListEntries(this.autoCompleteList, keyinsToDataListEntries(newKeyins));
}
}
private findKeyins(): string[] {
const keyins: string[] = [];
const tools = IModelApp.tools.getToolList();
for (const tool of tools) {
switch (this._localization) {
case KeyinFieldLocalization.Localized:
keyins.push(tool.keyin);
break;
case KeyinFieldLocalization.Both:
keyins.push(tool.keyin);
if (tool.keyin === tool.englishKeyin)
break;
/* falls through */
default:
case KeyinFieldLocalization.NonLocalized:
keyins.push(tool.englishKeyin);
break;
}
}
return keyins;
}
}