// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. /** * A variety of convenience methods for maintaining a current selection */ import { ArrayExt } from '@lumino/algorithm'; import { ISignal, Signal } from '@lumino/signaling'; export class Selection { constructor(sequence: ReadonlyArray, options: Selection.IOptions = {}) { this._array = sequence; this._insertBehavior = options.insertBehavior || 'select-item-if-needed'; this._removeBehavior = options.removeBehavior || 'select-item-after'; } /** * A signal emitted when the current item is changed. * * #### Notes * This signal is emitted when the currently selected item is changed either * through user or programmatic interaction. * * Notably, this signal is not emitted when the index of the current item * changes due to other items being inserted, removed, or moved, but the * current item remains the same. It is only emitted when the actual current * item is changed. */ get selectionChanged(): ISignal< Selection, Selection.ISelectionChangedArgs > { return this._selectionChanged; } /** * Adjust for setting an item. * * This should be called *after* the set. * * @param index - The index set. * @param oldValue - The old value at the index. */ adjustSelectionForSet(index: number): void { // We just need to send a signal if the currentValue changed. // Get the current index and value. const pi = this.index; const pv = this.value; // Exit early if this doesn't affect the selection if (index !== pi) { return; } this._updateSelectedValue(); const cv = this.value; // The previous item is now null, since it is no longer in the array. this._previousValue = null; // Send signal if there was a change if (pv !== cv) { // Emit the current changed signal. this._selectionChanged.emit({ previousIndex: pi, previousValue: pv, currentIndex: pi, currentValue: cv, }); } } /** * Get the currently selected item. * * #### Notes * This will be `null` if no item is selected. */ get value(): T | null { return this._value; } /** * Set the currently selected item. * * #### Notes * If the item does not exist in the vector, the currentValue will be set to * `null`. This selects the first entry equal to the desired item. */ set value(value: T | null) { if (value === null || this._array === null) { this.index = null; } else { this.index = ArrayExt.firstIndexOf(this._array, value); } } /** * Get the index of the currently selected item. * * #### Notes * This will be `null` if no item is selected. */ get index(): number | null { return this._index; } /** * Set the index of the currently selected tab. * * @param index - The index to select. * * #### Notes * If the value is out of range, the index will be set to `null`, which * indicates no item is selected. */ set index(index: number | null) { // Coerce the value to an index. let i; if (index !== null && this._array !== null) { i = Math.floor(index); if (i < 0 || i >= this._array.length) { i = null; } } else { i = null; } // Bail early if the index will not change. if (this._index === i) { return; } // Look up the previous index and item. const pi = this._index; const pv = this._value; // Update the state this._index = i; this._updateSelectedValue(); this._previousValue = pv; // Emit the current changed signal. this._selectionChanged.emit({ previousIndex: pi, previousValue: pv, currentIndex: i, currentValue: this._value, }); } /** * Get the selection behavior when inserting a tab. */ get insertBehavior(): Selection.InsertBehavior { return this._insertBehavior; } /** * Set the selection behavior when inserting a tab. */ set insertBehavior(value: Selection.InsertBehavior) { this._insertBehavior = value; } /** * Get the selection behavior when removing a tab. */ get removeBehavior(): Selection.RemoveBehavior { return this._removeBehavior; } /** * Set the selection behavior when removing a tab. */ set removeBehavior(value: Selection.RemoveBehavior) { this._removeBehavior = value; } /** * Adjust the current index for a tab insert operation. * * @param i - The new index of the inserted item. * @param j - The inserted item. * * #### Notes * This method accounts for the tab bar's insertion behavior when adjusting * the current index and emitting the changed signal. This should be called * after the insertion. */ adjustSelectionForInsert(i: number, item: T): void { // Lookup commonly used variables. const cv = this._value; const ci = this._index; const bh = this._insertBehavior; // Handle the behavior where the new item is always selected, // or the behavior where the new item is selected if needed. if ( bh === 'select-item' || (bh === 'select-item-if-needed' && ci === null) ) { this._index = i; this._value = item; this._previousValue = cv; this._selectionChanged.emit({ previousIndex: ci, previousValue: cv, currentIndex: i, currentValue: item, }); return; } // Otherwise, silently adjust the current index if needed. if (ci !== null && ci >= i) { this._index!++; } } /** * Clear the selection and history. */ clearSelection(): void { // Get the current index and item. const pi = this._index; const pv = this._value; // Reset the current index and previous item. this._index = null; this._value = null; this._previousValue = null; // If no item was selected, there's nothing else to do. if (pi === null) { return; } // Emit the current changed signal. this._selectionChanged.emit({ previousIndex: pi, previousValue: pv, currentIndex: this._index, currentValue: this._value, }); } /** * Adjust the current index for an item remove operation. * * @param i - The former index of the removed item. * @param item - The removed item. * * #### Notes * This method accounts for the remove behavior when adjusting the current * index and emitting the changed signal. It should be called after the item * is removed. */ adjustSelectionForRemove(i: number, item: T | null): void { // If we have no selection, there is nothing to do if (this._index === null) { return; } // Lookup commonly used variables. const ci = this._index; const bh = this._removeBehavior; // Silently adjust the index if the current item is not removed. if (ci !== i) { if (ci > i) { this._index!--; } return; } // No item gets selected if the vector is empty. if (!this._array || this._array.length === 0) { // Reset the current index and previous item. this._index = null; this._value = null; this._previousValue = null; this._selectionChanged.emit({ previousIndex: i, previousValue: item, currentIndex: this._index, currentValue: this._value, }); return; } // Handle behavior where the next sibling item is selected. if (bh === 'select-item-after') { this._index = Math.min(i, this._array.length - 1); this._updateSelectedValue(); this._previousValue = null; this._selectionChanged.emit({ previousIndex: i, previousValue: item, currentIndex: this._index, currentValue: this._value, }); return; } // Handle behavior where the previous sibling item is selected. if (bh === 'select-item-before') { this._index = Math.max(0, i - 1); this._updateSelectedValue(); this._previousValue = null; this._selectionChanged.emit({ previousIndex: i, previousValue: item, currentIndex: this._index, currentValue: this._value, }); return; } // Handle behavior where the previous history item is selected. if (bh === 'select-previous-item') { if (this._previousValue) { this.value = this._previousValue; } else { this._index = Math.min(i, this._array.length - 1); this._updateSelectedValue(); } this._previousValue = null; this._selectionChanged.emit({ previousIndex: i, previousValue: item, currentIndex: this._index, currentValue: this.value, }); return; } // Otherwise, no item gets selected. this._index = null; this._value = null; this._previousValue = null; this._selectionChanged.emit({ previousIndex: i, previousValue: item, currentIndex: this._index, currentValue: this._value, }); } /** * Set the current value based on the current index. */ private _updateSelectedValue(): void { const i = this._index; this._value = i !== null && this._array ? this._array[i] : null; } private _array: ReadonlyArray | null = null; private _index: number | null; private _value: T | null = null; private _previousValue: T | null = null; private _insertBehavior: Selection.InsertBehavior; private _removeBehavior: Selection.RemoveBehavior; private _selectionChanged = new Signal< Selection, Selection.ISelectionChangedArgs >(this); } export namespace Selection { /** * An options object for creating a tab bar. */ export interface IOptions { /** * The selection behavior when inserting a tab. * * The default is `'select-tab-if-needed'`. */ insertBehavior?: Selection.InsertBehavior; /** * The selection behavior when removing a tab. * * The default is `'select-tab-after'`. */ removeBehavior?: Selection.RemoveBehavior; } /** * The arguments object for the `currentChanged` signal. */ export interface ISelectionChangedArgs { /** * The previously selected index. */ previousIndex: number | null; /** * The previous selected item. */ previousValue: T | null; /** * The currently selected index. */ currentIndex: number | null; /** * The currently selected item. */ currentValue: T | null; } /** * A type alias for the selection behavior on item insert. */ export type InsertBehavior = /** * The selected item will not be changed. */ | 'none' /** * The inserted item will be selected. */ | 'select-item' /** * The inserted item will be selected if the current item is null. */ | 'select-item-if-needed'; /** * A type alias for the selection behavior on item remove. */ export type RemoveBehavior = /** * No item will be selected. */ | 'none' /** * The item after the removed item will be selected if possible. */ | 'select-item-after' /** * The item before the removed item will be selected if possible. */ | 'select-item-before' /** * The previously selected item will be selected if possible. */ | 'select-previous-item'; }