blob: 10c7dd20289006158ade26e0854294af9db9aadb [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 Lee99367a62019-10-10 19:03:4917import {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 Lee3f8dae92019-08-09 22:37:0939export class TabElement extends CustomElement {
John Lee912fb9c02019-08-02 01:28:2140 static get template() {
41 return `{__html_template__}`;
42 }
43
John Lee3f8dae92019-08-09 22:37:0944 constructor() {
45 super();
46
John Leea8e39e682019-10-18 02:19:3847 this.alertIndicatorsEl_ = /** @type {!AlertIndicatorsElement} */
48 (this.shadowRoot.querySelector('tabstrip-alert-indicators'));
49 // Normally, custom elements will get upgraded automatically once added to
50 // the DOM, but TabElement may need to update properties on
51 // AlertIndicatorElement before this happens, so upgrade it manually.
52 customElements.upgrade(this.alertIndicatorsEl_);
53
John Lee7d416b782019-08-12 22:32:1354 /** @private {!HTMLElement} */
55 this.closeButtonEl_ =
56 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#close'));
John Lee61bc1b52019-10-24 22:27:2357 this.closeButtonEl_.setAttribute(
58 'aria-label', loadTimeData.getString('closeTab'));
John Lee7d416b782019-08-12 22:32:1359
John Lee2bafb2f2019-08-21 19:20:0360 /** @private {!HTMLElement} */
John Lee61bc1b52019-10-24 22:27:2361 this.tabEl_ =
62 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#tab'));
John Lee3b309d52019-09-18 19:08:0963
64 /** @private {!HTMLElement} */
John Lee2bafb2f2019-08-21 19:20:0365 this.faviconEl_ =
66 /** @type {!HTMLElement} */ (this.shadowRoot.querySelector('#favicon'));
67
Collin Bakere21f723d2019-09-05 20:05:4168 /** @private {!HTMLElement} */
69 this.thumbnailContainer_ =
70 /** @type {!HTMLElement} */ (
71 this.shadowRoot.querySelector('#thumbnail'));
72
73 /** @private {!Image} */
74 this.thumbnail_ =
75 /** @type {!Image} */ (this.shadowRoot.querySelector('#thumbnailImg'));
76
John Lee99367a62019-10-10 19:03:4977 /** @private {!TabData} */
John Lee3f8dae92019-08-09 22:37:0978 this.tab_;
79
John Lee7d416b782019-08-12 22:32:1380 /** @private {!TabsApiProxy} */
81 this.tabsApi_ = TabsApiProxy.getInstance();
82
Collin Baker1ac2e432019-10-16 18:17:2183 /** @private {!TabStripEmbedderProxy} */
84 this.embedderApi_ = TabStripEmbedderProxy.getInstance();
85
John Lee3f8dae92019-08-09 22:37:0986 /** @private {!HTMLElement} */
87 this.titleTextEl_ = /** @type {!HTMLElement} */ (
88 this.shadowRoot.querySelector('#titleText'));
John Lee3f8dae92019-08-09 22:37:0989
John Lee6b1d9572019-11-05 00:33:0690 this.tabEl_.addEventListener('click', () => this.onClick_());
91 this.tabEl_.addEventListener('contextmenu', e => this.onContextMenu_(e));
John Lee7f0c1922019-11-08 22:06:2592 this.tabEl_.addEventListener(
93 'keydown', e => this.onKeyDown_(/** @type {!KeyboardEvent} */ (e)));
John Lee6b1d9572019-11-05 00:33:0694 this.closeButtonEl_.addEventListener('click', e => this.onClose_(e));
95 this.addEventListener('swipe', () => this.onSwipe_());
96
97 /** @private @const {!TabSwiper} */
98 this.tabSwiper_ = new TabSwiper(this);
John Lee912fb9c02019-08-02 01:28:2199 }
John Lee3f8dae92019-08-09 22:37:09100
John Lee99367a62019-10-10 19:03:49101 /** @return {!TabData} */
John Lee3f8dae92019-08-09 22:37:09102 get tab() {
103 return this.tab_;
104 }
105
John Lee99367a62019-10-10 19:03:49106 /** @param {!TabData} tab */
John Lee3f8dae92019-08-09 22:37:09107 set tab(tab) {
John Lee81011102019-10-11 20:05:07108 assert(this.tab_ !== tab);
John Lee8e253602019-08-14 22:22:51109 this.toggleAttribute('active', tab.active);
John Lee61bc1b52019-10-24 22:27:23110 this.tabEl_.setAttribute('aria-selected', tab.active.toString());
John Lee81011102019-10-11 20:05:07111 this.toggleAttribute('hide-icon_', !tab.showIcon);
John Lee99367a62019-10-10 19:03:49112 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07113 'waiting_',
John Lee99367a62019-10-10 19:03:49114 !tab.shouldHideThrobber &&
115 tab.networkState === TabNetworkState.WAITING);
116 this.toggleAttribute(
John Lee81011102019-10-11 20:05:07117 'loading_',
John Lee99367a62019-10-10 19:03:49118 !tab.shouldHideThrobber &&
119 tab.networkState === TabNetworkState.LOADING);
John Leea8e39e682019-10-18 02:19:38120 this.toggleAttribute('pinned', tab.pinned);
John Lee4734ca12019-10-14 19:34:01121 this.toggleAttribute('blocked_', tab.blocked);
John Lee4bcbaf12019-10-21 23:29:46122 this.setAttribute('draggable', true);
John Lee02b3a742019-10-15 20:00:56123 this.toggleAttribute('crashed_', tab.crashed);
John Lee8e253602019-08-14 22:22:51124
John Lee2f3baa72019-11-12 19:58:14125 if (tab.title) {
John Lee3f8dae92019-08-09 22:37:09126 this.titleTextEl_.textContent = tab.title;
John Lee2f3baa72019-11-12 19:58:14127 } else if (
128 !tab.shouldHideThrobber &&
129 (tab.networkState === TabNetworkState.WAITING ||
130 tab.networkState === TabNetworkState.LOADING)) {
131 this.titleTextEl_.textContent = loadTimeData.getString('loadingTab');
132 } else {
133 this.titleTextEl_.textContent = loadTimeData.getString('defaultTabTitle');
John Lee3f8dae92019-08-09 22:37:09134 }
John Lee61bc1b52019-10-24 22:27:23135 this.titleTextEl_.setAttribute('aria-label', getAccessibleTitle(tab));
John Lee3f8dae92019-08-09 22:37:09136
John Lee81011102019-10-11 20:05:07137 if (tab.networkState === TabNetworkState.WAITING ||
138 (tab.networkState === TabNetworkState.LOADING &&
139 tab.isDefaultFavicon)) {
140 this.faviconEl_.style.backgroundImage = 'none';
141 } else if (tab.favIconUrl) {
142 this.faviconEl_.style.backgroundImage = `url(${tab.favIconUrl})`;
John Leed14bec62019-09-23 22:41:32143 } else {
John Lee81011102019-10-11 20:05:07144 this.faviconEl_.style.backgroundImage = getFavicon('');
John Lee2bafb2f2019-08-21 19:20:03145 }
146
John Lee3f8dae92019-08-09 22:37:09147 // Expose the ID to an attribute to allow easy querySelector use
148 this.setAttribute('data-tab-id', tab.id);
149
John Leea8e39e682019-10-18 02:19:38150 this.alertIndicatorsEl_.updateAlertStates(tab.alertStates)
151 .then((alertIndicatorsCount) => {
152 this.toggleAttribute('has-alert-states_', alertIndicatorsCount > 0);
153 });
154
John Lee6b1d9572019-11-05 00:33:06155 if (!this.tab_ || (this.tab_.pinned !== tab.pinned && !tab.pinned)) {
156 this.tabSwiper_.startObserving();
157 } else if (this.tab_.pinned !== tab.pinned && tab.pinned) {
158 this.tabSwiper_.stopObserving();
159 }
160
John Lee3f8dae92019-08-09 22:37:09161 this.tab_ = Object.freeze(tab);
162 }
John Lee7d416b782019-08-12 22:32:13163
John Lee7f0c1922019-11-08 22:06:25164 focus() {
165 this.tabEl_.focus();
166 }
167
John Lee61bc1b52019-10-24 22:27:23168 /** @return {!HTMLElement} */
John Lee3b309d52019-09-18 19:08:09169 getDragImage() {
John Lee61bc1b52019-10-24 22:27:23170 return this.tabEl_;
John Lee3b309d52019-09-18 19:08:09171 }
172
173 /**
Collin Bakere21f723d2019-09-05 20:05:41174 * @param {string} imgData
175 */
176 updateThumbnail(imgData) {
177 this.thumbnail_.src = imgData;
178 }
179
John Lee7d416b782019-08-12 22:32:13180 /** @private */
John Lee8e253602019-08-14 22:22:51181 onClick_() {
John Lee6b1d9572019-11-05 00:33:06182 if (!this.tab_ || this.tabSwiper_.wasSwiping()) {
John Lee7d416b782019-08-12 22:32:13183 return;
184 }
185
John Lee8e253602019-08-14 22:22:51186 this.tabsApi_.activateTab(this.tab_.id);
John Lee4bcbaf12019-10-21 23:29:46187
188 if (tabStripOptions.autoCloseEnabled) {
189 this.embedderApi_.closeContainer();
190 }
John Lee8e253602019-08-14 22:22:51191 }
192
John Lee61bc1b52019-10-24 22:27:23193 /**
194 * @param {!Event} event
195 * @private
196 */
Collin Bakerdc3d2112019-10-10 18:28:20197 onContextMenu_(event) {
198 event.preventDefault();
199
200 if (!this.tab_) {
201 return;
202 }
203
Collin Baker1ac2e432019-10-16 18:17:21204 this.embedderApi_.showTabContextMenu(
Collin Bakerdc3d2112019-10-10 18:28:20205 this.tab_.id, event.clientX, event.clientY);
206 }
207
John Lee8e253602019-08-14 22:22:51208 /**
209 * @param {!Event} event
210 * @private
211 */
212 onClose_(event) {
213 if (!this.tab_) {
214 return;
215 }
216
217 event.stopPropagation();
John Lee7d416b782019-08-12 22:32:13218 this.tabsApi_.closeTab(this.tab_.id);
219 }
John Leea97ceab6f2019-09-04 22:26:46220
John Lee6b1d9572019-11-05 00:33:06221 /** @private */
222 onSwipe_() {
223 // Prevent slideOut animation from playing.
224 this.remove();
225 this.tabsApi_.closeTab(this.tab_.id);
226 }
227
John Leea97ceab6f2019-09-04 22:26:46228 /**
John Lee7f0c1922019-11-08 22:06:25229 * @param {!KeyboardEvent} event
230 * @private
231 */
232 onKeyDown_(event) {
233 if (event.key === 'Enter' || event.key === ' ') {
234 this.onClick_();
235 }
236 }
237
238 /**
John Lee3b309d52019-09-18 19:08:09239 * @param {boolean} dragging
240 */
241 setDragging(dragging) {
John Lee81011102019-10-11 20:05:07242 this.toggleAttribute('dragging_', dragging);
John Lee3b309d52019-09-18 19:08:09243 }
244
245 /**
John Leea97ceab6f2019-09-04 22:26:46246 * @return {!Promise}
247 */
248 slideIn() {
John Leec6bc69d2019-11-18 21:53:17249 // TODO(crbug.com/1025390): margin-inline-end cannot be animated yet.
250 const marginInlineEnd = isRTL() ? 'marginLeft' : 'marginRight';
251
252 const startState = {
253 maxWidth: 0,
254 transform: `scale(0)`,
255 };
256 startState[marginInlineEnd] = 0;
257
258 const finishState = {
259 maxWidth: `var(--tabstrip-tab-width)`,
260 transform: `scale(1)`,
261 };
262 finishState[marginInlineEnd] = 'var(--tabstrip-tab-margin-inline-end)';
263
John Leea97ceab6f2019-09-04 22:26:46264 return new Promise(resolve => {
John Leec6bc69d2019-11-18 21:53:17265 const animation = this.animate([startState, finishState], {
266 duration: 300,
267 easing: 'cubic-bezier(.4, 0, 0, 1)',
268 });
John Lee6b1d9572019-11-05 00:33:06269 animation.onfinish = () => {
John Lee6b1d9572019-11-05 00:33:06270 resolve();
271 };
John Leea97ceab6f2019-09-04 22:26:46272 });
273 }
274
275 /**
276 * @return {!Promise}
277 */
278 slideOut() {
John Leed71aaac2019-11-14 20:37:50279 if (!this.embedderApi_.isVisible()) {
280 // There is no point in animating if the tab strip is hidden.
281 this.remove();
282 return Promise.resolve();
283 }
284
John Leea97ceab6f2019-09-04 22:26:46285 return new Promise(resolve => {
John Leed71aaac2019-11-14 20:37:50286 const finishCallback = () => {
287 this.remove();
288 resolve();
289 };
290
John Leea97ceab6f2019-09-04 22:26:46291 const animation = this.animate(
292 [
John Lee4df86c42019-10-25 20:00:03293 {maxWidth: 'var(--tabstrip-tab-width)', opacity: 1},
John Leea97ceab6f2019-09-04 22:26:46294 {maxWidth: 0, opacity: 0},
295 ],
296 {
297 duration: DEFAULT_ANIMATION_DURATION,
298 fill: 'forwards',
299 });
John Leed71aaac2019-11-14 20:37:50300
301 const visibilityChangeListener = () => {
302 if (!this.embedderApi_.isVisible()) {
303 // If a tab strip becomes hidden during the animation, the onfinish
304 // event will not get fired until the tab strip becomes visible again.
305 // Therefore, when the tab strip becomes hidden, immediately call the
306 // finish callback.
307 animation.cancel();
308 finishCallback();
309 }
310 };
311
312 document.addEventListener(
313 'visibilitychange', visibilityChangeListener, {once: true});
John Leea97ceab6f2019-09-04 22:26:46314 animation.onfinish = () => {
John Leed71aaac2019-11-14 20:37:50315 document.removeEventListener(
316 'visibilitychange', visibilityChangeListener);
317 finishCallback();
John Leea97ceab6f2019-09-04 22:26:46318 };
319 });
320 }
John Lee912fb9c02019-08-02 01:28:21321}
322
John Lee3f8dae92019-08-09 22:37:09323customElements.define('tabstrip-tab', TabElement);