UNPKG

14.8 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 extends IBaseOptions {
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 * An optional renderer for dialog items. Defaults to a shared
32 * default renderer.
33 */
34 renderer?: Dialog.IRenderer;
35
36 /**
37 * Label for ok button.
38 */
39 okLabel?: string;
40
41 /**
42 * Label for cancel button.
43 */
44 cancelLabel?: string;
45
46 /**
47 * The checkbox to display in the footer. Defaults no checkbox.
48 */
49 checkbox?: Partial<Dialog.ICheckbox> | null;
50
51 /**
52 * The index of the default button. Defaults to the last button.
53 */
54 defaultButton?: number;
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 * Pattern used by the browser to validate the input value.
220 */
221 pattern?: string;
222 /**
223 * Whether the input is required (has to be non-empty).
224 */
225 required?: boolean;
226 }
227
228 /**
229 * Create and show a input dialog for a text.
230 *
231 * @param options - The dialog setup options.
232 *
233 * @returns A promise that resolves with whether the dialog was accepted
234 */
235 export function getText(
236 options: ITextOptions
237 ): Promise<Dialog.IResult<string>> {
238 return showDialog({
239 ...options,
240 body: new InputTextDialog(options),
241 buttons: [
242 Dialog.cancelButton({ label: options.cancelLabel }),
243 Dialog.okButton({ label: options.okLabel })
244 ],
245 focusNodeSelector: 'input'
246 });
247 }
248
249 /**
250 * Create and show a input dialog for a password.
251 *
252 * @param options - The dialog setup options.
253 *
254 * @returns A promise that resolves with whether the dialog was accepted
255 */
256 export function getPassword(
257 options: Omit<ITextOptions, 'selectionRange'>
258 ): Promise<Dialog.IResult<string>> {
259 return showDialog({
260 ...options,
261 body: new InputPasswordDialog(options),
262 buttons: [
263 Dialog.cancelButton({ label: options.cancelLabel }),
264 Dialog.okButton({ label: options.okLabel })
265 ],
266 focusNodeSelector: 'input'
267 });
268 }
269}
270
271/**
272 * Constructor options for base input dialog body.
273 */
274interface IBaseOptions {
275 /**
276 * Label of the requested input
277 */
278 label?: string;
279
280 /**
281 * Additional prefix string preceding the input (e.g. £).
282 */
283 prefix?: string;
284
285 /**
286 * Additional suffix string following the input (e.g. $).
287 */
288 suffix?: string;
289}
290
291/**
292 * Base widget for input dialog body
293 */
294class InputDialogBase<T> extends Widget implements Dialog.IBodyWidget<T> {
295 /**
296 * InputDialog constructor
297 *
298 * @param label Input field label
299 */
300 constructor(options: IBaseOptions) {
301 super();
302 this.addClass(INPUT_DIALOG_CLASS);
303
304 this._input = document.createElement('input');
305 this._input.classList.add('jp-mod-styled');
306 this._input.id = 'jp-dialog-input-id';
307
308 if (options.label !== undefined) {
309 const labelElement = document.createElement('label');
310 labelElement.textContent = options.label;
311 labelElement.htmlFor = this._input.id;
312
313 // Initialize the node
314 this.node.appendChild(labelElement);
315 }
316
317 const wrapper = document.createElement('div');
318 wrapper.className = 'jp-InputDialog-inputWrapper';
319
320 if (options.prefix) {
321 const prefix = document.createElement('span');
322 prefix.className = 'jp-InputDialog-inputPrefix';
323 prefix.textContent = options.prefix;
324 // Both US WDS (https://designsystem.digital.gov/components/input-prefix-suffix/)
325 // and UK DS (https://design-system.service.gov.uk/components/text-input/) recommend
326 // hiding prefixes and suffixes from screen readers.
327 prefix.ariaHidden = 'true';
328 wrapper.appendChild(prefix);
329 }
330 wrapper.appendChild(this._input);
331 if (options.suffix) {
332 const suffix = document.createElement('span');
333 suffix.className = 'jp-InputDialog-inputSuffix';
334 suffix.textContent = options.suffix;
335 suffix.ariaHidden = 'true';
336 wrapper.appendChild(suffix);
337 }
338
339 this.node.appendChild(wrapper);
340 }
341
342 /** Input HTML node */
343 protected _input: HTMLInputElement;
344}
345
346/**
347 * Widget body for input boolean dialog
348 */
349class InputBooleanDialog extends InputDialogBase<boolean> {
350 /**
351 * InputBooleanDialog constructor
352 *
353 * @param options Constructor options
354 */
355 constructor(options: InputDialog.IBooleanOptions) {
356 super(options);
357 this.addClass(INPUT_BOOLEAN_DIALOG_CLASS);
358
359 this._input.type = 'checkbox';
360 this._input.checked = options.value ? true : false;
361 }
362
363 /**
364 * Get the text specified by the user
365 */
366 getValue(): boolean {
367 return this._input.checked;
368 }
369}
370
371/**
372 * Widget body for input number dialog
373 */
374class InputNumberDialog extends InputDialogBase<number> {
375 /**
376 * InputNumberDialog constructor
377 *
378 * @param options Constructor options
379 */
380 constructor(options: InputDialog.INumberOptions) {
381 super(options);
382
383 this._input.type = 'number';
384 this._input.value = options.value ? options.value.toString() : '0';
385 }
386
387 /**
388 * Get the number specified by the user.
389 */
390 getValue(): number {
391 if (this._input.value) {
392 return Number(this._input.value);
393 } else {
394 return Number.NaN;
395 }
396 }
397}
398
399/**
400 * Base widget body for input text/password/email dialog
401 */
402class InputDialogTextBase extends InputDialogBase<string> {
403 /**
404 * InputDialogTextBase constructor
405 *
406 * @param options Constructor options
407 */
408 constructor(options: Omit<InputDialog.ITextOptions, 'selectionRange'>) {
409 super(options);
410 this._input.value = options.text ? options.text : '';
411 if (options.placeholder) {
412 this._input.placeholder = options.placeholder;
413 }
414 if (options.pattern) {
415 this._input.pattern = options.pattern;
416 }
417 if (options.required) {
418 this._input.required = options.required;
419 }
420 }
421
422 /**
423 * Get the text specified by the user
424 */
425 getValue(): string {
426 return this._input.value;
427 }
428}
429
430/**
431 * Widget body for input text dialog
432 */
433class InputTextDialog extends InputDialogTextBase {
434 /**
435 * InputTextDialog constructor
436 *
437 * @param options Constructor options
438 */
439 constructor(options: InputDialog.ITextOptions) {
440 super(options);
441 this._input.type = 'text';
442
443 this._initialSelectionRange = Math.min(
444 this._input.value.length,
445 Math.max(0, options.selectionRange ?? this._input.value.length)
446 );
447 }
448
449 /**
450 * A message handler invoked on an `'after-attach'` message.
451 */
452 protected onAfterAttach(msg: Message): void {
453 super.onAfterAttach(msg);
454 if (this._initialSelectionRange > 0 && this._input.value) {
455 this._input.setSelectionRange(0, this._initialSelectionRange);
456 }
457 }
458
459 private _initialSelectionRange: number;
460}
461
462/**
463 * Widget body for input password dialog
464 */
465class InputPasswordDialog extends InputDialogTextBase {
466 /**
467 * InputPasswordDialog constructor
468 *
469 * @param options Constructor options
470 */
471 constructor(options: InputDialog.ITextOptions) {
472 super(options);
473 this._input.type = 'password';
474 }
475
476 /**
477 * A message handler invoked on an `'after-attach'` message.
478 */
479 protected onAfterAttach(msg: Message): void {
480 super.onAfterAttach(msg);
481 if (this._input.value) {
482 this._input.select();
483 }
484 }
485}
486
487/**
488 * Widget body for input list dialog
489 */
490class InputItemsDialog extends InputDialogBase<string> {
491 /**
492 * InputItemsDialog constructor
493 *
494 * @param options Constructor options
495 */
496 constructor(options: InputDialog.IItemOptions) {
497 super(options);
498
499 this._editable = options.editable || false;
500
501 let current = options.current || 0;
502 let defaultIndex: number;
503 if (typeof current === 'number') {
504 defaultIndex = Math.max(0, Math.min(current, options.items.length - 1));
505 current = '';
506 }
507
508 this._list = document.createElement('select');
509 options.items.forEach((item, index) => {
510 const option = document.createElement('option');
511 if (index === defaultIndex) {
512 option.selected = true;
513 current = item;
514 }
515 option.value = item;
516 option.textContent = item;
517 this._list.appendChild(option);
518 });
519
520 if (options.editable) {
521 /* Use of list and datalist */
522 const data = document.createElement('datalist');
523 data.id = 'input-dialog-items';
524 data.appendChild(this._list);
525
526 this._input.type = 'list';
527 this._input.value = current;
528 this._input.setAttribute('list', data.id);
529 if (options.placeholder) {
530 this._input.placeholder = options.placeholder;
531 }
532 this.node.appendChild(data);
533 } else {
534 /* Use select directly */
535 this._input.parentElement!.replaceChild(this._list, this._input);
536 }
537 }
538
539 /**
540 * Get the user choice
541 */
542 getValue(): string {
543 if (this._editable) {
544 return this._input.value;
545 } else {
546 return this._list.value;
547 }
548 }
549
550 private _list: HTMLSelectElement;
551 private _editable: boolean;
552}
553
554/**
555 * Widget body for input list dialog
556 */
557class InputMultipleItemsDialog extends InputDialogBase<string> {
558 /**
559 * InputMultipleItemsDialog constructor
560 *
561 * @param options Constructor options
562 */
563 constructor(options: InputDialog.IMultipleItemsOptions) {
564 super(options);
565
566 let defaults = options.defaults || [];
567
568 this._list = document.createElement('select');
569 this._list.setAttribute('multiple', '');
570
571 options.items.forEach(item => {
572 const option = document.createElement('option');
573 option.value = item;
574 option.textContent = item;
575 this._list.appendChild(option);
576 });
577
578 // use the select
579 this._input.remove();
580 this.node.appendChild(this._list);
581
582 // select the current ones
583 const htmlOptions = this._list.options;
584 for (let i: number = 0; i < htmlOptions.length; i++) {
585 const option = htmlOptions[i];
586 if (defaults.includes(option.value)) {
587 option.selected = true;
588 } else {
589 option.selected = false;
590 }
591 }
592 }
593
594 /**
595 * Get the user choices
596 */
597 getValue(): string[] {
598 let result = [];
599 for (let opt of this._list.options) {
600 if (opt.selected && !opt.classList.contains('hidden')) {
601 result.push(opt.value || opt.text);
602 }
603 }
604 return result;
605 }
606
607 private _list: HTMLSelectElement;
608}