1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5
6/**
7 * @fileoverview This implements a tab control.
8 *
9 * An individual tab within a tab control is, unsurprisingly, a Tab.
10 * Tabs must be explicitly added/removed from the control.
11 *
12 * Tab titles are based on the label attribute of each child:
13 *
14 * <div>
15 * <div label='Tab 1'>Hello</div>
16 * <div label='Tab 2'>World</div>
17 * </div>
18 *
19 * Results in:
20 *
21 * ---------------
22 * | Tab1 | Tab2 |
23 * | ---------------------------------------
24 * | Hello World |
25 * -----------------------------------------
26 *
27 */
28cr.define('gpu', function() {
29  /**
30   * Creates a new tab element. A tab element is one of multiple tabs
31   * within a TabControl.
32   * @constructor
33   * @param {Object=} opt_propertyBag Optional properties.
34   * @extends {HTMLDivElement}
35   */
36  var Tab = cr.ui.define('div');
37  Tab.prototype = {
38    __proto__: HTMLDivElement.prototype,
39
40    decorate: function() {
41    }
42  };
43
44  /**
45   * Title for the tab.
46   * @type {String}
47   */
48  cr.defineProperty(Tab, 'label', cr.PropertyKind.ATTR);
49
50  /**
51   * Whether the item is selected.
52   * @type {boolean}
53   */
54  cr.defineProperty(Tab, 'selected', cr.PropertyKind.BOOL_ATTR);
55
56
57  /**
58   * Creates a new tab button element in the tabstrip
59   * @constructor
60   * @param {Object=} opt_propertyBag Optional properties.
61   * @extends {HTMLDivElement}
62   */
63  var TabButton = cr.ui.define('a');
64  TabButton.prototype = {
65    __proto__: HTMLAnchorElement.prototype,
66
67    decorate: function() {
68      this.classList.add('tab-button');
69      this.onclick = function() {
70        if (this.tab_)
71          this.parentNode.parentNode.selectedTab = this.tab_;
72      }.bind(this);
73    },
74    get tab() {
75      return this.tab_;
76    },
77    set tab(tab) {
78      if (this.tab_)
79        throw Error('Cannot set tab once set.');
80      this.tab_ = tab;
81      this.tab_.addEventListener('titleChange', this.onTabChanged_.bind(this));
82      this.tab_.addEventListener('selectedChange',
83                                 this.onTabChanged_.bind(this));
84      this.onTabChanged_();
85    },
86
87    onTabChanged_: function(e) {
88      if (this.tab_) {
89        this.textContent = this.tab_.label;
90        this.selected = this.tab_.selected;
91      }
92    }
93
94  };
95
96  /**
97   * Whether the TabButton is selected.
98   * @type {boolean}
99   */
100  cr.defineProperty(TabButton, 'selected', cr.PropertyKind.BOOL_ATTR);
101
102
103  /**
104   * Creates a new tab control element.
105   * @param {Object=} opt_propertyBag Optional properties.
106   * @constructor
107   * @extends {HTMLDivElement}
108   */
109  var TabControl = cr.ui.define('div');
110  TabControl.prototype = {
111    __proto__: HTMLDivElement.prototype,
112
113    selectedTab_: null,
114
115    /**
116     * Initializes the tab control element.
117     * Any child elements pre-existing on the element will become tabs.
118     */
119    decorate: function() {
120      this.classList.add('tab-control');
121
122      this.tabStrip_ = this.ownerDocument.createElement('div');
123      this.tabStrip_.classList.add('tab-strip');
124
125      this.tabs_ = this.ownerDocument.createElement('div');
126      this.tabs_.classList.add('tabs');
127
128      this.insertBefore(this.tabs_, this.firstChild);
129      this.insertBefore(this.tabStrip_, this.firstChild);
130
131      this.boundOnTabSelectedChange_ = this.onTabSelectedChange_.bind(this);
132
133      // Reparent existing tabs to the tabs_ div.
134      while (this.children.length > 2)
135        this.addTab(this.children[2]);
136    },
137
138    /**
139     * Adds an element to the tab control.
140     */
141    addTab: function(tab) {
142      if (tab.parentNode == this.tabs_)
143        throw Error('Tab is already part of this control.');
144      if (!(tab instanceof Tab))
145        throw Error('Provided element is not instanceof Tab.');
146      this.tabs_.appendChild(tab);
147
148      tab.addEventListener('selectedChange', this.boundOnTabSelectedChange_);
149
150      var button = new TabButton();
151      button.tab = tab;
152      tab.tabStripButton_ = button;
153
154      this.tabStrip_.appendChild(button);
155
156      if (this.tabs_.length == 1)
157        this.tabs_.children[0].selected = true;
158    },
159
160    /**
161     * Removes a tab from the tab control.
162     * changing the selected tab if needed.
163     */
164    removeTab: function(tab) {
165      if (tab.parentNode != this.tabs_)
166        throw new Error('Tab is not attached to this control.');
167
168      tab.removeEventListener('selectedChange', this.boundOnTabSelectedChange_);
169
170      if (this.selectedTab_ == tab) {
171        if (this.tabs_.children.length) {
172          this.tabs_.children[0].selected = true;
173        } else {
174          this.selectedTab_ = undefined;
175        }
176      }
177
178      this.tabs_.removeChild(tab);
179      tab.tabStripButton_.parentNode.removeChild(
180          tab.tabStripButton_);
181    },
182
183    /**
184     * Gets the currently selected tab element.
185     */
186    get selectedTab() {
187      return this.selectedTab_;
188    },
189
190    /**
191     * Sets the currently selected tab element.
192     */
193    set selectedTab(tab) {
194      if (tab.parentNode != this.tabs_)
195        throw Error('Tab is not part of this TabControl.');
196      tab.selected = true;
197    },
198
199    /**
200     * Hides the previously selected tab element and dispatches a
201     * 'selectedTabChanged' event.
202     */
203    onTabSelectedChange_: function(e) {
204      var tab = e.target;
205      if (!e.newValue) {
206        // Usually we can ignore this event, as the tab becoming unselected
207        // needs no corrective action. However, if the currently selected
208        // tab is deselected, we do need to do some work.
209        if (tab == this.selectedTab_) {
210          var previousTab = this.selectedTab_;
211          var newTab;
212          for (var i = 0; i < this.tabs_.children.length; ++i) {
213            if (this.tabs_.children[i] != tab) {
214              newTab = this.tabs_.children[i];
215              break;
216            }
217          }
218          if (newTab) {
219            newTab.selected = true;
220          } else {
221            this.selectedTab_ = undefined;
222            cr.dispatchPropertyChange(
223                this, 'selectedTab', this.selectedTab_, previousTab);
224          }
225        }
226      } else {
227        var previousTab = this.selectedTab_;
228        this.selectedTab_ = tab;
229        if (previousTab)
230          previousTab.selected = false;
231        cr.dispatchPropertyChange(
232            this, 'selectedTab', this.selectedTab_, previousTab);
233      }
234    },
235
236    /**
237     * Returns an array of all the tabs within this control.  This is
238     * not the same as this.children because the actual tab elements are
239     * attached to the tabs_ element.
240     */
241    get tabs() {
242      return Array.prototype.slice.call(this.tabs_.children);
243    }
244  };
245
246  return {
247    Tab: Tab,
248    TabControl: TabControl
249  };
250});
251