Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(195)

Side by Side Diff: tracing/tracing/ui/base/tab_view.html

Issue 2023283002: [polymer] Rewrite the analysis tab view (Closed) Base URL: git@github.com:zeptonaut/catapult.git@polymer10_rewrite_tab_view2
Patch Set: Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698