1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4  
  5 TKController.inherits = TKObject;
  6 TKController.synthetizes = ['view', 'navigableElements', 'actions', 'outlets', 'scrollable', 'backButton', 'navigatesTo'];
  7 
  8 /**
  9  *  The hash in which we keep references to all controller instantiated throughout the lifecycle of the booklet. Use a controller's {@link #id} to
 10  *  access the controller for that id.
 11  *  @type Object
 12  */
 13 TKController.controllers = {};
 14 
 15 /**
 16  *  The fraction of the scrollable element's width or height that is being scrolled by in order to increment or decrement the scroll offset.
 17  *  @constant
 18  *  @type float
 19  *  @private
 20  */
 21 const TKControllerScrollIncrementFraction = 0.75;
 22 /**
 23  *  The time in miliseconds that the scrolling animation lasts.
 24  *  @constant
 25  *  @type int
 26  *  @private
 27  */
 28 const TKControllerScrollDuration = 500;
 29 /**
 30  *  The spline used for the scrolling animation.
 31  *  @constant
 32  *  @type Array
 33  *  @private
 34  */
 35 const TKControllerScrollSpline = [0.211196, 0.811224, 0.641221, 0.979592];
 36 const TKControllerScrollDirectionUp = 0;
 37 const TKControllerScrollDirectionDown = 1;
 38 const TKControllerScrollDirectionLeft = 0;
 39 const TKControllerScrollDirectionRight = 1;
 40 
 41 /**
 42  *  @class
 43  *
 44  *  <p>The TKController class is the base class for all TuneKit controllers. Controllers are useful objects that control all the core functionalities of
 45  *  a screen or sub-screen: view-loading, interaction, navigation, etc.</p>
 46  *
 47  *  @extends TKObject
 48  *  @since TuneKit 1.0
 49  *
 50  *  @param {Object} data A hash of properties to use as this object is initialized.
 51  */
 52 function TKController (data) {
 53   this.callSuper();
 54   //
 55   this.propertiesToRestoreOnLoad = [];
 56   // synthetized property
 57   this._view = null;
 58   /**
 59    *  @name TKController.prototype
 60    *  @property {Array} navigableElements The complete list of all elements that can be navigated to within this controller and all of its sub-controllers.
 61    *  The contents of this array should not be directly manipulated, instead use the {@link #addNavigableElement} and {@link #removeNavigableElement} methods.
 62    */
 63   this._navigableElements = [];
 64   /**
 65    *  The controller directly containing this controller instance, <code>null</code> if the controller is not attached to any yet.
 66    *  @type TKController
 67    */
 68   this.parentController = null;
 69   // default transition styles for navigation
 70   this.enforcesCustomTransitions = false; // whether we should use the custom transitions on ATV only
 71   /**
 72    *  The animated transition to use for this controller's view when the controller becomes inactive.
 73    *  @type TKTransitionDefinition
 74    */
 75   this.becomesInactiveTransition = TKViewTransitionDissolveOut;
 76   /**
 77    *  The animated transition to use for this controller's view when the controller becomes active.
 78    *  @type TKTransitionDefinition
 79    */
 80   this.becomesActiveTransition = TKViewTransitionDissolveIn;
 81   // default properties
 82   /**
 83    *  The unique id for this controller's view. This is the same string that will be used for the {@link #view}'s HTML <code>id</code> attribute, as well
 84    *  as a key in the {@link TKController.controllers} hash, and thus must be adequate for both uses. The controller's id is used in the view-loading mechanism,
 85    *  such that if there is an HTML file in the booklet's <code>views/</code> directory that shares the same name, it is that file that is loaded to provide
 86    *  the view's content.
 87    *  @type String
 88    */
 89   this.id = data.id;
 90   /**
 91    *  A DOM element to be used as the view for this controller, which overrides the default view-loading mechanism in case it's set before the view is loaded.
 92    *  @type Element
 93    *  @private
 94    */
 95   this.explicitView = null;
 96   /**
 97    *  The name of the template to be used to create the view's content. If there is an HTML file in the <code>templates/</code> directory with that name, the
 98    *  view is loaded by cloning the content of that file and replacing the ID with that provided by the {@link #id} property.
 99    *  @type String
100    */
101   this.template = null;
102   /**
103    *  A list of image URIs to preload.
104    *  @type Array
105    */
106   this.preloads = [];
107   this._navigatesTo = null;
108   this._actions = null;
109   this._outlets = null;
110   this._scrollable = null;
111   this._backButton = null;
112   /**
113    *  The highlighted element within that controller. This is only unique to the view managed by this controller, and not to the entire booklet or even any
114    *  controllers that might be contained within this controller.
115    *  @type Element
116    *  @private
117    */
118   this.highlightedElement = null;
119   /**
120    *  Indicates that the view has not appeared on screen yet.
121    *  @type bool
122    *  @private
123    */
124   this.viewNeverAppeared = true;
125   /**
126    *  Indicates that the view was fully processed.
127    *  @type bool
128    *  @private
129    */
130   this.viewWasProcessed = false;
131   /**
132    *  The CSS selector for the default scrollable element. If this value is non-<code>null</code> then the up and down keys scroll the element specified
133    *  by the selector.
134    *  @type String
135    */
136   this.scrollableElement = null;
137   /**
138    *  The animator managing the currently scrolling element.
139    *  @type TKAnimator
140    *  @private
141    */
142   this.animator = new TKAnimator(TKControllerScrollDuration, null, TKControllerScrollSpline);
143   this.upScrollData = {
144     direction: TKControllerScrollDirectionUp,
145     animator: this.animator
146   }
147   this.downScrollData = {
148     direction: TKControllerScrollDirectionDown,
149     animator: this.animator
150   }
151   // copy properties
152   this.copyNonSynthetizedProperties(data);
153   // register controller
154   TKController.controllers[this.id] = this;
155 };
156 
157 /**
158  *  A utility method to get the controller from an Object that is either the {@link TKController#id} of a controller or a controller directly.
159  *
160  *  @param {Object} stringOrControllerReference Either the {@link TKController#id} of a controller or a controller directly
161  *  @returns {TKController} The controller.
162  */
163 TKController.resolveController = function (stringOrControllerReference) {
164   return (TKUtils.objectIsString(stringOrControllerReference)) ? TKController.controllers[stringOrControllerReference] : stringOrControllerReference;
165 };
166 
167 /**
168  *  A utility method to copy all properties from another object onto the controller, ignoring any property that is synthetized.
169  *
170  *  @param {Object} properties An object containing a set of properties to be copied across to the receiver.
171  *  @private
172  */
173 TKController.prototype.copyNonSynthetizedProperties = function (properties) {
174   for (var property in properties) {
175     // don't copy synthetized properties but track them for later
176     if (this.__lookupSetter__(property)) {
177       this.propertiesToRestoreOnLoad[property] = properties[property];
178       continue;
179     }
180     this[property] = properties[property];
181   }
182 };
183 
184 /* ==================== Managing the View ==================== */
185 
186 TKController.prototype.getView = function () {
187   // create the view if it's not set yet
188   if (this._view === null) {
189     this.loadView();
190   }
191   return this._view;
192 };
193 
194 TKController.prototype.setView = function (view) {
195   this.explicitView = view;
196 };
197 
198 TKController.prototype.loadView = function () {
199   this.viewNeverAppeared = false;
200   // first, check if we have an element already defined
201   var view;
202   if (this.explicitView !== null) {
203     view = this.explicitView;
204     // check if node is already in the document
205     this.viewNeverAppeared = !TKUtils.isNodeChildOfOtherNode(view, document);
206   }
207   // check if our view already exists in the DOM
208   else {
209     view = document.getElementById(this.id);
210   }
211   // if not, load it from the views directory
212   if (view === null) {
213     this.viewNeverAppeared = true;
214     view = this.loadFragment('views', this.id);
215     // there was no such view available, try and see if we have a template available
216     if (view === null) {
217       if (this.template !== null) {
218         view = this.loadFragment('templates', this.template);
219       }
220       // no template, just create an empty <div> then
221       if (view === null) {
222         view = document.createElement('div');
223       }
224     }
225   }
226   // make sure we know when the view is added to the document if it
227   // wasn't part of the DOM already
228   if (this.viewNeverAppeared) {
229     view.addEventListener('DOMNodeInsertedIntoDocument', this, false);
230   }
231   // set up the correct id on our view
232   view.id = this.id;
233   // link the view to our controller
234   view._controller = this;
235   // and remember our view
236   this._view = view;
237   // let our object perform more setup code
238   this.viewDidLoad();
239   // do post-loading processing
240   this.processView();
241   //
242   this.viewWasProcessed = true;
243 };
244 
245 TKController.prototype.loadFragment = function (directory, id) {
246   var imported_fragment = null;
247   //
248   var request = new XMLHttpRequest();
249   var failed = false;
250   request.open('GET', directory + '/' + id + '.html', false);
251   try {
252     request.send();
253   } catch (err) {
254     // iTunes will throw an error if the request doesn't exist
255     // when using the booklet:// scheme
256     // Mark the error here so we can take the FAIL path below, which
257     // is actually what we want.
258     failed = true;
259   }
260   // everything went well
261   // XXX: we should do more work to differentitate between http:// and file:// URLs here
262   if (!failed && ((request.status <= 0 && request.responseText !== '') || request.status == 200)) {
263     // XXX: this is way dirty
264     var loaded_fragment = document.implementation.createHTMLDocument();
265     loaded_fragment.write(request.responseText);
266     imported_fragment = document.importNode(loaded_fragment.getElementById(id), true);
267   }
268   return imported_fragment;
269 };
270 
271 /**
272  *  This method is called once the view has been loaded and allows the controller to post-process it.
273  */
274 TKController.prototype.processView = function () {
275   var view = this._view;
276   // restore properties that have not been set yet since construction
277   this.restoreProperty('navigatesTo');
278   this.restoreProperty('actions');
279   this.restoreProperty('outlets');
280   this.restoreProperty('backButton');
281   this.restoreProperty('scrollable');
282   // process highlightedElement
283   if (this.highlightedElement !== null && !(this.highlightedElement instanceof Element)) {
284     this.highlightedElement =  this._view.querySelector(this.highlightedElement);
285   }
286   // process links
287   var links = view.querySelectorAll('a');
288   for (var i = 0; i < links.length; i++) {
289     this.addNavigableElement(links[i]);
290   }
291   // process assets to pre-load
292   for (var i = 0; i < this.preloads.length; i++) {
293     new Image().src = this.preloads[i];
294   }
295 };
296 
297 TKController.prototype.restoreProperty = function (property) {
298   var value = this.propertiesToRestoreOnLoad[property];
299   if (value !== undefined && this['_' + property] === null) {
300     this[property] = value;
301   }
302 };
303 
304 TKController.prototype.getArchivedProperty = function (property) {
305   var archived_value;
306   try {
307     var archived_value = bookletController.archive.controllers[this.id][property];
308   }
309   catch (e) {}
310   return archived_value;
311 };
312 
313 /**
314  *  Indicates whether the view the controller manages was loaded yet.
315  *
316  *  @returns {bool} Whether the view was loaded yet.
317  */
318 TKController.prototype.isViewLoaded = function () {
319   return (this._view !== null);
320 };
321 
322 /* ==================== Dealing with the various properties ==================== */
323 
324 /**
325  *  @name TKController.prototype
326  *  @property {Array} navigatesTo A list of objects defining elements to act as anchors to navigate to other controllers. Each object in the array is an
327  *  ad-hoc object with <code>selector</code> and <code>controller</code> properties. The <code>selector</code> is a string describing a CSS selector used
328  *  to match the element within the {@link #view} that will act as an anchor to navigate to the controller. The <code>controller</code> is either a string matching
329  *  the {@link #id} of an existing controller or an reference to a {@link TKController}.
330  */
331 TKController.prototype.setNavigatesTo = function (navigatesTo) {
332   if (navigatesTo === null) {
333     return;
334   }
335   // unregister the previous elements if we have any
336   if (this._navigatesTo !== null) {
337     for (var i = 0; i < this._navigatesTo.length; i++) {
338       var element = this._navigatesTo[i];
339       element._navigationData = undefined;
340       this.removeNavigableElement(element);
341     }
342   }
343   // register the new elements
344   this._navigatesTo = [];
345   for (var i = 0; i < navigatesTo.length; i++) {
346     var item = navigatesTo[i];
347     var element = this._view.querySelector(item.selector);
348     if (element) {
349       element._navigationData = item;
350       this._navigatesTo.push(element);
351       this.addNavigableElement(element);
352     }
353   }
354 };
355 
356 /**
357  *  @name TKController.prototype
358  *  @property {Array} actions A list of objects defining elements to act as anchors to trigger a JavaScript callback. Each object in the array is an
359  *  ad-hoc object with <code>selector</code>, <code>action</code> and, optionally, <code>arguments</code> properties. The <code>selector</code> is a
360  *  string describing a CSS selector used to match the element within the {@link #view} that will act as a trigger for the action. The <code>action</code>
361  *  specifies the function to call when the action is triggered, which is either a string matching the name of a method on this controller instance or
362  *  a direct reference to a function. Optionally, the <code>arguments</code> property can be specified in order to provide a list of arguments to be passed
363  *  to the callback when the action is triggered.
364  */
365 TKController.prototype.setActions = function (actions) {
366   if (actions === null || this._actions === actions) {
367     return;
368   }
369   // unregister the previous elements if we have any
370   if (this._actions !== null) {
371     for (var i = 0; i < this._actions.length; i++) {
372       var element = this._actions[i];
373       element._actionData = undefined;
374       this.removeNavigableElement(element);
375     }
376   }
377   // register the new elements
378   this._actions = [];
379   for (var i = 0; i < actions.length; i++) {
380     var item = actions[i];
381     var element = this._view.querySelector(item.selector);
382     if (element) {
383       element._actionData = item;
384       this._actions.push(element);
385       this.addNavigableElement(element);
386     }
387   }
388 };
389 
390 /**
391  *  @name TKController.prototype
392  *  @property {Array} outlets A list of objects defining elements to which we want to create an automatic reference on the controller instance. Each object in
393  *  the array has a <code>selector</code> and a <code>name</code> property. The <code>selector</code> is a string describing a CSS selector used to match the   
394  *  element within the {@link #view} to which we want to create a reference. The <code>name</code> specifies the name of the JavaScript property that will be holding 
395  *  that reference on the controller instance.
396  */
397 TKController.prototype.setOutlets = function (outlets) {
398   if (outlets === null) {
399     return;
400   }
401   // unregister the previous outlets if we have any
402   if (this._outlets !== null) {
403     for (var i = 0; i < this._outlets.length; i++) {
404       this[this._outlets[i]] = undefined;
405     }
406   }
407   // register the new outlets
408   for (var i = 0; i < outlets.length; i++) {
409     var item = outlets[i];
410     this[item.name] = this._view.querySelector(item.selector);
411   }
412   this._outlets = outlets;
413 };
414 
415 /**
416  *  @name TKController.prototype
417  *  @property {String} backButton A CSS selector that matches an element in the {@link #view} that acts as the back button.
418  */
419 TKController.prototype.setBackButton = function (backButton) {
420   if (backButton === null) {
421     return;
422   }
423   // forget the old back button if we have one
424   if (this._backButton !== null) {
425     this._backButton._backButton = undefined;
426     // restore display type on ATV
427     if (IS_APPLE_TV) {
428       this._backButton.style.display = this._backButton._previousDisplayStyle;
429     }
430     this.removeNavigableElement(this._backButton);
431   }
432   // set up the new one
433   if (backButton !== null) {
434     var element = this._view.querySelector(backButton);
435     element._backButton = true;
436     // hide it on ATV
437     if (IS_APPLE_TV) {
438       element._previousDisplayStyle = element.style.display;
439       element.style.display = 'none';
440     }
441     this.addNavigableElement(element);
442     this._backButton = element;
443   }
444 };
445 
446 /* ==================== Notification Methods ==================== */
447 
448 /**
449  *  This method is called when the view has been fully loaded but not yet processed. Override this method in order to customize the content of the view.
450  */
451 TKController.prototype.viewDidLoad = function () {};
452 TKController.prototype.viewDidUnload = function () {};
453 
454 TKController.prototype._viewWillAppear = function () {};
455 /**
456  *  This method is called when the view managed by this controller is about to appear on screen, probably after an animated transition.
457  */
458 TKController.prototype.viewWillAppear = function () {};
459 
460 TKController.prototype._viewDidAppear = function () {};
461 /**
462  *  This method is called when the view managed by this controller appeared on screen, probably after an animated transition.
463  */
464 TKController.prototype.viewDidAppear = function () {};
465 
466 TKController.prototype._viewWillDisappear = function () {};
467 /**
468  *  This method is called when the view managed by this controller is about to disappear from the screen, probably after an animated transition.
469  */
470 TKController.prototype.viewWillDisappear = function () {};
471 
472 TKController.prototype._viewDidDisappear = function () {};
473 /**
474  *  This method is called when the view managed by this controller has disappeared from the screen, probably after an animated transition.
475  */
476 TKController.prototype.viewDidDisappear = function () {};
477 
478 /* ==================== Event Handling ==================== */
479 
480 /**
481  *  Entry point for all event handling, since <code>TKController</code> implements the DOM <code>EventListener</code> protocol. This method may be subclassed
482  *  but it is important to call the superclass's implementation of this method as essential event routing happens there.
483  *
484  *  @param {Event} event The event.
485  */
486 TKController.prototype.handleEvent = function (event) {
487   switch (event.type) {
488     case 'click' : 
489       this.elementWasActivated(event.currentTarget);
490       break;
491     case 'highlight' : 
492       this.elementWasHighlighted(event.currentTarget, event.relatedTarget);
493       break;
494     case 'unhighlight' : 
495       this.elementWasUnhighlighted(event.currentTarget, event.relatedTarget);
496       break;
497     case 'mouseover' : 
498       this.elementWasHovered(event.currentTarget);
499       break;
500     case 'mouseout' : 
501       this.elementWasUnhovered(event.currentTarget);
502       break;
503     case 'DOMNodeInsertedIntoDocument' : 
504       this.viewWasInsertedIntoDocument(event);
505       break;
506   }
507 };
508 
509 /**
510  *  Triggered when an element is activated by the user, no matter what the host's input methods are as TuneKit abstracts the interaction that yields
511  *  an element's activation. This method may be subclassed but it is important to call the superclass's implementation of this method as navigation anchors,
512  *  actions, etc. are handled in that method directly.
513  *
514  *  @param {Element} element The element that was just activated.
515  */
516 TKController.prototype.elementWasActivated = function (element) {
517   if (element._navigationData !== undefined) {
518     // pointer to the controller
519     var controller = TKController.resolveController(element._navigationData.controller);
520     // error if we have an undefined object
521     if (controller === undefined) {
522       console.error('TKController.elementWasActivated: trying to push an undefined controller');
523       return;
524     }
525     // otherwise, navigate to it
526     TKSpatialNavigationManager.sharedManager.highlightElement(element);
527     TKNavigationController.sharedNavigation.pushController(controller);
528   }
529   else if (element._actionData !== undefined) {
530     TKSpatialNavigationManager.sharedManager.highlightElement(element);
531     // get the callback for this action
532     var callback = element._actionData.action;
533     // see if it's a string in which case we need to get the function dynamically
534     if (TKUtils.objectIsString(callback) && TKUtils.objectHasMethod(this, callback)) {
535       callback = this[element._actionData.action];
536     }
537     // see if we have custom arguments
538     if (TKUtils.objectIsArray(element._actionData.arguments)) {
539       callback.apply(this, element._actionData.arguments);
540     }
541     // otherwise just call the callback
542     else {
543       callback.apply(this);
544     }
545   }
546   else if (element._backButton !== undefined) {
547     TKSpatialNavigationManager.soundToPlay = SOUND_EXIT;
548     TKSpatialNavigationManager.sharedManager.highlightElement(element);
549     TKNavigationController.sharedNavigation.popController();
550   }
551   else if (element.localName == 'a' && IS_APPLE_TV) {
552     element.dispatchEvent(TKUtils.createEvent('click', null));
553   }
554   else if (element._scrollableData !== undefined) {
555     this.scrollWithData(element._scrollableData);
556   }
557 };
558 
559 /**
560  *  Triggered when an element receives highlight.
561  *
562  *  @param {Element} element The element that is newly being highlighted.
563  *  @param {Element} previouslyHighlightedElement The element that previously was highlighted, or <code>null</code> if there was none.
564  */
565 TKController.prototype.elementWasHighlighted = function (element, previouslyHighlightedElement) {
566 };
567 
568 /**
569  *  Triggered when an element loses highlight.
570  *
571  *  @param {Element} element The element that is newly being highlighted.
572  *  @param {Element} nextHighlightedElement The element that is going to be highlighted next, or <code>null</code> if there is none.
573  */
574 TKController.prototype.elementWasUnhighlighted = function (element, nextHighlightedElement) {
575 };
576 
577 /**
578  *  Triggered when an element is hovered, which only happens when a mouse is present.
579  *
580  *  @param {Element} element The element that is being hovered.
581  */
582 TKController.prototype.elementWasHovered = function (element) {
583 };
584 
585 /**
586  *  Triggered when an element is not hovered any longer, which only happens when a mouse is present.
587  *
588  *  @param {Element} element The element that is not hovered any longer.
589  */
590 TKController.prototype.elementWasUnhovered = function (element) {
591 };
592 
593 /**
594  *  Triggered when the view is first inserted into the document.
595  */
596 TKController.prototype.viewWasInsertedIntoDocument = function () {
597   this.viewNeverAppeared = false;
598 };
599 
600 /**
601  *  Indicates whether the receiver is a descendent of the controller passed as an argument.
602  *
603  *  @param {TKController} purportedParentController The controller that we assume is a parent controller to the receiver.
604  *  @returns {bool} Whether the controller is a descendent of the other controller passed as a parameter.
605  */
606 TKController.prototype.isDescendentOfController = function (purportedParentController) {
607   var is_descendent = false;
608   var parent = this.parentController;
609   while (parent !== null) {
610     if (parent === purportedParentController) {
611       is_descendent = true;
612       break;
613     }
614     parent = parent.parentController;
615   }
616   return is_descendent;
617 };
618 
619 /* ==================== Keyboard Navigation ==================== */
620 
621 /**
622  *  Adds an element within the controller's view to the list of navigable elements. Any element that is interactive should be registered as navigable, even
623  *  when a mouse is available.
624  *
625  *  @param {Element} element The element we wish to make navigable.
626  */
627 TKController.prototype.addNavigableElement = function (element) {
628   // nothing to do if we already know about this element
629   if (this._navigableElements.indexOf(element) > -1) {
630     return;
631   }
632   //
633   if (!IS_APPLE_TV) {
634     element.addEventListener('click', this, false);
635   }
636   element.addEventListener('highlight', this, false);
637   element.addEventListener('unhighlight', this, false);
638   element._controller = this;
639   //
640   this._navigableElements.push(element);
641   //
642   TKSpatialNavigationManager.sharedManager.addNavigableElement(element);
643 };
644 
645 /**
646  *  Removes an element within the controller's view from the list of navigable elements.
647  *
648  *  @param {Element} element The element we wish to remove from the navigable elements list.
649  */
650 TKController.prototype.removeNavigableElement = function (element) {
651   // find the index for this element
652   var index = this._navigableElements.indexOf(element);
653   if (index < 0) {
654     return;
655   }
656   //
657   element.removeEventListener('click', this, false);
658   element.removeEventListener('highlight', this, false);
659   element.removeEventListener('unhighlight', this, false);
660   element._controller = undefined;
661   // remove elements from the tracking arrays
662   this._navigableElements.splice(index, 1);
663   //
664   TKSpatialNavigationManager.sharedManager.removeNavigableElement(element);
665 };
666 
667 /**
668  *  Allows to specify custom metrics for an element displayed on screen. This method is called by the spatial navigation manager when determining what the
669  *  element to highlight is after the uses presses a directional key on the Apple remote. By default, the CSS metrics for the elements are used, but in
670  *  certain cases the author may wish to use different metrics that are more logical for the navigation. Return <code>null</code> in order to specify that
671  *  the element has no custom metrics.
672  *
673  *  @param {Element} element The element which the spatial navigation manager is inspecting.
674  *  @returns {TKRect} The custom metrics for the given element.
675  */
676 TKController.prototype.customMetricsForElement = function (element) {
677   return null;
678 };
679 
680 /**
681  *  Allows the controller to provide the spatial navigation manager with a prefered element to highlight, overriding the default behavior of using CSS metrics.
682  *  The default implementation returns <code>undefined</code>, which indicates that the automatic behavior should be used, while returning <code>null</code> 
683  *  means that there should be no element highlighted in the provided direction.
684  *
685  *  @param {Element} currentElement The element that is currently highlighted.
686  *  @param {int} direction The direction the user is navigation towards.
687  *  @returns {Element} The preferred element to highlight in the provided direction.
688  */
689 TKController.prototype.preferredElementToHighlightInDirection = function (currentElement, direction) {
690   return undefined;
691 };
692 
693 // private method meant to be over-ridden by a controller sub-classes to provide a custom element
694 // to highlight, returning null means there's nothing custom to report
695 // XXX: we really need a better mechanism to do this stuff, having a private method for subclasses and one for instances is way dirty
696 TKController.prototype._preferredElementToHighlightInDirection = function (currentElement, direction) {
697   return undefined;
698 };
699 
700 /* ==================== Scrolling ==================== */
701 
702 TKController.prototype.setScrollable = function (scrollable) {
703   // remove all scrollable regions if we already had some set up
704   if (this._scrollable !== null) {
705     for (var i = 0; i < this._scrollable.length; i++) {
706       var element = this._scrollable[i];
707       element._scrollableData = undefined;
708       this.removeNavigableElement(element);
709     }
710   }
711   // process scrollable regions
712   this._scrollable = [];
713   for (var i = 0; i < scrollable.length; i++) {
714     var scrollable_data = scrollable[i];
715     // create an animator for this scrollable element
716     scrollable_data.animator = new TKAnimator(TKControllerScrollDuration, null, TKControllerScrollSpline);
717     // check if we have an up element
718     if (scrollable_data.up !== undefined) {
719       var up_button = this._view.querySelector(scrollable_data.up);
720       up_button._scrollableData = {
721         direction: TKControllerScrollDirectionUp,
722         target: scrollable_data.target,
723         animator: scrollable_data.animator
724       };
725       this._scrollable.push(up_button);
726       this.addNavigableElement(up_button);
727     }
728     // check if we have a down element
729     if (scrollable_data.down !== undefined) {
730       var down_button = this._view.querySelector(scrollable_data.down);
731       down_button._scrollableData = {
732         direction: TKControllerScrollDirectionDown,
733         target: scrollable_data.target,
734         animator: scrollable_data.animator
735       };
736       this._scrollable.push(down_button);
737       this.addNavigableElement(down_button);
738     }
739     // check if we have a left element
740     if (scrollable_data.left !== undefined) {
741       var left_button = this._view.querySelector(scrollable_data.left);
742       left_button._scrollableData = {
743         direction: TKControllerScrollDirectionLeft,
744         target: scrollable_data.target,
745         animator: scrollable_data.animator
746       };
747       this._scrollable.push(left_button);
748       this.addNavigableElement(left_button);
749     }
750     // check if we have a right element
751     if (scrollable_data.right !== undefined) {
752       var right_button = this._view.querySelector(scrollable_data.right);
753       right_button._scrollableData = {
754         direction: TKControllerScrollDirectionRight,
755         target: scrollable_data.target,
756         animator: scrollable_data.animator
757       };
758       this._scrollable.push(right_button);
759       this.addNavigableElement(right_button);
760     }
761   }
762 };
763 
764 TKController.prototype.scrollWithData = function (scrollData) {
765   // stop any running animation for this scrollable region
766   scrollData.animator.stop();
767   // get a pointer to the target element
768   var element = this._view.querySelector(scrollData.target);
769   // stop right there if there's no such element
770   if (!(element instanceof Element)) {
771     TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT;
772     return;
773   }
774   // figure out which direction we're scrolling
775   var vertical_scrolling = (scrollData.direction == TKControllerScrollDirectionUp || scrollData.direction == TKControllerScrollDirectionDown);
776   // get the increment for this scroll
777   var increment = element[vertical_scrolling ? 'offsetHeight' : 'offsetWidth'] * TKControllerScrollIncrementFraction;
778   // get the start value
779   var start_value = element[vertical_scrolling ? 'scrollTop' : 'scrollLeft'];
780   // get the target value
781   var target_value;
782   if (scrollData.direction == TKControllerScrollDirectionUp) {
783     target_value = Math.max(element.scrollTop - increment, 0);
784   }
785   else if (scrollData.direction == TKControllerScrollDirectionDown) {
786     target_value = Math.min(element.scrollTop + increment, element.scrollHeight - element.offsetHeight);
787   }
788   else if (scrollData.direction == TKControllerScrollDirectionLeft) {
789     target_value = Math.max(element.scrollLeft - increment, 0);
790   }
791   else if (scrollData.direction == TKControllerScrollDirectionRight) {
792     target_value = Math.min(element.scrollLeft + increment, element.scrollWidth - element.offsetWidth);
793   }
794   // only run if we have different values
795   if (start_value == target_value) {
796     TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT;
797     return;
798   }
799   // set the delegate
800   scrollData.animator.delegate = {
801     animationDidIterate : function (fraction) {
802       element[vertical_scrolling ? 'scrollTop' : 'scrollLeft'] = start_value + fraction * (target_value - start_value);
803     }
804   }
805   // start the animation
806   scrollData.animator.start();
807   // play the move sound since we're scrolling
808   TKSpatialNavigationManager.soundToPlay = SOUND_MOVED;
809 };
810 
811 TKController.prototype.scrollUp = function () {
812   this.upScrollData.target = this.scrollableElement;
813   this.scrollWithData(this.upScrollData);
814 };
815 
816 TKController.prototype.scrollDown = function () {
817   this.downScrollData.target = this.scrollableElement;
818   this.scrollWithData(this.downScrollData);
819 };
820 
821 /* ==================== Keyboard Navigation ==================== */
822 
823 /**
824  *  Indicates whether the controller is interested in providing event handling for the key with the given identifier. By default, this method returns
825  *  <code>false</code>, letting the spatial navigation manager take care of the event in order to perform navigation. In case the method returns 
826  *  <code>true</code>, the {@link #keyWasPressed} method is called to let the controller provide its own custom key event handling.
827  *
828  *  @param {int} key The identifier for the key that was pressed.
829  *  @returns {bool} Whether the controller wants to provide its own custom key event handling.
830  */
831 TKController.prototype.wantsToHandleKey = function (key) {
832   return (this.scrollableElement !== null && (key == KEYBOARD_UP || key == KEYBOARD_DOWN));
833 };
834 
835 /**
836  *  Triggered when a key was pressed and the receiver has expressed explicit interest in providing custom key event handling by returning <code>true</code> in
837  *  the {@link #wantsToHandleKey} method.
838  *
839  *  @param {int} key The identifier for the key that was pressed.
840  */
841 TKController.prototype.keyWasPressed = function (key) {
842   // up should scroll
843   if (key == KEYBOARD_UP) {
844     this.scrollUp();
845   }
846   // down should scroll
847   else if (key == KEYBOARD_DOWN) {
848     this.scrollDown();
849   }
850   // done, play the sound
851   TKUtils.playSound(TKSpatialNavigationManager.soundToPlay);
852 };
853 
854 /* ==================== Archival ==================== */
855 
856 /**
857  *  Called when the booklet's state is being archived. This method needs to return a list of properties reflecting the current state for the receiver.
858  *
859  *  @returns {Object} The list of properties to archive as a hash-like object.
860  *  @private
861  */
862 TKController.prototype.archive = function () {
863   var archive = {
864     id: this.id
865   };
866   // see if we can add a highlighted element index
867   if (this.highlightedElement !== null) {
868     archive.highlightedElementIndex = this.navigableElements.indexOf(this.highlightedElement);
869   }
870   //
871   return archive;
872 };
873 
874 TKClass(TKController);
875