forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			653 lines
		
	
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			653 lines
		
	
	
	
		
			18 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/. */
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| import { getChromeWindow } from "resource:///modules/syncedtabs/util.sys.mjs";
 | |
| 
 | |
| function getContextMenu(window) {
 | |
|   return getChromeWindow(window).document.getElementById(
 | |
|     "SyncedTabsSidebarContext"
 | |
|   );
 | |
| }
 | |
| 
 | |
| function getTabsFilterContextMenu(window) {
 | |
|   return getChromeWindow(window).document.getElementById(
 | |
|     "SyncedTabsSidebarTabsFilterContext"
 | |
|   );
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * TabListView
 | |
|  *
 | |
|  * Given a state, this object will render the corresponding DOM.
 | |
|  * It maintains no state of it's own. It listens for DOM events
 | |
|  * and triggers actions that may cause the state to change and
 | |
|  * ultimately the view to rerender.
 | |
|  */
 | |
| export function TabListView(window, props) {
 | |
|   this.props = props;
 | |
| 
 | |
|   this._window = window;
 | |
|   this._doc = this._window.document;
 | |
| 
 | |
|   this._tabsContainerTemplate = this._doc.getElementById(
 | |
|     "tabs-container-template"
 | |
|   );
 | |
|   this._clientTemplate = this._doc.getElementById("client-template");
 | |
|   this._emptyClientTemplate = this._doc.getElementById("empty-client-template");
 | |
|   this._tabTemplate = this._doc.getElementById("tab-template");
 | |
|   this.tabsFilter = this._doc.querySelector(".tabsFilter");
 | |
| 
 | |
|   this.container = this._doc.createElement("div");
 | |
| 
 | |
|   this._attachFixedListeners();
 | |
| 
 | |
|   this._setupContextMenu();
 | |
| }
 | |
| 
 | |
| TabListView.prototype = {
 | |
|   render(state) {
 | |
|     // Don't rerender anything; just update attributes, e.g. selection
 | |
|     if (state.canUpdateAll) {
 | |
|       this._update(state);
 | |
|       return;
 | |
|     }
 | |
|     // Rerender the tab list
 | |
|     if (state.canUpdateInput) {
 | |
|       this._updateSearchBox(state);
 | |
|       this._createList(state);
 | |
|       return;
 | |
|     }
 | |
|     // Create the world anew
 | |
|     this._create(state);
 | |
|   },
 | |
| 
 | |
|   // Create the initial DOM from templates
 | |
|   _create(state) {
 | |
|     let wrapper = this._doc.importNode(
 | |
|       this._tabsContainerTemplate.content,
 | |
|       true
 | |
|     ).firstElementChild;
 | |
|     this._clearChilden();
 | |
|     this.container.appendChild(wrapper);
 | |
| 
 | |
|     this.list = this.container.querySelector(".list");
 | |
| 
 | |
|     this._createList(state);
 | |
|     this._updateSearchBox(state);
 | |
| 
 | |
|     this._attachListListeners();
 | |
|   },
 | |
| 
 | |
|   _createList(state) {
 | |
|     this._clearChilden(this.list);
 | |
|     for (let client of state.clients) {
 | |
|       if (state.filter) {
 | |
|         this._renderFilteredClient(client);
 | |
|       } else {
 | |
|         this._renderClient(client);
 | |
|       }
 | |
|     }
 | |
|     if (this.list.firstElementChild) {
 | |
|       const firstTab = this.list.firstElementChild.querySelector(
 | |
|         ".item.tab:first-child .item-title"
 | |
|       );
 | |
|       if (firstTab) {
 | |
|         firstTab.setAttribute("tabindex", 2);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   destroy() {
 | |
|     this._teardownContextMenu();
 | |
|     this.container.remove();
 | |
|   },
 | |
| 
 | |
|   _update(state) {
 | |
|     this._updateSearchBox(state);
 | |
|     for (let client of state.clients) {
 | |
|       let clientNode = this._doc.getElementById("item-" + client.id);
 | |
|       if (clientNode) {
 | |
|         this._updateClient(client, clientNode);
 | |
|       }
 | |
| 
 | |
|       client.tabs.forEach((tab, index) => {
 | |
|         let tabNode = this._doc.getElementById(
 | |
|           "tab-" + client.id + "-" + index
 | |
|         );
 | |
|         this._updateTab(tab, tabNode, index);
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Client rows are hidden when the list is filtered
 | |
|   _renderFilteredClient(client, filter) {
 | |
|     client.tabs.forEach((tab, index) => {
 | |
|       let node = this._renderTab(client, tab, index);
 | |
|       this.list.appendChild(node);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   _updateLastSyncTitle(lastModified, itemNode) {
 | |
|     let lastSync = new Date(lastModified);
 | |
|     let lastSyncTitle = getChromeWindow(this._window).gSync.formatLastSyncDate(
 | |
|       lastSync
 | |
|     );
 | |
|     itemNode.setAttribute("title", lastSyncTitle);
 | |
|   },
 | |
| 
 | |
|   _renderClient(client) {
 | |
|     let itemNode = client.tabs.length
 | |
|       ? this._createClient(client)
 | |
|       : this._createEmptyClient(client);
 | |
| 
 | |
|     itemNode.addEventListener("mouseover", () =>
 | |
|       this._updateLastSyncTitle(client.lastModified, itemNode)
 | |
|     );
 | |
| 
 | |
|     this._updateClient(client, itemNode);
 | |
| 
 | |
|     let tabsList = itemNode.querySelector(".item-tabs-list");
 | |
|     client.tabs.forEach((tab, index) => {
 | |
|       let node = this._renderTab(client, tab, index);
 | |
|       tabsList.appendChild(node);
 | |
|     });
 | |
| 
 | |
|     this.list.appendChild(itemNode);
 | |
|     return itemNode;
 | |
|   },
 | |
| 
 | |
|   _renderTab(client, tab, index) {
 | |
|     let itemNode = this._createTab(tab);
 | |
|     this._updateTab(tab, itemNode, index);
 | |
|     return itemNode;
 | |
|   },
 | |
| 
 | |
|   _createClient() {
 | |
|     return this._doc.importNode(this._clientTemplate.content, true)
 | |
|       .firstElementChild;
 | |
|   },
 | |
| 
 | |
|   _createEmptyClient() {
 | |
|     return this._doc.importNode(this._emptyClientTemplate.content, true)
 | |
|       .firstElementChild;
 | |
|   },
 | |
| 
 | |
|   _createTab() {
 | |
|     return this._doc.importNode(this._tabTemplate.content, true)
 | |
|       .firstElementChild;
 | |
|   },
 | |
| 
 | |
|   _clearChilden(node) {
 | |
|     let parent = node || this.container;
 | |
|     while (parent.firstChild) {
 | |
|       parent.firstChild.remove();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // These listeners are attached only once, when we initialize the view
 | |
|   _attachFixedListeners() {
 | |
|     this.tabsFilter.addEventListener("command", this.onFilter.bind(this));
 | |
|     this.tabsFilter.addEventListener("focus", this.onFilterFocus.bind(this));
 | |
|     this.tabsFilter.addEventListener("blur", this.onFilterBlur.bind(this));
 | |
|   },
 | |
| 
 | |
|   // These listeners have to be re-created every time since we re-create the list
 | |
|   _attachListListeners() {
 | |
|     this.list.addEventListener("click", this.onClick.bind(this));
 | |
|     this.list.addEventListener("mouseup", this.onMouseUp.bind(this));
 | |
|     this.list.addEventListener("keydown", this.onKeyDown.bind(this));
 | |
|   },
 | |
| 
 | |
|   _updateSearchBox(state) {
 | |
|     this.tabsFilter.value = state.filter;
 | |
|     if (state.inputFocused) {
 | |
|       this.tabsFilter.focus();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update the element representing an item, ensuring it's in sync with the
 | |
|    * underlying data.
 | |
|    * @param {client} item - Item to use as a source.
 | |
|    * @param {Element} itemNode - Element to update.
 | |
|    */
 | |
|   _updateClient(item, itemNode) {
 | |
|     itemNode.setAttribute("id", "item-" + item.id);
 | |
|     this._updateLastSyncTitle(item.lastModified, itemNode);
 | |
|     if (item.closed) {
 | |
|       itemNode.classList.add("closed");
 | |
|     } else {
 | |
|       itemNode.classList.remove("closed");
 | |
|     }
 | |
|     if (item.selected) {
 | |
|       itemNode.classList.add("selected");
 | |
|     } else {
 | |
|       itemNode.classList.remove("selected");
 | |
|     }
 | |
|     if (item.focused) {
 | |
|       itemNode.focus();
 | |
|     }
 | |
|     itemNode.setAttribute("clientType", item.clientType);
 | |
|     itemNode.dataset.id = item.id;
 | |
|     itemNode.querySelector(".item-title").textContent = item.name;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Update the element representing a tab, ensuring it's in sync with the
 | |
|    * underlying data.
 | |
|    * @param {tab} item - Item to use as a source.
 | |
|    * @param {Element} itemNode - Element to update.
 | |
|    */
 | |
|   _updateTab(item, itemNode, index) {
 | |
|     itemNode.setAttribute("title", `${item.title}\n${item.url}`);
 | |
|     itemNode.setAttribute("id", "tab-" + item.client + "-" + index);
 | |
|     if (item.selected) {
 | |
|       itemNode.classList.add("selected");
 | |
|     } else {
 | |
|       itemNode.classList.remove("selected");
 | |
|     }
 | |
|     if (item.focused) {
 | |
|       itemNode.focus();
 | |
|     }
 | |
|     itemNode.dataset.url = item.url;
 | |
| 
 | |
|     itemNode.querySelector(".item-title").textContent = item.title;
 | |
| 
 | |
|     if (item.icon) {
 | |
|       let icon = itemNode.querySelector(".item-icon-container");
 | |
|       icon.style.backgroundImage = "url(" + item.icon + ")";
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onMouseUp(event) {
 | |
|     if (event.which == 2) {
 | |
|       // Middle click
 | |
|       this.onClick(event);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onClick(event) {
 | |
|     let itemNode = this._findParentItemNode(event.target);
 | |
|     if (!itemNode) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (itemNode.classList.contains("tab")) {
 | |
|       let url = itemNode.dataset.url;
 | |
|       if (url) {
 | |
|         this.onOpenSelected(url, event);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Middle click on a client
 | |
|     if (itemNode.classList.contains("client")) {
 | |
|       let where = getChromeWindow(this._window).whereToOpenLink(event);
 | |
|       if (where != "current") {
 | |
|         this._openAllClientTabs(itemNode, where);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       event.target.classList.contains("item-twisty-container") &&
 | |
|       event.which != 2
 | |
|     ) {
 | |
|       this.props.onToggleBranch(itemNode.dataset.id);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let position = this._getSelectionPosition(itemNode);
 | |
|     this.props.onSelectRow(position);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handle a keydown event on the list box.
 | |
|    * @param {Event} event - Triggering event.
 | |
|    */
 | |
|   onKeyDown(event) {
 | |
|     if (event.keyCode == this._window.KeyEvent.DOM_VK_DOWN) {
 | |
|       event.preventDefault();
 | |
|       this.props.onMoveSelectionDown();
 | |
|     } else if (event.keyCode == this._window.KeyEvent.DOM_VK_UP) {
 | |
|       event.preventDefault();
 | |
|       this.props.onMoveSelectionUp();
 | |
|     } else if (event.keyCode == this._window.KeyEvent.DOM_VK_RETURN) {
 | |
|       let selectedNode = this.container.querySelector(".item.selected");
 | |
|       if (selectedNode.dataset.url) {
 | |
|         this.onOpenSelected(selectedNode.dataset.url, event);
 | |
|       } else if (selectedNode) {
 | |
|         this.props.onToggleBranch(selectedNode.dataset.id);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onBookmarkTab() {
 | |
|     let item = this._getSelectedTabNode();
 | |
|     if (item) {
 | |
|       let title = item.querySelector(".item-title").textContent;
 | |
|       this.props.onBookmarkTab(item.dataset.url, title);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onCopyTabLocation() {
 | |
|     let item = this._getSelectedTabNode();
 | |
|     if (item) {
 | |
|       this.props.onCopyTabLocation(item.dataset.url);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onOpenSelected(url, event) {
 | |
|     let where = getChromeWindow(this._window).whereToOpenLink(event);
 | |
|     this.props.onOpenTab(url, where, {});
 | |
|   },
 | |
| 
 | |
|   onOpenSelectedFromContextMenu(event) {
 | |
|     let item = this._getSelectedTabNode();
 | |
|     if (item) {
 | |
|       let where = event.target.getAttribute("where");
 | |
|       let params = {
 | |
|         private: event.target.hasAttribute("private"),
 | |
|       };
 | |
|       this.props.onOpenTab(item.dataset.url, where, params);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onOpenSelectedInContainerTab(event) {
 | |
|     let item = this._getSelectedTabNode();
 | |
|     if (item) {
 | |
|       this.props.onOpenTab(item.dataset.url, "tab", {
 | |
|         userContextId: parseInt(event.target?.dataset.usercontextid),
 | |
|       });
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onOpenAllInTabs() {
 | |
|     let item = this._getSelectedClientNode();
 | |
|     if (item) {
 | |
|       this._openAllClientTabs(item, "tab");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onFilter(event) {
 | |
|     let query = event.target.value;
 | |
|     if (query) {
 | |
|       this.props.onFilter(query);
 | |
|     } else {
 | |
|       this.props.onClearFilter();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onFilterFocus() {
 | |
|     this.props.onFilterFocus();
 | |
|   },
 | |
|   onFilterBlur() {
 | |
|     this.props.onFilterBlur();
 | |
|   },
 | |
| 
 | |
|   _getSelectedTabNode() {
 | |
|     let item = this.container.querySelector(".item.selected");
 | |
|     if (this._isTab(item) && item.dataset.url) {
 | |
|       return item;
 | |
|     }
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   _getSelectedClientNode() {
 | |
|     let item = this.container.querySelector(".item.selected");
 | |
|     if (this._isClient(item)) {
 | |
|       return item;
 | |
|     }
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   // Set up the custom context menu
 | |
|   _setupContextMenu() {
 | |
|     Services.els.addSystemEventListener(
 | |
|       this._window,
 | |
|       "contextmenu",
 | |
|       this,
 | |
|       false
 | |
|     );
 | |
|     for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) {
 | |
|       let menu = getMenu(this._window);
 | |
|       menu.addEventListener("popupshowing", this, true);
 | |
|       menu.addEventListener("command", this, true);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _teardownContextMenu() {
 | |
|     // Tear down context menu
 | |
|     Services.els.removeSystemEventListener(
 | |
|       this._window,
 | |
|       "contextmenu",
 | |
|       this,
 | |
|       false
 | |
|     );
 | |
|     for (let getMenu of [getContextMenu, getTabsFilterContextMenu]) {
 | |
|       let menu = getMenu(this._window);
 | |
|       menu.removeEventListener("popupshowing", this, true);
 | |
|       menu.removeEventListener("command", this, true);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   handleEvent(event) {
 | |
|     switch (event.type) {
 | |
|       case "contextmenu":
 | |
|         this.handleContextMenu(event);
 | |
|         break;
 | |
| 
 | |
|       case "popupshowing": {
 | |
|         if (
 | |
|           event.target.getAttribute("id") ==
 | |
|           "SyncedTabsSidebarTabsFilterContext"
 | |
|         ) {
 | |
|           this.handleTabsFilterContextMenuShown(event);
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       case "command": {
 | |
|         let menu = event.target.closest("menupopup");
 | |
|         switch (menu.getAttribute("id")) {
 | |
|           case "SyncedTabsSidebarContext":
 | |
|             this.handleContentContextMenuCommand(event);
 | |
|             break;
 | |
| 
 | |
|           case "SyncedTabsOpenSelectedInContainerTabMenu":
 | |
|             this.onOpenSelectedInContainerTab(event);
 | |
|             break;
 | |
| 
 | |
|           case "SyncedTabsSidebarTabsFilterContext":
 | |
|             this.handleTabsFilterContextMenuCommand(event);
 | |
|             break;
 | |
|         }
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   handleTabsFilterContextMenuShown(event) {
 | |
|     let document = event.target.ownerDocument;
 | |
|     let focusedElement = document.commandDispatcher.focusedElement;
 | |
|     if (focusedElement != this.tabsFilter.inputField) {
 | |
|       this.tabsFilter.focus();
 | |
|     }
 | |
|     for (let item of event.target.children) {
 | |
|       if (!item.hasAttribute("cmd")) {
 | |
|         continue;
 | |
|       }
 | |
|       let command = item.getAttribute("cmd");
 | |
|       let controller =
 | |
|         document.commandDispatcher.getControllerForCommand(command);
 | |
|       if (controller.isCommandEnabled(command)) {
 | |
|         item.removeAttribute("disabled");
 | |
|       } else {
 | |
|         item.setAttribute("disabled", "true");
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   handleContentContextMenuCommand(event) {
 | |
|     let id = event.target.getAttribute("id");
 | |
|     switch (id) {
 | |
|       case "syncedTabsOpenSelected":
 | |
|       case "syncedTabsOpenSelectedInTab":
 | |
|       case "syncedTabsOpenSelectedInWindow":
 | |
|       case "syncedTabsOpenSelectedInPrivateWindow":
 | |
|         this.onOpenSelectedFromContextMenu(event);
 | |
|         break;
 | |
|       case "syncedTabsOpenAllInTabs":
 | |
|         this.onOpenAllInTabs();
 | |
|         break;
 | |
|       case "syncedTabsBookmarkSelected":
 | |
|         this.onBookmarkTab();
 | |
|         break;
 | |
|       case "syncedTabsCopySelected":
 | |
|         this.onCopyTabLocation();
 | |
|         break;
 | |
|       case "syncedTabsRefresh":
 | |
|       case "syncedTabsRefreshFilter":
 | |
|         this.props.onSyncRefresh();
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   handleTabsFilterContextMenuCommand(event) {
 | |
|     let command = event.target.getAttribute("cmd");
 | |
|     let dispatcher = getChromeWindow(this._window).document.commandDispatcher;
 | |
|     let controller =
 | |
|       dispatcher.focusedElement.controllers.getControllerForCommand(command);
 | |
|     controller.doCommand(command);
 | |
|   },
 | |
| 
 | |
|   handleContextMenu(event) {
 | |
|     let menu;
 | |
| 
 | |
|     if (event.target == this.tabsFilter) {
 | |
|       menu = getTabsFilterContextMenu(this._window);
 | |
|     } else {
 | |
|       let itemNode = this._findParentItemNode(event.target);
 | |
|       if (itemNode) {
 | |
|         let position = this._getSelectionPosition(itemNode);
 | |
|         this.props.onSelectRow(position);
 | |
|       }
 | |
|       menu = getContextMenu(this._window);
 | |
|       this.adjustContextMenu(menu);
 | |
|     }
 | |
| 
 | |
|     menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
 | |
|   },
 | |
| 
 | |
|   adjustContextMenu(menu) {
 | |
|     let item = this.container.querySelector(".item.selected");
 | |
|     let showTabOptions = this._isTab(item);
 | |
| 
 | |
|     let el = menu.firstElementChild;
 | |
| 
 | |
|     while (el) {
 | |
|       let show = false;
 | |
|       if (showTabOptions) {
 | |
|         if (el.getAttribute("id") == "syncedTabsOpenSelectedInPrivateWindow") {
 | |
|           show = lazy.PrivateBrowsingUtils.enabled;
 | |
|         } else if (
 | |
|           el.getAttribute("id") === "syncedTabsOpenSelectedInContainerTab"
 | |
|         ) {
 | |
|           show =
 | |
|             Services.prefs.getBoolPref("privacy.userContext.enabled", false) &&
 | |
|             !lazy.PrivateBrowsingUtils.isWindowPrivate(
 | |
|               getChromeWindow(this._window)
 | |
|             );
 | |
|         } else if (
 | |
|           el.getAttribute("id") != "syncedTabsOpenAllInTabs" &&
 | |
|           el.getAttribute("id") != "syncedTabsManageDevices"
 | |
|         ) {
 | |
|           show = true;
 | |
|         }
 | |
|       } else if (el.getAttribute("id") == "syncedTabsOpenAllInTabs") {
 | |
|         const tabs = item.querySelectorAll(".item-tabs-list > .item.tab");
 | |
|         show = !!tabs.length;
 | |
|       } else if (el.getAttribute("id") == "syncedTabsRefresh") {
 | |
|         show = true;
 | |
|       } else if (el.getAttribute("id") == "syncedTabsManageDevices") {
 | |
|         show = true;
 | |
|       }
 | |
|       el.hidden = !show;
 | |
| 
 | |
|       el = el.nextElementSibling;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Find the parent item element, from a given child element.
 | |
|    * @param {Element} node - Child element.
 | |
|    * @return {Element} Element for the item, or null if not found.
 | |
|    */
 | |
|   _findParentItemNode(node) {
 | |
|     while (
 | |
|       node &&
 | |
|       node !== this.list &&
 | |
|       node !== this._doc.documentElement &&
 | |
|       !node.classList.contains("item")
 | |
|     ) {
 | |
|       node = node.parentNode;
 | |
|     }
 | |
| 
 | |
|     if (node !== this.list && node !== this._doc.documentElement) {
 | |
|       return node;
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   _findParentBranchNode(node) {
 | |
|     while (
 | |
|       node &&
 | |
|       !node.classList.contains("list") &&
 | |
|       node !== this._doc.documentElement &&
 | |
|       !node.parentNode.classList.contains("list")
 | |
|     ) {
 | |
|       node = node.parentNode;
 | |
|     }
 | |
| 
 | |
|     if (node !== this.list && node !== this._doc.documentElement) {
 | |
|       return node;
 | |
|     }
 | |
| 
 | |
|     return null;
 | |
|   },
 | |
| 
 | |
|   _getSelectionPosition(itemNode) {
 | |
|     let parent = this._findParentBranchNode(itemNode);
 | |
|     let parentPosition = this._indexOfNode(parent.parentNode, parent);
 | |
|     let childPosition = -1;
 | |
|     // if the node is not a client, find its position within the parent
 | |
|     if (parent !== itemNode) {
 | |
|       childPosition = this._indexOfNode(itemNode.parentNode, itemNode);
 | |
|     }
 | |
|     return [parentPosition, childPosition];
 | |
|   },
 | |
| 
 | |
|   _indexOfNode(parent, child) {
 | |
|     return Array.prototype.indexOf.call(parent.children, child);
 | |
|   },
 | |
| 
 | |
|   _isTab(item) {
 | |
|     return item && item.classList.contains("tab");
 | |
|   },
 | |
| 
 | |
|   _isClient(item) {
 | |
|     return item && item.classList.contains("client");
 | |
|   },
 | |
| 
 | |
|   _openAllClientTabs(clientNode, where) {
 | |
|     const tabs = clientNode.querySelector(".item-tabs-list").children;
 | |
|     const urls = [...tabs].map(tab => tab.dataset.url);
 | |
|     this.props.onOpenTabs(urls, where);
 | |
|   },
 | |
| };
 | 
