Path: blob/main/src/vs/base/browser/ui/toggle/toggle.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IAction } from '../../../common/actions.js';6import { Codicon } from '../../../common/codicons.js';7import { Emitter, Event } from '../../../common/event.js';8import { KeyCode } from '../../../common/keyCodes.js';9import { ThemeIcon } from '../../../common/themables.js';10import { $, addDisposableListener, EventType, isActiveElement } from '../../dom.js';11import { IKeyboardEvent } from '../../keyboardEvent.js';12import { BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js';13import type { IManagedHover } from '../hover/hover.js';14import { IHoverDelegate } from '../hover/hoverDelegate.js';15import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';16import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';17import { Widget } from '../widget.js';18import './toggle.css';1920export interface IToggleOpts extends IToggleStyles {21readonly actionClassName?: string;22readonly icon?: ThemeIcon;23readonly title: string;24readonly isChecked: boolean;25readonly notFocusable?: boolean;26readonly hoverDelegate?: IHoverDelegate;27}2829export interface IToggleStyles {30readonly inputActiveOptionBorder: string | undefined;31readonly inputActiveOptionForeground: string | undefined;32readonly inputActiveOptionBackground: string | undefined;33}3435export interface ICheckboxStyles {36readonly checkboxBackground: string | undefined;37readonly checkboxBorder: string | undefined;38readonly checkboxForeground: string | undefined;39readonly checkboxDisabledBackground: string | undefined;40readonly checkboxDisabledForeground: string | undefined;41readonly size?: number;42readonly hoverDelegate?: IHoverDelegate;43}4445export const unthemedToggleStyles = {46inputActiveOptionBorder: '#007ACC00',47inputActiveOptionForeground: '#FFFFFF',48inputActiveOptionBackground: '#0E639C50'49};5051export class ToggleActionViewItem extends BaseActionViewItem {5253protected readonly toggle: Toggle;5455constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {56super(context, action, options);5758const title = (<IActionViewItemOptions>this.options).keybinding ?59`${this._action.label} (${(<IActionViewItemOptions>this.options).keybinding})` : this._action.label;60this.toggle = this._register(new Toggle({61actionClassName: this._action.class,62isChecked: !!this._action.checked,63title,64notFocusable: true,65inputActiveOptionBackground: options.toggleStyles?.inputActiveOptionBackground,66inputActiveOptionBorder: options.toggleStyles?.inputActiveOptionBorder,67inputActiveOptionForeground: options.toggleStyles?.inputActiveOptionForeground,68hoverDelegate: options.hoverDelegate69}));70this._register(this.toggle.onChange(() => {71this._action.checked = !!this.toggle && this.toggle.checked;72}));73}7475override render(container: HTMLElement): void {76this.element = container;77this.element.appendChild(this.toggle.domNode);7879this.updateChecked();80this.updateEnabled();81}8283protected override updateEnabled(): void {84if (this.toggle) {85if (this.isEnabled()) {86this.toggle.enable();87this.element?.classList.remove('disabled');88} else {89this.toggle.disable();90this.element?.classList.add('disabled');91}92}93}9495protected override updateChecked(): void {96this.toggle.checked = !!this._action.checked;97}9899protected override updateLabel(): void {100const title = (<IActionViewItemOptions>this.options).keybinding ?101`${this._action.label} (${(<IActionViewItemOptions>this.options).keybinding})` : this._action.label;102this.toggle.setTitle(title);103}104105override focus(): void {106this.toggle.domNode.tabIndex = 0;107this.toggle.focus();108}109110override blur(): void {111this.toggle.domNode.tabIndex = -1;112this.toggle.domNode.blur();113}114115override setFocusable(focusable: boolean): void {116this.toggle.domNode.tabIndex = focusable ? 0 : -1;117}118119}120121export class Toggle extends Widget {122123private readonly _onChange = this._register(new Emitter<boolean>());124get onChange(): Event<boolean /* via keyboard */> { return this._onChange.event; }125126private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());127get onKeyDown(): Event<IKeyboardEvent> { return this._onKeyDown.event; }128129private readonly _opts: IToggleOpts;130private _icon: ThemeIcon | undefined;131readonly domNode: HTMLElement;132133private _checked: boolean;134private _hover: IManagedHover;135136constructor(opts: IToggleOpts) {137super();138139this._opts = opts;140this._checked = this._opts.isChecked;141142const classes = ['monaco-custom-toggle'];143if (this._opts.icon) {144this._icon = this._opts.icon;145classes.push(...ThemeIcon.asClassNameArray(this._icon));146}147if (this._opts.actionClassName) {148classes.push(...this._opts.actionClassName.split(' '));149}150if (this._checked) {151classes.push('checked');152}153154this.domNode = document.createElement('div');155this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title));156this.domNode.classList.add(...classes);157if (!this._opts.notFocusable) {158this.domNode.tabIndex = 0;159}160this.domNode.setAttribute('role', 'checkbox');161this.domNode.setAttribute('aria-checked', String(this._checked));162this.domNode.setAttribute('aria-label', this._opts.title);163164this.applyStyles();165166this.onclick(this.domNode, (ev) => {167if (this.enabled) {168this.checked = !this._checked;169this._onChange.fire(false);170ev.preventDefault();171}172});173174this._register(this.ignoreGesture(this.domNode));175176this.onkeydown(this.domNode, (keyboardEvent) => {177if (!this.enabled) {178return;179}180181if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) {182this.checked = !this._checked;183this._onChange.fire(true);184keyboardEvent.preventDefault();185keyboardEvent.stopPropagation();186return;187}188189this._onKeyDown.fire(keyboardEvent);190});191}192193get enabled(): boolean {194return this.domNode.getAttribute('aria-disabled') !== 'true';195}196197focus(): void {198this.domNode.focus();199}200201get checked(): boolean {202return this._checked;203}204205set checked(newIsChecked: boolean) {206this._checked = newIsChecked;207208this.domNode.setAttribute('aria-checked', String(this._checked));209this.domNode.classList.toggle('checked', this._checked);210211this.applyStyles();212}213214setIcon(icon: ThemeIcon | undefined): void {215if (this._icon) {216this.domNode.classList.remove(...ThemeIcon.asClassNameArray(this._icon));217}218this._icon = icon;219if (this._icon) {220this.domNode.classList.add(...ThemeIcon.asClassNameArray(this._icon));221}222}223224width(): number {225return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;226}227228protected applyStyles(): void {229if (this.domNode) {230this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || '';231this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit';232this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || '';233}234}235236enable(): void {237this.domNode.setAttribute('aria-disabled', String(false));238this.domNode.classList.remove('disabled');239}240241disable(): void {242this.domNode.setAttribute('aria-disabled', String(true));243this.domNode.classList.add('disabled');244}245246setTitle(newTitle: string): void {247this._hover.update(newTitle);248this.domNode.setAttribute('aria-label', newTitle);249}250251set visible(visible: boolean) {252this.domNode.style.display = visible ? '' : 'none';253}254255get visible() {256return this.domNode.style.display !== 'none';257}258}259260261abstract class BaseCheckbox extends Widget {262static readonly CLASS_NAME = 'monaco-checkbox';263264protected readonly _onChange = this._register(new Emitter<boolean>());265readonly onChange: Event<boolean /* via keyboard */> = this._onChange.event;266267constructor(268protected readonly checkbox: Toggle,269readonly domNode: HTMLElement,270protected readonly styles: ICheckboxStyles271) {272super();273274this.applyStyles();275}276277get enabled(): boolean {278return this.checkbox.enabled;279}280281focus(): void {282this.domNode.focus();283}284285hasFocus(): boolean {286return isActiveElement(this.domNode);287}288289enable(): void {290this.checkbox.enable();291this.applyStyles(true);292}293294disable(): void {295this.checkbox.disable();296this.applyStyles(false);297}298299setTitle(newTitle: string): void {300this.checkbox.setTitle(newTitle);301}302303protected applyStyles(enabled = this.enabled): void {304this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || '';305this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || '';306this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || '';307308const size = this.styles.size || 18;309this.domNode.style.width =310this.domNode.style.height =311this.domNode.style.fontSize = `${size}px`;312this.domNode.style.fontSize = `${size - 2}px`;313}314}315316export class Checkbox extends BaseCheckbox {317constructor(title: string, isChecked: boolean, styles: ICheckboxStyles) {318const toggle = new Toggle({ title, isChecked, icon: Codicon.check, actionClassName: BaseCheckbox.CLASS_NAME, hoverDelegate: styles.hoverDelegate, ...unthemedToggleStyles });319super(toggle, toggle.domNode, styles);320321this._register(toggle);322this._register(this.checkbox.onChange(keyboard => {323this.applyStyles();324this._onChange.fire(keyboard);325}));326}327328get checked(): boolean {329return this.checkbox.checked;330}331332set checked(newIsChecked: boolean) {333this.checkbox.checked = newIsChecked;334this.applyStyles();335}336337protected override applyStyles(enabled?: boolean): void {338if (this.checkbox.checked) {339this.checkbox.setIcon(Codicon.check);340} else {341this.checkbox.setIcon(undefined);342}343super.applyStyles(enabled);344}345}346347export class TriStateCheckbox extends BaseCheckbox {348constructor(349title: string,350private _state: boolean | 'partial',351styles: ICheckboxStyles352) {353let icon: ThemeIcon | undefined;354switch (_state) {355case true:356icon = Codicon.check;357break;358case 'partial':359icon = Codicon.dash;360break;361case false:362icon = undefined;363break;364}365const checkbox = new Toggle({366title,367isChecked: _state === true,368icon,369actionClassName: Checkbox.CLASS_NAME,370hoverDelegate: styles.hoverDelegate,371...unthemedToggleStyles372});373super(374checkbox,375checkbox.domNode,376styles377);378379this._register(checkbox);380this._register(this.checkbox.onChange(keyboard => {381this._state = this.checkbox.checked;382this.applyStyles();383this._onChange.fire(keyboard);384}));385}386387get checked(): boolean | 'partial' {388return this._state;389}390391set checked(newState: boolean | 'partial') {392if (this._state !== newState) {393this._state = newState;394this.checkbox.checked = newState === true;395this.applyStyles();396}397}398399protected override applyStyles(enabled?: boolean): void {400switch (this._state) {401case true:402this.checkbox.setIcon(Codicon.check);403break;404case 'partial':405this.checkbox.setIcon(Codicon.dash);406break;407case false:408this.checkbox.setIcon(undefined);409break;410}411super.applyStyles(enabled);412}413}414415export interface ICheckboxActionViewItemOptions extends IActionViewItemOptions {416checkboxStyles: ICheckboxStyles;417}418419export class CheckboxActionViewItem extends BaseActionViewItem {420421protected readonly toggle: Checkbox;422private cssClass?: string;423424constructor(context: unknown, action: IAction, options: ICheckboxActionViewItemOptions) {425super(context, action, options);426427this.toggle = this._register(new Checkbox(this._action.label, !!this._action.checked, options.checkboxStyles));428this._register(this.toggle.onChange(() => this.onChange()));429}430431override render(container: HTMLElement): void {432this.element = container;433this.element.classList.add('checkbox-action-item');434this.element.appendChild(this.toggle.domNode);435if ((<IActionViewItemOptions>this.options).label && this._action.label) {436const label = this.element.appendChild($('span.checkbox-label', undefined, this._action.label));437this._register(addDisposableListener(label, EventType.CLICK, (e: MouseEvent) => {438this.toggle.checked = !this.toggle.checked;439e.stopPropagation();440e.preventDefault();441this.onChange();442}));443}444445this.updateEnabled();446this.updateClass();447this.updateChecked();448}449450private onChange(): void {451this._action.checked = !!this.toggle && this.toggle.checked;452this.actionRunner.run(this._action, this._context);453}454455protected override updateEnabled(): void {456if (this.isEnabled()) {457this.toggle.enable();458} else {459this.toggle.disable();460}461if (this.action.enabled) {462this.element?.classList.remove('disabled');463} else {464this.element?.classList.add('disabled');465}466}467468protected override updateChecked(): void {469this.toggle.checked = !!this._action.checked;470}471472protected override updateClass(): void {473if (this.cssClass) {474this.toggle.domNode.classList.remove(...this.cssClass.split(' '));475}476this.cssClass = this.getClass();477if (this.cssClass) {478this.toggle.domNode.classList.add(...this.cssClass.split(' '));479}480}481482override focus(): void {483this.toggle.domNode.tabIndex = 0;484this.toggle.focus();485}486487override blur(): void {488this.toggle.domNode.tabIndex = -1;489this.toggle.domNode.blur();490}491492override setFocusable(focusable: boolean): void {493this.toggle.domNode.tabIndex = focusable ? 0 : -1;494}495496}497498499