@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
225 lines • 9.8 kB
JavaScript
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeSelectionState = exports.FocusableTreeSelection = void 0;
const tree_iterator_1 = require("./tree-iterator");
const tree_selection_1 = require("./tree-selection");
var FocusableTreeSelection;
(function (FocusableTreeSelection) {
/**
* `true` if the argument is a focusable tree selection. Otherwise, `false`.
*/
function is(arg) {
return tree_selection_1.TreeSelection.is(arg) && 'focus' in arg;
}
FocusableTreeSelection.is = is;
/**
* Returns with the tree node that has the focus if the argument is a focusable tree selection.
* Otherwise, returns `undefined`.
*/
function focus(arg) {
return is(arg) ? arg.focus : undefined;
}
FocusableTreeSelection.focus = focus;
})(FocusableTreeSelection = exports.FocusableTreeSelection || (exports.FocusableTreeSelection = {}));
/**
* Class for representing and managing the selection state and the focus of a tree.
*/
class TreeSelectionState {
constructor(tree, selectionStack = []) {
this.tree = tree;
this.selectionStack = selectionStack;
}
nextState(selection) {
const { node, type } = Object.assign({ type: tree_selection_1.TreeSelection.SelectionType.DEFAULT }, selection);
switch (type) {
case tree_selection_1.TreeSelection.SelectionType.DEFAULT: return this.handleDefault(this, node);
case tree_selection_1.TreeSelection.SelectionType.TOGGLE: return this.handleToggle(this, node);
case tree_selection_1.TreeSelection.SelectionType.RANGE: return this.handleRange(this, node);
default: throw new Error(`Unexpected tree selection type: ${type}.`);
}
}
selection() {
const copy = this.checkNoDefaultSelection(this.selectionStack);
const nodeIds = new Set();
for (let i = 0; i < copy.length; i++) {
const { node, type } = copy[i];
if (tree_selection_1.TreeSelection.isRange(type)) {
const selection = copy[i];
for (const id of this.selectionRange(selection).map(n => n.id)) {
nodeIds.add(id);
}
}
else if (tree_selection_1.TreeSelection.isToggle(type)) {
if (nodeIds.has(node.id)) {
nodeIds.delete(node.id);
}
else {
nodeIds.add(node.id);
}
}
}
return Array.from(nodeIds.keys()).map(id => this.tree.getNode(id)).filter(tree_selection_1.SelectableTreeNode.is).reverse();
}
get focus() {
var _a;
const copy = this.checkNoDefaultSelection(this.selectionStack);
const candidate = (_a = copy[copy.length - 1]) === null || _a === void 0 ? void 0 : _a.focus;
return this.toSelectableTreeNode(candidate);
}
get node() {
var _a;
const copy = this.checkNoDefaultSelection(this.selectionStack);
return this.toSelectableTreeNode((_a = copy[copy.length - 1]) === null || _a === void 0 ? void 0 : _a.node);
}
handleDefault(state, node) {
const { tree } = state;
return new TreeSelectionState(tree, [{
node,
type: tree_selection_1.TreeSelection.SelectionType.TOGGLE,
focus: node
}]);
}
handleToggle(state, node) {
const { tree, selectionStack } = state;
const copy = this.checkNoDefaultSelection(selectionStack).slice();
const focus = (() => {
const allRanges = copy.filter(selection => tree_selection_1.TreeSelection.isRange(selection));
for (let i = allRanges.length - 1; i >= 0; i--) {
const latestRangeIndex = copy.indexOf(allRanges[i]);
const latestRangeSelection = copy[latestRangeIndex];
const latestRange = (latestRangeSelection === null || latestRangeSelection === void 0 ? void 0 : latestRangeSelection.focus) ? this.selectionRange(latestRangeSelection) : [];
if (latestRange.indexOf(node) !== -1) {
if (this.focus === latestRangeSelection.focus) {
return latestRangeSelection.focus || node;
}
else {
return this.focus;
}
}
}
return node;
})();
return new TreeSelectionState(tree, [...copy, {
node,
type: tree_selection_1.TreeSelection.SelectionType.TOGGLE,
focus
}]);
}
handleRange(state, node) {
const { tree, selectionStack } = state;
const copy = this.checkNoDefaultSelection(selectionStack).slice();
let focus = FocusableTreeSelection.focus(copy[copy.length - 1]);
// Drop the previous range when we are trying to modify that.
if (tree_selection_1.TreeSelection.isRange(copy[copy.length - 1])) {
const range = this.selectionRange(copy.pop());
// And we drop all preceding individual nodes that were contained in the range we are dropping.
// That means, anytime we cover individual nodes with a range, they will belong to the range so we need to drop them now.
for (let i = copy.length - 1; i >= 0; i--) {
if (range.indexOf(copy[i].node) !== -1) {
// Make sure to keep a reference to the focus while we are discarding previous elements. Otherwise, we lose this information.
focus = copy[i].focus;
copy.splice(i, 1);
}
}
}
return new TreeSelectionState(tree, [...copy, {
node,
type: tree_selection_1.TreeSelection.SelectionType.RANGE,
focus
}]);
}
/**
* Returns with an array of items representing the selection range. The from node is the `focus` the to node
* is the selected node itself on the tree selection. Both the `from` node and the `to` node are inclusive.
*/
selectionRange(selection) {
const fromNode = selection.focus;
const toNode = selection.node;
if (fromNode === undefined) {
return [];
}
if (toNode === fromNode) {
return [toNode];
}
const { root } = this.tree;
if (root === undefined) {
return [];
}
const to = this.tree.validateNode(toNode);
if (to === undefined) {
return [];
}
const from = this.tree.validateNode(fromNode);
if (from === undefined) {
return [];
}
let started = false;
let finished = false;
const range = [];
for (const node of new tree_iterator_1.DepthFirstTreeIterator(root, { pruneCollapsed: true })) {
if (finished) {
break;
}
// Only collect items which are between (inclusive) the `from` node and the `to` node.
if (node === from || node === to) {
if (started) {
finished = true;
}
else {
started = true;
}
}
if (started) {
range.push(node);
}
}
// We need to reverse the selection range order.
if (range.indexOf(from) > range.indexOf(to)) {
range.reverse();
}
return range.filter(tree_selection_1.SelectableTreeNode.is);
}
toSelectableTreeNode(node) {
if (!!node) {
const candidate = this.tree.getNode(node.id);
if (!!candidate) {
if (tree_selection_1.SelectableTreeNode.is(candidate)) {
return candidate;
}
else {
console.warn(`Could not map to a selectable tree node. Node with ID: ${node.id} is not a selectable node.`);
}
}
else {
console.warn(`Could not map to a selectable tree node. Node does not exist with ID: ${node.id}.`);
}
}
return undefined;
}
/**
* Checks whether the argument contains any `DEFAULT` tree selection type. If yes, throws an error, otherwise returns with a reference the argument.
*/
checkNoDefaultSelection(selections) {
if (selections.some(selection => selection.type === undefined || selection.type === tree_selection_1.TreeSelection.SelectionType.DEFAULT)) {
throw new Error(`Unexpected DEFAULT selection type. [${selections.map(selection => `ID: ${selection.node.id} | ${selection.type}`).join(', ')}]`);
}
return selections;
}
}
exports.TreeSelectionState = TreeSelectionState;
//# sourceMappingURL=tree-selection-state.js.map
;