1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 /**
  6  *  @class
  7  *  @name TKTabControllerDelegate
  8  *  @since TuneKit 1.0
  9  */
 10 
 11 /**
 12  *  Indicates that a new controller is becoming the tab controller's selected controller and is about to become visible on screen.
 13  *
 14  *  @name tabControllerWillShowController
 15  *  @function
 16  *
 17  *  @param {TKTabController} tabController The tab controller.
 18  *  @param {TKController} controller The controller that is about to be shown by the tab controller.
 19  *  @memberOf TKTabControllerDelegate.prototype
 20  */
 21 const TKTabControllerWillShowController = 'tabControllerWillShowController';  
 22 /**
 23  *  Indicates that a new controller has become the tab controller's selected controller and is fully visible on screen.
 24  *
 25  *  @name tabControllerDidShowController
 26  *  @function
 27  *
 28  *  @param {TKTabController} tabController The tab controller.
 29  *  @param {TKController} controller The controller that is being shown by the tab controller.
 30  *  @memberOf TKTabControllerDelegate.prototype
 31  */
 32 const TKTabControllerDidShowController = 'tabControllerDidShowController';  
 33 
 34 /**
 35  *  The CSS class name applied to an element acting as a tab once it is the selected tab.
 36  *  @constant
 37  *  @type String
 38  */
 39 const TKTabControllerSelectedCSSClass = 'tk-tab-selected';
 40 
 41 TKTabController.inherits = TKController;
 42 TKTabController.synthetizes = ['selectedController', 'selectedIndex', 'tabsSelector'];
 43 
 44 /**
 45  *  @class
 46  *
 47  *  <p>A tab controller allows to bind a series of elements within the view, the tabs, to display each a given controller, the tab controller only
 48  *  allowing a single tab to be selected at once. Tabs are specified using the {@link #tabsSelector} CSS selector, and a controller for each tab needs to be
 49  *  stored in the {@link #controllers} array. By implementing the {@link TKTabControllerDelegate} protocol, a tab controller's {@link #delegate} can track as
 50  *  the user navigates between tabs. At any given time, the {@link #selectedController} property allows to find out which of the controllers is currently
 51  *  selected.</p>
 52  *
 53  *  @extends TKController
 54  *  @since TuneKit 1.0
 55  *
 56  *  @param {Object} data A hash of properties to use as this object is initialized.
 57  */
 58 function TKTabController (data) {
 59   this._tabsSelector = null;
 60   this._selectedController = null;
 61   /**
 62    *  The controllers managed by this tab controller, ordered in the same way as the elements matched by {@link #tabsSelector} are.
 63    *  @type Array
 64    */
 65   this.controllers = [];
 66   this.tabs = [];
 67   /**
 68    *  The delegate for this tab controller, an object implementing the {@link TKTabControllerDelegate} protocol.
 69    *  @type Object
 70    */
 71   this.delegate = null;
 72   this.previousController = null;
 73   //
 74   this.busy = false;
 75   //
 76   this.callSuper(data);
 77 };
 78 
 79 /* ==================== Additional View Processing ==================== */
 80 
 81 TKTabController.prototype.processView = function () {
 82   this.callSuper();
 83   //
 84   this.host = this._view.appendChild(document.createElement('div'));
 85   this.host.addClassName('tk-tab-contents');
 86   // restore properties that have not been set yet since construction
 87   this.restoreProperty('tabsSelector');
 88   this.restoreProperty('selectedIndex');
 89 };
 90 
 91 /* ==================== Synthetized Properties ==================== */
 92 
 93 /**
 94  *  @name TKTabController.prototype
 95  *  @property {String} tabsSelector A CSS selector matching the elements within the controller's view that should act as triggers to display the matching 
 96  *  controller in the {@link #controllers} array.
 97  */
 98 TKTabController.prototype.setTabsSelector = function (tabsSelector) {
 99   if (tabsSelector === null) {
100     return;
101   }
102   // forget the old tabs
103   for (var i = 0; i < this.tabs.length; i++) {
104     var element = this.tabs[i];
105     element._tabIndex = undefined;
106     element._controller = undefined;
107     this.removeNavigableElement(element);
108   }
109   // get the new tabs
110   this._tabsSelector = tabsSelector;
111   this.tabs = this._view.querySelectorAll(this._tabsSelector);
112   // nothing to do if we don't have tabs
113   if (this.tabs.length < 1) {
114     return;
115   }
116   for (var i = 0; i < this.tabs.length; i++) {
117     var tab = this.tabs[i];
118     tab._tabIndex = i;
119     tab._controller = this;
120     this.addNavigableElement(tab);
121   }
122   // reset to the first tab, unless we have an archived one
123   var archived_index = this.getArchivedProperty('selectedIndex');
124   this.selectedIndex = (archived_index === undefined || archived_index < 0) ? 0 : archived_index;
125 };
126 
127 /* ==================== Keyboard Handling ==================== */
128 
129 TKTabController.prototype.getNavigableElements = function () {
130   return this._navigableElements.concat(this.selectedController.navigableElements);
131 };
132 
133 TKTabController.prototype.handleEvent = function (event) {
134   this.callSuper(event);
135   //
136   if (event.currentTarget._tabIndex !== undefined) {
137     if (event.type == 'highlight') {
138       this.selectedIndex = event.currentTarget._tabIndex;
139     }
140   }
141 };
142 
143 TKTabController.prototype.elementWasActivated = function (element) {
144   this.callSuper(element);
145   // tab was activated
146   if (element._tabIndex !== undefined) {
147     // highlight in case we weren't already
148     if (this.highlightedElement !== element) {
149       TKSpatialNavigationManager.sharedManager.highlightElement(element);
150     }
151     // notify we were activated
152     else {
153       this.tabAtIndexWasActivated(element._tabIndex);
154     }
155   }
156 };
157 
158 /**
159  *  Indicates that a new tab has been activated at the given index.
160  *
161  *  @param {int} index The index for the tab that was just activated.
162  */
163 TKTabController.prototype.tabAtIndexWasActivated = function (index) {};
164 
165 /* ==================== Controllers ==================== */
166 
167 /**
168  *  @name TKTabController.prototype
169  *  @property {int} selectedIndex The index of the selected tab.
170  */
171 TKTabController.prototype.getSelectedIndex = function () {
172   return (this._selectedController === null) ? -1 : this.controllers.indexOf(this._selectedController);
173 };
174 
175 TKTabController.prototype.setSelectedIndex = function (index) {
176   var selected_index = this.selectedIndex;
177   if (index !== selected_index && index >= 0 && index < this.controllers.length) {
178     // move to the new controller
179     this.selectedController = this.controllers[index];
180   }
181 };
182 
183 /**
184  *  @name TKTabController.prototype
185  *  @property {TKController} selectedController The selected controller.
186  */
187 TKTabController.prototype.setSelectedController = function (controller) {
188   var selected_index = this.controllers.indexOf(controller);
189   // do nothing if we don't know about such a controller, or we're already on this controller
190   if (controller === this._selectedController || selected_index == -1) {
191     return;
192   }
193   // clean up before starting a new transition
194   if (this.busy) {
195     this.transitionDidComplete(null);
196   }
197   //
198   TKTransaction.begin();
199   // mark that a transition is now in progress
200   this.busy = true;
201   // get pointers to object we'll manipulate
202   var previous_controller = this._selectedController;
203   var next_view = controller.view;
204   // fire delegate saying we're moving to a new controller
205   if (TKUtils.objectHasMethod(this.delegate, TKTabControllerWillShowController)) {
206     this.delegate[TKTabControllerWillShowController](this, controller);
207   }
208   // track this is our newly selected controller
209   this._selectedController = controller;
210   // notify of upcoming change and update tabs styling
211   if (previous_controller !== null) {
212     previous_controller._viewWillDisappear();
213     previous_controller.viewWillDisappear();
214   }
215   controller._viewWillAppear();
216   controller.viewWillAppear();
217   // add it to the tree
218   this.host.appendChild(controller.view);
219   // transition
220   var manager = TKSpatialNavigationManager.sharedManager;
221   if (previous_controller !== null) {
222     // unregister the old controller
223     previous_controller.parentController = null;
224     manager.unregisterController(previous_controller);
225     // remove the selected CSS class from the previous tab, if any
226     this.tabs[this.controllers.indexOf(previous_controller)].removeClassName(TKTabControllerSelectedCSSClass);
227     this.transitionToController(previous_controller, controller);
228   }
229   else {
230     this.busy = true;
231     this.transitionDidComplete();
232     // highlight the element
233     if (this.highlightedElement === null) {
234       this.highlightedElement = this.tabs[this.selectedIndex]
235     }
236   }
237   // add the selected CSS class to the new controller's tab
238   this.tabs[selected_index].addClassName(TKTabControllerSelectedCSSClass)
239   // also ensure that element has highlight
240   manager.highlightElement(this.tabs[selected_index]);
241   // and that the new controller is registered for navigation
242   controller.parentController = this;
243   manager.registerController(controller);
244   //
245   TKTransaction.commit();
246 };
247 
248 /* ==================== Transition ==================== */
249 
250 TKTabController.prototype.transitionToController = function (previous_controller, top_controller) {
251   // record some parameters that we will need at the end of the transition
252   this.previousController = previous_controller;
253   // figure out transitions
254   if (previous_controller !== null) {
255     previous_controller.view.applyTransition(previous_controller.becomesInactiveTransition, false);
256   }
257   var top_controller_transition = top_controller.becomesActiveTransition;
258   top_controller_transition.delegate = this;
259   top_controller.view.applyTransition(top_controller_transition, false);
260 };
261 
262 TKTabController.prototype.transitionDidComplete = function (transition) {
263   // update the highlightable items and notify of completed change
264   if (this.previousController !== null) {
265     if (this.previousController.view.parentNode === this.host) {
266       this.host.removeChild(this.previousController.view);
267     }
268     this.previousController._viewDidDisappear();
269     this.previousController.viewDidDisappear();
270   }
271   this._selectedController._viewDidAppear();
272   this._selectedController.viewDidAppear();
273   // fire delegate saying we've moved to a new controller
274   if (TKUtils.objectHasMethod(this.delegate, TKTabControllerDidShowController)) {
275     this.delegate[TKTabControllerDidShowController](this, this._selectedController);
276   }
277   // not busy anymore
278   this.busy = false;
279 };
280 
281 /* ==================== Archival ==================== */
282 
283 TKTabController.prototype.archive = function () {
284   var archive = this.callSuper();
285   archive.selectedIndex = this.selectedIndex;
286   return archive;
287 };
288 
289 TKClass(TKTabController);
290