Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/dialog/dialog.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './dialog.css';
7
import { localize } from '../../../../nls.js';
8
import { $, addDisposableListener, addStandardDisposableListener, clearNode, EventHelper, EventType, getWindow, hide, isActiveElement, isAncestor, show } from '../../dom.js';
9
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
10
import { ActionBar } from '../actionbar/actionbar.js';
11
import { ButtonBar, ButtonBarAlignment, ButtonWithDescription, ButtonWithDropdown, IButton, IButtonStyles, IButtonWithDropdownOptions } from '../button/button.js';
12
import { ICheckboxStyles, Checkbox } from '../toggle/toggle.js';
13
import { IInputBoxStyles, InputBox } from '../inputbox/inputBox.js';
14
import { Action, toAction } from '../../../common/actions.js';
15
import { Codicon } from '../../../common/codicons.js';
16
import { ThemeIcon } from '../../../common/themables.js';
17
import { KeyCode, KeyMod } from '../../../common/keyCodes.js';
18
import { mnemonicButtonLabel } from '../../../common/labels.js';
19
import { Disposable, toDisposable } from '../../../common/lifecycle.js';
20
import { isLinux, isMacintosh, isWindows } from '../../../common/platform.js';
21
import { isActionProvider } from '../dropdown/dropdown.js';
22
23
export interface IDialogInputOptions {
24
readonly placeholder?: string;
25
readonly type?: 'text' | 'password';
26
readonly value?: string;
27
}
28
29
export enum DialogContentsAlignment {
30
/**
31
* Dialog contents align from left to right (icon, message, buttons on a separate row).
32
*
33
* Note: this is the default alignment for dialogs.
34
*/
35
Horizontal = 0,
36
37
/**
38
* Dialog contents align from top to bottom (icon, message, buttons stack on top of each other)
39
*/
40
Vertical
41
}
42
43
export interface IDialogOptions {
44
readonly cancelId?: number;
45
readonly detail?: string;
46
readonly alignment?: DialogContentsAlignment;
47
readonly checkboxLabel?: string;
48
readonly checkboxChecked?: boolean;
49
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
50
readonly extraClasses?: string[];
51
readonly inputs?: IDialogInputOptions[];
52
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
53
readonly renderBody?: (container: HTMLElement) => void;
54
readonly renderFooter?: (container: HTMLElement) => void;
55
readonly icon?: ThemeIcon;
56
readonly buttonOptions?: Array<undefined | { sublabel?: string; styleButton?: (button: IButton) => void }>;
57
readonly primaryButtonDropdown?: IButtonWithDropdownOptions;
58
readonly disableCloseAction?: boolean;
59
readonly disableCloseButton?: boolean;
60
readonly disableDefaultAction?: boolean;
61
readonly buttonStyles: IButtonStyles;
62
readonly checkboxStyles: ICheckboxStyles;
63
readonly inputBoxStyles: IInputBoxStyles;
64
readonly dialogStyles: IDialogStyles;
65
}
66
67
export interface IDialogResult {
68
readonly button: number;
69
readonly checkboxChecked?: boolean;
70
readonly values?: string[];
71
}
72
73
export interface IDialogStyles {
74
readonly dialogForeground: string | undefined;
75
readonly dialogBackground: string | undefined;
76
readonly dialogShadow: string | undefined;
77
readonly dialogBorder: string | undefined;
78
readonly errorIconForeground: string | undefined;
79
readonly warningIconForeground: string | undefined;
80
readonly infoIconForeground: string | undefined;
81
readonly textLinkForeground: string | undefined;
82
}
83
84
interface ButtonMapEntry {
85
readonly label: string;
86
readonly index: number;
87
}
88
89
export class Dialog extends Disposable {
90
91
private readonly element: HTMLElement;
92
93
private readonly shadowElement: HTMLElement;
94
private modalElement: HTMLElement | undefined;
95
private readonly buttonsContainer: HTMLElement;
96
private readonly messageDetailElement: HTMLElement;
97
private readonly messageContainer: HTMLElement;
98
private readonly footerContainer: HTMLElement | undefined;
99
private readonly iconElement: HTMLElement;
100
private readonly checkbox: Checkbox | undefined;
101
private readonly toolbarContainer: HTMLElement;
102
private buttonBar: ButtonBar | undefined;
103
private focusToReturn: HTMLElement | undefined;
104
private readonly inputs: InputBox[];
105
private readonly buttons: string[];
106
private readonly buttonStyles: IButtonStyles;
107
108
constructor(private container: HTMLElement, private message: string, buttons: string[] | undefined, private readonly options: IDialogOptions) {
109
super();
110
111
// Modal background blocker
112
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`));
113
this._register(addStandardDisposableListener(this.modalElement, EventType.CLICK, e => {
114
if (e.target === this.modalElement) {
115
this.element.focus(); // guide users back into the dialog if clicked elsewhere
116
}
117
}));
118
119
// Dialog Box
120
this.shadowElement = this.modalElement.appendChild($('.dialog-shadow'));
121
this.element = this.shadowElement.appendChild($('.monaco-dialog-box'));
122
if (options.alignment === DialogContentsAlignment.Vertical) {
123
this.element.classList.add('align-vertical');
124
}
125
if (options.extraClasses) {
126
this.element.classList.add(...options.extraClasses);
127
}
128
this.element.setAttribute('role', 'dialog');
129
this.element.tabIndex = -1;
130
hide(this.element);
131
132
// Footer
133
if (this.options.renderFooter) {
134
this.footerContainer = this.element.appendChild($('.dialog-footer-row'));
135
136
const customFooter = this.footerContainer.appendChild($('#monaco-dialog-footer.dialog-footer'));
137
this.options.renderFooter(customFooter);
138
139
for (const el of this.footerContainer.querySelectorAll('a')) {
140
el.tabIndex = 0;
141
}
142
}
143
144
// Buttons
145
this.buttonStyles = options.buttonStyles;
146
147
if (Array.isArray(buttons) && buttons.length > 0) {
148
this.buttons = buttons;
149
} else if (!this.options.disableDefaultAction) {
150
this.buttons = [localize('ok', "OK")];
151
} else {
152
this.buttons = [];
153
}
154
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
155
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
156
157
// Message
158
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
159
this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon'));
160
this.iconElement.setAttribute('aria-label', this.getIconAriaLabel());
161
this.messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
162
163
if (this.options.detail || this.options.renderBody) {
164
const messageElement = this.messageContainer.appendChild($('.dialog-message'));
165
const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text'));
166
messageTextElement.innerText = this.message;
167
}
168
169
this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail'));
170
if (this.options.detail || !this.options.renderBody) {
171
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
172
} else {
173
this.messageDetailElement.style.display = 'none';
174
}
175
176
if (this.options.renderBody) {
177
const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body'));
178
this.options.renderBody(customBody);
179
180
for (const el of this.messageContainer.querySelectorAll('a')) {
181
el.tabIndex = 0;
182
}
183
}
184
185
// Inputs
186
if (this.options.inputs) {
187
this.inputs = this.options.inputs.map(input => {
188
const inputRowElement = this.messageContainer.appendChild($('.dialog-message-input'));
189
190
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
191
placeholder: input.placeholder,
192
type: input.type ?? 'text',
193
inputBoxStyles: options.inputBoxStyles
194
}));
195
196
if (input.value) {
197
inputBox.value = input.value;
198
}
199
200
return inputBox;
201
});
202
} else {
203
this.inputs = [];
204
}
205
206
// Checkbox
207
if (this.options.checkboxLabel) {
208
const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row'));
209
210
const checkbox = this.checkbox = this._register(
211
new Checkbox(this.options.checkboxLabel, !!this.options.checkboxChecked, options.checkboxStyles)
212
);
213
214
checkboxRowElement.appendChild(checkbox.domNode);
215
216
const checkboxMessageElement = checkboxRowElement.appendChild($('.dialog-checkbox-message'));
217
checkboxMessageElement.innerText = this.options.checkboxLabel;
218
this._register(addDisposableListener(checkboxMessageElement, EventType.CLICK, () => checkbox.checked = !checkbox.checked));
219
}
220
221
// Toolbar
222
const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row'));
223
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
224
225
this.applyStyles();
226
}
227
228
private getIconAriaLabel(): string {
229
let typeLabel = localize('dialogInfoMessage', 'Info');
230
switch (this.options.type) {
231
case 'error':
232
typeLabel = localize('dialogErrorMessage', 'Error');
233
break;
234
case 'warning':
235
typeLabel = localize('dialogWarningMessage', 'Warning');
236
break;
237
case 'pending':
238
typeLabel = localize('dialogPendingMessage', 'In Progress');
239
break;
240
case 'none':
241
case 'info':
242
case 'question':
243
default:
244
break;
245
}
246
247
return typeLabel;
248
}
249
250
updateMessage(message: string): void {
251
this.messageDetailElement.innerText = message;
252
}
253
254
async show(): Promise<IDialogResult> {
255
this.focusToReturn = this.container.ownerDocument.activeElement as HTMLElement;
256
257
return new Promise<IDialogResult>(resolve => {
258
clearNode(this.buttonsContainer);
259
260
const close = () => {
261
resolve({
262
button: this.options.cancelId || 0,
263
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
264
});
265
return;
266
};
267
this._register(toDisposable(close));
268
269
const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer, { alignment: this.options?.alignment === DialogContentsAlignment.Vertical ? ButtonBarAlignment.Vertical : ButtonBarAlignment.Horizontal }));
270
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
271
272
const onButtonClick = (index: number) => {
273
resolve({
274
button: buttonMap[index].index,
275
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
276
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
277
});
278
};
279
280
// Buttons
281
buttonMap.forEach((_, index) => {
282
const primary = buttonMap[index].index === 0;
283
284
let button: IButton;
285
const buttonOptions = this.options.buttonOptions?.[buttonMap[index]?.index];
286
if (primary && this.options?.primaryButtonDropdown) {
287
const actions = isActionProvider(this.options.primaryButtonDropdown.actions) ? this.options.primaryButtonDropdown.actions.getActions() : this.options.primaryButtonDropdown.actions;
288
button = this._register(buttonBar.addButtonWithDropdown({
289
...this.options.primaryButtonDropdown,
290
...this.buttonStyles,
291
dropdownLayer: 2600, // ensure the dropdown is above the dialog
292
actions: actions.map(action => toAction({
293
...action,
294
run: async () => {
295
await action.run();
296
297
onButtonClick(index);
298
}
299
}))
300
}));
301
} else if (buttonOptions?.sublabel) {
302
button = this._register(buttonBar.addButtonWithDescription({ secondary: !primary, ...this.buttonStyles }));
303
} else {
304
button = this._register(buttonBar.addButton({ secondary: !primary, ...this.buttonStyles }));
305
}
306
307
if (buttonOptions?.styleButton) {
308
buttonOptions.styleButton(button);
309
}
310
311
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
312
if (button instanceof ButtonWithDescription) {
313
if (buttonOptions?.sublabel) {
314
button.description = buttonOptions?.sublabel;
315
}
316
}
317
this._register(button.onDidClick(e => {
318
if (e) {
319
EventHelper.stop(e);
320
}
321
322
onButtonClick(index);
323
}));
324
});
325
326
// Handle keyboard events globally: Tab, Arrow-Left/Right
327
const window = getWindow(this.container);
328
this._register(addDisposableListener(window, 'keydown', e => {
329
const evt = new StandardKeyboardEvent(e);
330
331
if (evt.equals(KeyMod.Alt)) {
332
evt.preventDefault();
333
}
334
335
if (evt.equals(KeyCode.Enter)) {
336
337
// Enter in input field should OK the dialog
338
if (this.inputs.some(input => input.hasFocus())) {
339
EventHelper.stop(e);
340
341
resolve({
342
button: buttonMap.find(button => button.index !== this.options.cancelId)?.index ?? 0,
343
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
344
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
345
});
346
}
347
348
return; // leave default handling
349
}
350
351
// Cmd+D (trigger the "no"/"do not save"-button) (macOS only)
352
if (isMacintosh && evt.equals(KeyMod.CtrlCmd | KeyCode.KeyD)) {
353
EventHelper.stop(e);
354
355
const noButton = buttonMap.find(button => button.index === 1 && button.index !== this.options.cancelId);
356
if (noButton) {
357
resolve({
358
button: noButton.index,
359
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
360
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
361
});
362
}
363
364
return; // leave default handling
365
}
366
367
if (evt.equals(KeyCode.Space)) {
368
return; // leave default handling
369
}
370
371
let eventHandled = false;
372
373
// Focus: Next / Previous
374
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow) || evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
375
376
// Build a list of focusable elements in their visual order
377
const focusableElements: { focus: () => void }[] = [];
378
let focusedIndex = -1;
379
380
if (this.messageContainer) {
381
const links = this.messageContainer.querySelectorAll('a');
382
for (const link of links) {
383
focusableElements.push(link);
384
if (isActiveElement(link)) {
385
focusedIndex = focusableElements.length - 1;
386
}
387
}
388
}
389
390
for (const input of this.inputs) {
391
focusableElements.push(input);
392
if (input.hasFocus()) {
393
focusedIndex = focusableElements.length - 1;
394
}
395
}
396
397
if (this.checkbox) {
398
focusableElements.push(this.checkbox);
399
if (this.checkbox.hasFocus()) {
400
focusedIndex = focusableElements.length - 1;
401
}
402
}
403
404
if (this.buttonBar) {
405
for (const button of this.buttonBar.buttons) {
406
if (button instanceof ButtonWithDropdown) {
407
focusableElements.push(button.primaryButton);
408
if (button.primaryButton.hasFocus()) {
409
focusedIndex = focusableElements.length - 1;
410
}
411
focusableElements.push(button.dropdownButton);
412
if (button.dropdownButton.hasFocus()) {
413
focusedIndex = focusableElements.length - 1;
414
}
415
} else {
416
focusableElements.push(button);
417
if (button.hasFocus()) {
418
focusedIndex = focusableElements.length - 1;
419
}
420
}
421
}
422
}
423
424
if (this.footerContainer) {
425
const links = this.footerContainer.querySelectorAll('a');
426
for (const link of links) {
427
focusableElements.push(link);
428
if (isActiveElement(link)) {
429
focusedIndex = focusableElements.length - 1;
430
}
431
}
432
}
433
434
// Focus next element (with wrapping)
435
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
436
const newFocusedIndex = (focusedIndex + 1) % focusableElements.length;
437
focusableElements[newFocusedIndex].focus();
438
}
439
440
// Focus previous element (with wrapping)
441
else {
442
if (focusedIndex === -1) {
443
focusedIndex = focusableElements.length; // default to focus last element if none have focus
444
}
445
446
let newFocusedIndex = focusedIndex - 1;
447
if (newFocusedIndex === -1) {
448
newFocusedIndex = focusableElements.length - 1;
449
}
450
451
focusableElements[newFocusedIndex].focus();
452
}
453
454
eventHandled = true;
455
}
456
457
if (eventHandled) {
458
EventHelper.stop(e, true);
459
} else if (this.options.keyEventProcessor) {
460
this.options.keyEventProcessor(evt);
461
}
462
}, true));
463
464
this._register(addDisposableListener(window, 'keyup', e => {
465
EventHelper.stop(e, true);
466
const evt = new StandardKeyboardEvent(e);
467
468
if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape)) {
469
close();
470
}
471
}, true));
472
473
// Detect focus out
474
this._register(addDisposableListener(this.element, 'focusout', e => {
475
if (!!e.relatedTarget && !!this.element) {
476
if (!isAncestor(e.relatedTarget as HTMLElement, this.element)) {
477
this.focusToReturn = e.relatedTarget as HTMLElement;
478
479
if (e.target) {
480
(e.target as HTMLElement).focus();
481
EventHelper.stop(e, true);
482
}
483
}
484
}
485
}, false));
486
487
const spinModifierClassName = 'codicon-modifier-spin';
488
489
this.iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.dialogError), ...ThemeIcon.asClassNameArray(Codicon.dialogWarning), ...ThemeIcon.asClassNameArray(Codicon.dialogInfo), ...ThemeIcon.asClassNameArray(Codicon.loading), spinModifierClassName);
490
491
if (this.options.icon) {
492
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(this.options.icon));
493
} else {
494
switch (this.options.type) {
495
case 'error':
496
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogError));
497
break;
498
case 'warning':
499
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogWarning));
500
break;
501
case 'pending':
502
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), spinModifierClassName);
503
break;
504
case 'none':
505
this.iconElement.classList.add('no-codicon');
506
break;
507
case 'info':
508
case 'question':
509
default:
510
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogInfo));
511
break;
512
}
513
}
514
515
if (!this.options.disableCloseAction && !this.options.disableCloseButton) {
516
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
517
518
const action = this._register(new Action('dialog.close', localize('dialogClose', "Close Dialog"), ThemeIcon.asClassName(Codicon.dialogClose), true, async () => {
519
resolve({
520
button: this.options.cancelId || 0,
521
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
522
});
523
}));
524
525
actionBar.push(action, { icon: true, label: false });
526
}
527
528
this.applyStyles();
529
530
this.element.setAttribute('aria-modal', 'true');
531
this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text');
532
this.element.setAttribute('aria-describedby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body monaco-dialog-footer');
533
show(this.element);
534
535
// Focus first element (input or button)
536
if (this.inputs.length > 0) {
537
this.inputs[0].focus();
538
this.inputs[0].select();
539
} else {
540
buttonMap.forEach((value, index) => {
541
if (value.index === 0) {
542
buttonBar.buttons[index].focus();
543
}
544
});
545
}
546
});
547
}
548
549
private applyStyles() {
550
const style = this.options.dialogStyles;
551
552
const fgColor = style.dialogForeground;
553
const bgColor = style.dialogBackground;
554
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
555
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
556
const linkFgColor = style.textLinkForeground;
557
558
this.shadowElement.style.boxShadow = shadowColor;
559
560
this.element.style.color = fgColor ?? '';
561
this.element.style.backgroundColor = bgColor ?? '';
562
this.element.style.border = border;
563
564
if (linkFgColor) {
565
for (const el of [...this.messageContainer.getElementsByTagName('a'), ...this.footerContainer?.getElementsByTagName('a') ?? []]) {
566
el.style.color = linkFgColor;
567
}
568
}
569
570
let color;
571
switch (this.options.type) {
572
case 'none':
573
break;
574
case 'error':
575
color = style.errorIconForeground;
576
break;
577
case 'warning':
578
color = style.warningIconForeground;
579
break;
580
default:
581
color = style.infoIconForeground;
582
break;
583
}
584
if (color) {
585
this.iconElement.style.color = color;
586
}
587
}
588
589
override dispose(): void {
590
super.dispose();
591
592
if (this.modalElement) {
593
this.modalElement.remove();
594
this.modalElement = undefined;
595
}
596
597
if (this.focusToReturn && isAncestor(this.focusToReturn, this.container.ownerDocument.body)) {
598
this.focusToReturn.focus();
599
this.focusToReturn = undefined;
600
}
601
}
602
603
private rearrangeButtons(buttons: Array<string>, cancelId: number | undefined): ButtonMapEntry[] {
604
605
// Maps each button to its current label and old index
606
// so that when we move them around it's not a problem
607
const buttonMap: ButtonMapEntry[] = buttons.map((label, index) => ({ label, index }));
608
609
if (buttons.length < 2 || this.options.alignment === DialogContentsAlignment.Vertical) {
610
return buttonMap; // only need to rearrange if there are 2+ buttons and the alignment is left-to-right
611
}
612
613
if (isMacintosh || isLinux) {
614
615
// Linux: the GNOME HIG (https://developer.gnome.org/hig/patterns/feedback/dialogs.html?highlight=dialog)
616
// recommend the following:
617
// "Always ensure that the cancel button appears first, before the affirmative button. In left-to-right
618
// locales, this is on the left. This button order ensures that users become aware of, and are reminded
619
// of, the ability to cancel prior to encountering the affirmative button."
620
621
// macOS: the HIG (https://developer.apple.com/design/human-interface-guidelines/components/presentation/alerts)
622
// recommend the following:
623
// "Place buttons where people expect. In general, place the button people are most likely to choose on the trailing side in a
624
// row of buttons or at the top in a stack of buttons. Always place the default button on the trailing side of a row or at the
625
// top of a stack. Cancel buttons are typically on the leading side of a row or at the bottom of a stack."
626
627
if (typeof cancelId === 'number' && buttonMap[cancelId]) {
628
const cancelButton = buttonMap.splice(cancelId, 1)[0];
629
buttonMap.splice(1, 0, cancelButton);
630
}
631
632
buttonMap.reverse();
633
} else if (isWindows) {
634
635
// Windows: the HIG (https://learn.microsoft.com/en-us/windows/win32/uxguide/win-dialog-box)
636
// recommend the following:
637
// "One of the following sets of concise commands: Yes/No, Yes/No/Cancel, [Do it]/Cancel,
638
// [Do it]/[Don't do it], [Do it]/[Don't do it]/Cancel."
639
640
if (typeof cancelId === 'number' && buttonMap[cancelId]) {
641
const cancelButton = buttonMap.splice(cancelId, 1)[0];
642
buttonMap.push(cancelButton);
643
}
644
}
645
646
return buttonMap;
647
}
648
}
649
650