Bug 1845017 - Disable the TestPHCExhaustion test r=glandium
[gecko.git] / browser / components / syncedtabs / SyncedTabsListStore.sys.mjs
blob67adcfdace9114920f573f211d5e77f81edd0e1d
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";
7 /**
8  * SyncedTabsListStore
9  *
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.
13  */
14 export function SyncedTabsListStore(SyncedTabs) {
15   EventEmitter.call(this);
16   this._SyncedTabs = SyncedTabs;
17   this.data = [];
18   this._closedClients = {};
19   this._selectedRow = [-1, -1];
20   this.filter = "";
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).
31   _change(updateType) {
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, {});
37     let tabCount = 0;
39     data.forEach((client, index) => {
40       client.closed = !!this._closedClients[client.id];
42       if (rowSelected || selectedParent < 0) {
43         return;
44       }
45       if (this.filter) {
46         if (selectedParent < tabCount + client.tabs.length) {
47           client.tabs[selectedParent - tabCount].selected = true;
48           client.tabs[selectedParent - tabCount].focused = !this.inputFocused;
49           rowSelected = true;
50         } else {
51           tabCount += client.tabs.length;
52         }
53         return;
54       }
55       if (selectedParent === index && selectedChild === -1) {
56         client.selected = true;
57         client.focused = !this.inputFocused;
58         rowSelected = true;
59       } else if (selectedParent === index) {
60         client.tabs[selectedChild].selected = true;
61         client.tabs[selectedChild].focused = !this.inputFocused;
62         rowSelected = true;
63       }
64     });
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.
70     this.emit("change", {
71       clients: data,
72       canUpdateAll: updateType === "all",
73       canUpdateInput: updateType === "searchbox",
74       filter: this.filter,
75       inputFocused: this.inputFocused,
76     });
77   },
79   /**
80    * Moves the row selection from a child to its parent,
81    * which occurs when the parent of a selected row closes.
82    */
83   _selectParentRow() {
84     this._selectedRow[1] = -1;
85   },
87   _toggleBranch(id, closed) {
88     this._closedClients[id] = closed;
89     if (this._closedClients[id]) {
90       this._selectParentRow();
91     }
92     this._change("all");
93   },
95   _isOpen(client) {
96     return !this._closedClients[client.id];
97   },
99   moveSelectionDown() {
100     let branchRow = this._selectedRow[0];
101     let childRow = this._selectedRow[1];
102     let branch = this.data[branchRow];
104     if (this.filter) {
105       this.selectRow(branchRow + 1);
106       return;
107     }
109     if (branchRow < 0) {
110       this.selectRow(0, -1);
111     } else if (
112       (!branch.tabs.length ||
113         childRow >= branch.tabs.length - 1 ||
114         !this._isOpen(branch)) &&
115       branchRow < this.data.length
116     ) {
117       this.selectRow(branchRow + 1, -1);
118     } else if (childRow < branch.tabs.length) {
119       this.selectRow(branchRow, childRow + 1);
120     }
121   },
123   moveSelectionUp() {
124     let branchRow = this._selectedRow[0];
125     let childRow = this._selectedRow[1];
127     if (this.filter) {
128       this.selectRow(branchRow - 1);
129       return;
130     }
132     if (branchRow < 0) {
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
138         : -1;
139       this.selectRow(branchRow - 1, newChildRow);
140     } else if (childRow >= 0) {
141       this.selectRow(branchRow, childRow - 1);
142     }
143   },
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;
149     if (parent <= -1) {
150       parentRow = 0;
151     } else if (parent >= maxParentRow) {
152       return;
153     }
155     let childRow = child;
156     if (
157       parentRow === -1 ||
158       this.filter ||
159       typeof child === "undefined" ||
160       child < -1
161     ) {
162       childRow = -1;
163     } else if (child >= this.data[parentRow].tabs.length) {
164       childRow = this.data[parentRow].tabs.length - 1;
165     }
167     if (
168       this._selectedRow[0] === parentRow &&
169       this._selectedRow[1] === childRow
170     ) {
171       return;
172     }
174     this._selectedRow = [parentRow, childRow];
175     this.inputFocused = false;
176     this._change("all");
177     // Record the telemetry event
178     let extraOptions = {
179       tab_pos: this._selectedRow[1].toString(),
180       filter: this.filter,
181     };
182     this._SyncedTabs.recordSyncedTabsTelemetry(
183       "synced_tabs_sidebar",
184       "click",
185       extraOptions
186     );
187   },
189   _tabCount() {
190     return this.data.reduce((prev, curr) => curr.tabs.length + prev, 0);
191   },
193   toggleBranch(id) {
194     this._toggleBranch(id, !this._closedClients[id]);
195   },
197   closeBranch(id) {
198     this._toggleBranch(id, true);
199   },
201   openBranch(id) {
202     this._toggleBranch(id, false);
203   },
205   focusInput() {
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.
209     this._change("all");
210   },
212   blurInput() {
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.
216     this._change("all");
217   },
219   clearFilter() {
220     this.filter = "";
221     this._selectedRow = [-1, -1];
222     return this.getData();
223   },
225   // Fetches data from the SyncedTabs module and triggers
226   // and update
227   getData(filter) {
228     let updateType;
229     let hasFilter = typeof filter !== "undefined";
230     if (hasFilter) {
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
236       // field's focus.
237       updateType = "searchbox";
238     }
240     // return promise for tests
241     return this._SyncedTabs
242       .getTabClients(this.filter)
243       .then(result => {
244         if (!hasFilter) {
245           // Only sort clients and tabs if we're rendering the whole list.
246           this._SyncedTabs.sortTabClientsByLastUsed(result);
247         }
248         this.data = result;
249         this._change(updateType);
250       })
251       .catch(console.error);
252   },