blob: 4258949d50c503649e512d2b33bde3c5788f697f [file] [log] [blame]
John Lee912fb9c02019-08-02 01:28:211// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
John Lee61bc1b52019-10-24 22:27:235import './strings.m.js';
6
John Lee81011102019-10-11 20:05:077import {assert} from 'chrome://resources/js/assert.m.js';
8import {getFavicon} from 'chrome://resources/js/icon.m.js';
John Lee61bc1b52019-10-24 22:27:239import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
John Leec6bc69d2019-11-18 21:53:1710import {isRTL} from 'chrome://resources/js/util.m.js';
John Lee2bafb2f2019-08-21 19:20:0311
John Leea8e39e682019-10-18 02:19:3812import {AlertIndicatorsElement} from './alert_indicators.js';
John Lee912fb9c02019-08-02 01:28:2113import {CustomElement} from './custom_element.js';
John Lee103bb77b2020-05-19 18:59:2114import {TabStripEmbedderProxy, TabStripEmbedderProxyImpl} from './tab_strip_embedder_proxy.js';
John Lee6b1d9572019-11-05 00:33:0615import {TabSwiper} from './tab_swiper.js';
dpapad3d9fd7362020-05-15 17:12:0116import {CloseTabAction, TabData, TabNetworkState, TabsApiProxy, TabsApiProxyImpl} from './tabs_api_proxy.js';
John Lee912fb9c02019-08-02 01:28:2117
John Lee6b1d9572019-11-05 00:33:0618const DEFAULT_ANIMATION_DURATION = 125;
John Leea97ceab6f2019-09-04 22:26:4619
John Lee61bc1b52019-10-24 22:27:2320/**
21 * @param {!TabData} tab
22 * @return {string}
23 */
24function getAccessibleTitle(tab) {
25 const tabTitle = tab.title;
26
27 if (tab.crashed) {
28 return loadTimeData.getStringF('tabCrashed', tabTitle);
29 }
30
31 if (tab.networkState === TabNetworkState.ERROR) {
32 return loadTimeData.getStringF('tabNetworkError', tabTitle);
33 }
34
35 return tabTitle;
36}
37
John Lee7bbf7ad2019-11-21 23:38:5138/**
John Leed96ee0532019-11-22 22:06:3939 * TODO(crbug.com/1025390): padding-inline-end cannot be animated yet.
John Lee7bbf7ad2019-11-21 23:38:5140 * @return {string}
41 */
John Leed96ee0532019-11-22 22:06:3942function getPaddingInlineEndProperty() {
43 return isRTL() ? 'paddingLeft' : 'paddingRight';
John Lee7bbf7ad2019-11-21 23:38:5144}
45
John Lee3f8dae92019-08-09 22:37:0946export class TabElement extends CustomElement {
John Lee912fb9c02019-08-02 01:28:2147 static get template() {
48 return `{__html_template__}`;
49 }
50
John Lee3f8dae92019-08-09 22:37:0951 constructor() {
52 super();
53
John Leea8e39e682019-10-18 02:19:3854 this.alertIndicatorsEl_ = /** @type {!AlertIndicatorsElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2855 (this.$('tabstrip-alert-indicators'));
John Leea8e39e682019-10-18 02:19:3856 // Normally, custom elements will get upgraded automatically once added to
57 // the DOM, but TabElement may need to update properties on
58 // AlertIndicatorElement before this happens, so upgrade it manually.
59 customElements.upgrade(this.alertIndicatorsEl_);
60
John Lee7d416b782019-08-12 22:32:1361 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2862 this.closeButtonEl_ = /** @type {!HTMLElement} */ (this.$('#close'));
John Lee61bc1b52019-10-24 22:27:2363 this.closeButtonEl_.setAttribute(
64 'aria-label', loadTimeData.getString('closeTab'));
John Lee7d416b782019-08-12 22:32:1365
John Lee2bafb2f2019-08-21 19:20:0366 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2867 this.dragImageEl_ = /** @type {!HTMLElement} */ (this.$('#dragImage'));
John Leedff2fc82019-12-19 04:06:1668
69 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2870 this.tabEl_ = /** @type {!HTMLElement} */ (this.$('#tab'));
John Lee3b309d52019-09-18 19:08:0971
72 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2873 this.faviconEl_ = /** @type {!HTMLElement} */ (this.$('#favicon'));
John Lee2bafb2f2019-08-21 19:20:0374
Collin Bakere21f723d2019-09-05 20:05:4175 /** @private {!HTMLElement} */
76 this.thumbnailContainer_ =
Demetrios Papadopoulos94a15132020-01-27 21:45:2877 /** @type {!HTMLElement} */ (this.$('#thumbnail'));
Collin Bakere21f723d2019-09-05 20:05:4178
79 /** @private {!Image} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2880 this.thumbnail_ = /** @type {!Image} */ (this.$('#thumbnailImg'));
Collin Bakere21f723d2019-09-05 20:05:4181
John Lee99367a62019-10-10 19:03:4982 /** @private {!TabData} */
John Lee3f8dae92019-08-09 22:37:0983 this.tab_;
84
John Lee7d416b782019-08-12 22:32:1385 /** @private {!TabsApiProxy} */
dpapad3d9fd7362020-05-15 17:12:0186 this.tabsApi_ = TabsApiProxyImpl.getInstance();
John Lee7d416b782019-08-12 22:32:1387
Collin Baker1ac2e432019-10-16 18:17:2188 /** @private {!TabStripEmbedderProxy} */
John Lee103bb77b2020-05-19 18:59:2189 this.embedderApi_ = TabStripEmbedderProxyImpl.getInstance();
Collin Baker1ac2e432019-10-16 18:17:2190
John Lee3f8dae92019-08-09 22:37:0991 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2892 this.titleTextEl_ = /** @type {!HTMLElement} */ (this.$('#titleText'));
John Lee3f8dae92019-08-09 22:37:0993
John Lee1a8ab212020-06-05 18:06:4394 /**
95 * Flag indicating if this TabElement can accept dragover events. This
96 * is used to pause dragover events while animating as animating causes
97 * the elements below the pointer to shift.
98 * @private {boolean}
99 */
100 this.isValidDragOverTarget_ = true;
101
John Lee6b1d9572019-11-05 00:33:06102 this.tabEl_.addEventListener('click', () => this.onClick_());
103 this.tabEl_.addEventListener('contextmenu', e => this.onContextMenu_(e));
John Lee7f0c1922019-11-08 22:06:25104 this.tabEl_.addEventListener(
105 'keydown', e => this.onKeyDown_(/** @type {!KeyboardEvent} */ (e)));
John Leea00d58b2020-10-09 23:28:27106 this.tabEl_.addEventListener(
107 'pointerup', e => this.onPointerUp_(/** @type {!PointerEvent} */ (e)));
108
John Lee6b1d9572019-11-05 00:33:06109 this.closeButtonEl_.addEventListener('click', e => this.onClose_(e));
110 this.addEventListener('swipe', () => this.onSwipe_());
111
112 /** @private @const {!TabSwiper} */
113 this.tabSwiper_ = new TabSwiper(this);
Robert Liao6ab5f712019-12-04 02:15:01114
115 /** @private {!Function} */
116 this.onTabActivating_ = (tabId) => {};
John Lee912fb9c02019-08-02 01:28:21117 }
John Lee3f8dae92019-08-09 22:37:09118
John Lee99367a62019-10-10 19:03:49119 /** @return {!TabData} */
John Lee3f8dae92019-08-09 22:37:09120 get tab() {
121 return this.tab_;
122 }
123
John Lee99367a62019-10-10 19:03:49124 /** @param {!TabData} tab */
John Lee3f8dae92019-08-09 22:37:09125 set tab(tab) {
John Lee81011102019-10-11 20:05:07126 assert(this.tab_ !== tab);
John Lee8e253602019-08-14 22:22:51127 this.toggleAttribute('active', tab.active);
John Lee61bc1b52019-10-24 22:27:23128 this.tabEl_.setAttribute('aria-selected', tab.active.toString());
John Lee81011102019-10-11 20:05:07129 this.toggleAttribute('hide-icon_', !tab.showIcon);
John Lee99367a62019-10-10 19:03:49130 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07131 'waiting_',
John Lee99367a62019-10-10 19:03:49132 !tab.shouldHideThrobber &&
133 tab.networkState === TabNetworkState.WAITING);
134 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07135 'loading_',
John Lee99367a62019-10-10 19:03:49136 !tab.shouldHideThrobber &&
137 tab.networkState === TabNetworkState.LOADING);
John Leea8e39e682019-10-18 02:19:38138 this.toggleAttribute('pinned', tab.pinned);
John Lee4734ca12019-10-14 19:34:01139 this.toggleAttribute('blocked_', tab.blocked);
John Lee4bcbaf12019-10-21 23:29:46140 this.setAttribute('draggable', true);
John Lee02b3a742019-10-15 20:00:56141 this.toggleAttribute('crashed_', tab.crashed);
John Lee8e253602019-08-14 22:22:51142
John Lee2f3baa72019-11-12 19:58:14143 if (tab.title) {
John Lee3f8dae92019-08-09 22:37:09144 this.titleTextEl_.textContent = tab.title;
John Lee2f3baa72019-11-12 19:58:14145 } else if (
146 !tab.shouldHideThrobber &&
147 (tab.networkState === TabNetworkState.WAITING ||
148 tab.networkState === TabNetworkState.LOADING)) {
149 this.titleTextEl_.textContent = loadTimeData.getString('loadingTab');
150 } else {
151 this.titleTextEl_.textContent = loadTimeData.getString('defaultTabTitle');
John Lee3f8dae92019-08-09 22:37:09152 }
John Lee61bc1b52019-10-24 22:27:23153 this.titleTextEl_.setAttribute('aria-label', getAccessibleTitle(tab));
John Lee3f8dae92019-08-09 22:37:09154
John Lee81011102019-10-11 20:05:07155 if (tab.networkState === TabNetworkState.WAITING ||
156 (tab.networkState === TabNetworkState.LOADING &&
157 tab.isDefaultFavicon)) {
158 this.faviconEl_.style.backgroundImage = 'none';
159 } else if (tab.favIconUrl) {
160 this.faviconEl_.style.backgroundImage = `url(${tab.favIconUrl})`;
John Leed14bec62019-09-23 22:41:32161 } else {
John Lee81011102019-10-11 20:05:07162 this.faviconEl_.style.backgroundImage = getFavicon('');
John Lee2bafb2f2019-08-21 19:20:03163 }
164
John Lee3f8dae92019-08-09 22:37:09165 // Expose the ID to an attribute to allow easy querySelector use
166 this.setAttribute('data-tab-id', tab.id);
167
John Leea8e39e682019-10-18 02:19:38168 this.alertIndicatorsEl_.updateAlertStates(tab.alertStates)
169 .then((alertIndicatorsCount) => {
170 this.toggleAttribute('has-alert-states_', alertIndicatorsCount > 0);
171 });
172
John Lee6b1d9572019-11-05 00:33:06173 if (!this.tab_ || (this.tab_.pinned !== tab.pinned && !tab.pinned)) {
174 this.tabSwiper_.startObserving();
175 } else if (this.tab_.pinned !== tab.pinned && tab.pinned) {
176 this.tabSwiper_.stopObserving();
177 }
178
John Lee3f8dae92019-08-09 22:37:09179 this.tab_ = Object.freeze(tab);
180 }
John Lee7d416b782019-08-12 22:32:13181
John Lee1a8ab212020-06-05 18:06:43182 /** @return {boolean} */
183 get isValidDragOverTarget() {
184 return !this.hasAttribute('dragging_') && this.isValidDragOverTarget_;
185 }
186
187 /** @param {boolean} isValid */
188 set isValidDragOverTarget(isValid) {
189 this.isValidDragOverTarget_ = isValid;
190 }
191
Robert Liao6ab5f712019-12-04 02:15:01192 /** @param {!Function} callback */
193 set onTabActivating(callback) {
194 this.onTabActivating_ = callback;
195 }
196
John Lee7f0c1922019-11-08 22:06:25197 focus() {
198 this.tabEl_.focus();
199 }
200
John Lee61bc1b52019-10-24 22:27:23201 /** @return {!HTMLElement} */
John Lee3b309d52019-09-18 19:08:09202 getDragImage() {
John Leedff2fc82019-12-19 04:06:16203 return this.dragImageEl_;
John Lee3b309d52019-09-18 19:08:09204 }
205
John Lee10e2eac2020-07-20 18:27:54206 /** @return {!HTMLElement} */
207 getDragImageCenter() {
208 // dragImageEl_ has padding, so the drag image should be centered relative
209 // to tabEl_, the element within the padding.
210 return this.tabEl_;
211 }
212
John Lee3b309d52019-09-18 19:08:09213 /**
Collin Bakere21f723d2019-09-05 20:05:41214 * @param {string} imgData
215 */
216 updateThumbnail(imgData) {
217 this.thumbnail_.src = imgData;
218 }
219
John Lee7d416b782019-08-12 22:32:13220 /** @private */
John Lee8e253602019-08-14 22:22:51221 onClick_() {
John Lee6b1d9572019-11-05 00:33:06222 if (!this.tab_ || this.tabSwiper_.wasSwiping()) {
John Lee7d416b782019-08-12 22:32:13223 return;
224 }
225
Robert Liao6ab5f712019-12-04 02:15:01226 const tabId = this.tab_.id;
227 this.onTabActivating_(tabId);
228 this.tabsApi_.activateTab(tabId);
John Lee4bcbaf12019-10-21 23:29:46229
John Lee13f336f2020-07-08 02:27:42230 this.embedderApi_.closeContainer();
John Lee8e253602019-08-14 22:22:51231 }
232
John Lee61bc1b52019-10-24 22:27:23233 /**
234 * @param {!Event} event
235 * @private
236 */
Collin Bakerdc3d2112019-10-10 18:28:20237 onContextMenu_(event) {
238 event.preventDefault();
Collin Baker48a096b82019-11-26 01:21:27239 event.stopPropagation();
Collin Bakerdc3d2112019-10-10 18:28:20240 }
241
John Lee8e253602019-08-14 22:22:51242 /**
243 * @param {!Event} event
244 * @private
245 */
246 onClose_(event) {
John Leec24f4822020-03-10 21:32:55247 assert(this.tab_);
John Lee8e253602019-08-14 22:22:51248 event.stopPropagation();
John Leeadf33c82019-12-12 18:21:33249 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.CLOSE_BUTTON);
John Lee7d416b782019-08-12 22:32:13250 }
John Leea97ceab6f2019-09-04 22:26:46251
John Lee6b1d9572019-11-05 00:33:06252 /** @private */
253 onSwipe_() {
John Leec24f4822020-03-10 21:32:55254 assert(this.tab_);
John Leeadf33c82019-12-12 18:21:33255 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.SWIPED_TO_CLOSE);
John Lee6b1d9572019-11-05 00:33:06256 }
257
John Leea97ceab6f2019-09-04 22:26:46258 /**
John Lee7f0c1922019-11-08 22:06:25259 * @param {!KeyboardEvent} event
260 * @private
261 */
262 onKeyDown_(event) {
263 if (event.key === 'Enter' || event.key === ' ') {
264 this.onClick_();
265 }
266 }
267
John Leea00d58b2020-10-09 23:28:27268 /**
269 * @param {!PointerEvent} event
270 * @private
271 */
272 onPointerUp_(event) {
273 if (event.pointerType !== 'touch' && event.button === 2) {
274 this.embedderApi_.showTabContextMenu(
275 this.tab.id, event.clientX, event.clientY);
276 }
277 }
278
John Leec24f4822020-03-10 21:32:55279 resetSwipe() {
280 this.tabSwiper_.reset();
281 }
282
John Lee7f0c1922019-11-08 22:06:25283 /**
John Lee870be4192020-01-27 23:23:02284 * @param {boolean} isDragging
John Lee3b309d52019-09-18 19:08:09285 */
John Lee870be4192020-01-27 23:23:02286 setDragging(isDragging) {
287 this.toggleAttribute('dragging_', isDragging);
John Lee3b309d52019-09-18 19:08:09288 }
289
John Lee656beed2020-07-07 22:45:52290 /** @param {boolean} isDraggedOut */
291 setDraggedOut(isDraggedOut) {
292 this.toggleAttribute('dragged-out_', isDraggedOut);
293 }
294
John Lee3b309d52019-09-18 19:08:09295 /**
John Leea97ceab6f2019-09-04 22:26:46296 * @return {!Promise}
297 */
298 slideIn() {
John Leed96ee0532019-11-22 22:06:39299 const paddingInlineEnd = getPaddingInlineEndProperty();
John Leec6bc69d2019-11-18 21:53:17300
John Lee41620f832019-11-27 22:41:05301 // If this TabElement is the last tab, there needs to be enough space for
302 // the view to scroll to it. Therefore, immediately take up all the space
303 // it needs to and only animate the scale.
304 const isLastChild = this.nextElementSibling === null;
305
John Leec6bc69d2019-11-18 21:53:17306 const startState = {
John Lee41620f832019-11-27 22:41:05307 maxWidth: isLastChild ? 'var(--tabstrip-tab-width)' : 0,
John Leec6bc69d2019-11-18 21:53:17308 transform: `scale(0)`,
309 };
John Lee41620f832019-11-27 22:41:05310 startState[paddingInlineEnd] =
311 isLastChild ? 'var(--tabstrip-tab-spacing)' : 0;
John Leec6bc69d2019-11-18 21:53:17312
313 const finishState = {
314 maxWidth: `var(--tabstrip-tab-width)`,
315 transform: `scale(1)`,
316 };
John Leed96ee0532019-11-22 22:06:39317 finishState[paddingInlineEnd] = 'var(--tabstrip-tab-spacing)';
John Leec6bc69d2019-11-18 21:53:17318
John Leea97ceab6f2019-09-04 22:26:46319 return new Promise(resolve => {
John Leec6bc69d2019-11-18 21:53:17320 const animation = this.animate([startState, finishState], {
321 duration: 300,
322 easing: 'cubic-bezier(.4, 0, 0, 1)',
323 });
John Lee6b1d9572019-11-05 00:33:06324 animation.onfinish = () => {
John Lee6b1d9572019-11-05 00:33:06325 resolve();
326 };
John Lee29411b52019-12-20 02:16:15327
328 // TODO(crbug.com/1035678) By the next animation frame, the animation
329 // should start playing. By the time another animation frame happens,
330 // force play the animation if the animation has not yet begun. Remove
331 // if/when the Blink issue has been fixed.
332 requestAnimationFrame(() => {
333 requestAnimationFrame(() => {
334 if (animation.pending) {
335 animation.play();
336 }
337 });
338 });
John Leea97ceab6f2019-09-04 22:26:46339 });
340 }
341
342 /**
343 * @return {!Promise}
344 */
345 slideOut() {
John Leec24f4822020-03-10 21:32:55346 if (!this.embedderApi_.isVisible() || this.tab_.pinned ||
347 this.tabSwiper_.wasSwiping()) {
John Leed71aaac2019-11-14 20:37:50348 this.remove();
349 return Promise.resolve();
350 }
351
John Leea97ceab6f2019-09-04 22:26:46352 return new Promise(resolve => {
John Leed71aaac2019-11-14 20:37:50353 const finishCallback = () => {
354 this.remove();
355 resolve();
356 };
357
John Lee7bbf7ad2019-11-21 23:38:51358 const translateAnimation = this.animate(
John Leea97ceab6f2019-09-04 22:26:46359 {
John Lee7bbf7ad2019-11-21 23:38:51360 transform: ['translateY(0)', 'translateY(-100%)'],
361 },
362 {
363 duration: 150,
364 easing: 'cubic-bezier(.4, 0, 1, 1)',
John Leea97ceab6f2019-09-04 22:26:46365 fill: 'forwards',
366 });
John Lee7bbf7ad2019-11-21 23:38:51367 const opacityAnimation = this.animate(
368 {
369 opacity: [1, 0],
370 },
371 {
372 delay: 97.5,
373 duration: 50,
374 fill: 'forwards',
375 });
376
377 const widthAnimationKeyframes = {
378 maxWidth: ['var(--tabstrip-tab-width)', 0],
379 };
John Leed96ee0532019-11-22 22:06:39380 widthAnimationKeyframes[getPaddingInlineEndProperty()] =
381 ['var(--tabstrip-tab-spacing)', 0];
John Lee7bbf7ad2019-11-21 23:38:51382 const widthAnimation = this.animate(widthAnimationKeyframes, {
383 delay: 97.5,
384 duration: 300,
385 easing: 'cubic-bezier(.4, 0, 0, 1)',
386 fill: 'forwards',
387 });
John Leed71aaac2019-11-14 20:37:50388
389 const visibilityChangeListener = () => {
390 if (!this.embedderApi_.isVisible()) {
391 // If a tab strip becomes hidden during the animation, the onfinish
392 // event will not get fired until the tab strip becomes visible again.
393 // Therefore, when the tab strip becomes hidden, immediately call the
394 // finish callback.
John Lee7bbf7ad2019-11-21 23:38:51395 translateAnimation.cancel();
396 opacityAnimation.cancel();
397 widthAnimation.cancel();
John Leed71aaac2019-11-14 20:37:50398 finishCallback();
399 }
400 };
401
402 document.addEventListener(
403 'visibilitychange', visibilityChangeListener, {once: true});
John Lee7bbf7ad2019-11-21 23:38:51404 // The onfinish handler is put on the width animation, as it will end
405 // last.
406 widthAnimation.onfinish = () => {
John Leed71aaac2019-11-14 20:37:50407 document.removeEventListener(
408 'visibilitychange', visibilityChangeListener);
409 finishCallback();
John Leea97ceab6f2019-09-04 22:26:46410 };
411 });
412 }
John Lee912fb9c02019-08-02 01:28:21413}
414
John Lee3f8dae92019-08-09 22:37:09415customElements.define('tabstrip-tab', TabElement);
John Leec5bf0f22020-02-05 21:01:13416
417/**
418 * @param {!Element} element
419 * @return {boolean}
420 */
421export function isTabElement(element) {
422 return element.tagName === 'TABSTRIP-TAB';
423}