1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { EventEmitter } from "resource:///modules/syncedtabs/EventEmitter.sys.mjs";
10 * Instances of this store encapsulate all of the state associated with a synced tabs list view.
11 * The state includes the clients, their tabs, the row that is currently selected,
12 * and the filtered query.
14 export function SyncedTabsListStore(SyncedTabs) {
15 EventEmitter.call(this);
16 this._SyncedTabs = SyncedTabs;
18 this._closedClients = {};
19 this._selectedRow = [-1, -1];
21 this.inputFocused = false;
24 Object.assign(SyncedTabsListStore.prototype, EventEmitter.prototype, {
25 // This internal method triggers the "change" event that views
26 // listen for. It denormalizes the state so that it's easier for
27 // the view to deal with. updateType hints to the view what
28 // actually needs to be rerendered or just updated, and can be
29 // empty (to (re)render everything), "searchbox" (to rerender just the tab list),
30 // or "all" (to skip rendering and just update all attributes of existing nodes).
32 let selectedParent = this._selectedRow[0];
33 let selectedChild = this._selectedRow[1];
34 let rowSelected = false;
35 // clone the data so that consumers can't mutate internal storage
36 let data = Cu.cloneInto(this.data, {});
39 data.forEach((client, index) => {
40 client.closed = !!this._closedClients[client.id];
42 if (rowSelected || selectedParent < 0) {
46 if (selectedParent < tabCount + client.tabs.length) {
47 client.tabs[selectedParent - tabCount].selected = true;
48 client.tabs[selectedParent - tabCount].focused = !this.inputFocused;
51 tabCount += client.tabs.length;
55 if (selectedParent === index && selectedChild === -1) {
56 client.selected = true;
57 client.focused = !this.inputFocused;
59 } else if (selectedParent === index) {
60 client.tabs[selectedChild].selected = true;
61 client.tabs[selectedChild].focused = !this.inputFocused;
66 // If this were React the view would be smart enough
67 // to not re-render the whole list unless necessary. But it's
68 // not, so updateType is a hint to the view of what actually
69 // needs to be rerendered.
72 canUpdateAll: updateType === "all",
73 canUpdateInput: updateType === "searchbox",
75 inputFocused: this.inputFocused,
80 * Moves the row selection from a child to its parent,
81 * which occurs when the parent of a selected row closes.
84 this._selectedRow[1] = -1;
87 _toggleBranch(id, closed) {
88 this._closedClients[id] = closed;
89 if (this._closedClients[id]) {
90 this._selectParentRow();
96 return !this._closedClients[client.id];
100 let branchRow = this._selectedRow[0];
101 let childRow = this._selectedRow[1];
102 let branch = this.data[branchRow];
105 this.selectRow(branchRow + 1);
110 this.selectRow(0, -1);
112 (!branch.tabs.length ||
113 childRow >= branch.tabs.length - 1 ||
114 !this._isOpen(branch)) &&
115 branchRow < this.data.length
117 this.selectRow(branchRow + 1, -1);
118 } else if (childRow < branch.tabs.length) {
119 this.selectRow(branchRow, childRow + 1);
124 let branchRow = this._selectedRow[0];
125 let childRow = this._selectedRow[1];
128 this.selectRow(branchRow - 1);
133 this.selectRow(0, -1);
134 } else if (childRow < 0 && branchRow > 0) {
135 let prevBranch = this.data[branchRow - 1];
136 let newChildRow = this._isOpen(prevBranch)
137 ? prevBranch.tabs.length - 1
139 this.selectRow(branchRow - 1, newChildRow);
140 } else if (childRow >= 0) {
141 this.selectRow(branchRow, childRow - 1);
145 // Selects a row and makes sure the selection is within bounds
146 selectRow(parent, child) {
147 let maxParentRow = this.filter ? this._tabCount() : this.data.length;
148 let parentRow = parent;
151 } else if (parent >= maxParentRow) {
155 let childRow = child;
159 typeof child === "undefined" ||
163 } else if (child >= this.data[parentRow].tabs.length) {
164 childRow = this.data[parentRow].tabs.length - 1;
168 this._selectedRow[0] === parentRow &&
169 this._selectedRow[1] === childRow
174 this._selectedRow = [parentRow, childRow];
175 this.inputFocused = false;
177 // Record the telemetry event
179 tab_pos: this._selectedRow[1].toString(),
182 this._SyncedTabs.recordSyncedTabsTelemetry(
183 "synced_tabs_sidebar",
190 return this.data.reduce((prev, curr) => curr.tabs.length + prev, 0);
194 this._toggleBranch(id, !this._closedClients[id]);
198 this._toggleBranch(id, true);
202 this._toggleBranch(id, false);
206 this.inputFocused = true;
207 // A change type of "all" updates rather than rebuilds, which is what we
208 // want here - only the selection/focus has changed.
213 this.inputFocused = false;
214 // A change type of "all" updates rather than rebuilds, which is what we
215 // want here - only the selection/focus has changed.
221 this._selectedRow = [-1, -1];
222 return this.getData();
225 // Fetches data from the SyncedTabs module and triggers
229 let hasFilter = typeof filter !== "undefined";
231 this.filter = filter;
232 this._selectedRow = [-1, -1];
234 // When a filter is specified we tell the view that only the list
235 // needs to be rerendered so that it doesn't disrupt the input
237 updateType = "searchbox";
240 // return promise for tests
241 return this._SyncedTabs
242 .getTabClients(this.filter)
245 // Only sort clients and tabs if we're rendering the whole list.
246 this._SyncedTabs.sortTabClientsByLastUsed(result);
249 this._change(updateType);
251 .catch(console.error);