blob: f2599e53d11ba403c931b1b14847270e9b6b0e9e [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 Leedff2fc82019-12-19 04:06:1669 this.dragImageEl_ =
70 /** @type {!HTMLElement} */ (
71 this.shadowRoot.querySelector('#dragImage'));
72
73 /** @private {!HTMLElement} */
John Lee61bc1b52019-10-24 22:27:2374 this.tabEl_ =
75 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#tab'));
John Lee3b309d52019-09-18 19:08:0976
77 /** @private {!HTMLElement} */
John Lee2bafb2f2019-08-21 19:20:0378 this.faviconEl_ =
79 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#favicon'));
80
Collin Bakere21f723d2019-09-05 20:05:4181 /** @private {!HTMLElement} */
82 this.thumbnailContainer_ =
83 /** @type {!HTMLElement} */ (
84 this.shadowRoot.querySelector('#thumbnail'));
85
86 /** @private {!Image} */
87 this.thumbnail_ =
88 /** @type {!Image} */ (this.shadowRoot.querySelector('#thumbnailImg'));
89
John Lee99367a62019-10-10 19:03:4990 /** @private {!TabData} */
John Lee3f8dae92019-08-09 22:37:0991 this.tab_;
92
John Lee7d416b782019-08-12 22:32:1393 /** @private {!TabsApiProxy} */
94 this.tabsApi_ = TabsApiProxy.getInstance();
95
Collin Baker1ac2e432019-10-16 18:17:2196 /** @private {!TabStripEmbedderProxy} */
97 this.embedderApi_ = TabStripEmbedderProxy.getInstance();
98
John Lee3f8dae92019-08-09 22:37:0999 /** @private {!HTMLElement} */
100 this.titleTextEl_ = /** @type {!HTMLElement} */ (
101 this.shadowRoot.querySelector('#titleText'));
John Lee3f8dae92019-08-09 22:37:09102
John Lee6b1d9572019-11-05 00:33:06103 this.tabEl_.addEventListener('click', () => this.onClick_());
104 this.tabEl_.addEventListener('contextmenu', e => this.onContextMenu_(e));
John Lee7f0c1922019-11-08 22:06:25105 this.tabEl_.addEventListener(
106 'keydown', e => this.onKeyDown_(/** @type {!KeyboardEvent} */ (e)));
John Lee6b1d9572019-11-05 00:33:06107 this.closeButtonEl_.addEventListener('click', e => this.onClose_(e));
108 this.addEventListener('swipe', () => this.onSwipe_());
109
110 /** @private @const {!TabSwiper} */
111 this.tabSwiper_ = new TabSwiper(this);
Robert Liao6ab5f712019-12-04 02:15:01112
113 /** @private {!Function} */
114 this.onTabActivating_ = (tabId) => {};
John Lee912fb9c02019-08-02 01:28:21115 }
John Lee3f8dae92019-08-09 22:37:09116
John Lee99367a62019-10-10 19:03:49117 /** @return {!TabData} */
John Lee3f8dae92019-08-09 22:37:09118 get tab() {
119 return this.tab_;
120 }
121
John Lee99367a62019-10-10 19:03:49122 /** @param {!TabData} tab */
John Lee3f8dae92019-08-09 22:37:09123 set tab(tab) {
John Lee81011102019-10-11 20:05:07124 assert(this.tab_ !== tab);
John Lee8e253602019-08-14 22:22:51125 this.toggleAttribute('active', tab.active);
John Lee61bc1b52019-10-24 22:27:23126 this.tabEl_.setAttribute('aria-selected', tab.active.toString());
John Lee81011102019-10-11 20:05:07127 this.toggleAttribute('hide-icon_', !tab.showIcon);
John Lee99367a62019-10-10 19:03:49128 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07129 'waiting_',
John Lee99367a62019-10-10 19:03:49130 !tab.shouldHideThrobber &&
131 tab.networkState === TabNetworkState.WAITING);
132 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07133 'loading_',
John Lee99367a62019-10-10 19:03:49134 !tab.shouldHideThrobber &&
135 tab.networkState === TabNetworkState.LOADING);
John Leea8e39e682019-10-18 02:19:38136 this.toggleAttribute('pinned', tab.pinned);
John Lee4734ca12019-10-14 19:34:01137 this.toggleAttribute('blocked_', tab.blocked);
John Lee4bcbaf12019-10-21 23:29:46138 this.setAttribute('draggable', true);
John Lee02b3a742019-10-15 20:00:56139 this.toggleAttribute('crashed_', tab.crashed);
John Lee8e253602019-08-14 22:22:51140
John Lee2f3baa72019-11-12 19:58:14141 if (tab.title) {
John Lee3f8dae92019-08-09 22:37:09142 this.titleTextEl_.textContent = tab.title;
John Lee2f3baa72019-11-12 19:58:14143 } else if (
144 !tab.shouldHideThrobber &&
145 (tab.networkState === TabNetworkState.WAITING ||
146 tab.networkState === TabNetworkState.LOADING)) {
147 this.titleTextEl_.textContent = loadTimeData.getString('loadingTab');
148 } else {
149 this.titleTextEl_.textContent = loadTimeData.getString('defaultTabTitle');
John Lee3f8dae92019-08-09 22:37:09150 }
John Lee61bc1b52019-10-24 22:27:23151 this.titleTextEl_.setAttribute('aria-label', getAccessibleTitle(tab));
John Lee3f8dae92019-08-09 22:37:09152
John Lee81011102019-10-11 20:05:07153 if (tab.networkState === TabNetworkState.WAITING ||
154 (tab.networkState === TabNetworkState.LOADING &&
155 tab.isDefaultFavicon)) {
156 this.faviconEl_.style.backgroundImage = 'none';
157 } else if (tab.favIconUrl) {
158 this.faviconEl_.style.backgroundImage = `url(${tab.favIconUrl})`;
John Leed14bec62019-09-23 22:41:32159 } else {
John Lee81011102019-10-11 20:05:07160 this.faviconEl_.style.backgroundImage = getFavicon('');
John Lee2bafb2f2019-08-21 19:20:03161 }
162
John Lee3f8dae92019-08-09 22:37:09163 // Expose the ID to an attribute to allow easy querySelector use
164 this.setAttribute('data-tab-id', tab.id);
165
John Leea8e39e682019-10-18 02:19:38166 this.alertIndicatorsEl_.updateAlertStates(tab.alertStates)
167 .then((alertIndicatorsCount) => {
168 this.toggleAttribute('has-alert-states_', alertIndicatorsCount > 0);
169 });
170
John Lee6b1d9572019-11-05 00:33:06171 if (!this.tab_ || (this.tab_.pinned !== tab.pinned && !tab.pinned)) {
172 this.tabSwiper_.startObserving();
173 } else if (this.tab_.pinned !== tab.pinned && tab.pinned) {
174 this.tabSwiper_.stopObserving();
175 }
176
John Lee3f8dae92019-08-09 22:37:09177 this.tab_ = Object.freeze(tab);
178 }
John Lee7d416b782019-08-12 22:32:13179
Robert Liao6ab5f712019-12-04 02:15:01180 /** @param {!Function} callback */
181 set onTabActivating(callback) {
182 this.onTabActivating_ = callback;
183 }
184
John Lee7f0c1922019-11-08 22:06:25185 focus() {
186 this.tabEl_.focus();
187 }
188
John Lee61bc1b52019-10-24 22:27:23189 /** @return {!HTMLElement} */
John Lee3b309d52019-09-18 19:08:09190 getDragImage() {
John Leedff2fc82019-12-19 04:06:16191 return this.dragImageEl_;
John Lee3b309d52019-09-18 19:08:09192 }
193
194 /**
Collin Bakere21f723d2019-09-05 20:05:41195 * @param {string} imgData
196 */
197 updateThumbnail(imgData) {
198 this.thumbnail_.src = imgData;
199 }
200
John Lee7d416b782019-08-12 22:32:13201 /** @private */
John Lee8e253602019-08-14 22:22:51202 onClick_() {
John Lee6b1d9572019-11-05 00:33:06203 if (!this.tab_ || this.tabSwiper_.wasSwiping()) {
John Lee7d416b782019-08-12 22:32:13204 return;
205 }
206
Robert Liao6ab5f712019-12-04 02:15:01207 const tabId = this.tab_.id;
208 this.onTabActivating_(tabId);
209 this.tabsApi_.activateTab(tabId);
John Lee4bcbaf12019-10-21 23:29:46210
211 if (tabStripOptions.autoCloseEnabled) {
212 this.embedderApi_.closeContainer();
213 }
John Lee8e253602019-08-14 22:22:51214 }
215
John Lee61bc1b52019-10-24 22:27:23216 /**
217 * @param {!Event} event
218 * @private
219 */
Collin Bakerdc3d2112019-10-10 18:28:20220 onContextMenu_(event) {
221 event.preventDefault();
222
223 if (!this.tab_) {
224 return;
225 }
226
Collin Baker1ac2e432019-10-16 18:17:21227 this.embedderApi_.showTabContextMenu(
Collin Bakerdc3d2112019-10-10 18:28:20228 this.tab_.id, event.clientX, event.clientY);
Collin Baker48a096b82019-11-26 01:21:27229 event.stopPropagation();
Collin Bakerdc3d2112019-10-10 18:28:20230 }
231
John Lee8e253602019-08-14 22:22:51232 /**
233 * @param {!Event} event
234 * @private
235 */
236 onClose_(event) {
237 if (!this.tab_) {
238 return;
239 }
240
241 event.stopPropagation();
John Leeadf33c82019-12-12 18:21:33242 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.CLOSE_BUTTON);
John Lee7d416b782019-08-12 22:32:13243 }
John Leea97ceab6f2019-09-04 22:26:46244
John Lee6b1d9572019-11-05 00:33:06245 /** @private */
246 onSwipe_() {
247 // Prevent slideOut animation from playing.
248 this.remove();
John Leeadf33c82019-12-12 18:21:33249 this.tabsApi_.closeTab(this.tab_.id, CloseTabAction.SWIPED_TO_CLOSE);
John Lee6b1d9572019-11-05 00:33:06250 }
251
John Leea97ceab6f2019-09-04 22:26:46252 /**
John Lee7f0c1922019-11-08 22:06:25253 * @param {!KeyboardEvent} event
254 * @private
255 */
256 onKeyDown_(event) {
257 if (event.key === 'Enter' || event.key === ' ') {
258 this.onClick_();
259 }
260 }
261
262 /**
John Lee3b309d52019-09-18 19:08:09263 * @param {boolean} dragging
264 */
265 setDragging(dragging) {
John Lee81011102019-10-11 20:05:07266 this.toggleAttribute('dragging_', dragging);
John Lee3b309d52019-09-18 19:08:09267 }
268
269 /**
John Leea97ceab6f2019-09-04 22:26:46270 * @return {!Promise}
271 */
272 slideIn() {
John Leed96ee0532019-11-22 22:06:39273 const paddingInlineEnd = getPaddingInlineEndProperty();
John Leec6bc69d2019-11-18 21:53:17274
John Lee41620f832019-11-27 22:41:05275 // If this TabElement is the last tab, there needs to be enough space for
276 // the view to scroll to it. Therefore, immediately take up all the space
277 // it needs to and only animate the scale.
278 const isLastChild = this.nextElementSibling === null;
279
John Leec6bc69d2019-11-18 21:53:17280 const startState = {
John Lee41620f832019-11-27 22:41:05281 maxWidth: isLastChild ? 'var(--tabstrip-tab-width)' : 0,
John Leec6bc69d2019-11-18 21:53:17282 transform: `scale(0)`,
283 };
John Lee41620f832019-11-27 22:41:05284 startState[paddingInlineEnd] =
285 isLastChild ? 'var(--tabstrip-tab-spacing)' : 0;
John Leec6bc69d2019-11-18 21:53:17286
287 const finishState = {
288 maxWidth: `var(--tabstrip-tab-width)`,
289 transform: `scale(1)`,
290 };
John Leed96ee0532019-11-22 22:06:39291 finishState[paddingInlineEnd] = 'var(--tabstrip-tab-spacing)';
John Leec6bc69d2019-11-18 21:53:17292
John Leea97ceab6f2019-09-04 22:26:46293 return new Promise(resolve => {
John Leec6bc69d2019-11-18 21:53:17294 const animation = this.animate([startState, finishState], {
295 duration: 300,
296 easing: 'cubic-bezier(.4, 0, 0, 1)',
297 });
John Lee6b1d9572019-11-05 00:33:06298 animation.onfinish = () => {
John Lee6b1d9572019-11-05 00:33:06299 resolve();
300 };
John Leea97ceab6f2019-09-04 22:26:46301 });
302 }
303
304 /**
305 * @return {!Promise}
306 */
307 slideOut() {
John Lee7bbf7ad2019-11-21 23:38:51308 if (!this.embedderApi_.isVisible() || this.tab_.pinned) {
John Leed71aaac2019-11-14 20:37:50309 // There is no point in animating if the tab strip is hidden.
310 this.remove();
311 return Promise.resolve();
312 }
313
John Leea97ceab6f2019-09-04 22:26:46314 return new Promise(resolve => {
John Leed71aaac2019-11-14 20:37:50315 const finishCallback = () => {
316 this.remove();
317 resolve();
318 };
319
John Lee7bbf7ad2019-11-21 23:38:51320 const translateAnimation = this.animate(
John Leea97ceab6f2019-09-04 22:26:46321 {
John Lee7bbf7ad2019-11-21 23:38:51322 transform: ['translateY(0)', 'translateY(-100%)'],
323 },
324 {
325 duration: 150,
326 easing: 'cubic-bezier(.4, 0, 1, 1)',
John Leea97ceab6f2019-09-04 22:26:46327 fill: 'forwards',
328 });
John Lee7bbf7ad2019-11-21 23:38:51329 const opacityAnimation = this.animate(
330 {
331 opacity: [1, 0],
332 },
333 {
334 delay: 97.5,
335 duration: 50,
336 fill: 'forwards',
337 });
338
339 const widthAnimationKeyframes = {
340 maxWidth: ['var(--tabstrip-tab-width)', 0],
341 };
John Leed96ee0532019-11-22 22:06:39342 widthAnimationKeyframes[getPaddingInlineEndProperty()] =
343 ['var(--tabstrip-tab-spacing)', 0];
John Lee7bbf7ad2019-11-21 23:38:51344 const widthAnimation = this.animate(widthAnimationKeyframes, {
345 delay: 97.5,
346 duration: 300,
347 easing: 'cubic-bezier(.4, 0, 0, 1)',
348 fill: 'forwards',
349 });
John Leed71aaac2019-11-14 20:37:50350
351 const visibilityChangeListener = () => {
352 if (!this.embedderApi_.isVisible()) {
353 // If a tab strip becomes hidden during the animation, the onfinish
354 // event will not get fired until the tab strip becomes visible again.
355 // Therefore, when the tab strip becomes hidden, immediately call the
356 // finish callback.
John Lee7bbf7ad2019-11-21 23:38:51357 translateAnimation.cancel();
358 opacityAnimation.cancel();
359 widthAnimation.cancel();
John Leed71aaac2019-11-14 20:37:50360 finishCallback();
361 }
362 };
363
364 document.addEventListener(
365 'visibilitychange', visibilityChangeListener, {once: true});
John Lee7bbf7ad2019-11-21 23:38:51366 // The onfinish handler is put on the width animation, as it will end
367 // last.
368 widthAnimation.onfinish = () => {
John Leed71aaac2019-11-14 20:37:50369 document.removeEventListener(
370 'visibilitychange', visibilityChangeListener);
371 finishCallback();
John Leea97ceab6f2019-09-04 22:26:46372 };
373 });
374 }
John Lee912fb9c02019-08-02 01:28:21375}
376
John Lee3f8dae92019-08-09 22:37:09377customElements.define('tabstrip-tab', TabElement);