forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			253 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			253 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| import { EventEmitter } from "resource:///modules/syncedtabs/EventEmitter.sys.mjs";
 | |
| 
 | |
| /**
 | |
|  * SyncedTabsListStore
 | |
|  *
 | |
|  * Instances of this store encapsulate all of the state associated with a synced tabs list view.
 | |
|  * The state includes the clients, their tabs, the row that is currently selected,
 | |
|  * and the filtered query.
 | |
|  */
 | |
| export function SyncedTabsListStore(SyncedTabs) {
 | |
|   EventEmitter.call(this);
 | |
|   this._SyncedTabs = SyncedTabs;
 | |
|   this.data = [];
 | |
|   this._closedClients = {};
 | |
|   this._selectedRow = [-1, -1];
 | |
|   this.filter = "";
 | |
|   this.inputFocused = false;
 | |
| }
 | |
| 
 | |
| Object.assign(SyncedTabsListStore.prototype, EventEmitter.prototype, {
 | |
|   // This internal method triggers the "change" event that views
 | |
|   // listen for. It denormalizes the state so that it's easier for
 | |
|   // the view to deal with. updateType hints to the view what
 | |
|   // actually needs to be rerendered or just updated, and can be
 | |
|   // empty (to (re)render everything), "searchbox" (to rerender just the tab list),
 | |
|   // or "all" (to skip rendering and just update all attributes of existing nodes).
 | |
|   _change(updateType) {
 | |
|     let selectedParent = this._selectedRow[0];
 | |
|     let selectedChild = this._selectedRow[1];
 | |
|     let rowSelected = false;
 | |
|     // clone the data so that consumers can't mutate internal storage
 | |
|     let data = Cu.cloneInto(this.data, {});
 | |
|     let tabCount = 0;
 | |
| 
 | |
|     data.forEach((client, index) => {
 | |
|       client.closed = !!this._closedClients[client.id];
 | |
| 
 | |
|       if (rowSelected || selectedParent < 0) {
 | |
|         return;
 | |
|       }
 | |
|       if (this.filter) {
 | |
|         if (selectedParent < tabCount + client.tabs.length) {
 | |
|           client.tabs[selectedParent - tabCount].selected = true;
 | |
|           client.tabs[selectedParent - tabCount].focused = !this.inputFocused;
 | |
|           rowSelected = true;
 | |
|         } else {
 | |
|           tabCount += client.tabs.length;
 | |
|         }
 | |
|         return;
 | |
|       }
 | |
|       if (selectedParent === index && selectedChild === -1) {
 | |
|         client.selected = true;
 | |
|         client.focused = !this.inputFocused;
 | |
|         rowSelected = true;
 | |
|       } else if (selectedParent === index) {
 | |
|         client.tabs[selectedChild].selected = true;
 | |
|         client.tabs[selectedChild].focused = !this.inputFocused;
 | |
|         rowSelected = true;
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     // If this were React the view would be smart enough
 | |
|     // to not re-render the whole list unless necessary. But it's
 | |
|     // not, so updateType is a hint to the view of what actually
 | |
|     // needs to be rerendered.
 | |
|     this.emit("change", {
 | |
|       clients: data,
 | |
|       canUpdateAll: updateType === "all",
 | |
|       canUpdateInput: updateType === "searchbox",
 | |
|       filter: this.filter,
 | |
|       inputFocused: this.inputFocused,
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Moves the row selection from a child to its parent,
 | |
|    * which occurs when the parent of a selected row closes.
 | |
|    */
 | |
|   _selectParentRow() {
 | |
|     this._selectedRow[1] = -1;
 | |
|   },
 | |
| 
 | |
|   _toggleBranch(id, closed) {
 | |
|     this._closedClients[id] = closed;
 | |
|     if (this._closedClients[id]) {
 | |
|       this._selectParentRow();
 | |
|     }
 | |
|     this._change("all");
 | |
|   },
 | |
| 
 | |
|   _isOpen(client) {
 | |
|     return !this._closedClients[client.id];
 | |
|   },
 | |
| 
 | |
|   moveSelectionDown() {
 | |
|     let branchRow = this._selectedRow[0];
 | |
|     let childRow = this._selectedRow[1];
 | |
|     let branch = this.data[branchRow];
 | |
| 
 | |
|     if (this.filter) {
 | |
|       this.selectRow(branchRow + 1);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (branchRow < 0) {
 | |
|       this.selectRow(0, -1);
 | |
|     } else if (
 | |
|       (!branch.tabs.length ||
 | |
|         childRow >= branch.tabs.length - 1 ||
 | |
|         !this._isOpen(branch)) &&
 | |
|       branchRow < this.data.length
 | |
|     ) {
 | |
|       this.selectRow(branchRow + 1, -1);
 | |
|     } else if (childRow < branch.tabs.length) {
 | |
|       this.selectRow(branchRow, childRow + 1);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   moveSelectionUp() {
 | |
|     let branchRow = this._selectedRow[0];
 | |
|     let childRow = this._selectedRow[1];
 | |
| 
 | |
|     if (this.filter) {
 | |
|       this.selectRow(branchRow - 1);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (branchRow < 0) {
 | |
|       this.selectRow(0, -1);
 | |
|     } else if (childRow < 0 && branchRow > 0) {
 | |
|       let prevBranch = this.data[branchRow - 1];
 | |
|       let newChildRow = this._isOpen(prevBranch)
 | |
|         ? prevBranch.tabs.length - 1
 | |
|         : -1;
 | |
|       this.selectRow(branchRow - 1, newChildRow);
 | |
|     } else if (childRow >= 0) {
 | |
|       this.selectRow(branchRow, childRow - 1);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Selects a row and makes sure the selection is within bounds
 | |
|   selectRow(parent, child) {
 | |
|     let maxParentRow = this.filter ? this._tabCount() : this.data.length;
 | |
|     let parentRow = parent;
 | |
|     if (parent <= -1) {
 | |
|       parentRow = 0;
 | |
|     } else if (parent >= maxParentRow) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let childRow = child;
 | |
|     if (
 | |
|       parentRow === -1 ||
 | |
|       this.filter ||
 | |
|       typeof child === "undefined" ||
 | |
|       child < -1
 | |
|     ) {
 | |
|       childRow = -1;
 | |
|     } else if (child >= this.data[parentRow].tabs.length) {
 | |
|       childRow = this.data[parentRow].tabs.length - 1;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       this._selectedRow[0] === parentRow &&
 | |
|       this._selectedRow[1] === childRow
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this._selectedRow = [parentRow, childRow];
 | |
|     this.inputFocused = false;
 | |
|     this._change("all");
 | |
|     // Record the telemetry event
 | |
|     let extraOptions = {
 | |
|       tab_pos: this._selectedRow[1].toString(),
 | |
|       filter: this.filter,
 | |
|     };
 | |
|     this._SyncedTabs.recordSyncedTabsTelemetry(
 | |
|       "synced_tabs_sidebar",
 | |
|       "click",
 | |
|       extraOptions
 | |
|     );
 | |
|   },
 | |
| 
 | |
|   _tabCount() {
 | |
|     return this.data.reduce((prev, curr) => curr.tabs.length + prev, 0);
 | |
|   },
 | |
| 
 | |
|   toggleBranch(id) {
 | |
|     this._toggleBranch(id, !this._closedClients[id]);
 | |
|   },
 | |
| 
 | |
|   closeBranch(id) {
 | |
|     this._toggleBranch(id, true);
 | |
|   },
 | |
| 
 | |
|   openBranch(id) {
 | |
|     this._toggleBranch(id, false);
 | |
|   },
 | |
| 
 | |
|   focusInput() {
 | |
|     this.inputFocused = true;
 | |
|     // A change type of "all" updates rather than rebuilds, which is what we
 | |
|     // want here - only the selection/focus has changed.
 | |
|     this._change("all");
 | |
|   },
 | |
| 
 | |
|   blurInput() {
 | |
|     this.inputFocused = false;
 | |
|     // A change type of "all" updates rather than rebuilds, which is what we
 | |
|     // want here - only the selection/focus has changed.
 | |
|     this._change("all");
 | |
|   },
 | |
| 
 | |
|   clearFilter() {
 | |
|     this.filter = "";
 | |
|     this._selectedRow = [-1, -1];
 | |
|     return this.getData();
 | |
|   },
 | |
| 
 | |
|   // Fetches data from the SyncedTabs module and triggers
 | |
|   // and update
 | |
|   getData(filter) {
 | |
|     let updateType;
 | |
|     let hasFilter = typeof filter !== "undefined";
 | |
|     if (hasFilter) {
 | |
|       this.filter = filter;
 | |
|       this._selectedRow = [-1, -1];
 | |
| 
 | |
|       // When a filter is specified we tell the view that only the list
 | |
|       // needs to be rerendered so that it doesn't disrupt the input
 | |
|       // field's focus.
 | |
|       updateType = "searchbox";
 | |
|     }
 | |
| 
 | |
|     // return promise for tests
 | |
|     return this._SyncedTabs
 | |
|       .getTabClients(this.filter)
 | |
|       .then(result => {
 | |
|         if (!hasFilter) {
 | |
|           // Only sort clients and tabs if we're rendering the whole list.
 | |
|           this._SyncedTabs.sortTabClientsByLastUsed(result);
 | |
|         }
 | |
|         this.data = result;
 | |
|         this._change(updateType);
 | |
|       })
 | |
|       .catch(console.error);
 | |
|   },
 | |
| });
 | 
