"Fossies" - the Fresh Open Source Software Archive

Member "vscode-1.49.1/src/vs/base/browser/ui/actionbar/actionbar.ts" (16 Sep 2020, 14147 Bytes) of package /linux/misc/vscode-1.49.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "actionbar.ts": 1.48.2_vs_1.49.0.

    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 'vs/css!./actionbar';
    7 import { Disposable, dispose } from 'vs/base/common/lifecycle';
    8 import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator, IActionViewItem, IActionViewItemProvider } from 'vs/base/common/actions';
    9 import * as DOM from 'vs/base/browser/dom';
   10 import * as types from 'vs/base/common/types';
   11 import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
   12 import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
   13 import { Event, Emitter } from 'vs/base/common/event';
   14 import { IActionViewItemOptions, ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
   15 
   16 export const enum ActionsOrientation {
   17     HORIZONTAL,
   18     HORIZONTAL_REVERSE,
   19     VERTICAL,
   20     VERTICAL_REVERSE,
   21 }
   22 
   23 export interface ActionTrigger {
   24     keys: KeyCode[];
   25     keyDown: boolean;
   26 }
   27 
   28 export interface IActionBarOptions {
   29     readonly orientation?: ActionsOrientation;
   30     readonly context?: any;
   31     readonly actionViewItemProvider?: IActionViewItemProvider;
   32     readonly actionRunner?: IActionRunner;
   33     readonly ariaLabel?: string;
   34     readonly animated?: boolean;
   35     readonly triggerKeys?: ActionTrigger;
   36     readonly allowContextMenu?: boolean;
   37     readonly preventLoopNavigation?: boolean;
   38 }
   39 
   40 export interface IActionOptions extends IActionViewItemOptions {
   41     index?: number;
   42 }
   43 
   44 export class ActionBar extends Disposable implements IActionRunner {
   45 
   46     private readonly options: IActionBarOptions;
   47 
   48     private _actionRunner: IActionRunner;
   49     private _context: unknown;
   50     private _orientation: ActionsOrientation;
   51     private _triggerKeys: ActionTrigger;
   52 
   53     // View Items
   54     viewItems: IActionViewItem[];
   55     protected focusedItem?: number;
   56     private focusTracker: DOM.IFocusTracker;
   57 
   58     // Elements
   59     domNode: HTMLElement;
   60     protected actionsList: HTMLElement;
   61 
   62     private _onDidBlur = this._register(new Emitter<void>());
   63     readonly onDidBlur: Event<void> = this._onDidBlur.event;
   64 
   65     private _onDidCancel = this._register(new Emitter<void>());
   66     readonly onDidCancel: Event<void> = this._onDidCancel.event;
   67 
   68     private _onDidRun = this._register(new Emitter<IRunEvent>());
   69     readonly onDidRun: Event<IRunEvent> = this._onDidRun.event;
   70 
   71     private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
   72     readonly onDidBeforeRun: Event<IRunEvent> = this._onDidBeforeRun.event;
   73 
   74     constructor(container: HTMLElement, options: IActionBarOptions = {}) {
   75         super();
   76 
   77         this.options = options;
   78         this._context = options.context ?? null;
   79         this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL;
   80         this._triggerKeys = this.options.triggerKeys ?? {
   81             keys: [KeyCode.Enter, KeyCode.Space],
   82             keyDown: false
   83         };
   84 
   85         if (this.options.actionRunner) {
   86             this._actionRunner = this.options.actionRunner;
   87         } else {
   88             this._actionRunner = new ActionRunner();
   89             this._register(this._actionRunner);
   90         }
   91 
   92         this._register(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
   93         this._register(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
   94 
   95         this.viewItems = [];
   96         this.focusedItem = undefined;
   97 
   98         this.domNode = document.createElement('div');
   99         this.domNode.className = 'monaco-action-bar';
  100 
  101         if (options.animated !== false) {
  102             DOM.addClass(this.domNode, 'animated');
  103         }
  104 
  105         let previousKey: KeyCode;
  106         let nextKey: KeyCode;
  107 
  108         switch (this._orientation) {
  109             case ActionsOrientation.HORIZONTAL:
  110                 previousKey = KeyCode.LeftArrow;
  111                 nextKey = KeyCode.RightArrow;
  112                 break;
  113             case ActionsOrientation.HORIZONTAL_REVERSE:
  114                 previousKey = KeyCode.RightArrow;
  115                 nextKey = KeyCode.LeftArrow;
  116                 this.domNode.className += ' reverse';
  117                 break;
  118             case ActionsOrientation.VERTICAL:
  119                 previousKey = KeyCode.UpArrow;
  120                 nextKey = KeyCode.DownArrow;
  121                 this.domNode.className += ' vertical';
  122                 break;
  123             case ActionsOrientation.VERTICAL_REVERSE:
  124                 previousKey = KeyCode.DownArrow;
  125                 nextKey = KeyCode.UpArrow;
  126                 this.domNode.className += ' vertical reverse';
  127                 break;
  128         }
  129 
  130         this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_DOWN, e => {
  131             const event = new StandardKeyboardEvent(e);
  132             let eventHandled = true;
  133 
  134             if (event.equals(previousKey)) {
  135                 eventHandled = this.focusPrevious();
  136             } else if (event.equals(nextKey)) {
  137                 eventHandled = this.focusNext();
  138             } else if (event.equals(KeyCode.Escape)) {
  139                 this._onDidCancel.fire();
  140             } else if (this.isTriggerKeyEvent(event)) {
  141                 // Staying out of the else branch even if not triggered
  142                 if (this._triggerKeys.keyDown) {
  143                     this.doTrigger(event);
  144                 }
  145             } else {
  146                 eventHandled = false;
  147             }
  148 
  149             if (eventHandled) {
  150                 event.preventDefault();
  151                 event.stopPropagation();
  152             }
  153         }));
  154 
  155         this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_UP, e => {
  156             const event = new StandardKeyboardEvent(e);
  157 
  158             // Run action on Enter/Space
  159             if (this.isTriggerKeyEvent(event)) {
  160                 if (!this._triggerKeys.keyDown) {
  161                     this.doTrigger(event);
  162                 }
  163 
  164                 event.preventDefault();
  165                 event.stopPropagation();
  166             }
  167 
  168             // Recompute focused item
  169             else if (event.equals(KeyCode.Tab) || event.equals(KeyMod.Shift | KeyCode.Tab)) {
  170                 this.updateFocusedItem();
  171             }
  172         }));
  173 
  174         this.focusTracker = this._register(DOM.trackFocus(this.domNode));
  175         this._register(this.focusTracker.onDidBlur(() => {
  176             if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) {
  177                 this._onDidBlur.fire();
  178                 this.focusedItem = undefined;
  179             }
  180         }));
  181 
  182         this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
  183 
  184         this.actionsList = document.createElement('ul');
  185         this.actionsList.className = 'actions-container';
  186         this.actionsList.setAttribute('role', 'toolbar');
  187 
  188         if (this.options.ariaLabel) {
  189             this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
  190         }
  191 
  192         this.domNode.appendChild(this.actionsList);
  193 
  194         container.appendChild(this.domNode);
  195     }
  196 
  197     setAriaLabel(label: string): void {
  198         if (label) {
  199             this.actionsList.setAttribute('aria-label', label);
  200         } else {
  201             this.actionsList.removeAttribute('aria-label');
  202         }
  203     }
  204 
  205     private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
  206         let ret = false;
  207         this._triggerKeys.keys.forEach(keyCode => {
  208             ret = ret || event.equals(keyCode);
  209         });
  210 
  211         return ret;
  212     }
  213 
  214     private updateFocusedItem(): void {
  215         for (let i = 0; i < this.actionsList.children.length; i++) {
  216             const elem = this.actionsList.children[i];
  217             if (DOM.isAncestor(DOM.getActiveElement(), elem)) {
  218                 this.focusedItem = i;
  219                 break;
  220             }
  221         }
  222     }
  223 
  224     get context(): any {
  225         return this._context;
  226     }
  227 
  228     set context(context: any) {
  229         this._context = context;
  230         this.viewItems.forEach(i => i.setActionContext(context));
  231     }
  232 
  233     get actionRunner(): IActionRunner {
  234         return this._actionRunner;
  235     }
  236 
  237     set actionRunner(actionRunner: IActionRunner) {
  238         if (actionRunner) {
  239             this._actionRunner = actionRunner;
  240             this.viewItems.forEach(item => item.actionRunner = actionRunner);
  241         }
  242     }
  243 
  244     getContainer(): HTMLElement {
  245         return this.domNode;
  246     }
  247 
  248     push(arg: IAction | ReadonlyArray<IAction>, options: IActionOptions = {}): void {
  249         const actions: ReadonlyArray<IAction> = Array.isArray(arg) ? arg : [arg];
  250 
  251         let index = types.isNumber(options.index) ? options.index : null;
  252 
  253         actions.forEach((action: IAction) => {
  254             const actionViewItemElement = document.createElement('li');
  255             actionViewItemElement.className = 'action-item';
  256             actionViewItemElement.setAttribute('role', 'presentation');
  257 
  258             // Prevent native context menu on actions
  259             if (!this.options.allowContextMenu) {
  260                 this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
  261                     DOM.EventHelper.stop(e, true);
  262                 }));
  263             }
  264 
  265             let item: IActionViewItem | undefined;
  266 
  267             if (this.options.actionViewItemProvider) {
  268                 item = this.options.actionViewItemProvider(action);
  269             }
  270 
  271             if (!item) {
  272                 item = new ActionViewItem(this.context, action, options);
  273             }
  274 
  275             item.actionRunner = this._actionRunner;
  276             item.setActionContext(this.context);
  277             item.render(actionViewItemElement);
  278 
  279             if (index === null || index < 0 || index >= this.actionsList.children.length) {
  280                 this.actionsList.appendChild(actionViewItemElement);
  281                 this.viewItems.push(item);
  282             } else {
  283                 this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]);
  284                 this.viewItems.splice(index, 0, item);
  285                 index++;
  286             }
  287         });
  288         if (this.focusedItem) {
  289             // After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
  290             this.focus(this.focusedItem);
  291         }
  292     }
  293 
  294     getWidth(index: number): number {
  295         if (index >= 0 && index < this.actionsList.children.length) {
  296             const item = this.actionsList.children.item(index);
  297             if (item) {
  298                 return item.clientWidth;
  299             }
  300         }
  301 
  302         return 0;
  303     }
  304 
  305     getHeight(index: number): number {
  306         if (index >= 0 && index < this.actionsList.children.length) {
  307             const item = this.actionsList.children.item(index);
  308             if (item) {
  309                 return item.clientHeight;
  310             }
  311         }
  312 
  313         return 0;
  314     }
  315 
  316     pull(index: number): void {
  317         if (index >= 0 && index < this.viewItems.length) {
  318             this.actionsList.removeChild(this.actionsList.childNodes[index]);
  319             dispose(this.viewItems.splice(index, 1));
  320         }
  321     }
  322 
  323     clear(): void {
  324         dispose(this.viewItems);
  325         this.viewItems = [];
  326         DOM.clearNode(this.actionsList);
  327     }
  328 
  329     length(): number {
  330         return this.viewItems.length;
  331     }
  332 
  333     isEmpty(): boolean {
  334         return this.viewItems.length === 0;
  335     }
  336 
  337     focus(index?: number): void;
  338     focus(selectFirst?: boolean): void;
  339     focus(arg?: number | boolean): void {
  340         let selectFirst: boolean = false;
  341         let index: number | undefined = undefined;
  342         if (arg === undefined) {
  343             selectFirst = true;
  344         } else if (typeof arg === 'number') {
  345             index = arg;
  346         } else if (typeof arg === 'boolean') {
  347             selectFirst = arg;
  348         }
  349 
  350         if (selectFirst && typeof this.focusedItem === 'undefined') {
  351             // Focus the first enabled item
  352             this.focusedItem = -1;
  353             this.focusNext();
  354         } else {
  355             if (index !== undefined) {
  356                 this.focusedItem = index;
  357             }
  358 
  359             this.updateFocus();
  360         }
  361     }
  362 
  363     protected focusNext(): boolean {
  364         if (typeof this.focusedItem === 'undefined') {
  365             this.focusedItem = this.viewItems.length - 1;
  366         }
  367 
  368         const startIndex = this.focusedItem;
  369         let item: IActionViewItem;
  370 
  371         do {
  372             if (this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
  373                 this.focusedItem = startIndex;
  374                 return false;
  375             }
  376 
  377             this.focusedItem = (this.focusedItem + 1) % this.viewItems.length;
  378             item = this.viewItems[this.focusedItem];
  379         } while (this.focusedItem !== startIndex && !item.isEnabled());
  380 
  381         if (this.focusedItem === startIndex && !item.isEnabled()) {
  382             this.focusedItem = undefined;
  383         }
  384 
  385         this.updateFocus();
  386         return true;
  387     }
  388 
  389     protected focusPrevious(): boolean {
  390         if (typeof this.focusedItem === 'undefined') {
  391             this.focusedItem = 0;
  392         }
  393 
  394         const startIndex = this.focusedItem;
  395         let item: IActionViewItem;
  396 
  397         do {
  398             this.focusedItem = this.focusedItem - 1;
  399 
  400             if (this.focusedItem < 0) {
  401                 if (this.options.preventLoopNavigation) {
  402                     this.focusedItem = startIndex;
  403                     return false;
  404                 }
  405 
  406                 this.focusedItem = this.viewItems.length - 1;
  407             }
  408 
  409             item = this.viewItems[this.focusedItem];
  410         } while (this.focusedItem !== startIndex && !item.isEnabled());
  411 
  412         if (this.focusedItem === startIndex && !item.isEnabled()) {
  413             this.focusedItem = undefined;
  414         }
  415 
  416         this.updateFocus(true);
  417         return true;
  418     }
  419 
  420     protected updateFocus(fromRight?: boolean, preventScroll?: boolean): void {
  421         if (typeof this.focusedItem === 'undefined') {
  422             this.actionsList.focus({ preventScroll });
  423         }
  424 
  425         for (let i = 0; i < this.viewItems.length; i++) {
  426             const item = this.viewItems[i];
  427             const actionViewItem = item;
  428 
  429             if (i === this.focusedItem) {
  430                 if (types.isFunction(actionViewItem.isEnabled)) {
  431                     if (actionViewItem.isEnabled() && types.isFunction(actionViewItem.focus)) {
  432                         actionViewItem.focus(fromRight);
  433                     } else {
  434                         this.actionsList.focus({ preventScroll });
  435                     }
  436                 }
  437             } else {
  438                 if (types.isFunction(actionViewItem.blur)) {
  439                     actionViewItem.blur();
  440                 }
  441             }
  442         }
  443     }
  444 
  445     private doTrigger(event: StandardKeyboardEvent): void {
  446         if (typeof this.focusedItem === 'undefined') {
  447             return; //nothing to focus
  448         }
  449 
  450         // trigger action
  451         const actionViewItem = this.viewItems[this.focusedItem];
  452         if (actionViewItem instanceof BaseActionViewItem) {
  453             const context = (actionViewItem._context === null || actionViewItem._context === undefined) ? event : actionViewItem._context;
  454             this.run(actionViewItem._action, context);
  455         }
  456     }
  457 
  458     run(action: IAction, context?: unknown): Promise<void> {
  459         return this._actionRunner.run(action, context);
  460     }
  461 
  462     dispose(): void {
  463         dispose(this.viewItems);
  464         this.viewItems = [];
  465 
  466         DOM.removeNode(this.getContainer());
  467 
  468         super.dispose();
  469     }
  470 }
  471 
  472 export function prepareActions(actions: IAction[]): IAction[] {
  473     if (!actions.length) {
  474         return actions;
  475     }
  476 
  477     // Clean up leading separators
  478     let firstIndexOfAction = -1;
  479     for (let i = 0; i < actions.length; i++) {
  480         if (actions[i].id === Separator.ID) {
  481             continue;
  482         }
  483 
  484         firstIndexOfAction = i;
  485         break;
  486     }
  487 
  488     if (firstIndexOfAction === -1) {
  489         return [];
  490     }
  491 
  492     actions = actions.slice(firstIndexOfAction);
  493 
  494     // Clean up trailing separators
  495     for (let h = actions.length - 1; h >= 0; h--) {
  496         const isSeparator = actions[h].id === Separator.ID;
  497         if (isSeparator) {
  498             actions.splice(h, 1);
  499         } else {
  500             break;
  501         }
  502     }
  503 
  504     // Clean up separator duplicates
  505     let foundAction = false;
  506     for (let k = actions.length - 1; k >= 0; k--) {
  507         const isSeparator = actions[k].id === Separator.ID;
  508         if (isSeparator && !foundAction) {
  509             actions.splice(k, 1);
  510         } else if (!isSeparator) {
  511             foundAction = true;
  512         } else if (isSeparator) {
  513             foundAction = false;
  514         }
  515     }
  516 
  517     return actions;
  518 }