1<!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="tr-ui-a-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 } 66 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 } 79 </style> 80 81 <tab-strip> 82 <tab-strip-heading id="tshh"> 83 <span id="tsh"></span> 84 </tab-strip-heading> 85 <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({ 104 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 }, 148 149 get selectedTab() { 150 // 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 155 // 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 return; 184 185 this.changeSelectedTabById_(contentTabId); 186 }, 187 188 get tabsHidden() { 189 var ts = this.shadowRoot.querySelector('tab-strip'); 190 return ts.hasAttribute('tabs-hidden'); 191 }, 192 193 set tabsHidden(tabsHidden) { 194 tabsHidden = !!tabsHidden; 195 var ts = this.shadowRoot.querySelector('tab-strip'); 196 if (tabsHidden) 197 ts.setAttribute('tabs-hidden', true); 198 else 199 ts.removeAttribute('tabs-hidden'); 200 }, 201 202 get tabs() { 203 return this.tabs_.map(function(tabObject) { 204 return tabObject.content; 205 }); 206 }, 207 208 /** 209 * Function called on light-dom child addition. 210 */ 211 processAddedChild_: function(child) { 212 var observerAttributeSelected = new MutationObserver( 213 this.childAttributesChanged_.bind(this)); 214 var observerAttributeTabLabel = new MutationObserver( 215 this.childAttributesChanged_.bind(this)); 216 var tabObject = { 217 id: this.tabs_.length, 218 content: child, 219 label: child.getAttribute('tab-label'), 220 observers: { 221 forAttributeSelected: observerAttributeSelected, 222 forAttributeTabLabel: observerAttributeTabLabel 223 } 224 }; 225 226 this.tabs_.push(tabObject); 227 if (child.hasAttribute('selected')) { 228 // When receiving a child with the selected attribute, if we have no 229 // selected tab, mark the child as the selected tab, otherwise keep 230 // the previous selection. 231 if (this.selectedTab_) 232 child.removeAttribute('selected'); 233 else 234 this.setSelectedTabById_(tabObject.id); 235 } 236 237 // This is required because the user might have set the selected 238 // property before we got to process the child. 239 var previousSelected = child.selected; 240 241 var tabView = this; 242 243 Object.defineProperty( 244 child, 245 'selected', { 246 configurable: true, 247 set: function(value) { 248 if (value) { 249 tabView.changeSelectedTabById_(tabObject.id); 250 return; 251 } 252 253 var wasSelected = tabView.selectedTab_ === tabObject; 254 if (wasSelected) 255 tabView.changeSelectedTabById_(undefined); 256 }, 257 get: function() { 258 return this.hasAttribute('selected'); 259 } 260 }); 261 262 if (previousSelected) 263 child.selected = previousSelected; 264 265 observerAttributeSelected.observe(child, 266 { attributeFilter: ['selected'] }); 267 observerAttributeTabLabel.observe(child, 268 { attributeFilter: ['tab-label'] }); 269 270 }, 271 272 /** 273 * Function called on light-dom child removal. 274 */ 275 processRemovedChild_: function(child) { 276 for (var i = 0; i < this.tabs_.length; i++) { 277 // Make sure ids are the same as the tab position after removal. 278 this.tabs_[i].id = i; 279 if (this.tabs_[i].content === child) { 280 this.tabs_[i].observers.forAttributeSelected.disconnect(); 281 this.tabs_[i].observers.forAttributeTabLabel.disconnect(); 282 // The user has removed the currently selected tab. 283 if (this.tabs_[i] === this.selectedTab_) { 284 this.clearSelectedTab_(); 285 this.fire('selected-tab-change'); 286 } 287 child.removeAttribute('selected'); 288 delete child.selected; 289 // Remove the observer since we no longer care about this child. 290 this.tabs_.splice(i, 1); 291 i--; 292 } 293 } 294 }, 295 296 297 /** 298 * This function handles child attribute changes. The only relevant 299 * attributes for the tab-view are 'tab-label' and 'selected'. 300 */ 301 childAttributesChanged_: function(mutations, observer) { 302 var tabObject = undefined; 303 // First figure out which child has been changed. 304 for (var i = 0; i < this.tabs_.length; i++) { 305 var observers = this.tabs_[i].observers; 306 if (observers.forAttributeSelected === observer || 307 observers.forAttributeTabLabel === observer) { 308 tabObject = this.tabs_[i]; 309 break; 310 } 311 } 312 313 // This should not happen, unless the user has messed with our internal 314 // data structure. 315 if (!tabObject) 316 return; 317 318 // Next handle the attribute changes. 319 for (var i = 0; i < mutations.length; i++) { 320 var node = tabObject.content; 321 // 'tab-label' attribute has been changed. 322 if (mutations[i].attributeName === 'tab-label') 323 tabObject.label = node.getAttribute('tab-label'); 324 // 'selected' attribute has been changed. 325 if (mutations[i].attributeName === 'selected') { 326 // The attribute has been set. 327 var nodeIsSelected = node.hasAttribute('selected'); 328 if (nodeIsSelected) 329 this.changeSelectedTabById_(tabObject.id); 330 else 331 this.changeSelectedTabById_(undefined); 332 } 333 } 334 }, 335 336 /** 337 * This function handles light-dom additions and removals from the 338 * tab-view component. 339 */ 340 childrenUpdated_: function(mutations, observer) { 341 mutations.forEach(function(mutation) { 342 for (var i = 0; i < mutation.removedNodes.length; i++) 343 this.processRemovedChild_(mutation.removedNodes[i]); 344 for (var i = 0; i < mutation.addedNodes.length; i++) 345 this.processAddedChild_(mutation.addedNodes[i]); 346 }, this); 347 }, 348 349 /** 350 * Handler called when a click event happens on any of the tab buttons. 351 */ 352 tabButtonSelectHandler_: function(event, detail, sender) { 353 this.changeSelectedTabById_(sender.getAttribute('button-id')); 354 }, 355 356 /** 357 * This does the actual work. :) 358 */ 359 changeSelectedTabById_: function(id) { 360 var newTab = id !== undefined ? this.tabs_[id] : undefined; 361 var changed = this.selectedTab_ !== newTab; 362 this.saveCurrentTabScrollPosition_(); 363 this.clearSelectedTab_(); 364 if (id !== undefined) { 365 this.setSelectedTabById_(id); 366 this.restoreCurrentTabScrollPosition_(); 367 } 368 369 if (changed) 370 this.fire('selected-tab-change'); 371 }, 372 373 /** 374 * This function updates the currently selected tab based on its internal 375 * id. The corresponding light-dom element receives the selected attribute. 376 */ 377 setSelectedTabById_: function(id) { 378 this.selectedTab_ = this.tabs_[id]; 379 // Disconnect observer while we mutate the child. 380 this.selectedTab_.observers.forAttributeSelected.disconnect(); 381 this.selectedTab_.content.setAttribute('selected', 'selected'); 382 // Reconnect the observer to watch for changes in the future. 383 this.selectedTab_.observers.forAttributeSelected.observe( 384 this.selectedTab_.content, { attributeFilter: ['selected'] }); 385 386 }, 387 388 saveTabStates: function() { 389 // Scroll positions of unselected tabs have already been saved. 390 this.saveCurrentTabScrollPosition_(); 391 }, 392 393 saveCurrentTabScrollPosition_: function() { 394 if (this.selectedTab_) { 395 this.selectedTab_.content._savedScrollTop = 396 this.$['content-container'].scrollTop; 397 this.selectedTab_.content._savedScrollLeft = 398 this.$['content-container'].scrollLeft; 399 } 400 }, 401 402 restoreCurrentTabScrollPosition_: function() { 403 if (this.selectedTab_) { 404 this.$['content-container'].scrollTop = 405 this.selectedTab_.content._savedScrollTop || 0; 406 this.$['content-container'].scrollLeft = 407 this.selectedTab_.content._savedScrollLeft || 0; 408 } 409 }, 410 411 /** 412 * This function clears the currently selected tab. This handles removal 413 * of the selected attribute from the light-dom element. 414 */ 415 clearSelectedTab_: function() { 416 if (this.selectedTab_) { 417 // Disconnect observer while we mutate the child. 418 this.selectedTab_.observers.forAttributeSelected.disconnect(); 419 this.selectedTab_.content.removeAttribute('selected'); 420 // Reconnect the observer to watch for changes in the future. 421 this.selectedTab_.observers.forAttributeSelected.observe( 422 this.selectedTab_.content, { attributeFilter: ['selected'] }); 423 this.selectedTab_ = undefined; 424 } 425 } 426 }); 427 </script> 428</polymer-element> 429