3 Copyright (c) 2014 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
8 <polymer-element name="tracing-analysis-tab-view"
9 constructor="TracingAnalysisTabView">
14 flex-flow: column nowrap;
20 background-image: -webkit-gradient(linear, 0 0, 100% 0,
21 from(#E5E5E5), to(#D1D1D1));
22 border-bottom: 1px solid #8e8e8e;
23 border-top: 1px solid #fff;
32 /* By default, a tab is not selected, its background color is
33 * transparent, so this button has no background color set. */
34 border-right: 1px solid #8e8e8e;
39 padding: 2px 10px 2px 10px;
42 tab-button[selected='true'] {
43 background-color: #aaa;
46 tabs-content-container {
57 ::content > *:not([selected]) {
65 vertical-align: middle;
70 <template repeat="{{tab in tabs_}}">
72 button-id="{{ tab.id }}"
73 on-click="{{ tabButtonSelectHandler_ }}"
74 selected="{{ selectedTab_.id === tab.id }}">
75 <button-label>{{ tab.label ? tab.label : 'No Label'}}</button-label>
80 <tabs-content-container id='content-container'>
82 </tabs-content-container>
91 // Do not give access to the user to the inner data structure.
92 // A user should only be able to mutate the added tab content.
93 if (this.selectedTab_)
94 return this.selectedTab_.content;
98 set selectedTab(content) {
101 // Make sure we process any pending children additions / removals, before
102 // trying to select a tab. Otherwise, we might not find some children.
103 this.childrenUpdated_(
104 this.childrenObserver_.takeRecords(), this.childrenObserver_);
105 // Search for the specific node in our tabs list.
106 // If it is not there print a warning.
107 var contentTabId = undefined;
108 for (var i = 0; i < this.tabs_.length; i++)
109 if (this.tabs_[i].content === content) {
110 contentTabId = this.tabs_[i].id;
114 if (contentTabId === undefined) {
115 console.warn('Tab not in tabs list. Ignoring changed selection.');
119 this.saveCurrentTabScrollPosition_();
120 this.clearSelectedTab_();
121 this.setSelectedTabById_(contentTabId);
122 this.restoreCurrentTabScrollPosition_();
127 // A tab is represented by the following tuple:
128 // (id, label, content, observer, savedScrollTop, savedScrollLeft).
129 // The properties are used in the following way:
130 // id: Uniquely identifies a tab. It is the same number as the index
131 // in the tabs array. Used primarily by the on-click event attached
133 // label: A string, representing the label printed on the tab button.
134 // content: The light-dom child representing the contents of the tab.
135 // The content is appended to this tab-view by the user.
136 // observers: The observers attached to the content node to watch for
137 // attribute changes. The attributes of interest are: 'selected',
139 // savedScrollTop/Left: Used to return the scroll position upon switching
140 // tabs. The values are generally saved when a tab switch occurs.
142 // The order of the tabs is relevant for the tab ordering.
144 this.selectedTab_ = undefined;
146 // Register any already existing children.
147 for (var i = 0; i < this.children.length; i++)
148 this.processAddedChild_(this.children[i]);
150 // In case the user decides to add more tabs, make sure we watch for
151 // any child mutations.
152 this.childrenObserver_ = new MutationObserver(
153 this.childrenUpdated_.bind(this));
154 this.childrenObserver_.observe(this, { childList : 'true' });
159 * Function called on light-dom child addition.
161 processAddedChild_: function(child) {
162 var observerAttributeSelected = new MutationObserver(
163 this.childAttributesChanged_.bind(this));
164 var observerAttributeTabLabel = new MutationObserver(
165 this.childAttributesChanged_.bind(this));
167 id: this.tabs_.length,
169 label: child.getAttribute('tab-label'),
171 forAttributeSelected: observerAttributeSelected,
172 forAttributeTabLabel: observerAttributeTabLabel
178 this.tabs_.push(tabObject);
179 if (child.hasAttribute('selected')) {
180 // When receiving a child with the selected attribute, if we have no
181 // selected tab, mark the child as the selected tab, otherwise keep
182 // the previous selection.
183 if (this.selectedTab_)
184 child.removeAttribute('selected');
186 this.setSelectedTabById_(tabObject.id);
189 // This is required because the user might have set the selected
190 // property before we got to process the child.
191 var previousSelected = child.selected;
195 Object.defineProperty(
199 set: function(value) {
201 tabView.saveCurrentTabScrollPosition_();
202 tabView.clearSelectedTab_();
203 tabView.setSelectedTabById_(tabObject.id);
204 tabView.restoreCurrentTabScrollPosition_();
205 } else if (tabView.selectedTab_ === tabObject) {
206 tabView.saveCurrentTabScrollPosition_();
207 tabView.clearSelectedTab_();
211 return this.hasAttribute('selected');
215 if (previousSelected)
216 child.selected = previousSelected;
218 observerAttributeSelected.observe(child,
219 { attributeFilter: ['selected'] });
220 observerAttributeTabLabel.observe(child,
221 { attributeFilter: ['tab-label'] });
226 * Function called on light-dom child removal.
228 processRemovedChild_: function(child) {
229 for (var i = 0; i < this.tabs_.length; i++) {
230 // Make sure ids are the same as the tab position after removal.
231 this.tabs_[i].id = i;
232 if (this.tabs_[i].content === child) {
233 this.tabs_[i].observers.forAttributeSelected.disconnect();
234 this.tabs_[i].observers.forAttributeTabLabel.disconnect();
235 // The user has removed the currently selected tab.
236 if (this.tabs_[i] === this.selectedTab_)
237 this.clearSelectedTab_();
238 child.removeAttribute('selected');
239 delete child.selected;
240 // Remove the observer since we no longer care about this child.
241 this.tabs_.splice(i, 1);
249 * This function handles child attribute changes. The only relevant
250 * attributes for the tab-view are 'tab-label' and 'selected'.
252 childAttributesChanged_: function(mutations, observer) {
253 var tabObject = undefined;
254 // First figure out which child has been changed.
255 for (var i = 0; i < this.tabs_.length; i++) {
256 var observers = this.tabs_[i].observers;
257 if (observers.forAttributeSelected === observer ||
258 observers.forAttributeTabLabel === observer) {
259 tabObject = this.tabs_[i];
264 // This should not happen, unless the user has messed with our internal
269 // Next handle the attribute changes.
270 for (var i = 0; i < mutations.length; i++) {
271 var node = tabObject.content;
272 // 'tab-label' attribute has been changed.
273 if (mutations[i].attributeName === 'tab-label')
274 tabObject.label = node.getAttribute('tab-label');
275 // 'selected' attribute has been changed.
276 if (mutations[i].attributeName === 'selected') {
277 // The attribute has been set.
278 var nodeIsSelected = node.hasAttribute('selected');
279 this.saveCurrentTabScrollPosition_();
280 this.clearSelectedTab_();
281 if (nodeIsSelected) {
282 this.setSelectedTabById_(tabObject.id);
283 this.restoreCurrentTabScrollPosition_();
290 * This function handles light-dom additions and removals from the
291 * tab-view component.
293 childrenUpdated_: function(mutations, observer) {
294 mutations.forEach(function (mutation) {
295 for (var i = 0; i < mutation.removedNodes.length; i++)
296 this.processRemovedChild_(mutation.removedNodes[i]);
297 for (var i = 0; i < mutation.addedNodes.length; i++)
298 this.processAddedChild_(mutation.addedNodes[i]);
304 * This function updates the currently selected tab based on its internal
305 * id. The corresponding light-dom element receives the selected attribute.
307 setSelectedTabById_: function(id) {
308 this.selectedTab_ = this.tabs_[id];
309 // Disconnect observer while we mutate the child.
310 this.selectedTab_.observers.forAttributeSelected.disconnect();
311 this.selectedTab_.content.setAttribute('selected', 'selected');
312 // Reconnect the observer to watch for changes in the future.
313 this.selectedTab_.observers.forAttributeSelected.observe(
314 this.selectedTab_.content, { attributeFilter: ['selected'] });
318 saveCurrentTabScrollPosition_: function() {
319 if (this.selectedTab_) {
320 this.selectedTab_.savedScrollTop =
321 this.$['content-container'].scrollTop;
322 this.selectedTab_.savedScrollLeft =
323 this.$['content-container'].scrollLeft;
327 restoreCurrentTabScrollPosition_: function() {
328 if (this.selectedTab_) {
329 this.$['content-container'].scrollTop =
330 this.selectedTab_.savedScrollTop;
331 this.$['content-container'].scrollLeft =
332 this.selectedTab_.savedScrollLeft;
337 * This function clears the currently selected tab. This handles removal
338 * of the selected attribute from the light-dom element.
340 clearSelectedTab_: function() {
341 if (this.selectedTab_) {
342 // Disconnect observer while we mutate the child.
343 this.selectedTab_.observers.forAttributeSelected.disconnect();
344 this.selectedTab_.content.removeAttribute('selected');
345 // Reconnect the observer to watch for changes in the future.
346 this.selectedTab_.observers.forAttributeSelected.observe(
347 this.selectedTab_.content, { attributeFilter: ['selected'] });
348 this.selectedTab_ = undefined;
353 * Handler called when a click event happens on any of the tab buttons.
355 tabButtonSelectHandler_: function(event, detail, sender) {
356 this.saveCurrentTabScrollPosition_();
357 this.clearSelectedTab_();
358 this.setSelectedTabById_(sender.getAttribute('button-id'));
359 this.restoreCurrentTabScrollPosition_();