OLD | NEW |
---|---|
1 <!DOCTYPE html> | 1 <!DOCTYPE html> |
2 <!-- | 2 <!-- |
3 Copyright (c) 2014 The Chromium Authors. All rights reserved. | 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 | 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 controls which | |
10 analysis sub-view is being displayed. | |
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 Polymer.dom(this.$.subView).removeChild(this.selectedSubView_); |
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 Polymer.dom(this.$.subView).appendChild(subView); |
237 } | 115 |
aiolos (Not reviewing)
2016/06/01 17:40:46
You *might* need to flush the dom here, depending
charliea (OOO until 10-5)
2016/06/01 18:08:47
Acknowledged. I think my preference here is to onl
aiolos (Not reviewing)
2016/06/01 18:19:57
sgtm.
| |
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 onTabChanged_: function(event) { |
288 * Function called on light-dom child removal. | 139 this.selectedSubView = event.model.item; |
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 isChecked_: function(subView) { |
313 /** | 143 return this.selectedSubView_ === subView; |
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 }, | |
351 | |
352 /** | |
353 * This function handles light-dom additions and removals from the | |
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 } | 144 } |
454 | |
455 }); | 145 }); |
456 </script> | 146 </script> |
OLD | NEW |