Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/toggle/toggle.ts
5221 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 { IAction } from '../../../common/actions.js';
7
import { Codicon } from '../../../common/codicons.js';
8
import { Emitter, Event } from '../../../common/event.js';
9
import { IMarkdownString, isMarkdownString } from '../../../common/htmlContent.js';
10
import { getCodiconAriaLabel, stripIcons } from '../../../common/iconLabels.js';
11
import { KeyCode } from '../../../common/keyCodes.js';
12
import { ThemeIcon } from '../../../common/themables.js';
13
import { $, addDisposableListener, EventType, isActiveElement, isHTMLElement } from '../../dom.js';
14
import { IKeyboardEvent } from '../../keyboardEvent.js';
15
import { BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js';
16
import { IActionViewItemProvider } from '../actionbar/actionbar.js';
17
import { HoverStyle, IHoverLifecycleOptions } from '../hover/hover.js';
18
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
19
import { Widget } from '../widget.js';
20
import './toggle.css';
21
22
export interface IToggleOpts extends IToggleStyles {
23
readonly actionClassName?: string;
24
readonly icon?: ThemeIcon;
25
readonly title: string | IMarkdownString | HTMLElement;
26
readonly isChecked: boolean;
27
readonly notFocusable?: boolean;
28
readonly hoverLifecycleOptions?: IHoverLifecycleOptions;
29
}
30
31
export interface IToggleStyles {
32
readonly inputActiveOptionBorder: string | undefined;
33
readonly inputActiveOptionForeground: string | undefined;
34
readonly inputActiveOptionBackground: string | undefined;
35
}
36
37
export interface ICheckboxStyles {
38
readonly checkboxBackground: string | undefined;
39
readonly checkboxBorder: string | undefined;
40
readonly checkboxForeground: string | undefined;
41
readonly checkboxDisabledBackground: string | undefined;
42
readonly checkboxDisabledForeground: string | undefined;
43
readonly size?: number;
44
readonly hoverLifecycleOptions?: IHoverLifecycleOptions;
45
}
46
47
export const unthemedToggleStyles = {
48
inputActiveOptionBorder: '#007ACC00',
49
inputActiveOptionForeground: '#FFFFFF',
50
inputActiveOptionBackground: '#0E639C50'
51
};
52
53
export class ToggleActionViewItem extends BaseActionViewItem {
54
55
protected readonly toggle: Toggle;
56
57
constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {
58
super(context, action, options);
59
60
const title = (<IActionViewItemOptions>this.options).keybinding ?
61
`${this._action.label} (${(<IActionViewItemOptions>this.options).keybinding})` : this._action.label;
62
this.toggle = this._register(new Toggle({
63
actionClassName: this._action.class,
64
isChecked: !!this._action.checked,
65
title,
66
notFocusable: true,
67
inputActiveOptionBackground: options.toggleStyles?.inputActiveOptionBackground,
68
inputActiveOptionBorder: options.toggleStyles?.inputActiveOptionBorder,
69
inputActiveOptionForeground: options.toggleStyles?.inputActiveOptionForeground,
70
}));
71
this._register(this.toggle.onChange(() => {
72
this._action.checked = !!this.toggle && this.toggle.checked;
73
}));
74
}
75
76
override render(container: HTMLElement): void {
77
this.element = container;
78
this.element.appendChild(this.toggle.domNode);
79
80
this.updateChecked();
81
this.updateEnabled();
82
}
83
84
protected override updateEnabled(): void {
85
if (this.toggle) {
86
if (this.isEnabled()) {
87
this.toggle.enable();
88
this.element?.classList.remove('disabled');
89
} else {
90
this.toggle.disable();
91
this.element?.classList.add('disabled');
92
}
93
}
94
}
95
96
protected override updateChecked(): void {
97
this.toggle.checked = !!this._action.checked;
98
}
99
100
protected override updateLabel(): void {
101
const title = (<IActionViewItemOptions>this.options).keybinding ?
102
`${this._action.label} (${(<IActionViewItemOptions>this.options).keybinding})` : this._action.label;
103
this.toggle.setTitle(title);
104
}
105
106
override focus(): void {
107
this.toggle.domNode.tabIndex = 0;
108
this.toggle.focus();
109
}
110
111
override blur(): void {
112
this.toggle.domNode.tabIndex = -1;
113
this.toggle.domNode.blur();
114
}
115
116
override setFocusable(focusable: boolean): void {
117
this.toggle.domNode.tabIndex = focusable ? 0 : -1;
118
}
119
120
}
121
122
export class Toggle extends Widget {
123
124
private readonly _onChange = this._register(new Emitter<boolean>());
125
get onChange(): Event<boolean /* via keyboard */> { return this._onChange.event; }
126
127
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
128
get onKeyDown(): Event<IKeyboardEvent> { return this._onKeyDown.event; }
129
130
private readonly _opts: IToggleOpts;
131
private _title: string | IMarkdownString | HTMLElement;
132
private _icon: ThemeIcon | undefined;
133
readonly domNode: HTMLElement;
134
135
private _checked: boolean;
136
137
constructor(opts: IToggleOpts) {
138
super();
139
140
this._opts = opts;
141
this._title = this._opts.title;
142
this._checked = this._opts.isChecked;
143
144
const classes = ['monaco-custom-toggle'];
145
if (this._opts.icon) {
146
this._icon = this._opts.icon;
147
classes.push(...ThemeIcon.asClassNameArray(this._icon));
148
}
149
if (this._opts.actionClassName) {
150
classes.push(...this._opts.actionClassName.split(' '));
151
}
152
if (this._checked) {
153
classes.push('checked');
154
}
155
156
this.domNode = document.createElement('div');
157
this._register(getBaseLayerHoverDelegate().setupDelayedHover(this.domNode, () => ({
158
content: !isMarkdownString(this._title) && !isHTMLElement(this._title) ? stripIcons(this._title) : this._title,
159
style: HoverStyle.Pointer,
160
}), this._opts.hoverLifecycleOptions));
161
this.domNode.classList.add(...classes);
162
if (!this._opts.notFocusable) {
163
this.domNode.tabIndex = 0;
164
}
165
this.domNode.setAttribute('role', 'checkbox');
166
this.domNode.setAttribute('aria-checked', String(this._checked));
167
168
this.setTitle(this._opts.title);
169
this.applyStyles();
170
171
this.onclick(this.domNode, (ev) => {
172
if (this.enabled) {
173
this.checked = !this._checked;
174
this._onChange.fire(false);
175
ev.preventDefault();
176
ev.stopPropagation();
177
}
178
});
179
180
this._register(this.ignoreGesture(this.domNode));
181
182
this.onkeydown(this.domNode, (keyboardEvent) => {
183
if (!this.enabled) {
184
return;
185
}
186
187
if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) {
188
this.checked = !this._checked;
189
this._onChange.fire(true);
190
keyboardEvent.preventDefault();
191
keyboardEvent.stopPropagation();
192
return;
193
}
194
195
this._onKeyDown.fire(keyboardEvent);
196
});
197
}
198
199
get enabled(): boolean {
200
return this.domNode.getAttribute('aria-disabled') !== 'true';
201
}
202
203
focus(): void {
204
this.domNode.focus();
205
}
206
207
get checked(): boolean {
208
return this._checked;
209
}
210
211
set checked(newIsChecked: boolean) {
212
this._checked = newIsChecked;
213
214
this.domNode.setAttribute('aria-checked', String(this._checked));
215
this.domNode.classList.toggle('checked', this._checked);
216
217
this.applyStyles();
218
}
219
220
setIcon(icon: ThemeIcon | undefined): void {
221
if (this._icon) {
222
this.domNode.classList.remove(...ThemeIcon.asClassNameArray(this._icon));
223
}
224
this._icon = icon;
225
if (this._icon) {
226
this.domNode.classList.add(...ThemeIcon.asClassNameArray(this._icon));
227
}
228
}
229
230
width(): number {
231
return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
232
}
233
234
protected applyStyles(): void {
235
if (this.domNode) {
236
this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || '';
237
this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit';
238
this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || '';
239
}
240
}
241
242
enable(): void {
243
this.domNode.setAttribute('aria-disabled', String(false));
244
this.domNode.classList.remove('disabled');
245
}
246
247
disable(): void {
248
this.domNode.setAttribute('aria-disabled', String(true));
249
this.domNode.classList.add('disabled');
250
}
251
252
setTitle(newTitle: string | IMarkdownString | HTMLElement): void {
253
this._title = newTitle;
254
255
const ariaLabel = typeof newTitle === 'string' ? newTitle : isMarkdownString(newTitle) ? newTitle.value : newTitle.textContent;
256
257
this.domNode.setAttribute('aria-label', getCodiconAriaLabel(ariaLabel));
258
}
259
260
set visible(visible: boolean) {
261
this.domNode.style.display = visible ? '' : 'none';
262
}
263
264
get visible() {
265
return this.domNode.style.display !== 'none';
266
}
267
}
268
269
270
abstract class BaseCheckbox extends Widget {
271
static readonly CLASS_NAME = 'monaco-checkbox';
272
273
protected readonly _onChange = this._register(new Emitter<boolean>());
274
readonly onChange: Event<boolean /* via keyboard */> = this._onChange.event;
275
276
constructor(
277
protected readonly checkbox: Toggle,
278
readonly domNode: HTMLElement,
279
protected readonly styles: ICheckboxStyles
280
) {
281
super();
282
283
this.applyStyles();
284
}
285
286
get enabled(): boolean {
287
return this.checkbox.enabled;
288
}
289
290
focus(): void {
291
this.domNode.focus();
292
}
293
294
hasFocus(): boolean {
295
return isActiveElement(this.domNode);
296
}
297
298
enable(): void {
299
this.checkbox.enable();
300
this.applyStyles(true);
301
}
302
303
disable(): void {
304
this.checkbox.disable();
305
this.applyStyles(false);
306
}
307
308
setTitle(newTitle: string): void {
309
this.checkbox.setTitle(newTitle);
310
}
311
312
protected applyStyles(enabled = this.enabled): void {
313
this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || '';
314
this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || '';
315
this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || '';
316
317
const size = this.styles.size || 18;
318
this.domNode.style.width =
319
this.domNode.style.height =
320
this.domNode.style.fontSize = `${size}px`;
321
this.domNode.style.fontSize = `${size - 2}px`;
322
}
323
}
324
325
export class Checkbox extends BaseCheckbox {
326
constructor(title: string, isChecked: boolean, styles: ICheckboxStyles) {
327
const toggle = new Toggle({ title, isChecked, icon: Codicon.check, actionClassName: BaseCheckbox.CLASS_NAME, hoverLifecycleOptions: styles.hoverLifecycleOptions, ...unthemedToggleStyles });
328
super(toggle, toggle.domNode, styles);
329
330
this._register(toggle);
331
this._register(this.checkbox.onChange(keyboard => {
332
this.applyStyles();
333
this._onChange.fire(keyboard);
334
}));
335
}
336
337
get checked(): boolean {
338
return this.checkbox.checked;
339
}
340
341
set checked(newIsChecked: boolean) {
342
this.checkbox.checked = newIsChecked;
343
this.applyStyles();
344
}
345
346
protected override applyStyles(enabled?: boolean): void {
347
if (this.checkbox.checked) {
348
this.checkbox.setIcon(Codicon.check);
349
} else {
350
this.checkbox.setIcon(undefined);
351
}
352
super.applyStyles(enabled);
353
}
354
}
355
356
export class TriStateCheckbox extends BaseCheckbox {
357
constructor(
358
title: string,
359
private _state: boolean | 'mixed',
360
styles: ICheckboxStyles
361
) {
362
let icon: ThemeIcon | undefined;
363
switch (_state) {
364
case true:
365
icon = Codicon.check;
366
break;
367
case 'mixed':
368
icon = Codicon.dash;
369
break;
370
case false:
371
icon = undefined;
372
break;
373
}
374
const checkbox = new Toggle({
375
title,
376
isChecked: _state === true,
377
icon,
378
actionClassName: Checkbox.CLASS_NAME,
379
hoverLifecycleOptions: styles.hoverLifecycleOptions,
380
...unthemedToggleStyles
381
});
382
super(
383
checkbox,
384
checkbox.domNode,
385
styles
386
);
387
388
this._register(checkbox);
389
this._register(this.checkbox.onChange(keyboard => {
390
this._state = this.checkbox.checked;
391
this.applyStyles();
392
this._onChange.fire(keyboard);
393
}));
394
}
395
396
get checked(): boolean | 'mixed' {
397
return this._state;
398
}
399
400
set checked(newState: boolean | 'mixed') {
401
if (this._state !== newState) {
402
this._state = newState;
403
this.checkbox.checked = newState === true;
404
this.applyStyles();
405
}
406
}
407
408
protected override applyStyles(enabled?: boolean): void {
409
switch (this._state) {
410
case true:
411
this.checkbox.setIcon(Codicon.check);
412
break;
413
case 'mixed':
414
this.checkbox.setIcon(Codicon.dash);
415
break;
416
case false:
417
this.checkbox.setIcon(undefined);
418
break;
419
}
420
super.applyStyles(enabled);
421
}
422
}
423
424
export interface ICheckboxActionViewItemOptions extends IActionViewItemOptions {
425
checkboxStyles: ICheckboxStyles;
426
}
427
428
export class CheckboxActionViewItem extends BaseActionViewItem {
429
430
protected readonly toggle: Checkbox;
431
private cssClass?: string;
432
433
constructor(context: unknown, action: IAction, options: ICheckboxActionViewItemOptions) {
434
super(context, action, options);
435
436
this.toggle = this._register(new Checkbox(this._action.label, !!this._action.checked, options.checkboxStyles));
437
this._register(this.toggle.onChange(() => this.onChange()));
438
}
439
440
override render(container: HTMLElement): void {
441
this.element = container;
442
this.element.classList.add('checkbox-action-item');
443
this.element.appendChild(this.toggle.domNode);
444
if ((<IActionViewItemOptions>this.options).label && this._action.label) {
445
const label = this.element.appendChild($('span.checkbox-label', undefined, this._action.label));
446
this._register(addDisposableListener(label, EventType.CLICK, (e: MouseEvent) => {
447
this.toggle.checked = !this.toggle.checked;
448
e.stopPropagation();
449
e.preventDefault();
450
this.onChange();
451
}));
452
}
453
454
this.updateEnabled();
455
this.updateClass();
456
this.updateChecked();
457
}
458
459
private onChange(): void {
460
this._action.checked = !!this.toggle && this.toggle.checked;
461
this.actionRunner.run(this._action, this._context);
462
}
463
464
protected override updateEnabled(): void {
465
if (this.isEnabled()) {
466
this.toggle.enable();
467
} else {
468
this.toggle.disable();
469
}
470
if (this.action.enabled) {
471
this.element?.classList.remove('disabled');
472
} else {
473
this.element?.classList.add('disabled');
474
}
475
}
476
477
protected override updateChecked(): void {
478
this.toggle.checked = !!this._action.checked;
479
}
480
481
protected override updateClass(): void {
482
if (this.cssClass) {
483
this.toggle.domNode.classList.remove(...this.cssClass.split(' '));
484
}
485
this.cssClass = this.getClass();
486
if (this.cssClass) {
487
this.toggle.domNode.classList.add(...this.cssClass.split(' '));
488
}
489
}
490
491
override focus(): void {
492
this.toggle.domNode.tabIndex = 0;
493
this.toggle.focus();
494
}
495
496
override blur(): void {
497
this.toggle.domNode.tabIndex = -1;
498
this.toggle.domNode.blur();
499
}
500
501
override setFocusable(focusable: boolean): void {
502
this.toggle.domNode.tabIndex = focusable ? 0 : -1;
503
}
504
505
}
506
507
/**
508
* Creates an action view item provider that renders toggles for actions with a checked state
509
* and falls back to default button rendering for regular actions.
510
*
511
* @param toggleStyles - Optional styles to apply to toggle items
512
* @returns An IActionViewItemProvider that can be used with ActionBar
513
*/
514
export function createToggleActionViewItemProvider(toggleStyles?: IToggleStyles): IActionViewItemProvider {
515
return (action: IAction, options: IActionViewItemOptions) => {
516
// Only render as a toggle if the action has a checked property
517
if (action.checked !== undefined) {
518
return new ToggleActionViewItem(null, action, { ...options, toggleStyles });
519
}
520
// Return undefined to fall back to default button rendering
521
return undefined;
522
};
523
}
524
525