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
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 { IAction } from '../../../common/actions.js';
7
import { Codicon } from '../../../common/codicons.js';
8
import { Emitter, Event } from '../../../common/event.js';
9
import { KeyCode } from '../../../common/keyCodes.js';
10
import { ThemeIcon } from '../../../common/themables.js';
11
import { $, addDisposableListener, EventType, isActiveElement } from '../../dom.js';
12
import { IKeyboardEvent } from '../../keyboardEvent.js';
13
import { BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js';
14
import type { IManagedHover } from '../hover/hover.js';
15
import { IHoverDelegate } from '../hover/hoverDelegate.js';
16
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
17
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
18
import { Widget } from '../widget.js';
19
import './toggle.css';
20
21
export interface IToggleOpts extends IToggleStyles {
22
readonly actionClassName?: string;
23
readonly icon?: ThemeIcon;
24
readonly title: string;
25
readonly isChecked: boolean;
26
readonly notFocusable?: boolean;
27
readonly hoverDelegate?: IHoverDelegate;
28
}
29
30
export interface IToggleStyles {
31
readonly inputActiveOptionBorder: string | undefined;
32
readonly inputActiveOptionForeground: string | undefined;
33
readonly inputActiveOptionBackground: string | undefined;
34
}
35
36
export interface ICheckboxStyles {
37
readonly checkboxBackground: string | undefined;
38
readonly checkboxBorder: string | undefined;
39
readonly checkboxForeground: string | undefined;
40
readonly checkboxDisabledBackground: string | undefined;
41
readonly checkboxDisabledForeground: string | undefined;
42
readonly size?: number;
43
readonly hoverDelegate?: IHoverDelegate;
44
}
45
46
export const unthemedToggleStyles = {
47
inputActiveOptionBorder: '#007ACC00',
48
inputActiveOptionForeground: '#FFFFFF',
49
inputActiveOptionBackground: '#0E639C50'
50
};
51
52
export class ToggleActionViewItem extends BaseActionViewItem {
53
54
protected readonly toggle: Toggle;
55
56
constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {
57
super(context, action, options);
58
59
const title = (<IActionViewItemOptions>this.options).keybinding ?
60
`${this._action.label} (${(<IActionViewItemOptions>this.options).keybinding})` : this._action.label;
61
this.toggle = this._register(new Toggle({
62
actionClassName: this._action.class,
63
isChecked: !!this._action.checked,
64
title,
65
notFocusable: true,
66
inputActiveOptionBackground: options.toggleStyles?.inputActiveOptionBackground,
67
inputActiveOptionBorder: options.toggleStyles?.inputActiveOptionBorder,
68
inputActiveOptionForeground: options.toggleStyles?.inputActiveOptionForeground,
69
hoverDelegate: options.hoverDelegate
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 _icon: ThemeIcon | undefined;
132
readonly domNode: HTMLElement;
133
134
private _checked: boolean;
135
private _hover: IManagedHover;
136
137
constructor(opts: IToggleOpts) {
138
super();
139
140
this._opts = opts;
141
this._checked = this._opts.isChecked;
142
143
const classes = ['monaco-custom-toggle'];
144
if (this._opts.icon) {
145
this._icon = this._opts.icon;
146
classes.push(...ThemeIcon.asClassNameArray(this._icon));
147
}
148
if (this._opts.actionClassName) {
149
classes.push(...this._opts.actionClassName.split(' '));
150
}
151
if (this._checked) {
152
classes.push('checked');
153
}
154
155
this.domNode = document.createElement('div');
156
this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title));
157
this.domNode.classList.add(...classes);
158
if (!this._opts.notFocusable) {
159
this.domNode.tabIndex = 0;
160
}
161
this.domNode.setAttribute('role', 'checkbox');
162
this.domNode.setAttribute('aria-checked', String(this._checked));
163
this.domNode.setAttribute('aria-label', this._opts.title);
164
165
this.applyStyles();
166
167
this.onclick(this.domNode, (ev) => {
168
if (this.enabled) {
169
this.checked = !this._checked;
170
this._onChange.fire(false);
171
ev.preventDefault();
172
}
173
});
174
175
this._register(this.ignoreGesture(this.domNode));
176
177
this.onkeydown(this.domNode, (keyboardEvent) => {
178
if (!this.enabled) {
179
return;
180
}
181
182
if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) {
183
this.checked = !this._checked;
184
this._onChange.fire(true);
185
keyboardEvent.preventDefault();
186
keyboardEvent.stopPropagation();
187
return;
188
}
189
190
this._onKeyDown.fire(keyboardEvent);
191
});
192
}
193
194
get enabled(): boolean {
195
return this.domNode.getAttribute('aria-disabled') !== 'true';
196
}
197
198
focus(): void {
199
this.domNode.focus();
200
}
201
202
get checked(): boolean {
203
return this._checked;
204
}
205
206
set checked(newIsChecked: boolean) {
207
this._checked = newIsChecked;
208
209
this.domNode.setAttribute('aria-checked', String(this._checked));
210
this.domNode.classList.toggle('checked', this._checked);
211
212
this.applyStyles();
213
}
214
215
setIcon(icon: ThemeIcon | undefined): void {
216
if (this._icon) {
217
this.domNode.classList.remove(...ThemeIcon.asClassNameArray(this._icon));
218
}
219
this._icon = icon;
220
if (this._icon) {
221
this.domNode.classList.add(...ThemeIcon.asClassNameArray(this._icon));
222
}
223
}
224
225
width(): number {
226
return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
227
}
228
229
protected applyStyles(): void {
230
if (this.domNode) {
231
this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || '';
232
this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit';
233
this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || '';
234
}
235
}
236
237
enable(): void {
238
this.domNode.setAttribute('aria-disabled', String(false));
239
this.domNode.classList.remove('disabled');
240
}
241
242
disable(): void {
243
this.domNode.setAttribute('aria-disabled', String(true));
244
this.domNode.classList.add('disabled');
245
}
246
247
setTitle(newTitle: string): void {
248
this._hover.update(newTitle);
249
this.domNode.setAttribute('aria-label', newTitle);
250
}
251
252
set visible(visible: boolean) {
253
this.domNode.style.display = visible ? '' : 'none';
254
}
255
256
get visible() {
257
return this.domNode.style.display !== 'none';
258
}
259
}
260
261
262
abstract class BaseCheckbox extends Widget {
263
static readonly CLASS_NAME = 'monaco-checkbox';
264
265
protected readonly _onChange = this._register(new Emitter<boolean>());
266
readonly onChange: Event<boolean /* via keyboard */> = this._onChange.event;
267
268
constructor(
269
protected readonly checkbox: Toggle,
270
readonly domNode: HTMLElement,
271
protected readonly styles: ICheckboxStyles
272
) {
273
super();
274
275
this.applyStyles();
276
}
277
278
get enabled(): boolean {
279
return this.checkbox.enabled;
280
}
281
282
focus(): void {
283
this.domNode.focus();
284
}
285
286
hasFocus(): boolean {
287
return isActiveElement(this.domNode);
288
}
289
290
enable(): void {
291
this.checkbox.enable();
292
this.applyStyles(true);
293
}
294
295
disable(): void {
296
this.checkbox.disable();
297
this.applyStyles(false);
298
}
299
300
setTitle(newTitle: string): void {
301
this.checkbox.setTitle(newTitle);
302
}
303
304
protected applyStyles(enabled = this.enabled): void {
305
this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || '';
306
this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || '';
307
this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || '';
308
309
const size = this.styles.size || 18;
310
this.domNode.style.width =
311
this.domNode.style.height =
312
this.domNode.style.fontSize = `${size}px`;
313
this.domNode.style.fontSize = `${size - 2}px`;
314
}
315
}
316
317
export class Checkbox extends BaseCheckbox {
318
constructor(title: string, isChecked: boolean, styles: ICheckboxStyles) {
319
const toggle = new Toggle({ title, isChecked, icon: Codicon.check, actionClassName: BaseCheckbox.CLASS_NAME, hoverDelegate: styles.hoverDelegate, ...unthemedToggleStyles });
320
super(toggle, toggle.domNode, styles);
321
322
this._register(toggle);
323
this._register(this.checkbox.onChange(keyboard => {
324
this.applyStyles();
325
this._onChange.fire(keyboard);
326
}));
327
}
328
329
get checked(): boolean {
330
return this.checkbox.checked;
331
}
332
333
set checked(newIsChecked: boolean) {
334
this.checkbox.checked = newIsChecked;
335
this.applyStyles();
336
}
337
338
protected override applyStyles(enabled?: boolean): void {
339
if (this.checkbox.checked) {
340
this.checkbox.setIcon(Codicon.check);
341
} else {
342
this.checkbox.setIcon(undefined);
343
}
344
super.applyStyles(enabled);
345
}
346
}
347
348
export class TriStateCheckbox extends BaseCheckbox {
349
constructor(
350
title: string,
351
private _state: boolean | 'partial',
352
styles: ICheckboxStyles
353
) {
354
let icon: ThemeIcon | undefined;
355
switch (_state) {
356
case true:
357
icon = Codicon.check;
358
break;
359
case 'partial':
360
icon = Codicon.dash;
361
break;
362
case false:
363
icon = undefined;
364
break;
365
}
366
const checkbox = new Toggle({
367
title,
368
isChecked: _state === true,
369
icon,
370
actionClassName: Checkbox.CLASS_NAME,
371
hoverDelegate: styles.hoverDelegate,
372
...unthemedToggleStyles
373
});
374
super(
375
checkbox,
376
checkbox.domNode,
377
styles
378
);
379
380
this._register(checkbox);
381
this._register(this.checkbox.onChange(keyboard => {
382
this._state = this.checkbox.checked;
383
this.applyStyles();
384
this._onChange.fire(keyboard);
385
}));
386
}
387
388
get checked(): boolean | 'partial' {
389
return this._state;
390
}
391
392
set checked(newState: boolean | 'partial') {
393
if (this._state !== newState) {
394
this._state = newState;
395
this.checkbox.checked = newState === true;
396
this.applyStyles();
397
}
398
}
399
400
protected override applyStyles(enabled?: boolean): void {
401
switch (this._state) {
402
case true:
403
this.checkbox.setIcon(Codicon.check);
404
break;
405
case 'partial':
406
this.checkbox.setIcon(Codicon.dash);
407
break;
408
case false:
409
this.checkbox.setIcon(undefined);
410
break;
411
}
412
super.applyStyles(enabled);
413
}
414
}
415
416
export interface ICheckboxActionViewItemOptions extends IActionViewItemOptions {
417
checkboxStyles: ICheckboxStyles;
418
}
419
420
export class CheckboxActionViewItem extends BaseActionViewItem {
421
422
protected readonly toggle: Checkbox;
423
private cssClass?: string;
424
425
constructor(context: unknown, action: IAction, options: ICheckboxActionViewItemOptions) {
426
super(context, action, options);
427
428
this.toggle = this._register(new Checkbox(this._action.label, !!this._action.checked, options.checkboxStyles));
429
this._register(this.toggle.onChange(() => this.onChange()));
430
}
431
432
override render(container: HTMLElement): void {
433
this.element = container;
434
this.element.classList.add('checkbox-action-item');
435
this.element.appendChild(this.toggle.domNode);
436
if ((<IActionViewItemOptions>this.options).label && this._action.label) {
437
const label = this.element.appendChild($('span.checkbox-label', undefined, this._action.label));
438
this._register(addDisposableListener(label, EventType.CLICK, (e: MouseEvent) => {
439
this.toggle.checked = !this.toggle.checked;
440
e.stopPropagation();
441
e.preventDefault();
442
this.onChange();
443
}));
444
}
445
446
this.updateEnabled();
447
this.updateClass();
448
this.updateChecked();
449
}
450
451
private onChange(): void {
452
this._action.checked = !!this.toggle && this.toggle.checked;
453
this.actionRunner.run(this._action, this._context);
454
}
455
456
protected override updateEnabled(): void {
457
if (this.isEnabled()) {
458
this.toggle.enable();
459
} else {
460
this.toggle.disable();
461
}
462
if (this.action.enabled) {
463
this.element?.classList.remove('disabled');
464
} else {
465
this.element?.classList.add('disabled');
466
}
467
}
468
469
protected override updateChecked(): void {
470
this.toggle.checked = !!this._action.checked;
471
}
472
473
protected override updateClass(): void {
474
if (this.cssClass) {
475
this.toggle.domNode.classList.remove(...this.cssClass.split(' '));
476
}
477
this.cssClass = this.getClass();
478
if (this.cssClass) {
479
this.toggle.domNode.classList.add(...this.cssClass.split(' '));
480
}
481
}
482
483
override focus(): void {
484
this.toggle.domNode.tabIndex = 0;
485
this.toggle.focus();
486
}
487
488
override blur(): void {
489
this.toggle.domNode.tabIndex = -1;
490
this.toggle.domNode.blur();
491
}
492
493
override setFocusable(focusable: boolean): void {
494
this.toggle.domNode.tabIndex = focusable ? 0 : -1;
495
}
496
497
}
498
499