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