blob: d60ec117058437c6327e4675783d19baaa54e290 [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 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);
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
John Lee7f0c1922019-11-08 22:06:25172 focus() {
173 this.tabEl_.focus();
174 }
175
John Lee61bc1b52019-10-24 22:27:23176 /** @return {!HTMLElement} */
John Lee3b309d52019-09-18 19:08:09177 getDragImage() {
John Lee61bc1b52019-10-24 22:27:23178 return this.tabEl_;
John Lee3b309d52019-09-18 19:08:09179 }
180
181 /**
Collin Bakere21f723d2019-09-05 20:05:41182 * @param {string} imgData
183 */
184 updateThumbnail(imgData) {
185 this.thumbnail_.src = imgData;
186 }
187
John Lee7d416b782019-08-12 22:32:13188 /** @private */
John Lee8e253602019-08-14 22:22:51189 onClick_() {
John Lee6b1d9572019-11-05 00:33:06190 if (!this.tab_ || this.tabSwiper_.wasSwiping()) {
John Lee7d416b782019-08-12 22:32:13191 return;
192 }
193
John Lee8e253602019-08-14 22:22:51194 this.tabsApi_.activateTab(this.tab_.id);
John Lee4bcbaf12019-10-21 23:29:46195
196 if (tabStripOptions.autoCloseEnabled) {
197 this.embedderApi_.closeContainer();
198 }
John Lee8e253602019-08-14 22:22:51199 }
200
John Lee61bc1b52019-10-24 22:27:23201 /**
202 * @param {!Event} event
203 * @private
204 */
Collin Bakerdc3d2112019-10-10 18:28:20205 onContextMenu_(event) {
206 event.preventDefault();
207
208 if (!this.tab_) {
209 return;
210 }
211
Collin Baker1ac2e432019-10-16 18:17:21212 this.embedderApi_.showTabContextMenu(
Collin Bakerdc3d2112019-10-10 18:28:20213 this.tab_.id, event.clientX, event.clientY);
Collin Baker48a096b82019-11-26 01:21:27214 event.stopPropagation();
Collin Bakerdc3d2112019-10-10 18:28:20215 }
216
John Lee8e253602019-08-14 22:22:51217 /**
218 * @param {!Event} event
219 * @private
220 */
221 onClose_(event) {
222 if (!this.tab_) {
223 return;
224 }
225
226 event.stopPropagation();
John Lee7d416b782019-08-12 22:32:13227 this.tabsApi_.closeTab(this.tab_.id);
228 }
John Leea97ceab6f2019-09-04 22:26:46229
John Lee6b1d9572019-11-05 00:33:06230 /** @private */
231 onSwipe_() {
232 // Prevent slideOut animation from playing.
233 this.remove();
234 this.tabsApi_.closeTab(this.tab_.id);
235 }
236
John Leea97ceab6f2019-09-04 22:26:46237 /**
John Lee7f0c1922019-11-08 22:06:25238 * @param {!KeyboardEvent} event
239 * @private
240 */
241 onKeyDown_(event) {
242 if (event.key === 'Enter' || event.key === ' ') {
243 this.onClick_();
244 }
245 }
246
247 /**
John Lee3b309d52019-09-18 19:08:09248 * @param {boolean} dragging
249 */
250 setDragging(dragging) {
John Lee81011102019-10-11 20:05:07251 this.toggleAttribute('dragging_', dragging);
John Lee3b309d52019-09-18 19:08:09252 }
253
254 /**
John Leea97ceab6f2019-09-04 22:26:46255 * @return {!Promise}
256 */
257 slideIn() {
John Leed96ee0532019-11-22 22:06:39258 const paddingInlineEnd = getPaddingInlineEndProperty();
John Leec6bc69d2019-11-18 21:53:17259
John Lee41620f832019-11-27 22:41:05260 // If this TabElement is the last tab, there needs to be enough space for
261 // the view to scroll to it. Therefore, immediately take up all the space
262 // it needs to and only animate the scale.
263 const isLastChild = this.nextElementSibling === null;
264
John Leec6bc69d2019-11-18 21:53:17265 const startState = {
John Lee41620f832019-11-27 22:41:05266 maxWidth: isLastChild ? 'var(--tabstrip-tab-width)' : 0,
John Leec6bc69d2019-11-18 21:53:17267 transform: `scale(0)`,
268 };
John Lee41620f832019-11-27 22:41:05269 startState[paddingInlineEnd] =
270 isLastChild ? 'var(--tabstrip-tab-spacing)' : 0;
John Leec6bc69d2019-11-18 21:53:17271
272 const finishState = {
273 maxWidth: `var(--tabstrip-tab-width)`,
274 transform: `scale(1)`,
275 };
John Leed96ee0532019-11-22 22:06:39276 finishState[paddingInlineEnd] = 'var(--tabstrip-tab-spacing)';
John Leec6bc69d2019-11-18 21:53:17277
John Leea97ceab6f2019-09-04 22:26:46278 return new Promise(resolve => {
John Leec6bc69d2019-11-18 21:53:17279 const animation = this.animate([startState, finishState], {
280 duration: 300,
281 easing: 'cubic-bezier(.4, 0, 0, 1)',
282 });
John Lee6b1d9572019-11-05 00:33:06283 animation.onfinish = () => {
John Lee6b1d9572019-11-05 00:33:06284 resolve();
285 };
John Leea97ceab6f2019-09-04 22:26:46286 });
287 }
288
289 /**
290 * @return {!Promise}
291 */
292 slideOut() {
John Lee7bbf7ad2019-11-21 23:38:51293 if (!this.embedderApi_.isVisible() || this.tab_.pinned) {
John Leed71aaac2019-11-14 20:37:50294 // There is no point in animating if the tab strip is hidden.
295 this.remove();
296 return Promise.resolve();
297 }
298
John Leea97ceab6f2019-09-04 22:26:46299 return new Promise(resolve => {
John Leed71aaac2019-11-14 20:37:50300 const finishCallback = () => {
301 this.remove();
302 resolve();
303 };
304
John Lee7bbf7ad2019-11-21 23:38:51305 const translateAnimation = this.animate(
John Leea97ceab6f2019-09-04 22:26:46306 {
John Lee7bbf7ad2019-11-21 23:38:51307 transform: ['translateY(0)', 'translateY(-100%)'],
308 },
309 {
310 duration: 150,
311 easing: 'cubic-bezier(.4, 0, 1, 1)',
John Leea97ceab6f2019-09-04 22:26:46312 fill: 'forwards',
313 });
John Lee7bbf7ad2019-11-21 23:38:51314 const opacityAnimation = this.animate(
315 {
316 opacity: [1, 0],
317 },
318 {
319 delay: 97.5,
320 duration: 50,
321 fill: 'forwards',
322 });
323
324 const widthAnimationKeyframes = {
325 maxWidth: ['var(--tabstrip-tab-width)', 0],
326 };
John Leed96ee0532019-11-22 22:06:39327 widthAnimationKeyframes[getPaddingInlineEndProperty()] =
328 ['var(--tabstrip-tab-spacing)', 0];
John Lee7bbf7ad2019-11-21 23:38:51329 const widthAnimation = this.animate(widthAnimationKeyframes, {
330 delay: 97.5,
331 duration: 300,
332 easing: 'cubic-bezier(.4, 0, 0, 1)',
333 fill: 'forwards',
334 });
John Leed71aaac2019-11-14 20:37:50335
336 const visibilityChangeListener = () => {
337 if (!this.embedderApi_.isVisible()) {
338 // If a tab strip becomes hidden during the animation, the onfinish
339 // event will not get fired until the tab strip becomes visible again.
340 // Therefore, when the tab strip becomes hidden, immediately call the
341 // finish callback.
John Lee7bbf7ad2019-11-21 23:38:51342 translateAnimation.cancel();
343 opacityAnimation.cancel();
344 widthAnimation.cancel();
John Leed71aaac2019-11-14 20:37:50345 finishCallback();
346 }
347 };
348
349 document.addEventListener(
350 'visibilitychange', visibilityChangeListener, {once: true});
John Lee7bbf7ad2019-11-21 23:38:51351 // The onfinish handler is put on the width animation, as it will end
352 // last.
353 widthAnimation.onfinish = () => {
John Leed71aaac2019-11-14 20:37:50354 document.removeEventListener(
355 'visibilitychange', visibilityChangeListener);
356 finishCallback();
John Leea97ceab6f2019-09-04 22:26:46357 };
358 });
359 }
John Lee912fb9c02019-08-02 01:28:21360}
361
John Lee3f8dae92019-08-09 22:37:09362customElements.define('tabstrip-tab', TabElement);