blob: a5fa3db13b639bb01f53665f3eddfd12cd196c9b [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';
Collin Baker1ac2e432019-10-16 18:17:2114import {TabStripEmbedderProxy} from './tab_strip_embedder_proxy.js';
John Lee4bcbaf12019-10-21 23:29:4615import {tabStripOptions} from './tab_strip_options.js';
John Lee6b1d9572019-11-05 00:33:0616import {TabSwiper} from './tab_swiper.js';
John Leeadf33c82019-12-12 18:21:3317import {CloseTabAction, TabData, TabNetworkState, TabsApiProxy} from './tabs_api_proxy.js';
John Lee912fb9c02019-08-02 01:28:2118
John Lee6b1d9572019-11-05 00:33:0619const DEFAULT_ANIMATION_DURATION = 125;
John Leea97ceab6f2019-09-04 22:26:4620
John Lee61bc1b52019-10-24 22:27:2321/**
22 * @param {!TabData} tab
23 * @return {string}
24 */
25function getAccessibleTitle(tab) {
26 const tabTitle = tab.title;
27
28 if (tab.crashed) {
29 return loadTimeData.getStringF('tabCrashed', tabTitle);
30 }
31
32 if (tab.networkState === TabNetworkState.ERROR) {
33 return loadTimeData.getStringF('tabNetworkError', tabTitle);
34 }
35
36 return tabTitle;
37}
38
John Lee7bbf7ad2019-11-21 23:38:5139/**
John Leed96ee0532019-11-22 22:06:3940 * TODO(crbug.com/1025390): padding-inline-end cannot be animated yet.
John Lee7bbf7ad2019-11-21 23:38:5141 * @return {string}
42 */
John Leed96ee0532019-11-22 22:06:3943function getPaddingInlineEndProperty() {
44 return isRTL() ? 'paddingLeft' : 'paddingRight';
John Lee7bbf7ad2019-11-21 23:38:5145}
46
John Lee3f8dae92019-08-09 22:37:0947export class TabElement extends CustomElement {
John Lee912fb9c02019-08-02 01:28:2148 static get template() {
49 return `{__html_template__}`;
50 }
51
John Lee3f8dae92019-08-09 22:37:0952 constructor() {
53 super();
54
John Leea8e39e682019-10-18 02:19:3855 this.alertIndicatorsEl_ = /** @type {!AlertIndicatorsElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2856 (this.$('tabstrip-alert-indicators'));
John Leea8e39e682019-10-18 02:19:3857 // Normally, custom elements will get upgraded automatically once added to
58 // the DOM, but TabElement may need to update properties on
59 // AlertIndicatorElement before this happens, so upgrade it manually.
60 customElements.upgrade(this.alertIndicatorsEl_);
61
John Lee7d416b782019-08-12 22:32:1362 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2863 this.closeButtonEl_ = /** @type {!HTMLElement} */ (this.$('#close'));
John Lee61bc1b52019-10-24 22:27:2364 this.closeButtonEl_.setAttribute(
65 'aria-label', loadTimeData.getString('closeTab'));
John Lee7d416b782019-08-12 22:32:1366
John Lee2bafb2f2019-08-21 19:20:0367 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2868 this.dragImageEl_ = /** @type {!HTMLElement} */ (this.$('#dragImage'));
John Leedff2fc82019-12-19 04:06:1669
70 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2871 this.tabEl_ = /** @type {!HTMLElement} */ (this.$('#tab'));
John Lee3b309d52019-09-18 19:08:0972
73 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2874 this.faviconEl_ = /** @type {!HTMLElement} */ (this.$('#favicon'));
John Lee2bafb2f2019-08-21 19:20:0375
Collin Bakere21f723d2019-09-05 20:05:4176 /** @private {!HTMLElement} */
77 this.thumbnailContainer_ =
Demetrios Papadopoulos94a15132020-01-27 21:45:2878 /** @type {!HTMLElement} */ (this.$('#thumbnail'));
Collin Bakere21f723d2019-09-05 20:05:4179
80 /** @private {!Image} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2881 this.thumbnail_ = /** @type {!Image} */ (this.$('#thumbnailImg'));
Collin Bakere21f723d2019-09-05 20:05:4182
John Lee99367a62019-10-10 19:03:4983 /** @private {!TabData} */
John Lee3f8dae92019-08-09 22:37:0984 this.tab_;
85
John Lee7d416b782019-08-12 22:32:1386 /** @private {!TabsApiProxy} */
87 this.tabsApi_ = TabsApiProxy.getInstance();
88
Collin Baker1ac2e432019-10-16 18:17:2189 /** @private {!TabStripEmbedderProxy} */
90 this.embedderApi_ = TabStripEmbedderProxy.getInstance();
91
John Lee3f8dae92019-08-09 22:37:0992 /** @private {!HTMLElement} */
Demetrios Papadopoulos94a15132020-01-27 21:45:2893 this.titleTextEl_ = /** @type {!HTMLElement} */ (this.$('#titleText'));
John Lee3f8dae92019-08-09 22:37:0994
John Lee6b1d9572019-11-05 00:33:0695 this.tabEl_.addEventListener('click', () => this.onClick_());
96 this.tabEl_.addEventListener('contextmenu', e => this.onContextMenu_(e));
John Lee7f0c1922019-11-08 22:06:2597 this.tabEl_.addEventListener(
98 'keydown', e => this.onKeyDown_(/** @type {!KeyboardEvent} */ (e)));
John Lee6b1d9572019-11-05 00:33:0699 this.closeButtonEl_.addEventListener('click', e => this.onClose_(e));
100 this.addEventListener('swipe', () => this.onSwipe_());
101
102 /** @private @const {!TabSwiper} */
103 this.tabSwiper_ = new TabSwiper(this);
Robert Liao6ab5f712019-12-04 02:15:01104
105 /** @private {!Function} */
106 this.onTabActivating_ = (tabId) => {};
John Lee912fb9c02019-08-02 01:28:21107 }
John Lee3f8dae92019-08-09 22:37:09108
John Lee99367a62019-10-10 19:03:49109 /** @return {!TabData} */
John Lee3f8dae92019-08-09 22:37:09110 get tab() {
111 return this.tab_;
112 }
113
John Lee99367a62019-10-10 19:03:49114 /** @param {!TabData} tab */
John Lee3f8dae92019-08-09 22:37:09115 set tab(tab) {
John Lee81011102019-10-11 20:05:07116 assert(this.tab_ !== tab);
John Lee8e253602019-08-14 22:22:51117 this.toggleAttribute('active', tab.active);
John Lee61bc1b52019-10-24 22:27:23118 this.tabEl_.setAttribute('aria-selected', tab.active.toString());
John Lee81011102019-10-11 20:05:07119 this.toggleAttribute('hide-icon_', !tab.showIcon);
John Lee99367a62019-10-10 19:03:49120 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07121 'waiting_',
John Lee99367a62019-10-10 19:03:49122 !tab.shouldHideThrobber &&
123 tab.networkState === TabNetworkState.WAITING);
124 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07125 'loading_',
John Lee99367a62019-10-10 19:03:49126 !tab.shouldHideThrobber &&
127 tab.networkState === TabNetworkState.LOADING);
John Leea8e39e682019-10-18 02:19:38128 this.toggleAttribute('pinned', tab.pinned);
John Lee4734ca12019-10-14 19:34:01129 this.toggleAttribute('blocked_', tab.blocked);
John Lee4bcbaf12019-10-21 23:29:46130 this.setAttribute('draggable', true);
John Lee02b3a742019-10-15 20:00:56131 this.toggleAttribute('crashed_', tab.crashed);
John Lee8e253602019-08-14 22:22:51132
John Lee2f3baa72019-11-12 19:58:14133 if (tab.title) {
John Lee3f8dae92019-08-09 22:37:09134 this.titleTextEl_.textContent = tab.title;
John Lee2f3baa72019-11-12 19:58:14135 } else if (
136 !tab.shouldHideThrobber &&
137 (tab.networkState === TabNetworkState.WAITING ||
138 tab.networkState === TabNetworkState.LOADING)) {
139 this.titleTextEl_.textContent = loadTimeData.getString('loadingTab');
140 } else {
141 this.titleTextEl_.textContent = loadTimeData.getString('defaultTabTitle');
John Lee3f8dae92019-08-09 22:37:09142 }
John Lee61bc1b52019-10-24 22:27:23143 this.titleTextEl_.setAttribute('aria-label', getAccessibleTitle(tab));
John Lee3f8dae92019-08-09 22:37:09144
John Lee81011102019-10-11 20:05:07145 if (tab.networkState === TabNetworkState.WAITING ||
146 (tab.networkState === TabNetworkState.LOADING &&
147 tab.isDefaultFavicon)) {
148 this.faviconEl_.style.backgroundImage = 'none';
149 } else if (tab.favIconUrl) {
150 this.faviconEl_.style.backgroundImage = `url(${tab.favIconUrl})`;
John Leed14bec62019-09-23 22:41:32151 } else {
John Lee81011102019-10-11 20:05:07152 this.faviconEl_.style.backgroundImage = getFavicon('');
John Lee2bafb2f2019-08-21 19:20:03153 }
154
John Lee3f8dae92019-08-09 22:37:09155 // Expose the ID to an attribute to allow easy querySelector use
156 this.setAttribute('data-tab-id', tab.id);
157
John Leea8e39e682019-10-18 02:19:38158 this.alertIndicatorsEl_.updateAlertStates(tab.alertStates)
159 .then((alertIndicatorsCount) => {
160 this.toggleAttribute('has-alert-states_', alertIndicatorsCount > 0);
161 });
162
John Lee6b1d9572019-11-05 00:33:06163 if (!this.tab_ || (this.tab_.pinned !== tab.pinned && !tab.pinned)) {
164 this.tabSwiper_.startObserving();
165 } else if (this.tab_.pinned !== tab.pinned && tab.pinned) {
166 this.tabSwiper_.stopObserving();
167 }
168
John Lee3f8dae92019-08-09 22:37:09169 this.tab_ = Object.freeze(tab);
170 }
John Lee7d416b782019-08-12 22:32:13171
Robert Liao6ab5f712019-12-04 02:15:01172 /** @param {!Function} callback */
173 set onTabActivating(callback) {
174 this.onTabActivating_ = callback;
175 }
176
John Lee7f0c1922019-11-08 22:06:25177 focus() {
178 this.tabEl_.focus();
179 }
180
John Lee61bc1b52019-10-24 22:27:23181 /** @return {!HTMLElement} */
John Lee3b309d52019-09-18 19:08:09182 getDragImage() {
John Leedff2fc82019-12-19 04:06:16183 return this.dragImageEl_;
John Lee3b309d52019-09-18 19:08:09184 }
185
186 /**
Collin Bakere21f723d2019-09-05 20:05:41187 * @param {string} imgData
188 */
189 updateThumbnail(imgData) {
190 this.thumbnail_.src = imgData;
191 }
192
John Lee7d416b782019-08-12 22:32:13193 /** @private */
John Lee8e253602019-08-14 22:22:51194 onClick_() {
John Lee6b1d9572019-11-05 00:33:06195 if (!this.tab_ || this.tabSwiper_.wasSwiping()) {
John Lee7d416b782019-08-12 22:32:13196 return;
197 }
198
Robert Liao6ab5f712019-12-04 02:15:01199 const tabId = this.tab_.id;
200 this.onTabActivating_(tabId);
201 this.tabsApi_.activateTab(tabId);
John Lee4bcbaf12019-10-21 23:29:46202
203 if (tabStripOptions.autoCloseEnabled) {
204 this.embedderApi_.closeContainer();
205 }
John Lee8e253602019-08-14 22:22:51206 }
207
John Lee61bc1b52019-10-24 22:27:23208 /**
209 * @param {!Event} event
210 * @private
211 */
Collin Bakerdc3d2112019-10-10 18:28:20212 onContextMenu_(event) {
213 event.preventDefault();
214
215 if (!this.tab_) {
216 return;
217 }
218
Collin Baker1ac2e432019-10-16 18:17:21219 this.embedderApi_.showTabContextMenu(
Collin Bakerdc3d2112019-10-10 18:28:20220 this.tab_.id, event.clientX, event.clientY);
Collin Baker48a096b82019-11-26 01:21:27221 event.stopPropagation();
Collin Bakerdc3d2112019-10-10 18:28:20222 }
223
John Lee8e253602019-08-14 22:22:51224 /**
225 * @param {!Event} event
226 * @private
227 */
228 onClose_(event) {
229 if (!this.tab_) {
230 return;
231 }
232
233 event.stopPropagation();
John Leeadf33c82019-12-12 18:21:33234 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.CLOSE_BUTTON);
John Lee7d416b782019-08-12 22:32:13235 }
John Leea97ceab6f2019-09-04 22:26:46236
John Lee6b1d9572019-11-05 00:33:06237 /** @private */
238 onSwipe_() {
239 // Prevent slideOut animation from playing.
240 this.remove();
John Leeadf33c82019-12-12 18:21:33241 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.SWIPED_TO_CLOSE);
John Lee6b1d9572019-11-05 00:33:06242 }
243
John Leea97ceab6f2019-09-04 22:26:46244 /**
John Lee7f0c1922019-11-08 22:06:25245 * @param {!KeyboardEvent} event
246 * @private
247 */
248 onKeyDown_(event) {
249 if (event.key === 'Enter' || event.key === ' ') {
250 this.onClick_();
251 }
252 }
253
254 /**
John Lee3b309d52019-09-18 19:08:09255 * @param {boolean} dragging
256 */
257 setDragging(dragging) {
John Lee81011102019-10-11 20:05:07258 this.toggleAttribute('dragging_', dragging);
John Lee3b309d52019-09-18 19:08:09259 }
260
261 /**
John Leea97ceab6f2019-09-04 22:26:46262 * @return {!Promise}
263 */
264 slideIn() {
John Leed96ee0532019-11-22 22:06:39265 const paddingInlineEnd = getPaddingInlineEndProperty();
John Leec6bc69d2019-11-18 21:53:17266
John Lee41620f832019-11-27 22:41:05267 // If this TabElement is the last tab, there needs to be enough space for
268 // the view to scroll to it. Therefore, immediately take up all the space
269 // it needs to and only animate the scale.
270 const isLastChild = this.nextElementSibling === null;
271
John Leec6bc69d2019-11-18 21:53:17272 const startState = {
John Lee41620f832019-11-27 22:41:05273 maxWidth: isLastChild ? 'var(--tabstrip-tab-width)' : 0,
John Leec6bc69d2019-11-18 21:53:17274 transform: `scale(0)`,
275 };
John Lee41620f832019-11-27 22:41:05276 startState[paddingInlineEnd] =
277 isLastChild ? 'var(--tabstrip-tab-spacing)' : 0;
John Leec6bc69d2019-11-18 21:53:17278
279 const finishState = {
280 maxWidth: `var(--tabstrip-tab-width)`,
281 transform: `scale(1)`,
282 };
John Leed96ee0532019-11-22 22:06:39283 finishState[paddingInlineEnd] = 'var(--tabstrip-tab-spacing)';
John Leec6bc69d2019-11-18 21:53:17284
John Leea97ceab6f2019-09-04 22:26:46285 return new Promise(resolve => {
John Leec6bc69d2019-11-18 21:53:17286 const animation = this.animate([startState, finishState], {
287 duration: 300,
288 easing: 'cubic-bezier(.4, 0, 0, 1)',
289 });
John Lee6b1d9572019-11-05 00:33:06290 animation.onfinish = () => {
John Lee6b1d9572019-11-05 00:33:06291 resolve();
292 };
John Lee29411b52019-12-20 02:16:15293
294 // TODO(crbug.com/1035678) By the next animation frame, the animation
295 // should start playing. By the time another animation frame happens,
296 // force play the animation if the animation has not yet begun. Remove
297 // if/when the Blink issue has been fixed.
298 requestAnimationFrame(() => {
299 requestAnimationFrame(() => {
300 if (animation.pending) {
301 animation.play();
302 }
303 });
304 });
John Leea97ceab6f2019-09-04 22:26:46305 });
306 }
307
308 /**
309 * @return {!Promise}
310 */
311 slideOut() {
John Lee7bbf7ad2019-11-21 23:38:51312 if (!this.embedderApi_.isVisible() || this.tab_.pinned) {
John Leed71aaac2019-11-14 20:37:50313 // There is no point in animating if the tab strip is hidden.
314 this.remove();
315 return Promise.resolve();
316 }
317
John Leea97ceab6f2019-09-04 22:26:46318 return new Promise(resolve => {
John Leed71aaac2019-11-14 20:37:50319 const finishCallback = () => {
320 this.remove();
321 resolve();
322 };
323
John Lee7bbf7ad2019-11-21 23:38:51324 const translateAnimation = this.animate(
John Leea97ceab6f2019-09-04 22:26:46325 {
John Lee7bbf7ad2019-11-21 23:38:51326 transform: ['translateY(0)', 'translateY(-100%)'],
327 },
328 {
329 duration: 150,
330 easing: 'cubic-bezier(.4, 0, 1, 1)',
John Leea97ceab6f2019-09-04 22:26:46331 fill: 'forwards',
332 });
John Lee7bbf7ad2019-11-21 23:38:51333 const opacityAnimation = this.animate(
334 {
335 opacity: [1, 0],
336 },
337 {
338 delay: 97.5,
339 duration: 50,
340 fill: 'forwards',
341 });
342
343 const widthAnimationKeyframes = {
344 maxWidth: ['var(--tabstrip-tab-width)', 0],
345 };
John Leed96ee0532019-11-22 22:06:39346 widthAnimationKeyframes[getPaddingInlineEndProperty()] =
347 ['var(--tabstrip-tab-spacing)', 0];
John Lee7bbf7ad2019-11-21 23:38:51348 const widthAnimation = this.animate(widthAnimationKeyframes, {
349 delay: 97.5,
350 duration: 300,
351 easing: 'cubic-bezier(.4, 0, 0, 1)',
352 fill: 'forwards',
353 });
John Leed71aaac2019-11-14 20:37:50354
355 const visibilityChangeListener = () => {
356 if (!this.embedderApi_.isVisible()) {
357 // If a tab strip becomes hidden during the animation, the onfinish
358 // event will not get fired until the tab strip becomes visible again.
359 // Therefore, when the tab strip becomes hidden, immediately call the
360 // finish callback.
John Lee7bbf7ad2019-11-21 23:38:51361 translateAnimation.cancel();
362 opacityAnimation.cancel();
363 widthAnimation.cancel();
John Leed71aaac2019-11-14 20:37:50364 finishCallback();
365 }
366 };
367
368 document.addEventListener(
369 'visibilitychange', visibilityChangeListener, {once: true});
John Lee7bbf7ad2019-11-21 23:38:51370 // The onfinish handler is put on the width animation, as it will end
371 // last.
372 widthAnimation.onfinish = () => {
John Leed71aaac2019-11-14 20:37:50373 document.removeEventListener(
374 'visibilitychange', visibilityChangeListener);
375 finishCallback();
John Leea97ceab6f2019-09-04 22:26:46376 };
377 });
378 }
John Lee912fb9c02019-08-02 01:28:21379}
380
John Lee3f8dae92019-08-09 22:37:09381customElements.define('tabstrip-tab', TabElement);