blob: 749566a8847c61bb3a65c44a8852fb415daaedaf [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright (c) 2014 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<polymer-element name="tracing-analysis-tab-view"
constructor="TracingAnalysisTabView">
<template>
<style>
:host {
display: flex;
flex-flow: column nowrap;
overflow: hidden;
box-sizing: border-box;
}
tab-strip[tabs-hidden] {
display: none;
}
tab-strip {
background-color: rgb(236, 236, 236);
border-bottom: 1px solid #8e8e8e;
display: flex;
flex: 0 0 auto;
flex-flow: row;
overflow-x: auto;
padding: 0 10px 0 10px;
font-size: 12px;
}
tab-button {
display: block;
flex: 0 0 auto;
padding: 4px 15px 1px 15px;
margin-top: 2px;
}
tab-button[selected=true] {
background-color: white;
border: 1px solid rgb(163, 163, 163);
border-bottom: none;
padding: 3px 14px 1px 14px;
}
tabs-content-container {
display: flex;
flex: 1 1 auto;
overflow: auto;
width: 100%;
}
::content > * {
flex: 1 1 auto;
}
::content > *:not([selected]) {
display: none;
}
button-label {
display: inline;
}
tab-strip-heading {
display: block;
flex: 0 0 auto;
padding: 4px 15px 1px 15px;
margin-top: 2px;
margin-before: 20px;
margin-after: 10px;
}
#tsh {
display: inline;
font-weight: bold;
}
</style>
<tab-strip>
<tab-strip-heading id="tshh">
<span id="tsh"></span>
</tab-strip-heading>
<template repeat="{{tab in tabs_}}">
<tab-button
button-id="{{ tab.id }}"
on-click="{{ tabButtonSelectHandler_ }}"
selected="{{ selectedTab_.id === tab.id }}">
<button-label>{{ tab.label ? tab.label : 'No Label'}}</button-label>
</tab-button>
</template>
</tab-strip>
<tabs-content-container id='content-container'>
<content></content>
</tabs-content-container>
</template>
<script>
'use strict';
Polymer({
ready: function() {
this.$.tshh.style.display = 'none';
// A tab is represented by the following tuple:
// (id, label, content, observer, savedScrollTop, savedScrollLeft).
// The properties are used in the following way:
// id: Uniquely identifies a tab. It is the same number as the index
// in the tabs array. Used primarily by the on-click event attached
// to buttons.
// label: A string, representing the label printed on the tab button.
// content: The light-dom child representing the contents of the tab.
// The content is appended to this tab-view by the user.
// observers: The observers attached to the content node to watch for
// attribute changes. The attributes of interest are: 'selected',
// and 'tab-label'.
// savedScrollTop/Left: Used to return the scroll position upon switching
// tabs. The values are generally saved when a tab switch occurs.
//
// The order of the tabs is relevant for the tab ordering.
this.tabs_ = [];
this.selectedTab_ = undefined;
// Register any already existing children.
for (var i = 0; i < this.children.length; i++)
this.processAddedChild_(this.children[i]);
// In case the user decides to add more tabs, make sure we watch for
// any child mutations.
this.childrenObserver_ = new MutationObserver(
this.childrenUpdated_.bind(this));
this.childrenObserver_.observe(this, { childList: 'true' });
},
get tabStripHeadingText() {
return this.$.tsh.textContent;
},
set tabStripHeadingText(tabStripHeadingText) {
this.$.tsh.textContent = tabStripHeadingText;
if (!!tabStripHeadingText)
this.$.tshh.style.display = '';
else
this.$.tshh.style.display = 'none';
},
get selectedTab() {
// Make sure we process any pending children additions / removals, before
// trying to select a tab. Otherwise, we might not find some children.
this.childrenUpdated_(
this.childrenObserver_.takeRecords(), this.childrenObserver_);
// Do not give access to the user to the inner data structure.
// A user should only be able to mutate the added tab content.
if (this.selectedTab_)
return this.selectedTab_.content;
return undefined;
},
set selectedTab(content) {
// Make sure we process any pending children additions / removals, before
// trying to select a tab. Otherwise, we might not find some children.
this.childrenUpdated_(
this.childrenObserver_.takeRecords(), this.childrenObserver_);
if (content === undefined || content === null) {
this.changeSelectedTabById_(undefined);
return;
}
// Search for the specific node in our tabs list.
// If it is not there print a warning.
var contentTabId = undefined;
for (var i = 0; i < this.tabs_.length; i++)
if (this.tabs_[i].content === content) {
contentTabId = this.tabs_[i].id;
break;
}
if (contentTabId === undefined) {
console.warn('Tab not in tabs list. Ignoring changed selection.');
return;
}
this.changeSelectedTabById_(contentTabId);
},
get tabsHidden() {
var ts = this.shadowRoot.querySelector('tab-strip');
return ts.hasAttribute('tabs-hidden');
},
set tabsHidden(tabsHidden) {
tabsHidden = !!tabsHidden;
var ts = this.shadowRoot.querySelector('tab-strip');
if (tabsHidden)
ts.setAttribute('tabs-hidden', true);
else
ts.removeAttribute('tabs-hidden');
},
/**
* Function called on light-dom child addition.
*/
processAddedChild_: function(child) {
var observerAttributeSelected = new MutationObserver(
this.childAttributesChanged_.bind(this));
var observerAttributeTabLabel = new MutationObserver(
this.childAttributesChanged_.bind(this));
var tabObject = {
id: this.tabs_.length,
content: child,
label: child.getAttribute('tab-label'),
observers: {
forAttributeSelected: observerAttributeSelected,
forAttributeTabLabel: observerAttributeTabLabel
},
savedScrollTop: 0,
savedScrollLeft: 0
};
this.tabs_.push(tabObject);
if (child.hasAttribute('selected')) {
// When receiving a child with the selected attribute, if we have no
// selected tab, mark the child as the selected tab, otherwise keep
// the previous selection.
if (this.selectedTab_)
child.removeAttribute('selected');
else
this.setSelectedTabById_(tabObject.id);
}
// This is required because the user might have set the selected
// property before we got to process the child.
var previousSelected = child.selected;
var tabView = this;
Object.defineProperty(
child,
'selected', {
configurable: true,
set: function(value) {
if (value) {
tabView.changeSelectedTabById_(tabObject.id);
return;
}
var wasSelected = tabView.selectedTab_ === tabObject;
if (wasSelected)
tabView.changeSelectedTabById_(undefined);
},
get: function() {
return this.hasAttribute('selected');
}
});
if (previousSelected)
child.selected = previousSelected;
observerAttributeSelected.observe(child,
{ attributeFilter: ['selected'] });
observerAttributeTabLabel.observe(child,
{ attributeFilter: ['tab-label'] });
},
/**
* Function called on light-dom child removal.
*/
processRemovedChild_: function(child) {
for (var i = 0; i < this.tabs_.length; i++) {
// Make sure ids are the same as the tab position after removal.
this.tabs_[i].id = i;
if (this.tabs_[i].content === child) {
this.tabs_[i].observers.forAttributeSelected.disconnect();
this.tabs_[i].observers.forAttributeTabLabel.disconnect();
// The user has removed the currently selected tab.
if (this.tabs_[i] === this.selectedTab_) {
this.clearSelectedTab_();
this.fire('selected-tab-change');
}
child.removeAttribute('selected');
delete child.selected;
// Remove the observer since we no longer care about this child.
this.tabs_.splice(i, 1);
i--;
}
}
},
/**
* This function handles child attribute changes. The only relevant
* attributes for the tab-view are 'tab-label' and 'selected'.
*/
childAttributesChanged_: function(mutations, observer) {
var tabObject = undefined;
// First figure out which child has been changed.
for (var i = 0; i < this.tabs_.length; i++) {
var observers = this.tabs_[i].observers;
if (observers.forAttributeSelected === observer ||
observers.forAttributeTabLabel === observer) {
tabObject = this.tabs_[i];
break;
}
}
// This should not happen, unless the user has messed with our internal
// data structure.
if (!tabObject)
return;
// Next handle the attribute changes.
for (var i = 0; i < mutations.length; i++) {
var node = tabObject.content;
// 'tab-label' attribute has been changed.
if (mutations[i].attributeName === 'tab-label')
tabObject.label = node.getAttribute('tab-label');
// 'selected' attribute has been changed.
if (mutations[i].attributeName === 'selected') {
// The attribute has been set.
var nodeIsSelected = node.hasAttribute('selected');
if (nodeIsSelected)
this.changeSelectedTabById_(tabObject.id);
else
this.changeSelectedTabById_(undefined);
}
}
},
/**
* This function handles light-dom additions and removals from the
* tab-view component.
*/
childrenUpdated_: function(mutations, observer) {
mutations.forEach(function(mutation) {
for (var i = 0; i < mutation.removedNodes.length; i++)
this.processRemovedChild_(mutation.removedNodes[i]);
for (var i = 0; i < mutation.addedNodes.length; i++)
this.processAddedChild_(mutation.addedNodes[i]);
}, this);
},
/**
* Handler called when a click event happens on any of the tab buttons.
*/
tabButtonSelectHandler_: function(event, detail, sender) {
this.changeSelectedTabById_(sender.getAttribute('button-id'));
},
/**
* This does the actual work. :)
*/
changeSelectedTabById_: function(id) {
var newTab = id !== undefined ? this.tabs_[id] : undefined;
var changed = this.selectedTab_ !== newTab;
this.saveCurrentTabScrollPosition_();
this.clearSelectedTab_();
if (id !== undefined) {
this.setSelectedTabById_(id);
this.restoreCurrentTabScrollPosition_();
}
if (changed)
this.fire('selected-tab-change');
},
/**
* This function updates the currently selected tab based on its internal
* id. The corresponding light-dom element receives the selected attribute.
*/
setSelectedTabById_: function(id) {
this.selectedTab_ = this.tabs_[id];
// Disconnect observer while we mutate the child.
this.selectedTab_.observers.forAttributeSelected.disconnect();
this.selectedTab_.content.setAttribute('selected', 'selected');
// Reconnect the observer to watch for changes in the future.
this.selectedTab_.observers.forAttributeSelected.observe(
this.selectedTab_.content, { attributeFilter: ['selected'] });
},
saveCurrentTabScrollPosition_: function() {
if (this.selectedTab_) {
this.selectedTab_.savedScrollTop =
this.$['content-container'].scrollTop;
this.selectedTab_.savedScrollLeft =
this.$['content-container'].scrollLeft;
}
},
restoreCurrentTabScrollPosition_: function() {
if (this.selectedTab_) {
this.$['content-container'].scrollTop =
this.selectedTab_.savedScrollTop;
this.$['content-container'].scrollLeft =
this.selectedTab_.savedScrollLeft;
}
},
/**
* This function clears the currently selected tab. This handles removal
* of the selected attribute from the light-dom element.
*/
clearSelectedTab_: function() {
if (this.selectedTab_) {
// Disconnect observer while we mutate the child.
this.selectedTab_.observers.forAttributeSelected.disconnect();
this.selectedTab_.content.removeAttribute('selected');
// Reconnect the observer to watch for changes in the future.
this.selectedTab_.observers.forAttributeSelected.observe(
this.selectedTab_.content, { attributeFilter: ['selected'] });
this.selectedTab_ = undefined;
}
}
});
</script>
</polymer-element>