1 |
|
2 |
|
3 |
|
4 | import { Message } from '@lumino/messaging';
|
5 | import { Widget } from '@lumino/widgets';
|
6 | import { Dialog, showDialog } from './dialog';
|
7 |
|
8 | const INPUT_DIALOG_CLASS = 'jp-Input-Dialog';
|
9 |
|
10 | const INPUT_BOOLEAN_DIALOG_CLASS = 'jp-Input-Boolean-Dialog';
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | export namespace InputDialog {
|
16 | |
17 |
|
18 |
|
19 | export interface IOptions extends IBaseOptions {
|
20 | |
21 |
|
22 |
|
23 | title: Dialog.Header;
|
24 |
|
25 | |
26 |
|
27 |
|
28 | host?: HTMLElement;
|
29 |
|
30 | |
31 |
|
32 |
|
33 |
|
34 | renderer?: Dialog.IRenderer;
|
35 |
|
36 | |
37 |
|
38 |
|
39 | okLabel?: string;
|
40 |
|
41 | |
42 |
|
43 |
|
44 | cancelLabel?: string;
|
45 |
|
46 | |
47 |
|
48 |
|
49 | checkbox?: Partial<Dialog.ICheckbox> | null;
|
50 |
|
51 | |
52 |
|
53 |
|
54 | defaultButton?: number;
|
55 | }
|
56 |
|
57 | |
58 |
|
59 |
|
60 | export interface IBooleanOptions extends IOptions {
|
61 | |
62 |
|
63 |
|
64 | value?: boolean;
|
65 | }
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
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 |
|
90 |
|
91 | export interface INumberOptions extends IOptions {
|
92 | |
93 |
|
94 |
|
95 | value?: number;
|
96 | }
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
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 |
|
121 |
|
122 | export interface IItemOptions extends IOptions {
|
123 | |
124 |
|
125 |
|
126 | items: Array<string>;
|
127 | |
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | current?: number | string;
|
134 | |
135 |
|
136 |
|
137 | editable?: boolean;
|
138 | |
139 |
|
140 |
|
141 | placeholder?: string;
|
142 | }
|
143 |
|
144 | |
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
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 |
|
167 |
|
168 | export interface IMultipleItemsOptions extends IOptions {
|
169 | |
170 |
|
171 |
|
172 | items: Array<string>;
|
173 | |
174 |
|
175 |
|
176 | defaults?: string[];
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
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 |
|
201 |
|
202 | export interface ITextOptions extends IOptions {
|
203 | |
204 |
|
205 |
|
206 | text?: string;
|
207 | |
208 |
|
209 |
|
210 | placeholder?: string;
|
211 | |
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | selectionRange?: number;
|
218 | |
219 |
|
220 |
|
221 | pattern?: string;
|
222 | |
223 |
|
224 |
|
225 | required?: boolean;
|
226 | }
|
227 |
|
228 | |
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
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 |
|
251 |
|
252 |
|
253 |
|
254 |
|
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 |
|
273 |
|
274 | interface IBaseOptions {
|
275 | |
276 |
|
277 |
|
278 | label?: string;
|
279 |
|
280 | |
281 |
|
282 |
|
283 | prefix?: string;
|
284 |
|
285 | |
286 |
|
287 |
|
288 | suffix?: string;
|
289 | }
|
290 |
|
291 |
|
292 |
|
293 |
|
294 | class InputDialogBase<T> extends Widget implements Dialog.IBodyWidget<T> {
|
295 | |
296 |
|
297 |
|
298 |
|
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 |
|
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 |
|
325 |
|
326 |
|
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 |
|
343 | protected _input: HTMLInputElement;
|
344 | }
|
345 |
|
346 |
|
347 |
|
348 |
|
349 | class InputBooleanDialog extends InputDialogBase<boolean> {
|
350 | |
351 |
|
352 |
|
353 |
|
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 |
|
365 |
|
366 | getValue(): boolean {
|
367 | return this._input.checked;
|
368 | }
|
369 | }
|
370 |
|
371 |
|
372 |
|
373 |
|
374 | class InputNumberDialog extends InputDialogBase<number> {
|
375 | |
376 |
|
377 |
|
378 |
|
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 |
|
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 |
|
401 |
|
402 | class InputDialogTextBase extends InputDialogBase<string> {
|
403 | |
404 |
|
405 |
|
406 |
|
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 |
|
424 |
|
425 | getValue(): string {
|
426 | return this._input.value;
|
427 | }
|
428 | }
|
429 |
|
430 |
|
431 |
|
432 |
|
433 | class InputTextDialog extends InputDialogTextBase {
|
434 | |
435 |
|
436 |
|
437 |
|
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 |
|
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 |
|
464 |
|
465 | class InputPasswordDialog extends InputDialogTextBase {
|
466 | |
467 |
|
468 |
|
469 |
|
470 |
|
471 | constructor(options: InputDialog.ITextOptions) {
|
472 | super(options);
|
473 | this._input.type = 'password';
|
474 | }
|
475 |
|
476 | |
477 |
|
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 |
|
489 |
|
490 | class InputItemsDialog extends InputDialogBase<string> {
|
491 | |
492 |
|
493 |
|
494 |
|
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 |
|
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 |
|
535 | this._input.parentElement!.replaceChild(this._list, this._input);
|
536 | }
|
537 | }
|
538 |
|
539 | |
540 |
|
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 |
|
556 |
|
557 | class InputMultipleItemsDialog extends InputDialogBase<string> {
|
558 | |
559 |
|
560 |
|
561 |
|
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 |
|
579 | this._input.remove();
|
580 | this.node.appendChild(this._list);
|
581 |
|
582 |
|
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 |
|
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 | }
|