blob: c67ec76ed524e0e6527ca75495d686307aadae7f [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} */
56 (this.shadowRoot.querySelector('tabstrip-alert-indicators'));
57 // 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} */
63 this.closeButtonEl_ =
64 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#close'));
John Lee61bc1b52019-10-24 22:27:2365 this.closeButtonEl_.setAttribute(
66 'aria-label', loadTimeData.getString('closeTab'));
John Lee7d416b782019-08-12 22:32:1367
John Lee2bafb2f2019-08-21 19:20:0368 /** @private {!HTMLElement} */
John Lee61bc1b52019-10-24 22:27:2369 this.tabEl_ =
70 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#tab'));
John Lee3b309d52019-09-18 19:08:0971
72 /** @private {!HTMLElement} */
John Lee2bafb2f2019-08-21 19:20:0373 this.faviconEl_ =
74 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#favicon'));
75
Collin Bakere21f723d2019-09-05 20:05:4176 /** @private {!HTMLElement} */
77 this.thumbnailContainer_ =
78 /** @type {!HTMLElement} */ (
79 this.shadowRoot.querySelector('#thumbnail'));
80
81 /** @private {!Image} */
82 this.thumbnail_ =
83 /** @type {!Image} */ (this.shadowRoot.querySelector('#thumbnailImg'));
84
John Lee99367a62019-10-10 19:03:4985 /** @private {!TabData} */
John Lee3f8dae92019-08-09 22:37:0986 this.tab_;
87
John Lee7d416b782019-08-12 22:32:1388 /** @private {!TabsApiProxy} */
89 this.tabsApi_ = TabsApiProxy.getInstance();
90
Collin Baker1ac2e432019-10-16 18:17:2191 /** @private {!TabStripEmbedderProxy} */
92 this.embedderApi_ = TabStripEmbedderProxy.getInstance();
93
John Lee3f8dae92019-08-09 22:37:0994 /** @private {!HTMLElement} */
95 this.titleTextEl_ = /** @type {!HTMLElement} */ (
96 this.shadowRoot.querySelector('#titleText'));
John Lee3f8dae92019-08-09 22:37:0997
John Lee6b1d9572019-11-05 00:33:0698 this.tabEl_.addEventListener('click', () => this.onClick_());
99 this.tabEl_.addEventListener('contextmenu', e => this.onContextMenu_(e));
John Lee7f0c1922019-11-08 22:06:25100 this.tabEl_.addEventListener(
101 'keydown', e => this.onKeyDown_(/** @type {!KeyboardEvent} */ (e)));
John Lee6b1d9572019-11-05 00:33:06102 this.closeButtonEl_.addEventListener('click', e => this.onClose_(e));
103 this.addEventListener('swipe', () => this.onSwipe_());
104
105 /** @private @const {!TabSwiper} */
106 this.tabSwiper_ = new TabSwiper(this);
Robert Liao6ab5f712019-12-04 02:15:01107
108 /** @private {!Function} */
109 this.onTabActivating_ = (tabId) => {};
John Lee912fb9c02019-08-02 01:28:21110 }
John Lee3f8dae92019-08-09 22:37:09111
John Lee99367a62019-10-10 19:03:49112 /** @return {!TabData} */
John Lee3f8dae92019-08-09 22:37:09113 get tab() {
114 return this.tab_;
115 }
116
John Lee99367a62019-10-10 19:03:49117 /** @param {!TabData} tab */
John Lee3f8dae92019-08-09 22:37:09118 set tab(tab) {
John Lee81011102019-10-11 20:05:07119 assert(this.tab_ !== tab);
John Lee8e253602019-08-14 22:22:51120 this.toggleAttribute('active', tab.active);
John Lee61bc1b52019-10-24 22:27:23121 this.tabEl_.setAttribute('aria-selected', tab.active.toString());
John Lee81011102019-10-11 20:05:07122 this.toggleAttribute('hide-icon_', !tab.showIcon);
John Lee99367a62019-10-10 19:03:49123 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07124 'waiting_',
John Lee99367a62019-10-10 19:03:49125 !tab.shouldHideThrobber &&
126 tab.networkState === TabNetworkState.WAITING);
127 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07128 'loading_',
John Lee99367a62019-10-10 19:03:49129 !tab.shouldHideThrobber &&
130 tab.networkState === TabNetworkState.LOADING);
John Leea8e39e682019-10-18 02:19:38131 this.toggleAttribute('pinned', tab.pinned);
John Lee4734ca12019-10-14 19:34:01132 this.toggleAttribute('blocked_', tab.blocked);
John Lee4bcbaf12019-10-21 23:29:46133 this.setAttribute('draggable', true);
John Lee02b3a742019-10-15 20:00:56134 this.toggleAttribute('crashed_', tab.crashed);
John Lee8e253602019-08-14 22:22:51135
John Lee2f3baa72019-11-12 19:58:14136 if (tab.title) {
John Lee3f8dae92019-08-09 22:37:09137 this.titleTextEl_.textContent = tab.title;
John Lee2f3baa72019-11-12 19:58:14138 } else if (
139 !tab.shouldHideThrobber &&
140 (tab.networkState === TabNetworkState.WAITING ||
141 tab.networkState === TabNetworkState.LOADING)) {
142 this.titleTextEl_.textContent = loadTimeData.getString('loadingTab');
143 } else {
144 this.titleTextEl_.textContent = loadTimeData.getString('defaultTabTitle');
John Lee3f8dae92019-08-09 22:37:09145 }
John Lee61bc1b52019-10-24 22:27:23146 this.titleTextEl_.setAttribute('aria-label', getAccessibleTitle(tab));
John Lee3f8dae92019-08-09 22:37:09147
John Lee81011102019-10-11 20:05:07148 if (tab.networkState === TabNetworkState.WAITING ||
149 (tab.networkState === TabNetworkState.LOADING &&
150 tab.isDefaultFavicon)) {
151 this.faviconEl_.style.backgroundImage = 'none';
152 } else if (tab.favIconUrl) {
153 this.faviconEl_.style.backgroundImage = `url(${tab.favIconUrl})`;
John Leed14bec62019-09-23 22:41:32154 } else {
John Lee81011102019-10-11 20:05:07155 this.faviconEl_.style.backgroundImage = getFavicon('');
John Lee2bafb2f2019-08-21 19:20:03156 }
157
John Lee3f8dae92019-08-09 22:37:09158 // Expose the ID to an attribute to allow easy querySelector use
159 this.setAttribute('data-tab-id', tab.id);
160
John Leea8e39e682019-10-18 02:19:38161 this.alertIndicatorsEl_.updateAlertStates(tab.alertStates)
162 .then((alertIndicatorsCount) => {
163 this.toggleAttribute('has-alert-states_', alertIndicatorsCount > 0);
164 });
165
John Lee6b1d9572019-11-05 00:33:06166 if (!this.tab_ || (this.tab_.pinned !== tab.pinned && !tab.pinned)) {
167 this.tabSwiper_.startObserving();
168 } else if (this.tab_.pinned !== tab.pinned && tab.pinned) {
169 this.tabSwiper_.stopObserving();
170 }
171
John Lee3f8dae92019-08-09 22:37:09172 this.tab_ = Object.freeze(tab);
173 }
John Lee7d416b782019-08-12 22:32:13174
Robert Liao6ab5f712019-12-04 02:15:01175 /** @param {!Function} callback */
176 set onTabActivating(callback) {
177 this.onTabActivating_ = callback;
178 }
179
John Lee7f0c1922019-11-08 22:06:25180 focus() {
181 this.tabEl_.focus();
182 }
183
John Lee61bc1b52019-10-24 22:27:23184 /** @return {!HTMLElement} */
John Lee3b309d52019-09-18 19:08:09185 getDragImage() {
John Lee61bc1b52019-10-24 22:27:23186 return this.tabEl_;
John Lee3b309d52019-09-18 19:08:09187 }
188
189 /**
Collin Bakere21f723d2019-09-05 20:05:41190 * @param {string} imgData
191 */
192 updateThumbnail(imgData) {
193 this.thumbnail_.src = imgData;
194 }
195
John Lee7d416b782019-08-12 22:32:13196 /** @private */
John Lee8e253602019-08-14 22:22:51197 onClick_() {
John Lee6b1d9572019-11-05 00:33:06198 if (!this.tab_ || this.tabSwiper_.wasSwiping()) {
John Lee7d416b782019-08-12 22:32:13199 return;
200 }
201
Robert Liao6ab5f712019-12-04 02:15:01202 const tabId = this.tab_.id;
203 this.onTabActivating_(tabId);
204 this.tabsApi_.activateTab(tabId);
John Lee4bcbaf12019-10-21 23:29:46205
206 if (tabStripOptions.autoCloseEnabled) {
207 this.embedderApi_.closeContainer();
208 }
John Lee8e253602019-08-14 22:22:51209 }
210
John Lee61bc1b52019-10-24 22:27:23211 /**
212 * @param {!Event} event
213 * @private
214 */
Collin Bakerdc3d2112019-10-10 18:28:20215 onContextMenu_(event) {
216 event.preventDefault();
217
218 if (!this.tab_) {
219 return;
220 }
221
Collin Baker1ac2e432019-10-16 18:17:21222 this.embedderApi_.showTabContextMenu(
Collin Bakerdc3d2112019-10-10 18:28:20223 this.tab_.id, event.clientX, event.clientY);
Collin Baker48a096b82019-11-26 01:21:27224 event.stopPropagation();
Collin Bakerdc3d2112019-10-10 18:28:20225 }
226
John Lee8e253602019-08-14 22:22:51227 /**
228 * @param {!Event} event
229 * @private
230 */
231 onClose_(event) {
232 if (!this.tab_) {
233 return;
234 }
235
236 event.stopPropagation();
John Leeadf33c82019-12-12 18:21:33237 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.CLOSE_BUTTON);
John Lee7d416b782019-08-12 22:32:13238 }
John Leea97ceab6f2019-09-04 22:26:46239
John Lee6b1d9572019-11-05 00:33:06240 /** @private */
241 onSwipe_() {
242 // Prevent slideOut animation from playing.
243 this.remove();
John Leeadf33c82019-12-12 18:21:33244 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.SWIPED_TO_CLOSE);
John Lee6b1d9572019-11-05 00:33:06245 }
246
John Leea97ceab6f2019-09-04 22:26:46247 /**
John Lee7f0c1922019-11-08 22:06:25248 * @param {!KeyboardEvent} event
249 * @private
250 */
251 onKeyDown_(event) {
252 if (event.key === 'Enter' || event.key === ' ') {
253 this.onClick_();
254 }
255 }
256
257 /**
John Lee3b309d52019-09-18 19:08:09258 * @param {boolean} dragging
259 */
260 setDragging(dragging) {
John Lee81011102019-10-11 20:05:07261 this.toggleAttribute('dragging_', dragging);
John Lee3b309d52019-09-18 19:08:09262 }
263
264 /**
John Leea97ceab6f2019-09-04 22:26:46265 * @return {!Promise}
266 */
267 slideIn() {
John Leed96ee0532019-11-22 22:06:39268 const paddingInlineEnd = getPaddingInlineEndProperty();
John Leec6bc69d2019-11-18 21:53:17269
John Lee41620f832019-11-27 22:41:05270 // If this TabElement is the last tab, there needs to be enough space for
271 // the view to scroll to it. Therefore, immediately take up all the space
272 // it needs to and only animate the scale.
273 const isLastChild = this.nextElementSibling === null;
274
John Leec6bc69d2019-11-18 21:53:17275 const startState = {
John Lee41620f832019-11-27 22:41:05276 maxWidth: isLastChild ? 'var(--tabstrip-tab-width)' : 0,
John Leec6bc69d2019-11-18 21:53:17277 transform: `scale(0)`,
278 };
John Lee41620f832019-11-27 22:41:05279 startState[paddingInlineEnd] =
280 isLastChild ? 'var(--tabstrip-tab-spacing)' : 0;
John Leec6bc69d2019-11-18 21:53:17281
282 const finishState = {
283 maxWidth: `var(--tabstrip-tab-width)`,
284 transform: `scale(1)`,
285 };
John Leed96ee0532019-11-22 22:06:39286 finishState[paddingInlineEnd] = 'var(--tabstrip-tab-spacing)';
John Leec6bc69d2019-11-18 21:53:17287
John Leea97ceab6f2019-09-04 22:26:46288 return new Promise(resolve => {
John Leec6bc69d2019-11-18 21:53:17289 const animation = this.animate([startState, finishState], {
290 duration: 300,
291 easing: 'cubic-bezier(.4, 0, 0, 1)',
292 });
John Lee6b1d9572019-11-05 00:33:06293 animation.onfinish = () => {
John Lee6b1d9572019-11-05 00:33:06294 resolve();
295 };
John Leea97ceab6f2019-09-04 22:26:46296 });
297 }
298
299 /**
300 * @return {!Promise}
301 */
302 slideOut() {
John Lee7bbf7ad2019-11-21 23:38:51303 if (!this.embedderApi_.isVisible() || this.tab_.pinned) {
John Leed71aaac2019-11-14 20:37:50304 // There is no point in animating if the tab strip is hidden.
305 this.remove();
306 return Promise.resolve();
307 }
308
John Leea97ceab6f2019-09-04 22:26:46309 return new Promise(resolve => {
John Leed71aaac2019-11-14 20:37:50310 const finishCallback = () => {
311 this.remove();
312 resolve();
313 };
314
John Lee7bbf7ad2019-11-21 23:38:51315 const translateAnimation = this.animate(
John Leea97ceab6f2019-09-04 22:26:46316 {
John Lee7bbf7ad2019-11-21 23:38:51317 transform: ['translateY(0)', 'translateY(-100%)'],
318 },
319 {
320 duration: 150,
321 easing: 'cubic-bezier(.4, 0, 1, 1)',
John Leea97ceab6f2019-09-04 22:26:46322 fill: 'forwards',
323 });
John Lee7bbf7ad2019-11-21 23:38:51324 const opacityAnimation = this.animate(
325 {
326 opacity: [1, 0],
327 },
328 {
329 delay: 97.5,
330 duration: 50,
331 fill: 'forwards',
332 });
333
334 const widthAnimationKeyframes = {
335 maxWidth: ['var(--tabstrip-tab-width)', 0],
336 };
John Leed96ee0532019-11-22 22:06:39337 widthAnimationKeyframes[getPaddingInlineEndProperty()] =
338 ['var(--tabstrip-tab-spacing)', 0];
John Lee7bbf7ad2019-11-21 23:38:51339 const widthAnimation = this.animate(widthAnimationKeyframes, {
340 delay: 97.5,
341 duration: 300,
342 easing: 'cubic-bezier(.4, 0, 0, 1)',
343 fill: 'forwards',
344 });
John Leed71aaac2019-11-14 20:37:50345
346 const visibilityChangeListener = () => {
347 if (!this.embedderApi_.isVisible()) {
348 // If a tab strip becomes hidden during the animation, the onfinish
349 // event will not get fired until the tab strip becomes visible again.
350 // Therefore, when the tab strip becomes hidden, immediately call the
351 // finish callback.
John Lee7bbf7ad2019-11-21 23:38:51352 translateAnimation.cancel();
353 opacityAnimation.cancel();
354 widthAnimation.cancel();
John Leed71aaac2019-11-14 20:37:50355 finishCallback();
356 }
357 };
358
359 document.addEventListener(
360 'visibilitychange', visibilityChangeListener, {once: true});
John Lee7bbf7ad2019-11-21 23:38:51361 // The onfinish handler is put on the width animation, as it will end
362 // last.
363 widthAnimation.onfinish = () => {
John Leed71aaac2019-11-14 20:37:50364 document.removeEventListener(
365 'visibilitychange', visibilityChangeListener);
366 finishCallback();
John Leea97ceab6f2019-09-04 22:26:46367 };
368 });
369 }
John Lee912fb9c02019-08-02 01:28:21370}
371
John Lee3f8dae92019-08-09 22:37:09372customElements.define('tabstrip-tab', TabElement);