UNPKG

6.36 kBPlain TextView Raw
1// *****************************************************************************
2// Copyright (C) 2018 TypeFox and others.
3//
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License v. 2.0 which is available at
6// http://www.eclipse.org/legal/epl-2.0.
7//
8// This Source Code may also be made available under the following Secondary
9// Licenses when the conditions for such availability set forth in the Eclipse
10// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11// with the GNU Classpath Exception which is available at
12// https://www.gnu.org/software/classpath/license.html.
13//
14// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15// *****************************************************************************
16
17import { injectable, inject, postConstruct } from 'inversify';
18import { Tree, TreeNode } from './tree';
19import { Event, Emitter } from '../../common';
20import { TreeSelectionState, FocusableTreeSelection } from './tree-selection-state';
21import { TreeSelectionService, SelectableTreeNode, TreeSelection } from './tree-selection';
22import { TreeFocusService } from './tree-focus-service';
23
24@injectable()
25export class TreeSelectionServiceImpl implements TreeSelectionService {
26
27 @inject(Tree) protected readonly tree: Tree;
28 @inject(TreeFocusService) protected readonly focusService: TreeFocusService;
29
30 protected readonly onSelectionChangedEmitter = new Emitter<ReadonlyArray<Readonly<SelectableTreeNode>>>();
31
32 protected state: TreeSelectionState;
33
34 @postConstruct()
35 protected init(): void {
36 this.state = new TreeSelectionState(this.tree);
37 }
38
39 dispose(): void {
40 this.onSelectionChangedEmitter.dispose();
41 }
42
43 get selectedNodes(): ReadonlyArray<Readonly<SelectableTreeNode>> {
44 return this.state.selection();
45 }
46
47 get onSelectionChanged(): Event<ReadonlyArray<Readonly<SelectableTreeNode>>> {
48 return this.onSelectionChangedEmitter.event;
49 }
50
51 protected fireSelectionChanged(): void {
52 this.onSelectionChangedEmitter.fire(this.state.selection());
53 }
54
55 addSelection(selectionOrTreeNode: TreeSelection | Readonly<SelectableTreeNode>): void {
56 const selection = ((arg: TreeSelection | Readonly<SelectableTreeNode>): TreeSelection => {
57 const type = TreeSelection.SelectionType.DEFAULT;
58 if (TreeSelection.is(arg)) {
59 return {
60 type,
61 ...arg
62 };
63 }
64 return {
65 type,
66 node: arg
67 };
68 })(selectionOrTreeNode);
69
70 const node = this.validateNode(selection.node);
71 if (node === undefined) {
72 return;
73 }
74 Object.assign(selection, { node });
75
76 const newState = this.state.nextState(selection);
77 this.transiteTo(newState);
78 }
79
80 clearSelection(): void {
81 this.transiteTo(new TreeSelectionState(this.tree), false);
82 }
83
84 protected transiteTo(newState: TreeSelectionState, setFocus = true): void {
85 const oldNodes = this.state.selection();
86 const newNodes = newState.selection();
87
88 const toUnselect = this.difference(oldNodes, newNodes);
89 const toSelect = this.difference(newNodes, oldNodes);
90
91 this.unselect(toUnselect);
92 this.select(toSelect);
93 this.removeFocus(oldNodes, newNodes);
94 if (setFocus) {
95 this.addFocus(newState.node);
96 }
97
98 this.state = newState;
99 this.fireSelectionChanged();
100 }
101
102 protected unselect(nodes: ReadonlyArray<SelectableTreeNode>): void {
103 nodes.forEach(node => node.selected = false);
104 }
105
106 protected select(nodes: ReadonlyArray<SelectableTreeNode>): void {
107 nodes.forEach(node => node.selected = true);
108 }
109
110 protected removeFocus(...nodes: ReadonlyArray<SelectableTreeNode>[]): void {
111 nodes.forEach(node => node.forEach(n => n.focus = false));
112 }
113
114 protected addFocus(node: SelectableTreeNode | undefined): void {
115 if (node) {
116 node.focus = true;
117 }
118 this.focusService.setFocus(node);
119 }
120
121 /**
122 * Returns an array of the difference of two arrays. The returned array contains all elements that are contained by
123 * `left` and not contained by `right`. `right` may also contain elements not present in `left`: these are simply ignored.
124 */
125 protected difference<T>(left: ReadonlyArray<T>, right: ReadonlyArray<T>): ReadonlyArray<T> {
126 return left.filter(item => right.indexOf(item) === -1);
127 }
128
129 /**
130 * Returns a reference to the argument if the node exists in the tree. Otherwise, `undefined`.
131 */
132 protected validateNode(node: Readonly<TreeNode>): Readonly<TreeNode> | undefined {
133 const result = this.tree.validateNode(node);
134 return SelectableTreeNode.is(result) ? result : undefined;
135 }
136
137 storeState(): TreeSelectionServiceImpl.State {
138 return {
139 selectionStack: this.state.selectionStack.map(s => ({
140 focus: s.focus && s.focus.id || undefined,
141 node: s.node && s.node.id || undefined,
142 type: s.type
143 }))
144 };
145 }
146
147 restoreState(state: TreeSelectionServiceImpl.State): void {
148 const selectionStack: FocusableTreeSelection[] = [];
149 for (const selection of state.selectionStack) {
150 const node = selection.node && this.tree.getNode(selection.node) || undefined;
151 if (!SelectableTreeNode.is(node)) {
152 break;
153 }
154 const focus = selection.focus && this.tree.getNode(selection.focus) || undefined;
155 selectionStack.push({
156 node,
157 focus: SelectableTreeNode.is(focus) && focus || undefined,
158 type: selection.type
159 });
160 }
161 if (selectionStack.length) {
162 this.transiteTo(new TreeSelectionState(this.tree, selectionStack));
163 }
164 }
165
166}
167export namespace TreeSelectionServiceImpl {
168 export interface State {
169 selectionStack: ReadonlyArray<FocusableTreeSelectionState>
170 }
171 export interface FocusableTreeSelectionState {
172 focus?: string
173 node?: string
174 type?: TreeSelection.SelectionType
175 }
176}