1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import { injectable, inject, postConstruct } from 'inversify';
|
18 | import { Tree, TreeNode } from './tree';
|
19 | import { Event, Emitter } from '../../common';
|
20 | import { TreeSelectionState, FocusableTreeSelection } from './tree-selection-state';
|
21 | import { TreeSelectionService, SelectableTreeNode, TreeSelection } from './tree-selection';
|
22 | import { TreeFocusService } from './tree-focus-service';
|
23 |
|
24 | @injectable()
|
25 | export 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 |
|
123 |
|
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 |
|
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 | }
|
167 | export 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 | }
|