1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 const TKPageSliderControllerContainerCSSClass = 'tk-page-slider-controller-view';
  6 
  7 TKPageSliderController.inherits = TKController;
  8 TKPageSliderController.synthetizes = ['slidingViewData', 'pageControlData', 'previousPageButton', 'nextPageButton', 'highlightedPageIndex'];
  9 
 10 /**
 11  *  @class
 12  *
 13  *  <p>A page slider controller adds the ability to browse through a collection of elements, often images, with nice and smooth transitions
 14  *  set up in CSS. Using this controller type, you can easily track when a new page is highlighted or activated. Optionally, you can also
 15  *  set up a series of indicators giving the user an overview of the number of images and arrows to navigate through elements, the connection
 16  *  between the various components being automatically handled behind the scenes for you.</p>
 17  *
 18  *  @extends TKController
 19  *  @since TuneKit 1.0
 20  *
 21  *  @param {Object} data A hash of properties to use as this object is initialized.
 22  */
 23 function TKPageSliderController (data) {
 24   this._previousPageButton = null;
 25   this._nextPageButton = null;
 26   /**
 27    *  Indicates whether the pages managed by the controller are navigable with keys. Defaults to <code>true</code>.
 28    *  @type bool
 29    */
 30   this.highlightsFocusedPage = true;
 31   /**
 32    *  Indicates whether navigation of pages is done strictly with paging buttons. Defaults to <code>false</code>, allowing the Apple remote to
 33    *  be used to navigate between pages.
 34    *  @type bool
 35    */
 36   this.navigatesWithPagingButtonsOnly = false;
 37   /**
 38    *  Indicates whether the focused page can get activated. Defaults to <code>true</code>, setting this to <code>false</code> plays the limit sound when
 39    *  the user attempts to activate the focused page.
 40    *  @type bool
 41    */
 42   this.activatesFocusedPage = true;
 43   /**
 44    *  Provides the list of directions that the user can navigate out of the list of pages. By default, this array is empty, meaning that if the
 45    *  {@link #navigatesWithPagingButtonsOnly} property is set to <code>false</code> and pages can be navigated with arrow keys, then the user will not
 46    *  be able to move focus out of the pages from either ends. The directions allowed are the <code>TKSpatialNavigationManagerDirection</code> family
 47    *  of constants.
 48    *  @type Array
 49    */
 50   this.allowedOutwardNavigationDirections = [];
 51   // set up the sliding view
 52   /**
 53    *  The backing sliding view hosting the pages.
 54    *  @type TKSlidingView
 55    *  @private
 56    */
 57   this.slidingView = new TKSlidingView();
 58   this.slidingView.ready = false;
 59   this.slidingView.delegate = this;
 60   // set up the page control
 61   /**
 62    *  The backing page control hosting the page indicators.
 63    *  @type TKPageControl
 64    *  @private
 65    */
 66   this.pageControl = new TKPageControl();
 67   this.pageControl.delegate = this;
 68   //
 69   this._slidingViewData = null;
 70   this._pageControlData = null;
 71   //
 72   this.callSuper(data);
 73 };
 74 
 75 TKPageSliderController.prototype.processView = function () {
 76   this.callSuper();
 77   // restore properties that have not been set yet since construction
 78   this.restoreProperty('previousPageButton');
 79   this.restoreProperty('nextPageButton');
 80   this.restoreProperty('slidingViewData');
 81   this.restoreProperty('pageControlData');
 82   this.restoreProperty('highlightedPageIndex');
 83   // wire up actions if we have a previous and next button wired
 84   // add the sliding view and page control containers
 85   this.container = this._view.appendChild(document.createElement('div'));
 86   this.container.addClassName(TKPageSliderControllerContainerCSSClass);
 87   this.container.appendChild(this.slidingView.element);
 88   this.container.appendChild(this.pageControl.element);
 89   // ensure our first page gets told about its being highlighted
 90   this.pageWasHighlighted(this.highlightedPageIndex);
 91   this.syncPageButtons();
 92   // highlight the first page in case we have no explicit highlighted element
 93   if ((this.highlightedElement === null || !this.highlightedElement.isNavigable()) && this.slidingView.ready && this.highlightsFocusedPage) {
 94     this.highlightedElement = this.slidingView.activeElement;
 95   }
 96 };
 97 
 98 /**
 99  *  @name TKPageSliderController.prototype
100  *  @property {int} highlightedPageIndex The index of the page currently selected within the collection of pages.
101  */
102 TKPageSliderController.prototype.getHighlightedPageIndex = function () {
103   return this.slidingView.activeElementIndex;
104 };
105 
106 TKPageSliderController.prototype.setHighlightedPageIndex = function (index) {
107   if (index === null) {
108     return;
109   }
110   // apply to the sliding view if it's ready
111   if (this.slidingView.ready) {
112     this.slidingView.activeElementIndex = index;
113     if (this.highlightedElement.hasClassName(TKSlidingViewCSSElementClass)) {
114       this.registerNavigablePages();
115       this.highlightedElement = this.slidingView.activeElement;
116     }
117   }
118 };
119 
120 /**
121  *  @name TKPageSliderController.prototype
122  *  @property {String} previousPageButton A CSS selector matching a button to be used as the button to decrement the {@link #highlightedPageIndex}.
123  */
124 TKPageSliderController.prototype.setPreviousPageButton = function (previousPageButton) {
125   if (previousPageButton === null) {
126     return;
127   }
128   // forget old button
129   if (this._previousPageButton) {
130     this.removeNavigableElement(this._previousPageButton);
131   }
132   // process new one
133   if (previousPageButton !== null) {
134     this._previousPageButton = this.view.querySelector(previousPageButton);
135     if (this._previousPageButton !== null) {
136       if (IS_APPLE_TV && !this.navigatesWithPagingButtonsOnly) {
137         this._previousPageButton.style.display = 'none';
138       }
139       else {
140         this.addNavigableElement(this._previousPageButton);
141       }
142     }
143   }
144 };
145 
146 /**
147  *  @name TKPageSliderController.prototype
148  *  @property {String} nextPageButton A CSS selector matching a button to be used as the button to increment the {@link #highlightedPageIndex}.
149  */
150 TKPageSliderController.prototype.setNextPageButton = function (nextPageButton) {
151   if (nextPageButton === null) {
152     return;
153   }
154   // forget old button
155   if (this._nextPageButton) {
156     this.removeNavigableElement(this._nextPageButton);
157   }
158   // process new one
159   if (nextPageButton !== null) {
160     this._nextPageButton = this.view.querySelector(nextPageButton);
161     if (this._nextPageButton !== null) {
162       if (IS_APPLE_TV && !this.navigatesWithPagingButtonsOnly) {
163         this._nextPageButton.style.display = 'none';
164       }
165       else {
166         this.addNavigableElement(this._nextPageButton);
167       }
168     }
169   }
170 };
171 
172 /**
173  *  @name TKPageSliderController.prototype
174  *  @property {TKSlidingViewData} slidingViewData The set of properties used to set up the contents of the page slider.
175  */
176 TKPageSliderController.prototype.setSlidingViewData = function (data) {
177   if (data === null) {
178     return;
179   }
180   // set up the data source if we have .elements on the data object
181   if (!TKUtils.objectIsUndefined(data.elements)) {
182     this.slidingView.dataSource = new TKSlidingViewDataSourceHelper(data.elements);
183     delete data.element;
184   }
185   // see if we have some intersting bits to pass through
186   var archived_page_index = this.getArchivedProperty('highlightedPageIndex');
187   if (archived_page_index !== undefined) {
188     data.activeElementIndex = archived_page_index;
189   }
190   // copy properties
191   TKUtils.copyPropertiesFromSourceToTarget(data, this.slidingView);
192   // init our view
193   this.slidingView.init();
194   //
195   this.slidingView.ready = true;
196   //
197   this.syncPageButtons();
198   // add the currently focused element to the list of keyboard elements and highlight it
199   if (this.highlightsFocusedPage) {
200     this.registerNavigablePages();
201     this.highlightedElement = this.slidingView.activeElement;
202     if (this.viewWasProcessed) {
203       TKSpatialNavigationManager.sharedManager.highlightElement(this.slidingView.activeElement);
204     }
205   }
206 };
207 
208 /**
209  *  @name TKPageSliderController.prototype
210  *  @property {TKPageControlData} pageControlData The set of properties used to set up the contents of the optional page indicators.
211  */
212 TKPageSliderController.prototype.setPageControlData = function (data) {
213   if (data === null) {
214     return;
215   }
216   // set up the data source
217   this.pageControl.dataSource = new TKPageControlDataSourceHelper(data);
218   // copy properties
219   TKUtils.copyPropertiesFromSourceToTarget(data, this.pageControl);
220   // init our control
221   this.pageControl.init();
222   // get the current page from the sliding view if we have it set only after
223   if (this.slidingView.ready) {
224     this.pageControl.currentPage = this.highlightedPageIndex;
225   }
226 };
227 
228 /* ==================== Event Handling ==================== */
229 
230 // private method meant to be over-ridden by a controller sub-classes to provide a custom element
231 // to highlight, returning null means there's nothing custom to report
232 TKPageSliderController.prototype._preferredElementToHighlightInDirection = function (currentElement, direction) {
233   var can_exit_in_direction = (this.allowedOutwardNavigationDirections.indexOf(direction) != -1);
234   var element = this.callSuper(currentElement, direction);
235   if (!this.navigatesWithPagingButtonsOnly && currentElement.hasClassName(TKSlidingViewCSSElementClass)) {
236     if (direction == KEYBOARD_LEFT) {
237       if (this.slidingView.activeElementIndex <= 0 && !this.slidingView.loops) {
238         if (!can_exit_in_direction) {
239           element = null;
240         }
241       }
242       else {
243         element = this.slidingView.getElementAtIndex((this.slidingView.activeElementIndex + this.slidingView.numberOfElements - 1) % this.slidingView.numberOfElements);
244       }
245     }
246     else if (direction == KEYBOARD_RIGHT) {
247       if (this.slidingView.activeElementIndex >= this.slidingView.numberOfElements - 1 && !this.slidingView.loops) {
248         if (!can_exit_in_direction) {
249           element = null;
250         }
251       }
252       else {
253         element = this.slidingView.getElementAtIndex((this.slidingView.activeElementIndex + 1) % this.slidingView.numberOfElements);
254       }
255     }
256   }
257   return element;
258 };
259 
260 TKPageSliderController.prototype.elementWasActivated = function (element) {
261   // previous page button pressed
262   if (element === this._previousPageButton) {
263     var can_navigate = (this.slidingView.activeElementIndex > 0 || this.slidingView.loops);
264     TKSpatialNavigationManager.soundToPlay = (can_navigate) ? SOUND_MOVED : SOUND_LIMIT;
265     if (can_navigate) {
266       this.unregisterNavigablePages();
267     }
268     this.slidingView.activeElementIndex--;
269   }
270   // next page button pressed
271   else if (element === this._nextPageButton) {
272     var can_navigate = (this.slidingView.activeElementIndex < this.slidingView.numberOfElements - 1 || this.slidingView.loops);
273     TKSpatialNavigationManager.soundToPlay = (can_navigate) ? SOUND_MOVED : SOUND_LIMIT;
274     if (can_navigate) {
275       this.unregisterNavigablePages();
276     }
277     this.slidingView.activeElementIndex++;
278   }
279   // focused element in the sliding view
280   else if (element.hasClassName(TKSlidingViewCSSFocusedClass)) {
281     if (this.activatesFocusedPage) {
282       this.pageWasSelected(this.slidingView.activeElementIndex);
283     }
284     else {
285       TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT;
286     }
287   }
288   // fall back to default behavior
289   else {
290     this.callSuper(element);
291   }
292 };
293 
294 TKPageSliderController.prototype.elementWasHighlighted = function (element, previouslyHighlightedElement) {
295   if (element.hasClassName(TKSlidingViewCSSElementClass)) {
296     this.slidingView.activeElementIndex = element._slidingViewIndex;
297     // track navigation on all elements if we're getting highlight for the first time
298     if (previouslyHighlightedElement !== null &&
299         (!previouslyHighlightedElement.hasClassName(TKSlidingViewCSSElementClass) ||
300         previouslyHighlightedElement._controller !== this)) {
301       this.registerNavigablePages();
302     }
303   }
304 };
305 
306 TKPageSliderController.prototype.elementWasUnhighlighted = function (element, nextHighlightedElement) {
307   // are we focusing an element outside of our sliding view?
308   if (element.hasClassName(TKSlidingViewCSSElementClass) &&
309       (!nextHighlightedElement.hasClassName(TKSlidingViewCSSElementClass) ||
310       nextHighlightedElement._controller !== this)) {
311     // make all the other elements non-navigable
312     this.unregisterNavigablePages();
313     // save for the active one
314     this.addNavigableElement(this.slidingView.activeElement);
315   }
316 };
317 
318 TKPageSliderController.prototype.syncPageButtons = function () {
319   // nothing to do if the sliding view is looping
320   if (this.slidingView.loops || !this.isViewLoaded()) {
321     return;
322   }
323   // check if the previous page button needs hiding
324   if (this._previousPageButton instanceof Element) {
325     this._previousPageButton[(this.slidingView.activeElementIndex <= 0 ? 'add' : 'remove') + 'ClassName']('inactive');
326   }
327   // check if the next page button needs hiding
328   if (this._nextPageButton instanceof Element) {
329     this._nextPageButton[(this.slidingView.activeElementIndex >= this.slidingView.numberOfElements - 1 ? 'add' : 'remove') + 'ClassName']('inactive');
330   }
331 };
332 
333 TKPageSliderController.prototype.registerNavigablePages = function () {
334   if (this.navigatesWithPagingButtonsOnly) {
335     this.addNavigableElement(this.slidingView.activeElement);
336   }
337   else {
338     var elements = this.slidingView.element.querySelectorAll('.' + TKSlidingViewCSSElementClass);
339     for (var i = 0; i < elements.length; i++) {
340       this.addNavigableElement(elements[i]);
341     }
342   }
343 };
344 
345 TKPageSliderController.prototype.unregisterNavigablePages = function () {
346   var elements = this.slidingView.element.querySelectorAll('.' + TKSlidingViewCSSElementClass);
347   for (var i = 0; i < elements.length; i++) {
348     this.removeNavigableElement(elements[i]);
349   }
350 };
351 
352 /* ==================== TKSlidingView Protocol ==================== */
353 
354 TKPageSliderController.prototype.slidingViewDidFocusElementAtIndex = function (view, index) {
355   if (this.highlightsFocusedPage && this.slidingView.ready) {
356     if (this.highlightedElement.hasClassName(TKSlidingViewCSSElementClass)) {
357       this.registerNavigablePages();
358     }
359     else {
360       this.unregisterNavigablePages();
361       this.addNavigableElement(this.slidingView.activeElement);
362     }
363     // make sure the element has focus
364     if (this.viewWasProcessed) {
365       TKSpatialNavigationManager.sharedManager.highlightElement(this.slidingView.activeElement);
366     }
367   }
368   //
369   this.pageControl.currentPage = index;
370   this.pageWasHighlighted(index);
371   // update the states of previous and next buttons
372   this.syncPageButtons();
373 };
374 
375 TKPageSliderController.prototype.slidingViewDidBlurElementAtIndex = function (view, index) {
376   if (this.highlightsFocusedPage) {
377     this.unregisterNavigablePages();
378   }
379 };
380 
381 TKPageSliderController.prototype.slidingViewDidSelectActiveElement = function (view, index) {
382   this.pageWasSelected(index);
383 };
384 
385 TKPageSliderController.prototype.slidingViewStyleForItemAtIndex = function (view, index) {
386   return this.styleForPageAtIndex(index);
387 };
388 
389 TKPageSliderController.prototype.slidingViewDidHoverElementAtIndex = function (view, index) {
390   this.pageWasHovered(index);
391 };
392 
393 TKPageSliderController.prototype.slidingViewDidUnhoverElementAtIndex = function (view, index) {
394   this.pageWasUnhovered(index);
395 };
396 
397 /* ==================== Placeholder Methods ==================== */
398 
399 /**
400  *  Triggered as the {@link #highlightedPageIndex} property has changed when a new page became focused.
401  *
402  *  @param {int} index The index of the newly focused page
403  */
404 TKPageSliderController.prototype.pageWasHighlighted = function (index) {};
405 
406 /**
407  *  Triggered as the focused page was selected by the user, either from clicking on the page or using the play/pause remote key.
408  *
409  *  @param {int} index The index of the activated page
410  */
411 TKPageSliderController.prototype.pageWasSelected = function (index) {};
412 
413 TKPageSliderController.prototype.pageWasHovered = function (index) {};
414 
415 TKPageSliderController.prototype.pageWasUnhovered = function (index) {};
416 
417 /**
418  *  This method allows to provide custom style rules for a page programatically any time the {@link #highlightedPageIndex} property changes. The values in this
419  *  array are expected to be individual two-value arrays, where the first index holds the CSS property name, and the second index its value.
420  *
421  *  @param {Array} index The index of the page for which we are trying to obtain custom styles.
422  */
423 TKPageSliderController.prototype.styleForPageAtIndex = function (index) {
424   return [];
425 };
426 
427 /* ==================== TKPageControl Protocol ==================== */
428 
429 TKPageSliderController.prototype.pageControlDidUpdateCurrentPage = function (control, newPageIndex) {
430   this.slidingView.activeElementIndex = newPageIndex;
431   this.pageControl.updateCurrentPageDisplay();
432 };
433 
434 /* ==================== Archival ==================== */
435 
436 TKPageSliderController.prototype.archive = function () {
437   var archive = this.callSuper();
438   archive.highlightedPageIndex = this.highlightedPageIndex;
439   return archive;
440 };
441 
442 TKClass(TKPageSliderController);
443