1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 // ---------------------------------------------------
  6 // A two dimensional sliding view
  7 // ---------------------------------------------------
  8 
  9 /**
 10  *  @class
 11  *  @name TKSlidingViewData
 12  *  @since TuneKit 1.0
 13  */
 14 
 15 /**
 16  *  @name TKSlidingViewData.prototype
 17  *
 18  *  @property {int} sideElementsVisible The number of elements on each side of the selected page. If the {@link #incrementalLoading} property is set to
 19  *  <code>true</code>, this specifies the number of elements in the DOM on each side as others beyond that range are removed from the tree. Otherwise, this
 20  *  specifies the threshold after which elements in either direction from the selected page get the <code>sliding-view-element-hidden</code> CSS class applied.
 21  *
 22  *  @property {int} distanceBetweenElements The distance in pixels between the center points of each page, essentially the overall width of each page.
 23  *
 24  *  @property {int} sideOffsetBefore Any additional margin in pixels to the left of each page.
 25  *
 26  *  @property {int} sideOffsetAfter Any additional margin in pixels to the right of each page.
 27  *
 28  *  @property {Array} elements The elements for the sliding view.
 29  *
 30  *  @property {bool} incrementalLoading Indicates whether elements in the sliding view's DOM are added and removed gradually as we browse through it, or all
 31  *  available in one go, which is the default. If you are populating a sliding view with a large amount of content, you should consider setting this property
 32  *  to <code>true</code> in order to ease memory constraints and enhance performance.
 33  *
 34  *  @property {bool} loops Whether we loop through pages. Defaults to <code>false</code>.
 35  *
 36  */
 37 
 38 // data source method names
 39 const TKSlidingViewNumberOfElements = 'slidingViewNumberOfElements';
 40 const TKSlidingViewElementAtIndex = 'slidingViewElementAtIndex';
 41 
 42 // delegate method names
 43 const TKSlidingViewStyleForItemAtIndex = 'slidingViewStyleForItemAtIndex';
 44 const TKSlidingViewDidSelectActiveElement = 'slidingViewDidSelectActiveElement';
 45 const TKSlidingViewDidFocusElementAtIndex = 'slidingViewDidFocusElementAtIndex';
 46 const TKSlidingViewDidBlurElementAtIndex = 'slidingViewDidBlurElementAtIndex';
 47 const TKSlidingViewDidHideElementAtIndex = 'slidingViewDidHideElementAtIndex'; // TODO: XXX
 48 const TKSlidingViewWillUnhideElementAtIndex = 'slidingViewWillUnhideElementAtIndex'; // TODO: XXX
 49 
 50 const TKSlidingViewDidHoverElementAtIndex = 'slidingViewDidHoverElementAtIndex';
 51 const TKSlidingViewDidUnhoverElementAtIndex = 'slidingViewDidUnhoverElementAtIndex';
 52 
 53 // css protocol
 54 const TKSlidingViewCSSContainerClass = 'sliding-view';
 55 const TKSlidingViewCSSElementClass = 'sliding-view-element';
 56 const TKSlidingViewCSSFocusedClass = 'sliding-view-element-focused';
 57 const TKSlidingViewCSSSideBeforeClass = 'sliding-view-element-before';
 58 const TKSlidingViewCSSSideAfterClass = 'sliding-view-element-after';
 59 const TKSlidingViewCSSStagedBeforeClass = 'sliding-view-element-staged-before';
 60 const TKSlidingViewCSSStagedAfterClass = 'sliding-view-element-staged-after';
 61 const TKSlidingViewCSSHiddenClass = 'sliding-view-element-hidden';
 62 
 63 // orientations
 64 const TKSlidingViewOrientationHorizontal = 'horizontal';
 65 const TKSlidingViewOrientationVertical = 'vertical';
 66 
 67 TKSlidingView.synthetizes = ['dataSource',
 68                              'delegate',
 69                              'activeElement',
 70                              'activeElementIndex', // the index of the middle/focused element
 71                              'orientation', // whether the view should be horizontal or vertical
 72                              'interactive', // whether or not this view will listen for mouse events
 73                              'sideOffsetBefore', // gap between focused element and the elements before it
 74                              'sideOffsetAfter', // gap between focused element and the elements after it
 75                              'distanceBetweenElements', // general distance between elements in the layout
 76                              'sideElementsVisible', // the number of elements that are considered visible before and after the focus
 77                              'pageControl', // a TKPageControl object that should be linked to this slider (not needed)
 78                              'incrementalLoading', // whether or not the elements should be added to the view as required
 79                              'loops', // if true, the sliding view loops continuously
 80                              'raiseHoverEvents', // if true, the sliding view will use the Hover and Unhover delegate methods
 81                              'numberOfElements'];
 82 
 83 function TKSlidingView (element) {
 84   this.callSuper();
 85   // these defaults look ok for elements about 180 square
 86   this._activeElementIndex = 0;
 87   this._orientation = TKSlidingViewOrientationHorizontal;
 88   this._interactive = true;
 89   this._sideOffsetBefore = 160;
 90   this._sideOffsetAfter = 160;
 91   this._distanceBetweenElements = 25;
 92   this._sideElementsVisible = 4;
 93   this._pageControl = null;
 94   this._incrementalLoading = false;
 95   this._loops = false;
 96   this._raiseHoverEvents = false;
 97   
 98   this._elements = [];
 99   
100   if (element) {
101     this.element = element;
102   } else {
103     // create the element we'll use as a container
104     this.element = document.createElement("div");
105   }
106   this.element.addClassName(TKSlidingViewCSSContainerClass);
107 }
108 
109 TKSlidingView.prototype.init = function () {
110 
111   if (!this.dataSource ||
112       !TKUtils.objectHasMethod(this.dataSource, TKSlidingViewNumberOfElements) ||
113       !TKUtils.objectHasMethod(this.dataSource, TKSlidingViewElementAtIndex)) {
114     return;
115   }
116 
117   var numElements = this.numberOfElements;
118 
119   if (this._incrementalLoading) {
120     // add enough to be visible
121     this.bufferElements();
122   } else {
123     // add all the elements
124     for (var i=0; i < numElements; i++) {
125       var el = this.dataSource[TKSlidingViewElementAtIndex](this, i);
126       el.addClassName(TKSlidingViewCSSElementClass);
127       el._needsAppending = true;
128       this._elements[i] = el;
129       el._slidingViewIndex = i;
130       if (this._interactive) {
131         el.addEventListener("click", this, false);
132       }
133       if (this._raiseHoverEvents) {
134         el.addEventListener("mouseover", this, false);
135         el.addEventListener("mouseout", this, false);
136       }
137     }
138   }
139   
140   this.layout(true);
141 };
142 
143 TKSlidingView.prototype.setPageControl = function (newPageControl) {
144   this._pageControl = newPageControl;
145   this._pageControl.deferCurrentPageDisplay = true;
146   this._pageControl.delegate = this;
147   this._pageControl.currentPage = this._activeElementIndex;
148 };
149 
150 TKSlidingView.prototype.getActiveElement = function () {
151   return this._elements[this._activeElementIndex];
152 };
153 
154 TKSlidingView.prototype.setActiveElementIndex = function (newActiveElementIndex) {
155   
156   if ((this._loops || (newActiveElementIndex >= 0 && newActiveElementIndex < this.numberOfElements)) &&
157       newActiveElementIndex != this._activeElementIndex) {
158 
159     var needsForcedLayout = (this._activeElementIndex === undefined);
160 
161     // call delegate to inform blur of current active element
162     if (!needsForcedLayout && TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidBlurElementAtIndex)) {
163       this.delegate[TKSlidingViewDidBlurElementAtIndex](this, this._activeElementIndex);
164     }
165 
166     if (newActiveElementIndex < 0) {
167       this._activeElementIndex = (this.numberOfElements + newActiveElementIndex) % this.numberOfElements;
168     } else {
169       this._activeElementIndex = newActiveElementIndex % this.numberOfElements;
170     }
171 
172     // if there is a page control, tell it to update
173     if (this._pageControl) {
174       this._pageControl.currentPage = newActiveElementIndex;
175     }
176     
177     this.bufferElements();
178     this.layout(needsForcedLayout);
179 
180     // call delegate to inform focus of new active element
181     // this needs to be done at the very end so we're use any code depending on
182     // .activeElement actually works since we need elements buffered
183     if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidFocusElementAtIndex)) {
184       this.delegate[TKSlidingViewDidFocusElementAtIndex](this, this._activeElementIndex);
185     }
186   }
187 };
188 
189 TKSlidingView.prototype.getNumberOfElements = function () {
190   if (this.dataSource) {
191     return this.dataSource[TKSlidingViewNumberOfElements](this);
192   } else {
193     return 0;
194   }
195 };
196 
197 TKSlidingView.prototype.getElementAtIndex = function (index) {
198   return this._elements[index];
199 };
200 
201 // this method loads the elements that are necessary for
202 // display, and removes the ones that are not needed
203 TKSlidingView.prototype.bufferElements = function () {
204   if (this._incrementalLoading) {
205     var numElements = this.numberOfElements;
206     for (var i=0; i < numElements; i++) {
207       var offset = this._activeElementIndex - i;
208       var absOffset = Math.abs(offset);
209       if (this._loops) {
210         // FIXME: check this! doesn't seem right
211         var offset2 = offset + ((offset < 0) ? numElements : -numElements);
212         var absOffset2 = Math.abs(offset2);
213         if (absOffset2 <= this._sideElementsVisible) {
214           offset = offset2;
215           absOffset = absOffset2;
216         }
217       }
218       if (absOffset <= this._sideElementsVisible) {
219         if (!this._elements[i]) {
220           var el = this.dataSource[TKSlidingViewElementAtIndex](this, i);
221           el.addClassName(TKSlidingViewCSSElementClass);
222           el._needsAppending = true;
223           this._elements[i] = el;
224           el._slidingViewIndex = i;
225           if (this._interactive) {
226             el.addEventListener("click", this, false);
227           }
228           if (this._raiseHoverEvents) {
229             el.addEventListener("mouseover", this, false);
230             el.addEventListener("mouseout", this, false);
231           }
232           
233         }
234       } else {
235         // element isn't needed
236         if (this._elements[i]) {
237           this.element.removeChild(this._elements[i]);
238           this._elements[i] = null;
239         }
240       }
241     }
242   }
243 };
244 
245 TKSlidingView.prototype.layout = function (forceLayout) {
246   var numElements = this.numberOfElements;
247 
248   for (var i=0; i < numElements; i++) {
249     var offset = this._activeElementIndex - i;
250     var absOffset = Math.abs(offset);
251     if (this._loops) {
252       // FIXME: check this! doesn't seem right
253       var offset2 = offset + ((offset < 0) ? numElements : -numElements);
254       var absOffset2 = Math.abs(offset2);
255       if (absOffset2 <= this._sideElementsVisible) {
256         offset = offset2;
257         absOffset = absOffset2;
258       }
259     }
260 
261     var element = this._elements[i];
262 
263     if (!element) {
264       // only layout elements we need to
265       continue;
266     }
267 
268     // loaded elements might not yet have been added to the document
269     // this makes them appear in the right place
270     if (element._needsAppending) {
271       this.element.appendChild(element);
272       element._needsAppending = false;
273     }
274 
275     // Three cases for layout:
276     //    - element is inside (visible)
277     //    - element is just outside (one element outside each edge - called "staged")
278     //    - element is really outside (we call this "hidden" and inform delegate)
279 
280     var transform = null;
281     if (absOffset <= this._sideElementsVisible) {
282       if (offset > 0) {
283         if (this._orientation == TKSlidingViewOrientationHorizontal) {
284           transform = "translate3d(" + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0, 0)";
285         } else {
286           transform = "translate3d(0, " + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0)";
287         }
288         this.applySlidingClass(element, TKSlidingViewCSSSideBeforeClass);
289       } else if (offset < 0) {
290         if (this._orientation == TKSlidingViewOrientationHorizontal) {
291           transform = "translate3d(" + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0, 0)";
292         } else {
293           transform = "translate3d(0, " + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0)";
294         }
295         this.applySlidingClass(element, TKSlidingViewCSSSideAfterClass);
296       } else {
297         transform = "translate3d(0, 0, 0)";
298         this.applySlidingClass(element, TKSlidingViewCSSFocusedClass);
299       }
300       element.style.webkitTransform = transform;
301       element.style.opacity = 1;
302     } else if (absOffset == (this._sideElementsVisible + 1)) {
303       // FIXME: this is wrong!! should be staged classes - worried this will break things if I fix it
304       if (offset > 0) {
305         if (this._orientation == TKSlidingViewOrientationHorizontal) {
306           transform = "translate3d(" + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0, 0)";
307         } else {
308           transform = "translate3d(0, " + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0)";
309         }
310         this.applySlidingClass(element, TKSlidingViewCSSSideBeforeClass);
311       } else {
312         if (this._orientation == TKSlidingViewOrientationHorizontal) {
313           transform = "translate3d(" + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0, 0)";
314         } else {
315           transform = "translate3d(0, " + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0)";
316         }
317         this.applySlidingClass(element, TKSlidingViewCSSSideAfterClass);
318       }
319       element.style.webkitTransform = transform;
320       element.style.opacity = 0;
321     } else if (absOffset > this._sideElementsVisible || forceLayout) {
322       if (offset > 0) {
323         if (this._orientation == TKSlidingViewOrientationHorizontal) {
324           transform = "translate3d(" + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0, 0)";
325         } else {
326           transform = "translate3d(0, " + (-1 * (absOffset * this._distanceBetweenElements + this._sideOffsetBefore)) + "px, 0)";
327         }
328       } else {
329         if (this._orientation == TKSlidingViewOrientationHorizontal) {
330           transform = "translate3d(" + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0, 0)";
331         } else {
332           transform = "translate3d(0, " + (absOffset * this._distanceBetweenElements + this._sideOffsetAfter) + "px, 0)";
333         }
334       }
335       this.applySlidingClass(element, TKSlidingViewCSSHiddenClass);
336       element.style.webkitTransform = transform;
337       element.style.opacity = 0;
338     }
339     // now see if we have any over-ride styles to apply from the delegate
340     if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewStyleForItemAtIndex)) {
341       override_styles = this.delegate[TKSlidingViewStyleForItemAtIndex](this, i);
342       for (var j = 0; j < override_styles.length; j++) {
343         var override_style = override_styles[j];
344         element.style.setProperty(override_style[0], override_style[1], '');
345       }
346     }
347   }
348 };
349 
350 TKSlidingView.prototype.applySlidingClass = function (element, className) {
351   element.removeClassName(TKSlidingViewCSSFocusedClass);
352   element.removeClassName(TKSlidingViewCSSSideBeforeClass);
353   element.removeClassName(TKSlidingViewCSSSideAfterClass);
354   element.removeClassName(TKSlidingViewCSSStagedBeforeClass);
355   element.removeClassName(TKSlidingViewCSSStagedAfterClass);
356   element.removeClassName(TKSlidingViewCSSHiddenClass);
357   
358   element.addClassName(className);
359 };
360 
361 TKSlidingView.prototype.handleEvent = function (event) {
362   switch (event.type) {
363     case "click":
364       this.handleClick(event);
365       break;
366     case "mouseover":
367       this.handleMouseover(event);
368       break;
369     case "mouseout":
370       this.handleMouseout(event);
371       break;
372     default:
373       debug("unhandled event type in TKSlidingView: " + event.type);
374   }
375 };
376 
377 TKSlidingView.prototype.handleClick = function (event) {
378   // The event.target should have an _slidingViewIndex property. If
379   // not, then go up to parent
380   var target = event.target;
381   while (target && TKUtils.objectIsUndefined(target._slidingViewIndex)) {
382     target = target.parentNode;
383   }
384   if (!target) {
385     return;
386   }
387   
388   if (target._slidingViewIndex == this.activeElementIndex) {
389     if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidSelectActiveElement)) {
390       this.delegate[TKSlidingViewDidSelectActiveElement](this, this._activeElementIndex);
391     }
392   } else {
393     // Check if the click was before or after the focused element.
394     if (target._slidingViewIndex < this.activeElementIndex) {
395       if (this._loops && target._slidingViewIndex == 0) {
396         this.activeElementIndex = 0;
397       } else {
398         this.activeElementIndex--;
399       }
400     } else {
401       if (this._loops && target._slidingViewIndex == this.numberOfElements - 1) {
402         this.activeElementIndex = this.numberOfElements - 1;
403       } else {
404         this.activeElementIndex++;
405       }
406     }
407   }
408 };
409 
410 TKSlidingView.prototype.handleMouseover = function (event) {
411   // The event.target should have an _slidingViewIndex property. If
412   // not, then go up to parent
413   var target = event.target;
414   while (target && TKUtils.objectIsUndefined(target._slidingViewIndex)) {
415     target = target.parentNode;
416   }
417   if (!target) {
418     return;
419   }
420   
421   if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidHoverElementAtIndex)) {
422     this.delegate[TKSlidingViewDidHoverElementAtIndex](this, target._slidingViewIndex);
423   }
424 };
425 
426 TKSlidingView.prototype.handleMouseout = function (event) {
427   // The event.target should have an _slidingViewIndex property. If
428   // not, then go up to parent
429   var target = event.target;
430   while (target && TKUtils.objectIsUndefined(target._slidingViewIndex)) {
431     target = target.parentNode;
432   }
433   if (!target) {
434     return;
435   }
436   
437   if (TKUtils.objectHasMethod(this.delegate, TKSlidingViewDidUnhoverElementAtIndex)) {
438     this.delegate[TKSlidingViewDidUnhoverElementAtIndex](this, target._slidingViewIndex);
439   }
440 };
441 
442 // delegate for page control
443 TKSlidingView.prototype.pageControlDidUpdateCurrentPage = function (control, newPageIndex) {
444   if (control === this._pageControl) {
445     this.activeElementIndex = newPageIndex;
446     this._pageControl.updateCurrentPageDisplay();
447   }
448 };
449 
450 
451 TKClass(TKSlidingView);
452 
453 /* ====================== Datasource helper ====================== */
454 
455 function TKSlidingViewDataSourceHelper(data, incrementalLoading) {
456   this.data = data;
457   this.incrementalLoading = incrementalLoading;
458   this.elements = [];
459 };
460 
461 TKSlidingViewDataSourceHelper.prototype.slidingViewNumberOfElements = function(view) {
462   if (this.data) {
463     return this.data.length;
464   } else {
465     return 0;
466   }
467 };
468 
469 TKSlidingViewDataSourceHelper.prototype.slidingViewElementAtIndex = function(view, index) {
470   if (!this.data || index >= this.data.length) {
471     return null;
472   }
473   var element = this.elements[index];
474   if (!element) {
475     var source = this.data[index];
476     element = TKUtils.buildElement(source);
477   }
478   if (!this.incrementalLoading) {
479     this.elements[index] = element;
480   }
481   return element;
482 };
483 
484 /* ====================== Declarative helper ====================== */
485 
486 TKSlidingView.buildSlidingView = function(element, data) {
487   if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKSlidingView") {
488     return null;
489   }
490 
491   var slidingView = new TKSlidingView(element);
492   if (!TKUtils.objectIsUndefined(data.elements)) {
493     slidingView.dataSource = new TKSlidingViewDataSourceHelper(data.elements, data.incrementalLoading);
494   }
495 
496   TKSlidingView.synthetizes.forEach(function(prop) {
497     if (prop != "dataSource" && prop != "delegate") {
498       if (!TKUtils.objectIsUndefined(data[prop])) {
499         slidingView[prop] = data[prop];
500       }
501     }
502   });
503 
504   return slidingView;
505 };
506 
507