UNPKG

monaco-editor-core

Version:

A browser based code editor

418 lines (417 loc) • 18.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as DOM from '../../dom.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { ActionViewItem, BaseActionViewItem } from './actionViewItems.js'; import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js'; import { ActionRunner, Separator } from '../../../common/actions.js'; import { Emitter } from '../../../common/event.js'; import { Disposable, DisposableMap, DisposableStore, dispose } from '../../../common/lifecycle.js'; import * as types from '../../../common/types.js'; import './actionbar.css'; export class ActionBar extends Disposable { constructor(container, options = {}) { super(); this._actionRunnerDisposables = this._register(new DisposableStore()); this.viewItemDisposables = this._register(new DisposableMap()); // Trigger Key Tracking this.triggerKeyDown = false; this.focusable = true; this._onDidBlur = this._register(new Emitter()); this.onDidBlur = this._onDidBlur.event; this._onDidCancel = this._register(new Emitter({ onWillAddFirstListener: () => this.cancelHasListener = true })); this.onDidCancel = this._onDidCancel.event; this.cancelHasListener = false; this._onDidRun = this._register(new Emitter()); this.onDidRun = this._onDidRun.event; this._onWillRun = this._register(new Emitter()); this.onWillRun = this._onWillRun.event; this.options = options; this._context = options.context ?? null; this._orientation = this.options.orientation ?? 0 /* ActionsOrientation.HORIZONTAL */; this._triggerKeys = { keyDown: this.options.triggerKeys?.keyDown ?? false, keys: this.options.triggerKeys?.keys ?? [3 /* KeyCode.Enter */, 10 /* KeyCode.Space */] }; this._hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate()); if (this.options.actionRunner) { this._actionRunner = this.options.actionRunner; } else { this._actionRunner = new ActionRunner(); this._actionRunnerDisposables.add(this._actionRunner); } this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e))); this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e))); this.viewItems = []; this.focusedItem = undefined; this.domNode = document.createElement('div'); this.domNode.className = 'monaco-action-bar'; let previousKeys; let nextKeys; switch (this._orientation) { case 0 /* ActionsOrientation.HORIZONTAL */: previousKeys = [15 /* KeyCode.LeftArrow */]; nextKeys = [17 /* KeyCode.RightArrow */]; break; case 1 /* ActionsOrientation.VERTICAL */: previousKeys = [16 /* KeyCode.UpArrow */]; nextKeys = [18 /* KeyCode.DownArrow */]; this.domNode.className += ' vertical'; break; } this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); let eventHandled = true; const focusedItem = typeof this.focusedItem === 'number' ? this.viewItems[this.focusedItem] : undefined; if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) { eventHandled = this.focusPrevious(); } else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) { eventHandled = this.focusNext(); } else if (event.equals(9 /* KeyCode.Escape */) && this.cancelHasListener) { this._onDidCancel.fire(); } else if (event.equals(14 /* KeyCode.Home */)) { eventHandled = this.focusFirst(); } else if (event.equals(13 /* KeyCode.End */)) { eventHandled = this.focusLast(); } else if (event.equals(2 /* KeyCode.Tab */) && focusedItem instanceof BaseActionViewItem && focusedItem.trapsArrowNavigation) { // Tab, so forcibly focus next #219199 eventHandled = this.focusNext(undefined, true); } else if (this.isTriggerKeyEvent(event)) { // Staying out of the else branch even if not triggered if (this._triggerKeys.keyDown) { this.doTrigger(event); } else { this.triggerKeyDown = true; } } else { eventHandled = false; } if (eventHandled) { event.preventDefault(); event.stopPropagation(); } })); this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_UP, e => { const event = new StandardKeyboardEvent(e); // Run action on Enter/Space if (this.isTriggerKeyEvent(event)) { if (!this._triggerKeys.keyDown && this.triggerKeyDown) { this.triggerKeyDown = false; this.doTrigger(event); } event.preventDefault(); event.stopPropagation(); } // Recompute focused item else if (event.equals(2 /* KeyCode.Tab */) || event.equals(1024 /* KeyMod.Shift */ | 2 /* KeyCode.Tab */) || event.equals(16 /* KeyCode.UpArrow */) || event.equals(18 /* KeyCode.DownArrow */) || event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */)) { this.updateFocusedItem(); } })); this.focusTracker = this._register(DOM.trackFocus(this.domNode)); this._register(this.focusTracker.onDidBlur(() => { if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) { this._onDidBlur.fire(); this.previouslyFocusedItem = this.focusedItem; this.focusedItem = undefined; this.triggerKeyDown = false; } })); this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem())); this.actionsList = document.createElement('ul'); this.actionsList.className = 'actions-container'; if (this.options.highlightToggledItems) { this.actionsList.classList.add('highlight-toggled'); } this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar'); if (this.options.ariaLabel) { this.actionsList.setAttribute('aria-label', this.options.ariaLabel); } this.domNode.appendChild(this.actionsList); container.appendChild(this.domNode); } refreshRole() { if (this.length() >= 1) { this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar'); } else { this.actionsList.setAttribute('role', 'presentation'); } } // Some action bars should not be focusable at times // When an action bar is not focusable make sure to make all the elements inside it not focusable // When an action bar is focusable again, make sure the first item can be focused setFocusable(focusable) { this.focusable = focusable; if (this.focusable) { const firstEnabled = this.viewItems.find(vi => vi instanceof BaseActionViewItem && vi.isEnabled()); if (firstEnabled instanceof BaseActionViewItem) { firstEnabled.setFocusable(true); } } else { this.viewItems.forEach(vi => { if (vi instanceof BaseActionViewItem) { vi.setFocusable(false); } }); } } isTriggerKeyEvent(event) { let ret = false; this._triggerKeys.keys.forEach(keyCode => { ret = ret || event.equals(keyCode); }); return ret; } updateFocusedItem() { for (let i = 0; i < this.actionsList.children.length; i++) { const elem = this.actionsList.children[i]; if (DOM.isAncestor(DOM.getActiveElement(), elem)) { this.focusedItem = i; this.viewItems[this.focusedItem]?.showHover?.(); break; } } } get context() { return this._context; } set context(context) { this._context = context; this.viewItems.forEach(i => i.setActionContext(context)); } get actionRunner() { return this._actionRunner; } set actionRunner(actionRunner) { this._actionRunner = actionRunner; // when setting a new `IActionRunner` make sure to dispose old listeners and // start to forward events from the new listener this._actionRunnerDisposables.clear(); this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e))); this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e))); this.viewItems.forEach(item => item.actionRunner = actionRunner); } getContainer() { return this.domNode; } getAction(indexOrElement) { // by index if (typeof indexOrElement === 'number') { return this.viewItems[indexOrElement]?.action; } // by element if (DOM.isHTMLElement(indexOrElement)) { while (indexOrElement.parentElement !== this.actionsList) { if (!indexOrElement.parentElement) { return undefined; } indexOrElement = indexOrElement.parentElement; } for (let i = 0; i < this.actionsList.childNodes.length; i++) { if (this.actionsList.childNodes[i] === indexOrElement) { return this.viewItems[i].action; } } } return undefined; } push(arg, options = {}) { const actions = Array.isArray(arg) ? arg : [arg]; let index = types.isNumber(options.index) ? options.index : null; actions.forEach((action) => { const actionViewItemElement = document.createElement('li'); actionViewItemElement.className = 'action-item'; actionViewItemElement.setAttribute('role', 'presentation'); let item; const viewItemOptions = { hoverDelegate: this._hoverDelegate, ...options, isTabList: this.options.ariaRole === 'tablist' }; if (this.options.actionViewItemProvider) { item = this.options.actionViewItemProvider(action, viewItemOptions); } if (!item) { item = new ActionViewItem(this.context, action, viewItemOptions); } // Prevent native context menu on actions if (!this.options.allowContextMenu) { this.viewItemDisposables.set(item, DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e) => { DOM.EventHelper.stop(e, true); })); } item.actionRunner = this._actionRunner; item.setActionContext(this.context); item.render(actionViewItemElement); if (this.focusable && item instanceof BaseActionViewItem && this.viewItems.length === 0) { // We need to allow for the first enabled item to be focused on using tab navigation #106441 item.setFocusable(true); } if (index === null || index < 0 || index >= this.actionsList.children.length) { this.actionsList.appendChild(actionViewItemElement); this.viewItems.push(item); } else { this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]); this.viewItems.splice(index, 0, item); index++; } }); if (typeof this.focusedItem === 'number') { // After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128 this.focus(this.focusedItem); } this.refreshRole(); } clear() { if (this.isEmpty()) { return; } this.viewItems = dispose(this.viewItems); this.viewItemDisposables.clearAndDisposeAll(); DOM.clearNode(this.actionsList); this.refreshRole(); } length() { return this.viewItems.length; } isEmpty() { return this.viewItems.length === 0; } focus(arg) { let selectFirst = false; let index = undefined; if (arg === undefined) { selectFirst = true; } else if (typeof arg === 'number') { index = arg; } else if (typeof arg === 'boolean') { selectFirst = arg; } if (selectFirst && typeof this.focusedItem === 'undefined') { const firstEnabled = this.viewItems.findIndex(item => item.isEnabled()); // Focus the first enabled item this.focusedItem = firstEnabled === -1 ? undefined : firstEnabled; this.updateFocus(undefined, undefined, true); } else { if (index !== undefined) { this.focusedItem = index; } this.updateFocus(undefined, undefined, true); } } focusFirst() { this.focusedItem = this.length() - 1; return this.focusNext(true); } focusLast() { this.focusedItem = 0; return this.focusPrevious(true); } focusNext(forceLoop, forceFocus) { if (typeof this.focusedItem === 'undefined') { this.focusedItem = this.viewItems.length - 1; } else if (this.viewItems.length <= 1) { return false; } const startIndex = this.focusedItem; let item; do { if (!forceLoop && this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) { this.focusedItem = startIndex; return false; } this.focusedItem = (this.focusedItem + 1) % this.viewItems.length; item = this.viewItems[this.focusedItem]; } while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID)); this.updateFocus(undefined, undefined, forceFocus); return true; } focusPrevious(forceLoop) { if (typeof this.focusedItem === 'undefined') { this.focusedItem = 0; } else if (this.viewItems.length <= 1) { return false; } const startIndex = this.focusedItem; let item; do { this.focusedItem = this.focusedItem - 1; if (this.focusedItem < 0) { if (!forceLoop && this.options.preventLoopNavigation) { this.focusedItem = startIndex; return false; } this.focusedItem = this.viewItems.length - 1; } item = this.viewItems[this.focusedItem]; } while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID)); this.updateFocus(true); return true; } updateFocus(fromRight, preventScroll, forceFocus = false) { if (typeof this.focusedItem === 'undefined') { this.actionsList.focus({ preventScroll }); } if (this.previouslyFocusedItem !== undefined && this.previouslyFocusedItem !== this.focusedItem) { this.viewItems[this.previouslyFocusedItem]?.blur(); } const actionViewItem = this.focusedItem !== undefined ? this.viewItems[this.focusedItem] : undefined; if (actionViewItem) { let focusItem = true; if (!types.isFunction(actionViewItem.focus)) { focusItem = false; } if (this.options.focusOnlyEnabledItems && types.isFunction(actionViewItem.isEnabled) && !actionViewItem.isEnabled()) { focusItem = false; } if (actionViewItem.action.id === Separator.ID) { focusItem = false; } if (!focusItem) { this.actionsList.focus({ preventScroll }); this.previouslyFocusedItem = undefined; } else if (forceFocus || this.previouslyFocusedItem !== this.focusedItem) { actionViewItem.focus(fromRight); this.previouslyFocusedItem = this.focusedItem; } if (focusItem) { actionViewItem.showHover?.(); } } } doTrigger(event) { if (typeof this.focusedItem === 'undefined') { return; //nothing to focus } // trigger action const actionViewItem = this.viewItems[this.focusedItem]; if (actionViewItem instanceof BaseActionViewItem) { const context = (actionViewItem._context === null || actionViewItem._context === undefined) ? event : actionViewItem._context; this.run(actionViewItem._action, context); } } async run(action, context) { await this._actionRunner.run(action, context); } dispose() { this._context = undefined; this.viewItems = dispose(this.viewItems); this.getContainer().remove(); super.dispose(); } }