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
5258 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
// eslint-disable-next-line no-restricted-syntax
140
for (const el of this.footerContainer.querySelectorAll('a')) {
141
el.tabIndex = 0;
142
}
143
}
144
145
// Buttons
146
this.buttonStyles = options.buttonStyles;
147
148
if (Array.isArray(buttons) && buttons.length > 0) {
149
this.buttons = buttons;
150
} else if (!this.options.disableDefaultAction) {
151
this.buttons = [localize('ok', "OK")];
152
} else {
153
this.buttons = [];
154
}
155
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
156
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
157
158
// Message
159
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
160
this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon'));
161
this.iconElement.setAttribute('aria-label', this.getIconAriaLabel());
162
this.messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
163
164
if (this.options.detail || this.options.renderBody) {
165
const messageElement = this.messageContainer.appendChild($('.dialog-message'));
166
const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text'));
167
messageTextElement.innerText = this.message;
168
}
169
170
this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail'));
171
if (this.options.detail || !this.options.renderBody) {
172
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
173
} else {
174
this.messageDetailElement.style.display = 'none';
175
}
176
177
if (this.options.renderBody) {
178
const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body'));
179
this.options.renderBody(customBody);
180
181
// eslint-disable-next-line no-restricted-syntax
182
for (const el of this.messageContainer.querySelectorAll('a')) {
183
el.tabIndex = 0;
184
}
185
}
186
187
// Inputs
188
if (this.options.inputs) {
189
this.inputs = this.options.inputs.map(input => {
190
const inputRowElement = this.messageContainer.appendChild($('.dialog-message-input'));
191
192
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
193
placeholder: input.placeholder,
194
type: input.type ?? 'text',
195
inputBoxStyles: options.inputBoxStyles
196
}));
197
198
if (input.value) {
199
inputBox.value = input.value;
200
}
201
202
return inputBox;
203
});
204
} else {
205
this.inputs = [];
206
}
207
208
// Checkbox
209
if (this.options.checkboxLabel) {
210
const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row'));
211
212
const checkbox = this.checkbox = this._register(
213
new Checkbox(this.options.checkboxLabel, !!this.options.checkboxChecked, options.checkboxStyles)
214
);
215
216
checkboxRowElement.appendChild(checkbox.domNode);
217
218
const checkboxMessageElement = checkboxRowElement.appendChild($('.dialog-checkbox-message'));
219
checkboxMessageElement.innerText = this.options.checkboxLabel;
220
this._register(addDisposableListener(checkboxMessageElement, EventType.CLICK, () => checkbox.checked = !checkbox.checked));
221
}
222
223
// Toolbar
224
const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row'));
225
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
226
227
this.applyStyles();
228
}
229
230
private getIconAriaLabel(): string {
231
let typeLabel = localize('dialogInfoMessage', 'Info');
232
switch (this.options.type) {
233
case 'error':
234
typeLabel = localize('dialogErrorMessage', 'Error');
235
break;
236
case 'warning':
237
typeLabel = localize('dialogWarningMessage', 'Warning');
238
break;
239
case 'pending':
240
typeLabel = localize('dialogPendingMessage', 'In Progress');
241
break;
242
case 'none':
243
case 'info':
244
case 'question':
245
default:
246
break;
247
}
248
249
return typeLabel;
250
}
251
252
updateMessage(message: string): void {
253
this.messageDetailElement.innerText = message;
254
}
255
256
async show(): Promise<IDialogResult> {
257
this.focusToReturn = this.container.ownerDocument.activeElement as HTMLElement;
258
259
return new Promise<IDialogResult>(resolve => {
260
clearNode(this.buttonsContainer);
261
262
const close = () => {
263
resolve({
264
button: this.options.cancelId || 0,
265
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
266
});
267
return;
268
};
269
this._register(toDisposable(close));
270
271
const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer, { alignment: this.options?.alignment === DialogContentsAlignment.Vertical ? ButtonBarAlignment.Vertical : ButtonBarAlignment.Horizontal }));
272
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
273
274
const onButtonClick = (index: number) => {
275
resolve({
276
button: buttonMap[index].index,
277
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
278
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
279
});
280
};
281
282
// Buttons
283
buttonMap.forEach((_, index) => {
284
const primary = buttonMap[index].index === 0;
285
286
let button: IButton;
287
const buttonOptions = this.options.buttonOptions?.[buttonMap[index]?.index];
288
if (primary && this.options?.primaryButtonDropdown) {
289
const actions = isActionProvider(this.options.primaryButtonDropdown.actions) ? this.options.primaryButtonDropdown.actions.getActions() : this.options.primaryButtonDropdown.actions;
290
button = this._register(buttonBar.addButtonWithDropdown({
291
...this.options.primaryButtonDropdown,
292
...this.buttonStyles,
293
dropdownLayer: 2600, // ensure the dropdown is above the dialog
294
actions: actions.map(action => toAction({
295
...action,
296
run: async () => {
297
await action.run();
298
299
onButtonClick(index);
300
}
301
}))
302
}));
303
} else if (buttonOptions?.sublabel) {
304
button = this._register(buttonBar.addButtonWithDescription({ secondary: !primary, ...this.buttonStyles }));
305
} else {
306
button = this._register(buttonBar.addButton({ secondary: !primary, ...this.buttonStyles }));
307
}
308
309
if (buttonOptions?.styleButton) {
310
buttonOptions.styleButton(button);
311
}
312
313
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
314
if (button instanceof ButtonWithDescription) {
315
if (buttonOptions?.sublabel) {
316
button.description = buttonOptions?.sublabel;
317
}
318
}
319
this._register(button.onDidClick(e => {
320
if (e) {
321
EventHelper.stop(e);
322
}
323
324
onButtonClick(index);
325
}));
326
});
327
328
// Handle keyboard events globally: Tab, Arrow-Left/Right
329
const window = getWindow(this.container);
330
this._register(addDisposableListener(window, 'keydown', e => {
331
const evt = new StandardKeyboardEvent(e);
332
333
if (evt.equals(KeyMod.Alt)) {
334
evt.preventDefault();
335
}
336
337
if (evt.equals(KeyCode.Enter)) {
338
339
// Enter in input field should OK the dialog
340
if (this.inputs.some(input => input.hasFocus())) {
341
EventHelper.stop(e);
342
343
resolve({
344
button: buttonMap.find(button => button.index !== this.options.cancelId)?.index ?? 0,
345
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
346
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
347
});
348
}
349
350
return; // leave default handling
351
}
352
353
// Cmd+D (trigger the "no"/"do not save"-button) (macOS only)
354
if (isMacintosh && evt.equals(KeyMod.CtrlCmd | KeyCode.KeyD)) {
355
EventHelper.stop(e);
356
357
const noButton = buttonMap.find(button => button.index === 1 && button.index !== this.options.cancelId);
358
if (noButton) {
359
resolve({
360
button: noButton.index,
361
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
362
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
363
});
364
}
365
366
return; // leave default handling
367
}
368
369
if (evt.equals(KeyCode.Space)) {
370
return; // leave default handling
371
}
372
373
let eventHandled = false;
374
375
// Focus: Next / Previous
376
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow) || evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
377
378
// Build a list of focusable elements in their visual order
379
const focusableElements: { focus: () => void }[] = [];
380
let focusedIndex = -1;
381
382
if (this.messageContainer) {
383
// eslint-disable-next-line no-restricted-syntax
384
const links = this.messageContainer.querySelectorAll('a');
385
for (const link of links) {
386
focusableElements.push(link);
387
if (isActiveElement(link)) {
388
focusedIndex = focusableElements.length - 1;
389
}
390
}
391
}
392
393
for (const input of this.inputs) {
394
focusableElements.push(input);
395
if (input.hasFocus()) {
396
focusedIndex = focusableElements.length - 1;
397
}
398
}
399
400
if (this.checkbox) {
401
focusableElements.push(this.checkbox);
402
if (this.checkbox.hasFocus()) {
403
focusedIndex = focusableElements.length - 1;
404
}
405
}
406
407
if (this.buttonBar) {
408
for (const button of this.buttonBar.buttons) {
409
if (button instanceof ButtonWithDropdown) {
410
focusableElements.push(button.primaryButton);
411
if (button.primaryButton.hasFocus()) {
412
focusedIndex = focusableElements.length - 1;
413
}
414
focusableElements.push(button.dropdownButton);
415
if (button.dropdownButton.hasFocus()) {
416
focusedIndex = focusableElements.length - 1;
417
}
418
} else {
419
focusableElements.push(button);
420
if (button.hasFocus()) {
421
focusedIndex = focusableElements.length - 1;
422
}
423
}
424
}
425
}
426
427
if (this.footerContainer) {
428
// eslint-disable-next-line no-restricted-syntax
429
const links = this.footerContainer.querySelectorAll('a');
430
for (const link of links) {
431
focusableElements.push(link);
432
if (isActiveElement(link)) {
433
focusedIndex = focusableElements.length - 1;
434
}
435
}
436
}
437
438
// Focus next element (with wrapping)
439
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
440
const newFocusedIndex = (focusedIndex + 1) % focusableElements.length;
441
focusableElements[newFocusedIndex].focus();
442
}
443
444
// Focus previous element (with wrapping)
445
else {
446
if (focusedIndex === -1) {
447
focusedIndex = focusableElements.length; // default to focus last element if none have focus
448
}
449
450
let newFocusedIndex = focusedIndex - 1;
451
if (newFocusedIndex === -1) {
452
newFocusedIndex = focusableElements.length - 1;
453
}
454
455
focusableElements[newFocusedIndex].focus();
456
}
457
458
eventHandled = true;
459
}
460
461
if (eventHandled) {
462
EventHelper.stop(e, true);
463
} else if (this.options.keyEventProcessor) {
464
this.options.keyEventProcessor(evt);
465
}
466
}, true));
467
468
this._register(addDisposableListener(window, 'keyup', e => {
469
EventHelper.stop(e, true);
470
const evt = new StandardKeyboardEvent(e);
471
472
if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape)) {
473
close();
474
}
475
}, true));
476
477
// Detect focus out
478
this._register(addDisposableListener(this.element, 'focusout', e => {
479
if (!!e.relatedTarget && !!this.element) {
480
if (!isAncestor(e.relatedTarget as HTMLElement, this.element)) {
481
this.focusToReturn = e.relatedTarget as HTMLElement;
482
483
if (e.target) {
484
(e.target as HTMLElement).focus();
485
EventHelper.stop(e, true);
486
}
487
}
488
}
489
}, false));
490
491
const spinModifierClassName = 'codicon-modifier-spin';
492
493
this.iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.dialogError), ...ThemeIcon.asClassNameArray(Codicon.dialogWarning), ...ThemeIcon.asClassNameArray(Codicon.dialogInfo), ...ThemeIcon.asClassNameArray(Codicon.loading), spinModifierClassName);
494
495
if (this.options.icon) {
496
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(this.options.icon));
497
} else {
498
switch (this.options.type) {
499
case 'error':
500
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogError));
501
break;
502
case 'warning':
503
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogWarning));
504
break;
505
case 'pending':
506
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), spinModifierClassName);
507
break;
508
case 'none':
509
this.iconElement.classList.add('no-codicon');
510
break;
511
case 'info':
512
case 'question':
513
default:
514
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogInfo));
515
break;
516
}
517
}
518
519
if (!this.options.disableCloseAction && !this.options.disableCloseButton) {
520
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
521
522
const action = this._register(new Action('dialog.close', localize('dialogClose', "Close Dialog"), ThemeIcon.asClassName(Codicon.dialogClose), true, async () => {
523
resolve({
524
button: this.options.cancelId || 0,
525
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
526
});
527
}));
528
529
actionBar.push(action, { icon: true, label: false });
530
}
531
532
this.applyStyles();
533
534
this.element.setAttribute('aria-modal', 'true');
535
this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text');
536
this.element.setAttribute('aria-describedby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body monaco-dialog-footer');
537
show(this.element);
538
539
// Focus first element (input or button)
540
if (this.inputs.length > 0) {
541
this.inputs[0].focus();
542
this.inputs[0].select();
543
} else {
544
buttonMap.forEach((value, index) => {
545
if (value.index === 0) {
546
buttonBar.buttons[index].focus();
547
}
548
});
549
}
550
});
551
}
552
553
private applyStyles() {
554
const style = this.options.dialogStyles;
555
556
const fgColor = style.dialogForeground;
557
const bgColor = style.dialogBackground;
558
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
559
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
560
const linkFgColor = style.textLinkForeground;
561
562
this.shadowElement.style.boxShadow = shadowColor;
563
564
this.element.style.color = fgColor ?? '';
565
this.element.style.backgroundColor = bgColor ?? '';
566
this.element.style.border = border;
567
568
if (linkFgColor) {
569
// eslint-disable-next-line no-restricted-syntax
570
for (const el of [...this.messageContainer.getElementsByTagName('a'), ...this.footerContainer?.getElementsByTagName('a') ?? []]) {
571
el.style.color = linkFgColor;
572
}
573
}
574
575
let color;
576
switch (this.options.type) {
577
case 'none':
578
break;
579
case 'error':
580
color = style.errorIconForeground;
581
break;
582
case 'warning':
583
color = style.warningIconForeground;
584
break;
585
default:
586
color = style.infoIconForeground;
587
break;
588
}
589
if (color) {
590
this.iconElement.style.color = color;
591
}
592
}
593
594
override dispose(): void {
595
super.dispose();
596
597
if (this.modalElement) {
598
this.modalElement.remove();
599
this.modalElement = undefined;
600
}
601
602
if (this.focusToReturn && isAncestor(this.focusToReturn, this.container.ownerDocument.body)) {
603
this.focusToReturn.focus();
604
this.focusToReturn = undefined;
605
}
606
}
607
608
private rearrangeButtons(buttons: Array<string>, cancelId: number | undefined): ButtonMapEntry[] {
609
610
// Maps each button to its current label and old index
611
// so that when we move them around it's not a problem
612
const buttonMap: ButtonMapEntry[] = buttons.map((label, index) => ({ label, index }));
613
614
if (buttons.length < 2 || this.options.alignment === DialogContentsAlignment.Vertical) {
615
return buttonMap; // only need to rearrange if there are 2+ buttons and the alignment is left-to-right
616
}
617
618
if (isMacintosh || isLinux) {
619
620
// Linux: the GNOME HIG (https://developer.gnome.org/hig/patterns/feedback/dialogs.html?highlight=dialog)
621
// recommend the following:
622
// "Always ensure that the cancel button appears first, before the affirmative button. In left-to-right
623
// locales, this is on the left. This button order ensures that users become aware of, and are reminded
624
// of, the ability to cancel prior to encountering the affirmative button."
625
626
// macOS: the HIG (https://developer.apple.com/design/human-interface-guidelines/components/presentation/alerts)
627
// recommend the following:
628
// "Place buttons where people expect. In general, place the button people are most likely to choose on the trailing side in a
629
// 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
630
// top of a stack. Cancel buttons are typically on the leading side of a row or at the bottom of a stack."
631
632
if (typeof cancelId === 'number' && buttonMap[cancelId]) {
633
const cancelButton = buttonMap.splice(cancelId, 1)[0];
634
buttonMap.splice(1, 0, cancelButton);
635
}
636
637
buttonMap.reverse();
638
} else if (isWindows) {
639
640
// Windows: the HIG (https://learn.microsoft.com/en-us/windows/win32/uxguide/win-dialog-box)
641
// recommend the following:
642
// "One of the following sets of concise commands: Yes/No, Yes/No/Cancel, [Do it]/Cancel,
643
// [Do it]/[Don't do it], [Do it]/[Don't do it]/Cancel."
644
645
if (typeof cancelId === 'number' && buttonMap[cancelId]) {
646
const cancelButton = buttonMap.splice(cancelId, 1)[0];
647
buttonMap.push(cancelButton);
648
}
649
}
650
651
return buttonMap;
652
}
653
}
654
655