UNPKG

13.1 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { Message } from '@lumino/messaging';
5import { Widget } from '@lumino/widgets';
6import { Dialog, showDialog } from './dialog';
7
8const INPUT_DIALOG_CLASS = 'jp-Input-Dialog';
9
10const INPUT_BOOLEAN_DIALOG_CLASS = 'jp-Input-Boolean-Dialog';
11
12/**
13 * Namespace for input dialogs
14 */
15export namespace InputDialog {
16 /**
17 * Common constructor options for input dialogs
18 */
19 export interface IOptions {
20 /**
21 * The top level text for the dialog. Defaults to an empty string.
22 */
23 title: Dialog.Header;
24
25 /**
26 * The host element for the dialog. Defaults to `document.body`.
27 */
28 host?: HTMLElement;
29
30 /**
31 * Label of the requested input
32 */
33 label?: string;
34
35 /**
36 * An optional renderer for dialog items. Defaults to a shared
37 * default renderer.
38 */
39 renderer?: Dialog.IRenderer;
40
41 /**
42 * Label for ok button.
43 */
44 okLabel?: string;
45
46 /**
47 * Label for cancel button.
48 */
49 cancelLabel?: string;
50
51 /**
52 * The checkbox to display in the footer. Defaults no checkbox.
53 */
54 checkbox?: Partial<Dialog.ICheckbox> | null;
55 }
56
57 /**
58 * Constructor options for boolean input dialogs
59 */
60 export interface IBooleanOptions extends IOptions {
61 /**
62 * Default value
63 */
64 value?: boolean;
65 }
66
67 /**
68 * Create and show a input dialog for a boolean.
69 *
70 * @param options - The dialog setup options.
71 *
72 * @returns A promise that resolves with whether the dialog was accepted
73 */
74 export function getBoolean(
75 options: IBooleanOptions
76 ): Promise<Dialog.IResult<boolean>> {
77 return showDialog({
78 ...options,
79 body: new InputBooleanDialog(options),
80 buttons: [
81 Dialog.cancelButton({ label: options.cancelLabel }),
82 Dialog.okButton({ label: options.okLabel })
83 ],
84 focusNodeSelector: 'input'
85 });
86 }
87
88 /**
89 * Constructor options for number input dialogs
90 */
91 export interface INumberOptions extends IOptions {
92 /**
93 * Default value
94 */
95 value?: number;
96 }
97
98 /**
99 * Create and show a input dialog for a number.
100 *
101 * @param options - The dialog setup options.
102 *
103 * @returns A promise that resolves with whether the dialog was accepted
104 */
105 export function getNumber(
106 options: INumberOptions
107 ): Promise<Dialog.IResult<number>> {
108 return showDialog({
109 ...options,
110 body: new InputNumberDialog(options),
111 buttons: [
112 Dialog.cancelButton({ label: options.cancelLabel }),
113 Dialog.okButton({ label: options.okLabel })
114 ],
115 focusNodeSelector: 'input'
116 });
117 }
118
119 /**
120 * Constructor options for item selection input dialogs
121 */
122 export interface IItemOptions extends IOptions {
123 /**
124 * List of choices
125 */
126 items: Array<string>;
127 /**
128 * Default choice
129 *
130 * If the list is editable a string with a default value can be provided
131 * otherwise the index of the default choice should be given.
132 */
133 current?: number | string;
134 /**
135 * Is the item editable?
136 */
137 editable?: boolean;
138 /**
139 * Placeholder text for editable input
140 */
141 placeholder?: string;
142 }
143
144 /**
145 * Create and show a input dialog for a choice.
146 *
147 * @param options - The dialog setup options.
148 *
149 * @returns A promise that resolves with whether the dialog was accepted
150 */
151 export function getItem(
152 options: IItemOptions
153 ): Promise<Dialog.IResult<string>> {
154 return showDialog({
155 ...options,
156 body: new InputItemsDialog(options),
157 buttons: [
158 Dialog.cancelButton({ label: options.cancelLabel }),
159 Dialog.okButton({ label: options.okLabel })
160 ],
161 focusNodeSelector: options.editable ? 'input' : 'select'
162 });
163 }
164
165 /**
166 * Constructor options for item selection input dialogs
167 */
168 export interface IMultipleItemsOptions extends IOptions {
169 /**
170 * List of choices
171 */
172 items: Array<string>;
173 /**
174 * Default choices
175 */
176 defaults?: string[];
177 }
178
179 /**
180 * Create and show a input dialog for a choice.
181 *
182 * @param options - The dialog setup options.
183 *
184 * @returns A promise that resolves with whether the dialog was accepted
185 */
186 export function getMultipleItems(
187 options: IMultipleItemsOptions
188 ): Promise<Dialog.IResult<string[]>> {
189 return showDialog({
190 ...options,
191 body: new InputMultipleItemsDialog(options),
192 buttons: [
193 Dialog.cancelButton({ label: options.cancelLabel }),
194 Dialog.okButton({ label: options.okLabel })
195 ]
196 });
197 }
198
199 /**
200 * Constructor options for text input dialogs
201 */
202 export interface ITextOptions extends IOptions {
203 /**
204 * Default input text
205 */
206 text?: string;
207 /**
208 * Placeholder text
209 */
210 placeholder?: string;
211 /**
212 * Selection range
213 *
214 * Number of characters to pre-select when dialog opens.
215 * Default is to select the whole input text if present.
216 */
217 selectionRange?: number;
218 }
219
220 /**
221 * Create and show a input dialog for a text.
222 *
223 * @param options - The dialog setup options.
224 *
225 * @returns A promise that resolves with whether the dialog was accepted
226 */
227 export function getText(
228 options: ITextOptions
229 ): Promise<Dialog.IResult<string>> {
230 return showDialog({
231 ...options,
232 body: new InputTextDialog(options),
233 buttons: [
234 Dialog.cancelButton({ label: options.cancelLabel }),
235 Dialog.okButton({ label: options.okLabel })
236 ],
237 focusNodeSelector: 'input'
238 });
239 }
240
241 /**
242 * Create and show a input dialog for a password.
243 *
244 * @param options - The dialog setup options.
245 *
246 * @returns A promise that resolves with whether the dialog was accepted
247 */
248 export function getPassword(
249 options: Omit<ITextOptions, 'selectionRange'>
250 ): Promise<Dialog.IResult<string>> {
251 return showDialog({
252 ...options,
253 body: new InputPasswordDialog(options),
254 buttons: [
255 Dialog.cancelButton({ label: options.cancelLabel }),
256 Dialog.okButton({ label: options.okLabel })
257 ],
258 focusNodeSelector: 'input'
259 });
260 }
261}
262
263/**
264 * Base widget for input dialog body
265 */
266class InputDialogBase<T> extends Widget implements Dialog.IBodyWidget<T> {
267 /**
268 * InputDialog constructor
269 *
270 * @param label Input field label
271 */
272 constructor(label?: string) {
273 super();
274 this.addClass(INPUT_DIALOG_CLASS);
275
276 this._input = document.createElement('input');
277 this._input.classList.add('jp-mod-styled');
278 this._input.id = 'jp-dialog-input-id';
279
280 if (label !== undefined) {
281 const labelElement = document.createElement('label');
282 labelElement.textContent = label;
283 labelElement.htmlFor = this._input.id;
284
285 // Initialize the node
286 this.node.appendChild(labelElement);
287 }
288
289 this.node.appendChild(this._input);
290 }
291
292 /** Input HTML node */
293 protected _input: HTMLInputElement;
294}
295
296/**
297 * Widget body for input boolean dialog
298 */
299class InputBooleanDialog extends InputDialogBase<boolean> {
300 /**
301 * InputBooleanDialog constructor
302 *
303 * @param options Constructor options
304 */
305 constructor(options: InputDialog.IBooleanOptions) {
306 super(options.label);
307 this.addClass(INPUT_BOOLEAN_DIALOG_CLASS);
308
309 this._input.type = 'checkbox';
310 this._input.checked = options.value ? true : false;
311 }
312
313 /**
314 * Get the text specified by the user
315 */
316 getValue(): boolean {
317 return this._input.checked;
318 }
319}
320
321/**
322 * Widget body for input number dialog
323 */
324class InputNumberDialog extends InputDialogBase<number> {
325 /**
326 * InputNumberDialog constructor
327 *
328 * @param options Constructor options
329 */
330 constructor(options: InputDialog.INumberOptions) {
331 super(options.label);
332
333 this._input.type = 'number';
334 this._input.value = options.value ? options.value.toString() : '0';
335 }
336
337 /**
338 * Get the number specified by the user.
339 */
340 getValue(): number {
341 if (this._input.value) {
342 return Number(this._input.value);
343 } else {
344 return Number.NaN;
345 }
346 }
347}
348
349/**
350 * Widget body for input text dialog
351 */
352class InputTextDialog extends InputDialogBase<string> {
353 /**
354 * InputTextDialog constructor
355 *
356 * @param options Constructor options
357 */
358 constructor(options: InputDialog.ITextOptions) {
359 super(options.label);
360
361 this._input.type = 'text';
362 this._input.value = options.text ? options.text : '';
363 if (options.placeholder) {
364 this._input.placeholder = options.placeholder;
365 }
366 this._initialSelectionRange = Math.min(
367 this._input.value.length,
368 Math.max(0, options.selectionRange ?? this._input.value.length)
369 );
370 }
371
372 /**
373 * A message handler invoked on an `'after-attach'` message.
374 */
375 protected onAfterAttach(msg: Message): void {
376 super.onAfterAttach(msg);
377 if (this._initialSelectionRange > 0 && this._input.value) {
378 this._input.setSelectionRange(0, this._initialSelectionRange);
379 }
380 }
381
382 /**
383 * Get the text specified by the user
384 */
385 getValue(): string {
386 return this._input.value;
387 }
388
389 private _initialSelectionRange: number;
390}
391
392/**
393 * Widget body for input password dialog
394 */
395class InputPasswordDialog extends InputDialogBase<string> {
396 /**
397 * InputPasswordDialog constructor
398 *
399 * @param options Constructor options
400 */
401 constructor(options: InputDialog.ITextOptions) {
402 super(options.label);
403
404 this._input.type = 'password';
405 this._input.value = options.text ? options.text : '';
406 if (options.placeholder) {
407 this._input.placeholder = options.placeholder;
408 }
409 }
410
411 /**
412 * A message handler invoked on an `'after-attach'` message.
413 */
414 protected onAfterAttach(msg: Message): void {
415 super.onAfterAttach(msg);
416 if (this._input.value) {
417 this._input.select();
418 }
419 }
420
421 /**
422 * Get the text specified by the user
423 */
424 getValue(): string {
425 return this._input.value;
426 }
427}
428
429/**
430 * Widget body for input list dialog
431 */
432class InputItemsDialog extends InputDialogBase<string> {
433 /**
434 * InputItemsDialog constructor
435 *
436 * @param options Constructor options
437 */
438 constructor(options: InputDialog.IItemOptions) {
439 super(options.label);
440
441 this._editable = options.editable || false;
442
443 let current = options.current || 0;
444 let defaultIndex: number;
445 if (typeof current === 'number') {
446 defaultIndex = Math.max(0, Math.min(current, options.items.length - 1));
447 current = '';
448 }
449
450 this._list = document.createElement('select');
451 options.items.forEach((item, index) => {
452 const option = document.createElement('option');
453 if (index === defaultIndex) {
454 option.selected = true;
455 current = item;
456 }
457 option.value = item;
458 option.textContent = item;
459 this._list.appendChild(option);
460 });
461
462 if (options.editable) {
463 /* Use of list and datalist */
464 const data = document.createElement('datalist');
465 data.id = 'input-dialog-items';
466 data.appendChild(this._list);
467
468 this._input.type = 'list';
469 this._input.value = current;
470 this._input.setAttribute('list', data.id);
471 if (options.placeholder) {
472 this._input.placeholder = options.placeholder;
473 }
474 this.node.appendChild(data);
475 } else {
476 /* Use select directly */
477 this._input.remove();
478 this.node.appendChild(this._list);
479 }
480 }
481
482 /**
483 * Get the user choice
484 */
485 getValue(): string {
486 if (this._editable) {
487 return this._input.value;
488 } else {
489 return this._list.value;
490 }
491 }
492
493 private _list: HTMLSelectElement;
494 private _editable: boolean;
495}
496
497/**
498 * Widget body for input list dialog
499 */
500class InputMultipleItemsDialog extends InputDialogBase<string> {
501 /**
502 * InputMultipleItemsDialog constructor
503 *
504 * @param options Constructor options
505 */
506 constructor(options: InputDialog.IMultipleItemsOptions) {
507 super(options.label);
508
509 let defaults = options.defaults || [];
510
511 this._list = document.createElement('select');
512 this._list.setAttribute('multiple', '');
513
514 options.items.forEach(item => {
515 const option = document.createElement('option');
516 option.value = item;
517 option.textContent = item;
518 this._list.appendChild(option);
519 });
520
521 // use the select
522 this._input.remove();
523 this.node.appendChild(this._list);
524
525 // select the current ones
526 const htmlOptions = this._list.options;
527 for (let i: number = 0; i < htmlOptions.length; i++) {
528 const option = htmlOptions[i];
529 if (defaults.includes(option.value)) {
530 option.selected = true;
531 } else {
532 option.selected = false;
533 }
534 }
535 }
536
537 /**
538 * Get the user choices
539 */
540 getValue(): string[] {
541 let result = [];
542 for (let opt of this._list.options) {
543 if (opt.selected && !opt.classList.contains('hidden')) {
544 result.push(opt.value || opt.text);
545 }
546 }
547 return result;
548 }
549
550 private _list: HTMLSelectElement;
551}