Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 <!DOCTYPE html> | 1 <!DOCTYPE html> |
| 2 <!-- | 2 <!-- |
| 3 Copyright (c) 2014 The Chromium Authors. All rights reserved. | 3 Copyright 2016 The Chromium Authors. All rights reserved. |
|
petrcermak
2016/06/01 16:47:09
I don't think you should change this.
charliea (OOO until 10-5)
2016/06/01 17:33:03
Ah, sorry. I originally wrote this as tab_view2 wi
| |
| 4 Use of this source code is governed by a BSD-style license that can be | 4 Use of this source code is governed by a BSD-style license that can be |
| 5 found in the LICENSE file. | 5 found in the LICENSE file. |
| 6 --> | 6 --> |
| 7 | 7 |
| 8 <!-- | |
| 9 @fileoverview A series of tabs for the analysis view that control which analysis | |
|
petrcermak
2016/06/01 16:47:09
I think it should be "controlS" because it's "A se
charliea (OOO until 10-5)
2016/06/01 17:33:03
Doh. You're right.
| |
| 10 sub view is being display. | |
|
petrcermak
2016/06/01 16:47:09
s/display/displayed/
charliea (OOO until 10-5)
2016/06/01 17:33:03
:-( sloppy
| |
| 11 | |
| 12 We follow a fairly standard web convention of backing our tabs with hidden radio | |
| 13 buttons but visible radio button labels (the tabs themselves) which toggle the | |
| 14 input element when clicked. Using hidden radio buttons makes sense, as both tabs | |
| 15 and radio buttons are input elements that allow user selection through clicking | |
| 16 and limit users to having one option selected at a time. | |
| 17 --> | |
| 8 <dom-module id='tr-ui-a-tab-view'> | 18 <dom-module id='tr-ui-a-tab-view'> |
| 9 <template> | 19 <template> |
| 10 <style> | 20 <style> |
| 11 :host { | 21 #selection_description, #tabs { |
| 12 display: flex; | 22 font-size: 12px; |
| 13 flex-flow: column nowrap; | |
| 14 overflow: hidden; | |
| 15 box-sizing: border-box; | |
| 16 } | 23 } |
| 17 | 24 |
| 18 tab-strip.hidden { | 25 #selection_description { |
| 26 display: inline-block; | |
| 27 font-weight: bold; | |
| 28 margin: 9px 0px 4px 20px; | |
| 29 } | |
| 30 | |
| 31 #tabs { | |
| 32 display: block; | |
| 33 border-top: 1px solid #8e8e8e; | |
| 34 border-bottom: 1px solid #8e8e8e; | |
| 35 background-color: #ececec; | |
| 36 overflow: hidden; | |
| 37 margin: 0; | |
| 38 } | |
| 39 | |
| 40 #tabs input[type=radio] { | |
| 19 display: none; | 41 display: none; |
| 20 } | 42 } |
| 21 | 43 |
| 22 tab-strip { | 44 #tabs tab label { |
| 23 background-color: rgb(236, 236, 236); | 45 cursor: pointer; |
| 24 border-bottom: 1px solid #8e8e8e; | 46 display: inline-block; |
| 25 display: flex; | 47 border: 1px solid #ececec; |
| 26 flex: 0 0 auto; | 48 margin: 5px 0px 0px 15px; |
| 27 flex-flow: row; | 49 padding: 3px 10px 3px 10px; |
| 28 overflow-x: auto; | |
| 29 padding: 0 10px 0 10px; | |
| 30 font-size: 12px; | |
| 31 } | 50 } |
| 32 | 51 |
| 33 tab-button { | 52 #tabs input[type=radio]:checked ~ label { |
| 34 display: block; | |
| 35 flex: 0 0 auto; | |
| 36 padding: 4px 15px 1px 15px; | |
| 37 margin-top: 2px; | |
| 38 } | |
| 39 | |
| 40 tab-button[selected=true] { | |
| 41 background-color: white; | 53 background-color: white; |
| 42 border: 1px solid rgb(163, 163, 163); | 54 border: 1px solid #8e8e8e; |
| 43 border-bottom: none; | 55 border-bottom: 1px solid white; |
| 44 padding: 3px 14px 1px 14px; | |
| 45 } | |
| 46 | |
| 47 tabs-content-container { | |
| 48 display: flex; | |
| 49 flex: 1 1 auto; | |
| 50 overflow: auto; | |
| 51 width: 100%; | |
| 52 } | |
| 53 | |
| 54 tabs-content-container ::content > * { | |
| 55 flex: 1 1 auto; | |
| 56 } | |
| 57 | |
| 58 tabs-content-container ::content > *:not([selected]) { | |
| 59 display: none; | |
| 60 } | |
| 61 | |
| 62 button-label { | |
| 63 display: inline; | |
| 64 } | |
| 65 | |
| 66 tab-strip-heading { | |
| 67 display: block; | |
| 68 flex: 0 0 auto; | |
| 69 padding: 4px 15px 1px 15px; | |
| 70 margin-top: 2px; | |
| 71 margin-before: 20px; | |
| 72 margin-after: 10px; | |
| 73 } | |
| 74 #tsh { | |
| 75 display: inline; | |
| 76 font-weight: bold; | |
| 77 } | 56 } |
| 78 </style> | 57 </style> |
| 79 | 58 <div id='tabs'> |
| 80 <tab-strip> | 59 <label id=selection_description>[[label_]]</label> |
| 81 <tab-strip-heading id="tshh"> | 60 <template is=dom-repeat items=[[subViews_]]> |
| 82 <span id="tsh"></span> | 61 <tab> |
| 83 </tab-strip-heading> | 62 <input type=radio name=tabs id$=[[item.tagName]] |
| 84 <template is="dom-repeat" items="{{tabs_}}"> | 63 on-change='onTabChanged_' |
| 85 <tab-button | 64 checked$='[[isChecked_(item)]]' /> |
| 86 id="{{item.id}}" | 65 <label for$=[[item.tagName]]>[[item.tabLabel]]</label> |
| 87 on-click="tabButtonSelectHandler_" | 66 </tab> |
| 88 selected="{{computeTabSelectState_(selectedTab_, item)}}"> | |
| 89 <button-label>{{computeTabLabel_(item)}}</button-label> | |
| 90 </tab-button> | |
| 91 </template> | 67 </template> |
| 92 </tab-strip> | 68 </div> |
| 93 | 69 <div id='subView'></div> |
| 94 <tabs-content-container id='content-container'> | 70 <content> |
| 95 <content></content> | 71 </content> |
| 96 </tabs-content-container> | |
| 97 | |
| 98 </template> | 72 </template> |
| 99 </dom-module> | 73 </dom-module> |
| 100 <script> | 74 <script> |
| 101 'use strict'; | 75 'use strict'; |
| 102 window.TracingAnalysisTabView = Polymer({ | 76 |
| 77 Polymer({ | |
| 103 is: 'tr-ui-a-tab-view', | 78 is: 'tr-ui-a-tab-view', |
| 104 | 79 |
| 105 properties: { | 80 properties: { |
| 106 tabs_: { | 81 label_: { |
| 82 type: String, | |
| 83 value: () => '' | |
| 84 }, | |
| 85 subViews_: { | |
| 107 type: Array, | 86 type: Array, |
| 108 value: () => [] | 87 value: () => [] |
| 109 }, | 88 }, |
| 110 selectedTab_: { | 89 selectedSubView_: Object |
| 111 type: Object, | |
| 112 value: () => {} | |
| 113 } | |
| 114 }, | 90 }, |
| 115 | 91 |
| 116 ready: function() { | 92 set label(newLabel) { |
| 117 this.$.tshh.style.display = 'none'; | 93 this.set('label_', newLabel); |
| 118 | |
| 119 // A tab is represented by the following tuple: | |
| 120 // (id, label, content, observer, savedScrollTop, savedScrollLeft). | |
| 121 // The properties are used in the following way: | |
| 122 // id: Uniquely identifies a tab. It is the same number as the index | |
| 123 // in the tabs array. Used primarily by the on-click event attached | |
| 124 // to buttons. | |
| 125 // label: A string, representing the label printed on the tab button. | |
| 126 // content: The light-dom child representing the contents of the tab. | |
| 127 // The content is appended to this tab-view by the user. | |
| 128 // observers: The observers attached to the content node to watch for | |
| 129 // attribute changes. The attributes of interest are: 'selected', | |
| 130 // and 'tab-label'. | |
| 131 // savedScrollTop/Left: Used to return the scroll position upon switching | |
| 132 // tabs. The values are generally saved when a tab switch occurs. | |
| 133 // | |
| 134 // The order of the tabs is relevant for the tab ordering. | |
| 135 | |
| 136 // Register any already existing children. | |
| 137 for (var i = 0; i < Polymer.dom(this).children.length; i++) | |
| 138 this.processAddedChild_(Polymer.dom(this).children[i]); | |
| 139 | |
| 140 // In case the user decides to add more tabs, make sure we watch for | |
| 141 // any child mutations. | |
| 142 this.childrenObserver_ = new MutationObserver( | |
| 143 this.childrenUpdated_.bind(this)); | |
| 144 this.childrenObserver_.observe( | |
| 145 this.$['content-container'], { childList: 'true' }); | |
| 146 }, | |
| 147 | |
| 148 get tabStripHeadingText() { | |
| 149 return Polymer.dom(this.$.tsh).textContent; | |
| 150 }, | |
| 151 | |
| 152 set tabStripHeadingText(tabStripHeadingText) { | |
| 153 Polymer.dom(this.$.tsh).textContent = tabStripHeadingText; | |
| 154 if (!!tabStripHeadingText) | |
| 155 this.$.tshh.style.display = ''; | |
| 156 else | |
| 157 this.$.tshh.style.display = 'none'; | |
| 158 }, | |
| 159 | |
| 160 get selectedTab() { | |
| 161 // Make sure we process any pending children additions / removals, before | |
| 162 // trying to select a tab. Otherwise, we might not find some children. | |
| 163 this.childrenUpdated_( | |
| 164 this.childrenObserver_.takeRecords(), this.childrenObserver_); | |
| 165 | |
| 166 // Do not give access to the user to the inner data structure. | |
| 167 // A user should only be able to mutate the added tab content. | |
| 168 var selectedTab = this.get('selectedTab_'); | |
| 169 if (selectedTab) | |
| 170 return selectedTab.content; | |
| 171 | |
| 172 return undefined; | |
| 173 }, | |
| 174 | |
| 175 set selectedTab(content) { | |
| 176 // Make sure we process any pending children additions / removals, before | |
| 177 // trying to select a tab. Otherwise, we might not find some children. | |
| 178 this.childrenUpdated_( | |
| 179 this.childrenObserver_.takeRecords(), this.childrenObserver_); | |
| 180 | |
| 181 if (content === undefined || content === null) { | |
| 182 this.changeSelectedTabById_(undefined); | |
| 183 return; | |
| 184 } | |
| 185 | |
| 186 // Search for the specific node in our tabs list. | |
| 187 // If it is not there print a warning. | |
| 188 var contentTabId = undefined; | |
| 189 for (var i = 0; i < this.tabs_.length; i++) { | |
| 190 if (this.get('tabs_.' + i).content === content) { | |
| 191 contentTabId = this.get('tabs_.' + i).id; | |
| 192 break; | |
| 193 } | |
| 194 } | |
| 195 | |
| 196 if (contentTabId === undefined) | |
| 197 return; | |
| 198 | |
| 199 this.changeSelectedTabById_(contentTabId); | |
| 200 }, | |
| 201 | |
| 202 get tabsHidden() { | |
| 203 var ts = Polymer.dom(this.root).querySelector('tab-strip'); | |
| 204 return ts.classList.contains('hidden'); | |
| 205 }, | |
| 206 | |
| 207 set tabsHidden(tabsHidden) { | |
| 208 tabsHidden = !!tabsHidden; | |
| 209 var ts = Polymer.dom(this.root).querySelector('tab-strip'); | |
| 210 if (tabsHidden) | |
| 211 ts.classList.add('hidden'); | |
| 212 else | |
| 213 ts.classList.remove('hidden'); | |
| 214 }, | 94 }, |
| 215 | 95 |
| 216 get tabs() { | 96 get tabs() { |
| 217 return this.tabs_.map(function(tabObject) { | 97 return this.get('subViews_'); |
| 218 return tabObject.content; | |
| 219 }); | |
| 220 }, | 98 }, |
| 221 | 99 |
| 222 /** | 100 get selectedSubView() { |
| 223 * Function called on light-dom child addition. | 101 return this.selectedSubView_; |
| 224 */ | 102 }, |
| 225 processAddedChild_: function(child) { | 103 |
| 226 var observerAttributeSelected = new MutationObserver( | 104 set selectedSubView(subView) { |
| 227 this.childAttributesChanged_.bind(this)); | 105 if (subView === this.selectedSubView_) |
| 228 var observerAttributeTabLabel = new MutationObserver( | 106 return; |
| 229 this.childAttributesChanged_.bind(this)); | 107 |
| 230 var tabObject = { | 108 if (this.selectedSubView_) |
| 231 id: this.tabs_.length, | 109 this.$.subView.removeChild(this.selectedSubView_); |
|
aiolos (Not reviewing)
2016/06/01 16:47:37
Is there a reason you aren't using Polymer.dom in
charliea (OOO until 10-5)
2016/06/01 17:33:03
Ack, no. For some reason, I was thinking it wasn't
| |
| 232 content: child, | 110 |
| 233 label: child.getAttribute('tab-label'), | 111 this.set('selectedSubView_', subView); |
| 234 observers: { | 112 |
| 235 forAttributeSelected: observerAttributeSelected, | 113 if (subView) |
| 236 forAttributeTabLabel: observerAttributeTabLabel | 114 this.$.subView.appendChild(subView); |
| 237 } | 115 |
| 238 }; | 116 this.fire('selected-tab-change'); |
| 239 // this.tabs_.push(tabObject); | 117 }, |
| 240 this.push('tabs_', tabObject); | 118 |
| 241 if (child.hasAttribute('selected')) { | 119 clearSubViews: function() { |
| 242 // When receiving a child with the selected attribute, if we have no | 120 this.splice('subViews_', 0, this.subViews_.length); |
| 243 // selected tab, mark the child as the selected tab, otherwise keep | 121 this.selectedSubView = undefined; |
| 244 // the previous selection. | 122 }, |
| 245 if (this.get('selectedTab_')) | 123 |
| 246 Polymer.dom(child).removeAttribute('selected'); | 124 addSubView: function(subView) { |
| 247 else | 125 if (!(subView instanceof HTMLElement) || |
| 248 this.setSelectedTabById_(tabObject.id); | 126 !subView.behaviors || |
| 127 subView.behaviors.indexOf(Catapult.tr_ui_a_sub_view_behavior) < 0) { | |
| 128 throw new Error('Sub view being added must be a registered Polymer ' + | |
| 129 'element with the sub view behavior'); | |
| 249 } | 130 } |
| 250 | 131 |
| 251 // This is required because the user might have set the selected | 132 if (!this.selectedSubView_) |
| 252 // property before we got to process the child. | 133 this.selectedSubView = subView; |
| 253 var previousSelected = child.selected; | |
| 254 | 134 |
| 255 var tabView = this; | 135 this.push('subViews_', subView); |
| 256 | |
| 257 Object.defineProperty( | |
| 258 child, | |
| 259 'selected', { | |
| 260 configurable: true, | |
| 261 set: function(value) { | |
| 262 if (value) { | |
| 263 tabView.changeSelectedTabById_(tabObject.id); | |
| 264 return; | |
| 265 } | |
| 266 | |
| 267 var wasSelected = (tabView.get('selectedTab_') === tabObject); | |
| 268 if (wasSelected) | |
| 269 tabView.changeSelectedTabById_(undefined); | |
| 270 }, | |
| 271 get: function() { | |
| 272 return this.hasAttribute('selected'); | |
| 273 } | |
| 274 }); | |
| 275 | |
| 276 if (previousSelected) | |
| 277 child.selected = previousSelected; | |
| 278 | |
| 279 observerAttributeSelected.observe(child, { | |
| 280 attributeFilter: ['selected'] | |
| 281 }); | |
| 282 observerAttributeTabLabel.observe(child, { | |
| 283 attributeFilter: ['tab-label'] | |
| 284 }); | |
| 285 }, | 136 }, |
| 286 | 137 |
| 287 /** | 138 isSelected_: function(subView, selectedSubView) { |
|
petrcermak
2016/06/01 16:47:09
Do you need this method?
charliea (OOO until 10-5)
2016/06/01 17:33:03
Doh. No. It's a duplicate of isChecked_
| |
| 288 * Function called on light-dom child removal. | 139 return subView === selectedSubView; |
| 289 */ | |
| 290 processRemovedChild_: function(child) { | |
| 291 for (var i = 0; i < this.get('tabs_').length; i++) { | |
| 292 var tab = this.get('tabs_.' + i); | |
| 293 // Make sure ids are the same as the tab position after removal. | |
| 294 tab.id = i; | |
| 295 if (tab.content === child) { | |
| 296 tab.observers.forAttributeSelected.disconnect(); | |
| 297 tab.observers.forAttributeTabLabel.disconnect(); | |
| 298 // The user has removed the currently selected tab. | |
| 299 if (tab === this.get('selectedTab_')) { | |
| 300 this.clearSelectedTab_(); | |
| 301 this.fire('selected-tab-change'); | |
| 302 } | |
| 303 Polymer.dom(child).removeAttribute('selected'); | |
| 304 delete child.selected; | |
| 305 // Remove the observer since we no longer care about this child. | |
| 306 this.splice('tabs_', i, 1); | |
| 307 i--; | |
| 308 } | |
| 309 } | |
| 310 }, | 140 }, |
| 311 | 141 |
| 312 | 142 onTabChanged_: function(event) { |
| 313 /** | 143 this.selectedSubView = event.model.item; |
| 314 * This function handles child attribute changes. The only relevant | |
| 315 * attributes for the tab-view are 'tab-label' and 'selected'. | |
| 316 */ | |
| 317 childAttributesChanged_: function(mutations, observer) { | |
| 318 var tabObject = undefined; | |
| 319 // First figure out which child has been changed. | |
| 320 for (var i = 0; i < this.tabs_.length; i++) { | |
| 321 var observers = this.get('tabs_.' + i).observers; | |
| 322 if (observers.forAttributeSelected === observer || | |
| 323 observers.forAttributeTabLabel === observer) { | |
| 324 tabObject = this.get('tabs_.' + i); | |
| 325 break; | |
| 326 } | |
| 327 } | |
| 328 | |
| 329 // This should not happen, unless the user has messed with our internal | |
| 330 // data structure. | |
| 331 if (!tabObject) | |
| 332 return; | |
| 333 | |
| 334 // Next handle the attribute changes. | |
| 335 for (var i = 0; i < mutations.length; i++) { | |
| 336 var node = tabObject.content; | |
| 337 // 'tab-label' attribute has been changed. | |
| 338 if (mutations[i].attributeName === 'tab-label') | |
| 339 tabObject.label = node.getAttribute('tab-label'); | |
| 340 // 'selected' attribute has been changed. | |
| 341 if (mutations[i].attributeName === 'selected') { | |
| 342 // The attribute has been set. | |
| 343 var nodeIsSelected = node.hasAttribute('selected'); | |
| 344 if (nodeIsSelected) | |
| 345 this.changeSelectedTabById_(tabObject.id); | |
| 346 else | |
| 347 this.changeSelectedTabById_(undefined); | |
| 348 } | |
| 349 } | |
| 350 }, | 144 }, |
| 351 | 145 |
| 352 /** | 146 isChecked_: function(subView) { |
| 353 * This function handles light-dom additions and removals from the | 147 return this.selectedSubView_ === subView; |
| 354 * tab-view component. | |
| 355 */ | |
| 356 childrenUpdated_: function(mutations, observer) { | |
| 357 mutations.forEach(function(mutation) { | |
| 358 for (var i = 0; i < mutation.removedNodes.length; i++) | |
| 359 this.processRemovedChild_(mutation.removedNodes[i]); | |
| 360 for (var i = 0; i < mutation.addedNodes.length; i++) | |
| 361 this.processAddedChild_(mutation.addedNodes[i]); | |
| 362 }, this); | |
| 363 }, | |
| 364 | |
| 365 /** | |
| 366 * Handler called when a click event happens on any of the tab buttons. | |
| 367 */ | |
| 368 tabButtonSelectHandler_: function(event) { | |
| 369 this.changeSelectedTabById_(event.currentTarget.id); | |
| 370 }, | |
| 371 | |
| 372 /** | |
| 373 * This does the actual work. :) | |
| 374 */ | |
| 375 changeSelectedTabById_: function(id) { | |
| 376 var newTab = id !== undefined ? this.get('tabs_.' + id) : undefined; | |
| 377 var changed = this.get('selectedTab_') !== newTab; | |
| 378 this.saveCurrentTabScrollPosition_(); | |
| 379 this.clearSelectedTab_(); | |
| 380 if (id !== undefined) { | |
| 381 this.setSelectedTabById_(id); | |
| 382 this.restoreCurrentTabScrollPosition_(); | |
| 383 } | |
| 384 | |
| 385 if (changed) | |
| 386 this.fire('selected-tab-change'); | |
| 387 }, | |
| 388 | |
| 389 /** | |
| 390 * This function updates the currently selected tab based on its internal | |
| 391 * id. The corresponding light-dom element receives the selected attribute. | |
| 392 */ | |
| 393 setSelectedTabById_: function(id) { | |
| 394 this.set('selectedTab_', this.get('tabs_.' + id)); | |
| 395 // Disconnect observer while we mutate the child. | |
| 396 this.get('selectedTab_').observers.forAttributeSelected.disconnect(); | |
| 397 Polymer.dom(this.get('selectedTab_').content) | |
| 398 .setAttribute('selected', 'selected'); | |
| 399 // Reconnect the observer to watch for changes in the future. | |
| 400 this.get('selectedTab_').observers.forAttributeSelected.observe( | |
| 401 this.get('selectedTab_').content, { attributeFilter: ['selected'] }); | |
| 402 | |
| 403 }, | |
| 404 | |
| 405 saveTabStates: function() { | |
| 406 // Scroll positions of unselected tabs have already been saved. | |
| 407 this.saveCurrentTabScrollPosition_(); | |
| 408 }, | |
| 409 | |
| 410 saveCurrentTabScrollPosition_: function() { | |
| 411 var selectedTab = this.get('selectedTab_'); | |
| 412 if (selectedTab) { | |
| 413 selectedTab.content._savedScrollTop = | |
| 414 this.$['content-container'].scrollTop; | |
| 415 selectedTab.content._savedScrollLeft = | |
| 416 this.$['content-container'].scrollLeft; | |
| 417 } | |
| 418 }, | |
| 419 | |
| 420 restoreCurrentTabScrollPosition_: function() { | |
| 421 var selectedTab = this.get('selectedTab_'); | |
| 422 if (selectedTab) { | |
| 423 this.$['content-container'].scrollTop = | |
| 424 selectedTab.content._savedScrollTop || 0; | |
| 425 this.$['content-container'].scrollLeft = | |
| 426 selectedTab.content._savedScrollLeft || 0; | |
| 427 } | |
| 428 }, | |
| 429 | |
| 430 /** | |
| 431 * This function clears the currently selected tab. This handles removal | |
| 432 * of the selected attribute from the light-dom element. | |
| 433 */ | |
| 434 clearSelectedTab_: function() { | |
| 435 var selectedTab = this.get('selectedTab_'); | |
| 436 if (selectedTab) { | |
| 437 // Disconnect observer while we mutate the child. | |
| 438 selectedTab.observers.forAttributeSelected.disconnect(); | |
| 439 Polymer.dom(selectedTab.content).removeAttribute('selected'); | |
| 440 // Reconnect the observer to watch for changes in the future. | |
| 441 selectedTab.observers.forAttributeSelected.observe( | |
| 442 selectedTab.content, { attributeFilter: ['selected'] }); | |
| 443 this.set('selectedTab_', undefined); | |
| 444 } | |
| 445 }, | |
| 446 | |
| 447 computeTabLabel_: function(tab) { | |
| 448 return tab.label ? tab.label : 'No Label'; | |
| 449 }, | |
| 450 | |
| 451 computeTabSelectState_: function(selectedTab, tab) { | |
| 452 return selectedTab.id === tab.id; | |
| 453 } | 148 } |
| 454 | |
| 455 }); | 149 }); |
| 456 </script> | 150 </script> |
| OLD | NEW |