blob: 749566a8847c61bb3a65c44a8852fb415daaedaf [file] [log] [blame]
Chris Craikb122baf2015-03-05 13:58:42 -08001<!DOCTYPE html>
2<!--
3Copyright (c) 2014 The Chromium Authors. All rights reserved.
4Use of this source code is governed by a BSD-style license that can be
5found in the LICENSE file.
6-->
7
8<polymer-element name="tracing-analysis-tab-view"
9 constructor="TracingAnalysisTabView">
10 <template>
11 <style>
12 :host {
13 display: flex;
14 flex-flow: column nowrap;
15 overflow: hidden;
16 box-sizing: border-box;
17 }
18
19 tab-strip[tabs-hidden] {
20 display: none;
21 }
22
23 tab-strip {
24 background-color: rgb(236, 236, 236);
25 border-bottom: 1px solid #8e8e8e;
26 display: flex;
27 flex: 0 0 auto;
28 flex-flow: row;
29 overflow-x: auto;
30 padding: 0 10px 0 10px;
31 font-size: 12px;
32 }
33
34 tab-button {
35 display: block;
36 flex: 0 0 auto;
37 padding: 4px 15px 1px 15px;
38 margin-top: 2px;
39 }
40
41 tab-button[selected=true] {
42 background-color: white;
43 border: 1px solid rgb(163, 163, 163);
44 border-bottom: none;
45 padding: 3px 14px 1px 14px;
46 }
47
48 tabs-content-container {
49 display: flex;
50 flex: 1 1 auto;
51 overflow: auto;
52 width: 100%;
53 }
54
55 ::content > * {
56 flex: 1 1 auto;
57 }
58
59 ::content > *:not([selected]) {
60 display: none;
61 }
62
63 button-label {
64 display: inline;
65 }
Chris Craikbeca7ae2015-04-07 13:29:55 -070066
67 tab-strip-heading {
68 display: block;
69 flex: 0 0 auto;
70 padding: 4px 15px 1px 15px;
71 margin-top: 2px;
72 margin-before: 20px;
73 margin-after: 10px;
74 }
75 #tsh {
76 display: inline;
77 font-weight: bold;
78 }
Chris Craikb122baf2015-03-05 13:58:42 -080079 </style>
80
81 <tab-strip>
Chris Craikbeca7ae2015-04-07 13:29:55 -070082 <tab-strip-heading id="tshh">
83 <span id="tsh"></span>
84 </tab-strip-heading>
Chris Craikb122baf2015-03-05 13:58:42 -080085 <template repeat="{{tab in tabs_}}">
86 <tab-button
87 button-id="{{ tab.id }}"
88 on-click="{{ tabButtonSelectHandler_ }}"
89 selected="{{ selectedTab_.id === tab.id }}">
90 <button-label>{{ tab.label ? tab.label : 'No Label'}}</button-label>
91 </tab-button>
92 </template>
93 </tab-strip>
94
95 <tabs-content-container id='content-container'>
96 <content></content>
97 </tabs-content-container>
98
99 </template>
100
101 <script>
102 'use strict';
103 Polymer({
Chris Craikbeca7ae2015-04-07 13:29:55 -0700104 ready: function() {
105 this.$.tshh.style.display = 'none';
106
107 // A tab is represented by the following tuple:
108 // (id, label, content, observer, savedScrollTop, savedScrollLeft).
109 // The properties are used in the following way:
110 // id: Uniquely identifies a tab. It is the same number as the index
111 // in the tabs array. Used primarily by the on-click event attached
112 // to buttons.
113 // label: A string, representing the label printed on the tab button.
114 // content: The light-dom child representing the contents of the tab.
115 // The content is appended to this tab-view by the user.
116 // observers: The observers attached to the content node to watch for
117 // attribute changes. The attributes of interest are: 'selected',
118 // and 'tab-label'.
119 // savedScrollTop/Left: Used to return the scroll position upon switching
120 // tabs. The values are generally saved when a tab switch occurs.
121 //
122 // The order of the tabs is relevant for the tab ordering.
123 this.tabs_ = [];
124 this.selectedTab_ = undefined;
125
126 // Register any already existing children.
127 for (var i = 0; i < this.children.length; i++)
128 this.processAddedChild_(this.children[i]);
129
130 // In case the user decides to add more tabs, make sure we watch for
131 // any child mutations.
132 this.childrenObserver_ = new MutationObserver(
133 this.childrenUpdated_.bind(this));
134 this.childrenObserver_.observe(this, { childList: 'true' });
135 },
136
137 get tabStripHeadingText() {
138 return this.$.tsh.textContent;
139 },
140
141 set tabStripHeadingText(tabStripHeadingText) {
142 this.$.tsh.textContent = tabStripHeadingText;
143 if (!!tabStripHeadingText)
144 this.$.tshh.style.display = '';
145 else
146 this.$.tshh.style.display = 'none';
147 },
Chris Craikb122baf2015-03-05 13:58:42 -0800148
149 get selectedTab() {
Chris Craik44c28202015-05-12 17:25:16 -0700150 // Make sure we process any pending children additions / removals, before
151 // trying to select a tab. Otherwise, we might not find some children.
152 this.childrenUpdated_(
153 this.childrenObserver_.takeRecords(), this.childrenObserver_);
154
Chris Craikb122baf2015-03-05 13:58:42 -0800155 // Do not give access to the user to the inner data structure.
156 // A user should only be able to mutate the added tab content.
157 if (this.selectedTab_)
158 return this.selectedTab_.content;
159 return undefined;
160 },
161
162 set selectedTab(content) {
163 // Make sure we process any pending children additions / removals, before
164 // trying to select a tab. Otherwise, we might not find some children.
165 this.childrenUpdated_(
166 this.childrenObserver_.takeRecords(), this.childrenObserver_);
167
168 if (content === undefined || content === null) {
169 this.changeSelectedTabById_(undefined);
170 return;
171 }
172
173 // Search for the specific node in our tabs list.
174 // If it is not there print a warning.
175 var contentTabId = undefined;
176 for (var i = 0; i < this.tabs_.length; i++)
177 if (this.tabs_[i].content === content) {
178 contentTabId = this.tabs_[i].id;
179 break;
180 }
181
182 if (contentTabId === undefined) {
183 console.warn('Tab not in tabs list. Ignoring changed selection.');
184 return;
185 }
186
187 this.changeSelectedTabById_(contentTabId);
188 },
189
190 get tabsHidden() {
191 var ts = this.shadowRoot.querySelector('tab-strip');
192 return ts.hasAttribute('tabs-hidden');
193 },
194
195 set tabsHidden(tabsHidden) {
196 tabsHidden = !!tabsHidden;
197 var ts = this.shadowRoot.querySelector('tab-strip');
198 if (tabsHidden)
199 ts.setAttribute('tabs-hidden', true);
200 else
201 ts.removeAttribute('tabs-hidden');
202 },
203
Chris Craikb122baf2015-03-05 13:58:42 -0800204 /**
205 * Function called on light-dom child addition.
206 */
207 processAddedChild_: function(child) {
208 var observerAttributeSelected = new MutationObserver(
209 this.childAttributesChanged_.bind(this));
210 var observerAttributeTabLabel = new MutationObserver(
211 this.childAttributesChanged_.bind(this));
212 var tabObject = {
213 id: this.tabs_.length,
214 content: child,
215 label: child.getAttribute('tab-label'),
216 observers: {
217 forAttributeSelected: observerAttributeSelected,
218 forAttributeTabLabel: observerAttributeTabLabel
219 },
220 savedScrollTop: 0,
221 savedScrollLeft: 0
222 };
223
224 this.tabs_.push(tabObject);
225 if (child.hasAttribute('selected')) {
226 // When receiving a child with the selected attribute, if we have no
227 // selected tab, mark the child as the selected tab, otherwise keep
228 // the previous selection.
229 if (this.selectedTab_)
230 child.removeAttribute('selected');
231 else
232 this.setSelectedTabById_(tabObject.id);
233 }
234
235 // This is required because the user might have set the selected
236 // property before we got to process the child.
237 var previousSelected = child.selected;
238
239 var tabView = this;
240
241 Object.defineProperty(
242 child,
243 'selected', {
244 configurable: true,
245 set: function(value) {
246 if (value) {
247 tabView.changeSelectedTabById_(tabObject.id);
248 return;
249 }
250
251 var wasSelected = tabView.selectedTab_ === tabObject;
252 if (wasSelected)
253 tabView.changeSelectedTabById_(undefined);
254 },
255 get: function() {
256 return this.hasAttribute('selected');
257 }
258 });
259
260 if (previousSelected)
261 child.selected = previousSelected;
262
263 observerAttributeSelected.observe(child,
264 { attributeFilter: ['selected'] });
265 observerAttributeTabLabel.observe(child,
266 { attributeFilter: ['tab-label'] });
267
268 },
269
270 /**
271 * Function called on light-dom child removal.
272 */
273 processRemovedChild_: function(child) {
274 for (var i = 0; i < this.tabs_.length; i++) {
275 // Make sure ids are the same as the tab position after removal.
276 this.tabs_[i].id = i;
277 if (this.tabs_[i].content === child) {
278 this.tabs_[i].observers.forAttributeSelected.disconnect();
279 this.tabs_[i].observers.forAttributeTabLabel.disconnect();
280 // The user has removed the currently selected tab.
Chris Craik44c28202015-05-12 17:25:16 -0700281 if (this.tabs_[i] === this.selectedTab_) {
Chris Craikb122baf2015-03-05 13:58:42 -0800282 this.clearSelectedTab_();
Chris Craik44c28202015-05-12 17:25:16 -0700283 this.fire('selected-tab-change');
284 }
Chris Craikb122baf2015-03-05 13:58:42 -0800285 child.removeAttribute('selected');
286 delete child.selected;
287 // Remove the observer since we no longer care about this child.
288 this.tabs_.splice(i, 1);
289 i--;
290 }
291 }
292 },
293
294
295 /**
296 * This function handles child attribute changes. The only relevant
297 * attributes for the tab-view are 'tab-label' and 'selected'.
298 */
299 childAttributesChanged_: function(mutations, observer) {
300 var tabObject = undefined;
301 // First figure out which child has been changed.
302 for (var i = 0; i < this.tabs_.length; i++) {
303 var observers = this.tabs_[i].observers;
304 if (observers.forAttributeSelected === observer ||
305 observers.forAttributeTabLabel === observer) {
306 tabObject = this.tabs_[i];
307 break;
308 }
309 }
310
311 // This should not happen, unless the user has messed with our internal
312 // data structure.
313 if (!tabObject)
314 return;
315
316 // Next handle the attribute changes.
317 for (var i = 0; i < mutations.length; i++) {
318 var node = tabObject.content;
319 // 'tab-label' attribute has been changed.
320 if (mutations[i].attributeName === 'tab-label')
321 tabObject.label = node.getAttribute('tab-label');
322 // 'selected' attribute has been changed.
323 if (mutations[i].attributeName === 'selected') {
324 // The attribute has been set.
325 var nodeIsSelected = node.hasAttribute('selected');
326 if (nodeIsSelected)
327 this.changeSelectedTabById_(tabObject.id);
328 else
329 this.changeSelectedTabById_(undefined);
330 }
331 }
332 },
333
334 /**
335 * This function handles light-dom additions and removals from the
336 * tab-view component.
337 */
338 childrenUpdated_: function(mutations, observer) {
339 mutations.forEach(function(mutation) {
340 for (var i = 0; i < mutation.removedNodes.length; i++)
341 this.processRemovedChild_(mutation.removedNodes[i]);
342 for (var i = 0; i < mutation.addedNodes.length; i++)
343 this.processAddedChild_(mutation.addedNodes[i]);
344 }, this);
345 },
346
347 /**
348 * Handler called when a click event happens on any of the tab buttons.
349 */
350 tabButtonSelectHandler_: function(event, detail, sender) {
351 this.changeSelectedTabById_(sender.getAttribute('button-id'));
352 },
353
354 /**
355 * This does the actual work. :)
356 */
357 changeSelectedTabById_: function(id) {
358 var newTab = id !== undefined ? this.tabs_[id] : undefined;
359 var changed = this.selectedTab_ !== newTab;
360 this.saveCurrentTabScrollPosition_();
361 this.clearSelectedTab_();
362 if (id !== undefined) {
363 this.setSelectedTabById_(id);
364 this.restoreCurrentTabScrollPosition_();
365 }
366
367 if (changed)
368 this.fire('selected-tab-change');
369 },
370
371 /**
372 * This function updates the currently selected tab based on its internal
373 * id. The corresponding light-dom element receives the selected attribute.
374 */
375 setSelectedTabById_: function(id) {
376 this.selectedTab_ = this.tabs_[id];
377 // Disconnect observer while we mutate the child.
378 this.selectedTab_.observers.forAttributeSelected.disconnect();
379 this.selectedTab_.content.setAttribute('selected', 'selected');
380 // Reconnect the observer to watch for changes in the future.
381 this.selectedTab_.observers.forAttributeSelected.observe(
382 this.selectedTab_.content, { attributeFilter: ['selected'] });
383
384 },
385
386 saveCurrentTabScrollPosition_: function() {
387 if (this.selectedTab_) {
388 this.selectedTab_.savedScrollTop =
389 this.$['content-container'].scrollTop;
390 this.selectedTab_.savedScrollLeft =
391 this.$['content-container'].scrollLeft;
392 }
393 },
394
395 restoreCurrentTabScrollPosition_: function() {
396 if (this.selectedTab_) {
397 this.$['content-container'].scrollTop =
398 this.selectedTab_.savedScrollTop;
399 this.$['content-container'].scrollLeft =
400 this.selectedTab_.savedScrollLeft;
401 }
402 },
403
404 /**
405 * This function clears the currently selected tab. This handles removal
406 * of the selected attribute from the light-dom element.
407 */
408 clearSelectedTab_: function() {
409 if (this.selectedTab_) {
410 // Disconnect observer while we mutate the child.
411 this.selectedTab_.observers.forAttributeSelected.disconnect();
412 this.selectedTab_.content.removeAttribute('selected');
413 // Reconnect the observer to watch for changes in the future.
414 this.selectedTab_.observers.forAttributeSelected.observe(
415 this.selectedTab_.content, { attributeFilter: ['selected'] });
416 this.selectedTab_ = undefined;
417 }
418 }
419 });
420 </script>
421</polymer-element>