blob: 43bf19c710a64ef7959985134cbd63a3853c098d [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './strings.m.js';
import './tab.js';
import './tab_group.js';
import { ColorChangeUpdater } from 'chrome://resources/cr_components/color_change_listener/colors_css_updater.js';
import { assert } from 'chrome://resources/js/assert.js';
import { CustomElement } from 'chrome://resources/js/custom_element.js';
import { EventTracker } from 'chrome://resources/js/event_tracker.js';
import { FocusOutlineManager } from 'chrome://resources/js/focus_outline_manager.js';
import { isRTL } from 'chrome://resources/js/util.js';
import { DragManager } from './drag_manager.js';
import { isTabElement, TabElement } from './tab.js';
import { isDragHandle, isTabGroupElement } from './tab_group.js';
import { getTemplate } from './tab_list.html.js';
import { TabsApiProxyImpl } 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 SCROLL_PADDING = 32;
let scrollAnimationEnabled = true;
const TOUCH_CONTEXT_MENU_OFFSET_X = 8;
const TOUCH_CONTEXT_MENU_OFFSET_Y = -40;
/**
* Context menu should position below the element for touch.
*/
function getContextMenuPosition(element) {
const rect = element.getBoundingClientRect();
return {
x: rect.left + TOUCH_CONTEXT_MENU_OFFSET_X,
y: rect.bottom + TOUCH_CONTEXT_MENU_OFFSET_Y,
};
}
export function setScrollAnimationEnabledForTesting(enabled) {
scrollAnimationEnabled = enabled;
}
var LayoutVariable;
(function (LayoutVariable) {
LayoutVariable["VIEWPORT_WIDTH"] = "--tabstrip-viewport-width";
LayoutVariable["TAB_WIDTH"] = "--tabstrip-tab-thumbnail-width";
})(LayoutVariable || (LayoutVariable = {}));
/**
* Animates a series of elements to indicate that tabs have moved position.
*/
function animateElementMoved(movedElement, prevIndex, newIndex) {
// Direction is -1 for moving towards a lower index, +1 for moving
// towards a higher index. If moving towards a lower index, the TabList needs
// to animate everything from the movedElement's current index to its prev
// index by traversing the nextElementSibling of each element because the
// movedElement is now at a preceding position from all the elements it has
// slid across. If moving towards a higher index, the TabList needs to
// traverse the previousElementSiblings.
const direction = Math.sign(newIndex - prevIndex);
function getSiblingToAnimate(element) {
return direction === -1 ? element.nextElementSibling :
element.previousElementSibling;
}
let elementToAnimate = getSiblingToAnimate(movedElement);
for (let i = newIndex; i !== prevIndex && elementToAnimate; i -= direction) {
const elementToAnimatePrevIndex = i;
const elementToAnimateNewIndex = i - direction;
slideElement(elementToAnimate, elementToAnimatePrevIndex, elementToAnimateNewIndex);
elementToAnimate = getSiblingToAnimate(elementToAnimate);
}
slideElement(movedElement, prevIndex, newIndex);
}
/**
* Animates the slide of an element across the tab strip (both vertically and
* horizontally for pinned tabs, and horizontally for other tabs and groups).
*/
function slideElement(element, prevIndex, newIndex) {
let horizontalMovement = newIndex - prevIndex;
let verticalMovement = 0;
if (isTabElement(element) && element.tab.pinned) {
const pinnedTabsPerColumn = 3;
const columnChange = Math.floor(newIndex / pinnedTabsPerColumn) -
Math.floor(prevIndex / pinnedTabsPerColumn);
horizontalMovement = columnChange;
verticalMovement =
(newIndex - prevIndex) - (columnChange * pinnedTabsPerColumn);
}
horizontalMovement *= isRTL() ? -1 : 1;
const translateX = `calc(${horizontalMovement * -1} * ` +
'(var(--tabstrip-tab-width) + var(--tabstrip-tab-spacing)))';
const translateY = `calc(${verticalMovement * -1} * ` +
'(var(--tabstrip-tab-height) + var(--tabstrip-tab-spacing)))';
element.isValidDragOverTarget = false;
const animation = element.animate([
{ transform: `translate(${translateX}, ${translateY})` },
{ transform: 'translate(0, 0)' },
], {
duration: 120,
easing: 'ease-out',
});
function onComplete() {
element.isValidDragOverTarget = true;
}
animation.oncancel = onComplete;
animation.onfinish = onComplete;
}
export class TabListElement extends CustomElement {
animationPromises;
currentScrollUpdateFrame_;
draggedItem_;
dropPlaceholder_;
focusOutlineManager_;
thumbnailTracker_;
intersectionObserver_;
activatingTabId_;
activatingTabIdTimestamp_; // In ms.
eventTracker_;
lastTargetedItem_ = null;
lastTouchPoint_;
pinnedTabsElement_;
tabsApi_;
unpinnedTabsElement_;
scrollingTimeoutId_;
static get template() {
return getTemplate();
}
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.
*/
this.animationPromises = Promise.resolve();
/**
* The ID of the current animation frame that is in queue to update the
* scroll position.
*/
this.currentScrollUpdateFrame_ = null;
/**
* The element that is currently being dragged.
*/
this.draggedItem_;
this.dropPlaceholder_ = document.createElement('div');
this.dropPlaceholder_.id = 'dropPlaceholder';
this.focusOutlineManager_ = FocusOutlineManager.forDocument(document);
/**
* Map of tab IDs to whether or not the tab's thumbnail should be tracked.
*/
this.thumbnailTracker_ = new Map();
/**
* An intersection observer is needed to observe which TabElements are
* currently in view or close to being in view, which will help determine
* which thumbnails need to be tracked to stay fresh and which can be
* untracked until they become visible.
*/
this.intersectionObserver_ = new IntersectionObserver(entries => {
for (const entry of entries) {
this.thumbnailTracker_.set(entry.target.tab.id, entry.isIntersecting);
}
if (this.scrollingTimeoutId_ === -1) {
// If there is no need to wait for scroll to end, immediately process
// and request thumbnails.
this.flushThumbnailTracker_();
}
}, {
root: this,
// The horizontal root margin is set to 100% to also track thumbnails that
// are one standard finger swipe away.
rootMargin: '0% 100%',
});
this.eventTracker_ = new EventTracker();
this.pinnedTabsElement_ = this.$('#pinnedTabs');
this.tabsApi_ = TabsApiProxyImpl.getInstance();
this.unpinnedTabsElement_ = this.$('#unpinnedTabs');
/**
* Timeout that is created at every scroll event and is either canceled at
* each subsequent scroll event or resolves after a few milliseconds after
* the last scroll event.
*/
this.scrollingTimeoutId_ = -1;
const callbackRouter = this.tabsApi_.getCallbackRouter();
callbackRouter.layoutChanged.addListener(this.applyCssDictionary_.bind(this));
callbackRouter.tabThumbnailUpdated.addListener(this.tabThumbnailUpdated_.bind(this));
callbackRouter.longPress.addListener(() => this.handleLongPress_());
callbackRouter.contextMenuClosed.addListener(() => this.clearLastTargetedItem_());
callbackRouter.receivedKeyboardFocus.addListener(() => this.onReceivedKeyboardFocus_());
callbackRouter.themeChanged.addListener(() => {
// Refetch theme group color and tab favicons on theme change.
this.fetchAndUpdateGroupData_();
this.fetchAndUpdateTabs_();
});
this.eventTracker_.add(document, 'contextmenu', (e) => this.onContextMenu_(e));
this.eventTracker_.add(document, 'pointerup', (e) => this.onPointerUp_(e));
this.eventTracker_.add(document, 'visibilitychange', () => this.onDocumentVisibilityChange_());
this.eventTracker_.add(window, 'blur', () => this.onWindowBlur_());
this.eventTracker_.add(this, 'scroll', (e) => this.onScroll_(e));
this.eventTracker_.add(document, 'touchstart', (e) => this.onTouchStart_(e));
// Touchmove events happen when a user has started a touch gesture sequence
// and proceeded to move their touch pointer across the screen. Ensure that
// we clear the `last_targeted_item_` in these cases to ensure the pressed
// visual is cleared away.
this.eventTracker_.add(document, 'touchmove', () => this.clearLastTargetedItem_());
const dragManager = new DragManager(this);
dragManager.startObserving();
ColorChangeUpdater.forDocument().start();
}
addAnimationPromise_(promise) {
this.animationPromises = this.animationPromises.then(() => promise);
}
animateScrollPosition_(scrollBy) {
if (this.currentScrollUpdateFrame_) {
cancelAnimationFrame(this.currentScrollUpdateFrame_);
this.currentScrollUpdateFrame_ = null;
}
const prevScrollLeft = this.scrollLeft;
if (!scrollAnimationEnabled || !this.tabsApi_.isVisible()) {
// Do not animate if tab strip is not visible.
this.scrollLeft = prevScrollLeft + scrollBy;
return;
}
const duration = 350;
let startTime;
const onAnimationFrame = (currentTime) => {
if (!startTime) {
startTime = currentTime;
}
const elapsedRatio = Math.min(1, (currentTime - startTime) / duration);
// The elapsed ratio should be decelerated such that the elapsed time
// of the animation gets less and less further apart as time goes on,
// giving the effect of an animation that slows down towards the end. When
// 0ms has passed, the decelerated ratio should be 0. When the full
// duration has passed, the ratio should be 1.
const deceleratedRatio = 1 - (1 - elapsedRatio) / Math.pow(2, 6 * elapsedRatio);
this.scrollLeft = prevScrollLeft + (scrollBy * deceleratedRatio);
this.currentScrollUpdateFrame_ =
deceleratedRatio < 1 ? requestAnimationFrame(onAnimationFrame) : null;
};
this.currentScrollUpdateFrame_ = requestAnimationFrame(onAnimationFrame);
}
applyCssDictionary_(dictionary) {
for (const [cssVariable, value] of Object.entries(dictionary)) {
this.style.setProperty(cssVariable, value);
}
}
clearScrollTimeout_() {
clearTimeout(this.scrollingTimeoutId_);
this.scrollingTimeoutId_ = -1;
}
connectedCallback() {
this.tabsApi_.getLayout().then(({ layout }) => this.applyCssDictionary_(layout));
const getTabsStartTimestamp = Date.now();
this.tabsApi_.getTabs().then(({ tabs }) => {
this.tabsApi_.reportTabDataReceivedDuration(tabs.length, Date.now() - getTabsStartTimestamp);
const createTabsStartTimestamp = Date.now();
tabs.forEach(tab => this.onTabCreated_(tab));
this.fetchAndUpdateGroupData_();
this.tabsApi_.reportTabCreationDuration(tabs.length, Date.now() - createTabsStartTimestamp);
const callbackRouter = this.tabsApi_.getCallbackRouter();
callbackRouter.showContextMenu.addListener(() => this.onShowContextMenu_());
callbackRouter.tabCreated.addListener(this.onTabCreated_.bind(this));
callbackRouter.tabMoved.addListener(this.onTabMoved_.bind(this));
callbackRouter.tabRemoved.addListener(this.onTabRemoved_.bind(this));
callbackRouter.tabReplaced.addListener(this.onTabReplaced_.bind(this));
callbackRouter.tabUpdated.addListener(this.onTabUpdated_.bind(this));
callbackRouter.tabActiveChanged.addListener(this.onTabActivated_.bind(this));
callbackRouter.tabCloseCancelled.addListener(this.onTabCloseCancelled_.bind(this));
callbackRouter.tabGroupStateChanged.addListener(this.onTabGroupStateChanged_.bind(this));
callbackRouter.tabGroupClosed.addListener(this.onTabGroupClosed_.bind(this));
callbackRouter.tabGroupMoved.addListener(this.onTabGroupMoved_.bind(this));
callbackRouter.tabGroupVisualsChanged.addListener(this.onTabGroupVisualsChanged_.bind(this));
});
}
disconnectedCallback() {
this.eventTracker_.removeAll();
}
createTabElement_(tab) {
const tabElement = new TabElement();
tabElement.tab = tab;
tabElement.onTabActivating = (id) => {
this.onTabActivating_(id);
};
return tabElement;
}
findTabElement_(tabId) {
return this.$(`tabstrip-tab[data-tab-id="${tabId}"]`);
}
findTabGroupElement_(groupId) {
return this.$(`tabstrip-tab-group[data-group-id="${groupId}"]`);
}
fetchAndUpdateGroupData_() {
const tabGroupElements = this.$all('tabstrip-tab-group');
this.tabsApi_.getGroupVisualData().then(({ data }) => {
tabGroupElements.forEach(tabGroupElement => {
const visualData = data[tabGroupElement.dataset['groupId']];
assert(visualData);
tabGroupElement.updateVisuals(visualData);
});
});
}
fetchAndUpdateTabs_() {
this.tabsApi_.getTabs().then(({ tabs }) => {
tabs.forEach(tab => this.onTabUpdated_(tab));
});
}
getActiveTab_() {
return this.$('tabstrip-tab[active]');
}
getIndexOfTab(tabElement) {
return Array.prototype.indexOf.call(this.$all('tabstrip-tab'), tabElement);
}
/** @return in pixels */
getLayoutVariable_(variable) {
return parseInt(this.style.getPropertyValue(variable), 10);
}
handleLongPress_() {
if (this.lastTargetedItem_) {
this.lastTargetedItem_.setTouchPressed(true);
}
}
onContextMenu_(event) {
// Prevent the default context menu from triggering.
event.preventDefault();
}
onPointerUp_(event) {
event.stopPropagation();
if (event.pointerType !== 'touch' && event.button === 2) {
// If processing an uncaught right click event show the background context
// menu.
this.tabsApi_.showBackgroundContextMenu(event.clientX, event.clientY);
}
}
onDocumentVisibilityChange_() {
if (!this.tabsApi_.isVisible()) {
this.scrollToActiveTab_();
}
this.unpinnedTabsElement_.childNodes.forEach(element => {
if (isTabGroupElement(element)) {
element.childNodes.forEach(tabElement => this.updateThumbnailTrackStatus_(tabElement));
}
else {
this.updateThumbnailTrackStatus_(element);
}
});
}
onReceivedKeyboardFocus_() {
// FocusOutlineManager relies on the most recent event fired on the
// document. When the tab strip first gains keyboard focus, no such event
// exists yet, so the outline needs to be explicitly set to visible.
this.focusOutlineManager_.visible = true;
this.$('tabstrip-tab').focus();
}
updatePreviouslyActiveTabs_(activeTabId) {
// There may be more than 1 TabElement marked as active if other events
// have updated a Tab to have an active state. For example, if a
// tab is created with an already active state, there may be 2 active
// TabElements: the newly created tab and the previously active tab.
this.$all('tabstrip-tab[active]')
.forEach((previouslyActiveTab) => {
if (previouslyActiveTab.tab.id !== activeTabId) {
previouslyActiveTab.tab = /** @type {!Tab} */ (Object.assign({}, previouslyActiveTab.tab, { active: false }));
}
});
}
onTabActivated_(tabId) {
if (this.activatingTabId_ === tabId) {
this.tabsApi_.reportTabActivationDuration(Date.now() - this.activatingTabIdTimestamp_);
}
this.activatingTabId_ = undefined;
this.activatingTabIdTimestamp_ = undefined;
this.updatePreviouslyActiveTabs_(tabId);
const newlyActiveTab = this.findTabElement_(tabId);
if (newlyActiveTab) {
newlyActiveTab.tab =
Object.assign({}, newlyActiveTab.tab, { active: true });
if (!this.tabsApi_.isVisible()) {
this.scrollToTab_(newlyActiveTab);
}
}
}
onTabActivating_(id) {
// onTabActivating_() is called when the user clicks on a tab in JavaScript.
// We then expect a callback asynchronously from the browser after the tab
// we clicked on has finally activated. We may incur multiple calls to
// onTabActivating_() before the active tab actually changes so we only
// consider the most recent activating action when recording metrics. (See
// crbug.com/1333405)
const activeTab = this.getActiveTab_();
if (activeTab && activeTab.tab.id === id) {
return;
}
this.activatingTabId_ = id;
this.activatingTabIdTimestamp_ = Date.now();
}
onTabCloseCancelled_(id) {
const tabElement = this.findTabElement_(id);
if (!tabElement) {
return;
}
tabElement.resetSwipe();
}
onShowContextMenu_() {
// If we do not have a touch point don't show the context menu.
if (!this.lastTouchPoint_) {
return;
}
if (this.lastTargetedItem_ && isTabElement(this.lastTargetedItem_)) {
const position = getContextMenuPosition(this.lastTargetedItem_);
this.tabsApi_.showTabContextMenu(this.lastTargetedItem_.tab.id, position.x, position.y);
}
else {
this.tabsApi_.showBackgroundContextMenu(this.lastTouchPoint_.clientX, this.lastTouchPoint_.clientY);
}
}
onTabCreated_(tab) {
const droppedTabElement = this.findTabElement_(tab.id);
if (droppedTabElement) {
droppedTabElement.tab = tab;
droppedTabElement.setDragging(false);
this.tabsApi_.setThumbnailTracked(tab.id, true);
return;
}
const tabElement = this.createTabElement_(tab);
this.placeTabElement(tabElement, tab.index, tab.pinned, tab.groupId);
if (tab.active) {
this.updatePreviouslyActiveTabs_(tab.id);
this.scrollToTab_(tabElement);
}
}
onTabGroupClosed_(groupId) {
const tabGroupElement = this.findTabGroupElement_(groupId);
if (!tabGroupElement) {
return;
}
tabGroupElement.remove();
}
onTabGroupMoved_(groupId, index) {
const tabGroupElement = this.findTabGroupElement_(groupId);
if (!tabGroupElement) {
return;
}
this.placeTabGroupElement(tabGroupElement, index);
}
onTabGroupStateChanged_(tabId, index, groupId) {
const tabElement = this.findTabElement_(tabId);
tabElement.tab = Object.assign({}, tabElement.tab, { groupId: groupId });
this.placeTabElement(tabElement, index, false, groupId);
}
onTabGroupVisualsChanged_(groupId, visualData) {
const tabGroupElement = this.findTabGroupElement_(groupId);
tabGroupElement.updateVisuals(visualData);
}
onTabMoved_(tabId, newIndex, pinned) {
const movedTab = this.findTabElement_(tabId);
if (movedTab) {
this.placeTabElement(movedTab, newIndex, pinned, movedTab.tab.groupId);
if (movedTab.tab.active) {
this.scrollToTab_(movedTab);
}
}
}
onTabRemoved_(tabId) {
const tabElement = this.findTabElement_(tabId);
if (tabElement) {
this.addAnimationPromise_(tabElement.slideOut());
}
}
onTabReplaced_(oldId, newId) {
const tabElement = this.findTabElement_(oldId);
if (!tabElement) {
return;
}
tabElement.tab = Object.assign({}, tabElement.tab, { id: newId });
}
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.placeTabElement(tabElement, tab.index, tab.pinned, tab.groupId);
if (tab.active) {
this.scrollToTab_(tabElement);
}
this.updateThumbnailTrackStatus_(tabElement);
}
}
onWindowBlur_() {
if (this.shadowRoot.activeElement) {
// Blur the currently focused element when the window is blurred. This
// prevents the screen reader from momentarily reading out the
// previously focused element when the focus returns to this window.
this.shadowRoot.activeElement.blur();
}
}
onScroll_(_e) {
this.clearScrollTimeout_();
this.scrollingTimeoutId_ = setTimeout(() => {
this.flushThumbnailTracker_();
this.clearScrollTimeout_();
}, 100);
}
onTouchStart_(event) {
const composedPath = event.composedPath();
const dragOverTabElement = (composedPath.find(isTabElement) ||
composedPath.find(isTabGroupElement) || null);
// Make sure drag handle is under touch point when dragging a tab group.
if (dragOverTabElement && isTabGroupElement(dragOverTabElement) &&
!composedPath.find(isDragHandle)) {
return;
}
this.lastTargetedItem_ = dragOverTabElement;
const touch = event.changedTouches[0];
this.lastTouchPoint_ = { clientX: touch.clientX, clientY: touch.clientY };
}
clearLastTargetedItem_() {
if (this.lastTargetedItem_) {
this.lastTargetedItem_.setTouchPressed(false);
}
this.lastTargetedItem_ = null;
this.lastTouchPoint_ = undefined;
}
placeTabElement(element, index, pinned, groupId) {
const isInserting = !element.isConnected;
const previousIndex = isInserting ? -1 : this.getIndexOfTab(element);
const previousParent = element.parentElement;
this.updateTabElementDomPosition_(element, index, pinned, groupId);
if (!isInserting && previousParent === element.parentElement) {
// Only animate if the tab is being moved within the same parent. Tab
// moves that change pinned state or grouped states do not animate.
animateElementMoved(element, previousIndex, index);
}
if (isInserting) {
this.updateThumbnailTrackStatus_(element);
}
}
placeTabGroupElement(element, index) {
const previousDomIndex = Array.from(this.unpinnedTabsElement_.children).indexOf(element);
if (element.isConnected && element.childElementCount &&
this.getIndexOfTab(element.firstElementChild) < index) {
// If moving after its original position, the index value needs to be
// offset by 1 to consider itself already attached to the DOM.
index++;
}
let elementAtIndex = this.$all('tabstrip-tab')[index];
if (elementAtIndex && elementAtIndex.parentElement &&
isTabGroupElement(elementAtIndex.parentElement)) {
elementAtIndex = elementAtIndex.parentElement;
}
this.unpinnedTabsElement_.insertBefore(element, elementAtIndex);
// Animating the TabGroupElement move should be treated the same as
// animating a TabElement. Therefore, treat indices as if they were mere
// tabs and do not use the group's model index as they are not as accurate
// in representing DOM movements.
animateElementMoved(element, previousDomIndex, Array.from(this.unpinnedTabsElement_.children).indexOf(element));
}
flushThumbnailTracker_() {
this.thumbnailTracker_.forEach((shouldTrack, tabId) => {
this.tabsApi_.setThumbnailTracked(tabId, shouldTrack);
});
this.thumbnailTracker_.clear();
}
scrollToActiveTab_() {
const activeTab = this.getActiveTab_();
if (!activeTab) {
return;
}
this.scrollToTab_(activeTab);
}
scrollToTab_(tabElement) {
const tabElementWidth = this.getLayoutVariable_(LayoutVariable.TAB_WIDTH);
const tabElementRect = tabElement.getBoundingClientRect();
// In RTL languages, the TabElement's scale animation scales from right to
// left. Therefore, the value of its getBoundingClientRect().left may not be
// accurate of its final rendered size because the element may not have
// fully scaled to the left yet.
const tabElementLeft = isRTL() ? tabElementRect.right - tabElementWidth : tabElementRect.left;
const leftBoundary = SCROLL_PADDING;
let scrollBy = 0;
if (tabElementLeft === leftBoundary) {
// Perfectly aligned to the left.
return;
}
else if (tabElementLeft < leftBoundary) {
// If the element's left is to the left of the left boundary, scroll
// such that the element's left edge is aligned with the left boundary.
scrollBy = tabElementLeft - leftBoundary;
}
else {
const tabElementRight = tabElementLeft + tabElementWidth;
const rightBoundary = this.getLayoutVariable_(LayoutVariable.VIEWPORT_WIDTH) -
SCROLL_PADDING;
if (tabElementRight > rightBoundary) {
scrollBy = (tabElementRight) - rightBoundary;
}
else {
// Perfectly aligned to the right.
return;
}
}
this.animateScrollPosition_(scrollBy);
}
shouldPreventDrag(isDraggingTab) {
if (isDraggingTab) {
// Do not allow dragging a tab if there's only 1 tab.
return this.$all('tabstrip-tab').length === 1;
}
else {
// Do not allow dragging the tab group with no others outside of the tab
// group. In this case there is only 1 pinned and unpinned top level
// element, which is the dragging tab group itself.
return (this.pinnedTabsElement_.childElementCount +
this.unpinnedTabsElement_.childElementCount) === 1;
}
}
tabThumbnailUpdated_(tabId, imgData) {
const tab = this.findTabElement_(tabId);
if (tab) {
tab.updateThumbnail(imgData);
}
}
updateTabElementDomPosition_(element, index, pinned, groupId) {
// Remove the element if it already exists in the DOM. This simplifies
// the way indices work as it does not have to count its old index in
// the initial layout of the DOM.
element.remove();
if (pinned) {
this.pinnedTabsElement_.insertBefore(element, this.pinnedTabsElement_.childNodes[index]);
}
else {
let elementToInsert = element;
let elementAtIndex = this.$all('tabstrip-tab').item(index);
let parentElement = this.unpinnedTabsElement_;
if (groupId) {
let tabGroupElement = this.findTabGroupElement_(groupId);
if (tabGroupElement) {
// If a TabGroupElement already exists, add the TabElement to it.
parentElement = tabGroupElement;
}
else {
// If a TabGroupElement does not exist, create one and add the
// TabGroupElement into the DOM.
tabGroupElement = document.createElement('tabstrip-tab-group');
tabGroupElement.setAttribute('data-group-id', groupId);
tabGroupElement.appendChild(element);
elementToInsert = tabGroupElement;
}
}
if (elementAtIndex && elementAtIndex.parentElement &&
isTabGroupElement(elementAtIndex.parentElement) &&
(elementAtIndex.previousElementSibling === null &&
elementAtIndex.tab.groupId !== groupId)) {
// If the element at the model index is in a group, and the group is
// different from the new tab's group, and is the first element in its
// group, insert the new element before its TabGroupElement. If a
// TabElement is being sandwiched between two TabElements in a group, it
// can be assumed that the tab will eventually be inserted into the
// group as well.
elementAtIndex = elementAtIndex.parentElement;
}
if (elementAtIndex && elementAtIndex.parentElement === parentElement) {
parentElement.insertBefore(elementToInsert, elementAtIndex);
}
else {
parentElement.appendChild(elementToInsert);
}
}
}
updateThumbnailTrackStatus_(tabElement) {
if (!tabElement.hasTabModel()) {
return;
}
if (this.tabsApi_.isVisible() && !tabElement.tab.pinned) {
// If the tab strip is visible and the tab is not pinned, let the
// IntersectionObserver start observing the TabElement to automatically
// determine if the tab's thumbnail should be tracked.
this.intersectionObserver_.observe(tabElement);
}
else {
// If the tab strip is not visible or the tab is pinned, the tab does not
// need to show or update any thumbnails.
this.intersectionObserver_.unobserve(tabElement);
this.tabsApi_.setThumbnailTracked(tabElement.tab.id, false);
}
}
}
customElements.define('tabstrip-tab-list', TabListElement);