blob: d7736c8afd9656a0499549af935ba885fbc38bef [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './tab.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {addWebUIListener} from 'chrome://resources/js/cr.m.js';
import {CustomElement} from './custom_element.js';
import {TabElement} from './tab.js';
import {TabStripViewProxy} from './tab_strip_view_proxy.js';
import {TabData, TabsApiProxy} from './tabs_api_proxy.js';
/**
* The amount of padding to leave between the edge of the screen and the active
* tab when auto-scrolling. This should leave some room to show the previous or
* next tab to afford to users that there more tabs if the user scrolls.
* @const {number}
*/
const SCROLL_PADDING = 32;
/**
* @param {!Element} element
* @return {boolean}
*/
function isTabElement(element) {
return element.tagName === 'TABSTRIP-TAB';
}
class TabListElement extends CustomElement {
static get template() {
return `{__html_template__}`;
}
constructor() {
super();
/**
* A chain of promises that the tab list needs to keep track of. The chain
* is useful in cases when the list needs to wait for all animations to
* finish in order to get accurate pixels (such as getting the position of a
* tab) or accurate element counts.
* @type {!Promise}
*/
this.animationPromises = Promise.resolve();
/**
* The TabElement that is currently being dragged.
* @private {!TabElement|undefined}
*/
this.draggedItem_;
/** @private {!Element} */
this.pinnedTabsContainerElement_ =
/** @type {!Element} */ (
this.shadowRoot.querySelector('#pinnedTabsContainer'));
/** @private {!Element} */
this.scrollingParent_ = document.documentElement;
/** @private {!TabStripViewProxy} */
this.tabStripViewProxy_ = TabStripViewProxy.getInstance();
/** @private {!TabsApiProxy} */
this.tabsApi_ = TabsApiProxy.getInstance();
/** @private {!Element} */
this.tabsContainerElement_ =
/** @type {!Element} */ (
this.shadowRoot.querySelector('#tabsContainer'));
addWebUIListener('theme-changed', () => this.fetchAndUpdateColors_());
this.tabStripViewProxy_.observeThemeChanges();
addWebUIListener(
'tab-thumbnail-updated', this.tabThumbnailUpdated_.bind(this));
this.addEventListener(
'dragstart', (e) => this.onDragStart_(/** @type {!DragEvent} */ (e)));
this.addEventListener(
'dragend', (e) => this.onDragEnd_(/** @type {!DragEvent} */ (e)));
this.addEventListener(
'dragover', (e) => this.onDragOver_(/** @type {!DragEvent} */ (e)));
document.addEventListener(
'visibilitychange', () => this.moveOrScrollToActiveTab_());
}
/**
* @param {!Promise} promise
* @private
*/
addAnimationPromise_(promise) {
this.animationPromises = this.animationPromises.then(() => promise);
}
connectedCallback() {
this.fetchAndUpdateColors_();
this.tabsApi_.getTabs().then(tabs => {
tabs.forEach(tab => this.onTabCreated_(tab));
this.moveOrScrollToActiveTab_();
addWebUIListener('tab-created', tab => this.onTabCreated_(tab));
addWebUIListener(
'tab-moved', (tabId, newIndex) => this.onTabMoved_(tabId, newIndex));
addWebUIListener('tab-removed', tabId => this.onTabRemoved_(tabId));
addWebUIListener('tab-updated', tab => this.onTabUpdated_(tab));
addWebUIListener(
'tab-active-changed', tabId => this.onTabActivated_(tabId));
});
}
/**
* @param {!TabData} tab
* @return {!TabElement}
* @private
*/
createTabElement_(tab) {
const tabElement = new TabElement();
tabElement.tab = tab;
return tabElement;
}
/**
* @param {number} tabId
* @return {?TabElement}
* @private
*/
findTabElement_(tabId) {
return /** @type {?TabElement} */ (
this.shadowRoot.querySelector(`tabstrip-tab[data-tab-id="${tabId}"]`));
}
/** @private */
fetchAndUpdateColors_() {
this.tabStripViewProxy_.getColors().then(colors => {
for (const [cssVariable, rgbaValue] of Object.entries(colors)) {
this.style.setProperty(cssVariable, rgbaValue);
}
});
}
/**
* @return {?TabElement}
* @private
*/
getActiveTab_() {
return /** @type {?TabElement} */ (
this.shadowRoot.querySelector('tabstrip-tab[active]'));
}
/**
* @param {!TabElement} tabElement
* @param {number} index
* @private
*/
insertTabOrMoveTo_(tabElement, index) {
// Remove the tabElement if it already exists in the DOM
tabElement.remove();
if (tabElement.tab && tabElement.tab.pinned) {
this.pinnedTabsContainerElement_.insertBefore(
tabElement, this.pinnedTabsContainerElement_.childNodes[index]);
} else {
// Pinned tabs are in their own container, so the index of non-pinned
// tabs need to be offset by the number of pinned tabs
const offsetIndex =
index - this.pinnedTabsContainerElement_.childElementCount;
this.tabsContainerElement_.insertBefore(
tabElement, this.tabsContainerElement_.childNodes[offsetIndex]);
}
}
/** @private */
moveOrScrollToActiveTab_() {
const activeTab = this.getActiveTab_();
if (!activeTab) {
return;
}
if (!this.tabStripViewProxy_.isVisible() && !activeTab.tab.pinned &&
this.tabsContainerElement_.firstChild !== activeTab) {
this.tabsApi_.moveTab(
activeTab.tab.id, this.pinnedTabsContainerElement_.childElementCount);
} else {
this.scrollToTab_(activeTab);
}
}
/**
* @param {!DragEvent} event
* @private
*/
onDragEnd_(event) {
if (!this.draggedItem_) {
return;
}
this.draggedItem_.setDragging(false);
this.draggedItem_ = undefined;
}
/**
* @param {!DragEvent} event
* @private
*/
onDragOver_(event) {
event.preventDefault();
const dragOverItem = event.path.find((pathItem) => {
return pathItem !== this.draggedItem_ && isTabElement(pathItem);
});
if (!dragOverItem || !this.draggedItem_ || !dragOverItem.tab.pinned) {
return;
}
event.dataTransfer.dropEffect = 'move';
const dragOverIndex =
Array.from(dragOverItem.parentNode.children).indexOf(dragOverItem);
this.tabsApi_.moveTab(this.draggedItem_.tab.id, dragOverIndex);
}
/**
* @param {!DragEvent} event
* @private
*/
onDragStart_(event) {
const draggedItem = event.path[0];
if (!isTabElement(draggedItem)) {
return;
}
assert(draggedItem.tab.pinned);
this.draggedItem_ = /** @type {!TabElement} */ (draggedItem);
this.draggedItem_.setDragging(true);
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setDragImage(
this.draggedItem_.getDragImage(),
event.pageX - this.draggedItem_.offsetLeft,
event.pageY - this.draggedItem_.offsetTop);
}
/**
* @param {number} tabId
* @private
*/
onTabActivated_(tabId) {
const previouslyActiveTab = this.getActiveTab_();
if (previouslyActiveTab) {
previouslyActiveTab.tab = /** @type {!TabData} */ (
Object.assign({}, previouslyActiveTab.tab, {active: false}));
}
const newlyActiveTab = this.findTabElement_(tabId);
if (newlyActiveTab) {
newlyActiveTab.tab = /** @type {!TabData} */ (
Object.assign({}, newlyActiveTab.tab, {active: true}));
this.moveOrScrollToActiveTab_();
}
}
/**
* @param {!TabData} tab
* @private
*/
onTabCreated_(tab) {
const tabElement = this.createTabElement_(tab);
if (tab.active && !tab.pinned &&
tab.index !== this.pinnedTabsContainerElement_.childElementCount) {
// Newly created active tabs should first be moved to the very beginning
// of the tab strip to enforce the tab strip's most recently used ordering
this.tabsApi_
.moveTab(tab.id, this.pinnedTabsContainerElement_.childElementCount)
.then(() => {
this.insertTabOrMoveTo_(
tabElement, this.pinnedTabsContainerElement_.childElementCount);
this.addAnimationPromise_(tabElement.slideIn());
});
} else {
this.insertTabOrMoveTo_(tabElement, tab.index);
this.addAnimationPromise_(tabElement.slideIn());
}
}
/**
* @param {number} tabId
* @param {number} newIndex
* @private
*/
onTabMoved_(tabId, newIndex) {
const movedTab = this.findTabElement_(tabId);
if (movedTab) {
this.insertTabOrMoveTo_(movedTab, newIndex);
if (movedTab.tab.active) {
this.scrollToTab_(movedTab);
}
}
}
/**
* @param {number} tabId
* @private
*/
onTabRemoved_(tabId) {
const tabElement = this.findTabElement_(tabId);
if (tabElement) {
this.addAnimationPromise_(tabElement.slideOut());
}
}
/**
* @param {!TabData} tab
* @private
*/
onTabUpdated_(tab) {
const tabElement = this.findTabElement_(tab.id);
if (!tabElement) {
return;
}
const previousTab = tabElement.tab;
tabElement.tab = tab;
if (previousTab.pinned !== tab.pinned) {
// If the tab is being pinned or unpinned, we need to move it to its new
// location
this.insertTabOrMoveTo_(tabElement, tab.index);
if (tab.active) {
this.scrollToTab_(tabElement);
}
}
}
/**
* @param {!TabElement} tabElement
* @private
*/
scrollToTab_(tabElement) {
this.animationPromises.then(() => {
const screenLeft = this.scrollingParent_.scrollLeft;
const screenRight = screenLeft + this.scrollingParent_.offsetWidth;
if (screenLeft > tabElement.offsetLeft) {
// If the element's left is to the left of the visible screen, scroll
// such that the element's left edge is aligned with the screen's edge
this.scrollingParent_.scrollLeft =
tabElement.offsetLeft - SCROLL_PADDING;
} else if (screenRight < tabElement.offsetLeft + tabElement.offsetWidth) {
// If the element's right is to the right of the visible screen, scroll
// such that the element's right edge is aligned with the screen's right
// edge.
this.scrollingParent_.scrollLeft = tabElement.offsetLeft +
tabElement.offsetWidth - this.scrollingParent_.offsetWidth +
SCROLL_PADDING;
}
});
}
/**
* @param {number} tabId
* @param {string} imgData
* @private
*/
tabThumbnailUpdated_(tabId, imgData) {
const tab = this.findTabElement_(tabId);
if (tab) {
tab.updateThumbnail(imgData);
}
}
}
customElements.define('tabstrip-tab-list', TabListElement);