1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 // ---------------------------------------------------
  6 // A two dimensional view with absolutely positioned elements
  7 // ---------------------------------------------------
  8 
  9 // data source method names
 10 const TKPositioningViewNumberOfElements = 'positioningViewNumberOfElements';
 11 const TKPositioningViewElementAtIndex = 'positioningViewElementAtIndex';
 12 const TKPositioningViewPositionAtIndex = 'positioningViewPositionAtIndex';  // expects {x: y:} to be returned
 13 const TKPositioningViewViewportPositionAtIndex = 'positioningViewViewportPositionAtIndex';  // expects {x: y:} to be returned
 14 const TKPositioningViewViewportScaleAtIndex = 'positioningViewViewportScaleAtIndex';  // expects a float returned
 15 
 16 // delegate method names
 17 const TKPositioningViewDidSelectActiveElement = 'positioningViewDidSelectActiveElement';
 18 const TKPositioningViewDidFocusElementAtIndex = 'positioningViewDidFocusElementAtIndex';
 19 const TKPositioningViewDidBlurElementAtIndex = 'positioningViewDidBlurElementAtIndex';
 20 const TKPositioningViewDidHideElementAtIndex = 'positioningViewDidHideElementAtIndex'; // TODO: XXX
 21 const TKPositioningViewWillUnhideElementAtIndex = 'positioningViewWillUnhideElementAtIndex'; // TODO: XXX
 22 
 23 // css protocol
 24 const TKPositioningViewCSSRootClass = 'positioning-view';
 25 const TKPositioningViewCSSContainerClass = 'positioning-view-container';
 26 const TKPositioningViewCSSElementClass = 'positioning-view-element';
 27 
 28 const TKPositioningViewCSSFocusedClass = 'positioning-view-element-focused';
 29 const TKPositioningViewCSSBeforeClass = 'positioning-view-element-before';
 30 const TKPositioningViewCSSAfterClass = 'positioning-view-element-after';
 31 const TKPositioningViewCSSHiddenClass = 'positioning-view-element-hidden';
 32 
 33 TKPositioningView.synthetizes = ['dataSource',
 34                                  'delegate',
 35                                  'activeElementIndex', // the index of the middle/focused element
 36                                  'nonActiveElementsVisible', // the number of elements that are considered visible before and after the active element
 37                                  'pageControl', // a TKPageControl object that should be linked to this positioner (not needed)
 38                                  'interactive', // whether the view allows click to focus
 39                                  'incrementalLoading', // whether or not to load elements incrementally
 40                                  'numberOfElements'];
 41 
 42 function TKPositioningView (element) {
 43   this.callSuper();
 44   // these defaults look ok for elements about 180 square
 45   this._activeElementIndex = 0;
 46   this._nonActiveElementsVisible = 1;
 47   this._pageControl = null;
 48   this._interactive = false;
 49   this._incrementalLoading = false;
 50   
 51   this._elements = [];
 52   
 53   if (element) {
 54     this.element = element;
 55   } else {
 56     // create the element we'll use as root
 57     this.element = document.createElement("div");
 58   }
 59   this.element.addClassName(TKPositioningViewCSSRootClass);
 60   
 61   // create the positioner
 62   this.container = document.createElement("div");
 63   this.container.addClassName(TKPositioningViewCSSContainerClass);
 64   this.element.appendChild(this.container);
 65 }
 66 
 67 TKPositioningView.prototype.init = function () {
 68 
 69   if (!this.dataSource ||
 70       !TKUtils.objectHasMethod(this.dataSource, TKPositioningViewNumberOfElements) ||
 71       !TKUtils.objectHasMethod(this.dataSource, TKPositioningViewElementAtIndex) ||
 72       !TKUtils.objectHasMethod(this.dataSource, TKPositioningViewPositionAtIndex) ||
 73       !TKUtils.objectHasMethod(this.dataSource, TKPositioningViewViewportPositionAtIndex) ||
 74       !TKUtils.objectHasMethod(this.dataSource, TKPositioningViewViewportScaleAtIndex)) {
 75     debug("TKPositioningView: dataSource does not exist or does not have correct methods");
 76     return;
 77   }
 78 
 79   var numElements = this.numberOfElements;
 80 
 81   if (this._incrementalLoading) {
 82     // add enough to be visible
 83     this.bufferElements();
 84   } else {
 85     for (var i=0; i < numElements; i++) {
 86       var el = this.dataSource[TKPositioningViewElementAtIndex](this, i);
 87       el.addClassName(TKPositioningViewCSSElementClass);
 88       el._positioningViewIndex = i;
 89       el._needsAppending = true;
 90       this._elements[i] = el;
 91       if (this._interactive) {
 92         el.addEventListener("click", this, false);
 93       }
 94       var position = this.dataSource[TKPositioningViewPositionAtIndex](this, i);
 95       var translate = "translate3d(" + position.x + "px, " + position.y + "px, 0)";
 96       el.style.webkitTransform = translate;
 97       this.container.appendChild(el);
 98     }
 99   }
100   
101   this.positionContainer();
102   this.applyClassesToElements();
103 
104 };
105 
106 TKPositioningView.prototype.setPageControl = function (newPageControl) {
107   this._pageControl = newPageControl;
108   this._pageControl.deferCurrentPageDisplay = true;
109   this._pageControl.delegate = this;
110   this._pageControl.currentPage = this._activeElementIndex;
111 };
112 
113 TKPositioningView.prototype.setActiveElementIndex = function (newActiveElementIndex) {
114   if (newActiveElementIndex >= 0 &&
115       newActiveElementIndex < this.numberOfElements &&
116       newActiveElementIndex != this._activeElementIndex) {
117 
118     // call delegate to inform blur of current active element
119     if (TKUtils.objectHasMethod(this.delegate, TKPositioningViewDidBlurElementAtIndex)) {
120       this.delegate[TKPositioningViewDidBlurElementAtIndex](this, this._activeElementIndex);
121     }
122     
123     this._activeElementIndex = newActiveElementIndex;
124 
125     // call delegate to inform focus of new active element
126     if (TKUtils.objectHasMethod(this.delegate, TKPositioningViewDidFocusElementAtIndex)) {
127       this.delegate[TKPositioningViewDidFocusElementAtIndex](this, this._activeElementIndex);
128     }
129 
130     // if there is a page control, tell it to update
131     if (this._pageControl) {
132       this._pageControl.currentPage = newActiveElementIndex;
133     }
134 
135     this.bufferElements();
136     this.positionContainer();
137     this.applyClassesToElements();
138   }
139 };
140 
141 TKPositioningView.prototype.getNumberOfElements = function () {
142   return this.dataSource[TKPositioningViewNumberOfElements](this);
143 };
144 
145 // this method loads the elements that are necessary for
146 // display, and removes the ones that are not needed
147 TKPositioningView.prototype.bufferElements = function () {
148   if (this._incrementalLoading) {
149     var numElements = this.numberOfElements;
150     for (var i=0; i < numElements; i++) {
151       var offset = this._activeElementIndex - i;
152       var absOffset = Math.abs(offset);
153       if (absOffset <= this._nonActiveElementsVisible) {
154         if (!this._elements[i]) {
155           var el = this.dataSource[TKPositioningViewElementAtIndex](this, i);
156           el.addClassName(TKPositioningViewCSSElementClass);
157           el._positioningViewIndex = i;
158           el._needsAppending = true;
159           this._elements[i] = el;
160           if (this._interactive) {
161             el.addEventListener("click", this, false);
162           }
163           var position = this.dataSource[TKPositioningViewPositionAtIndex](this, i);
164           var translate = "translate3d(" + position.x + "px, " + position.y + "px, 0)";
165           el.style.webkitTransform = translate;
166           this.container.appendChild(el);
167         }
168       } else {
169         // element isn't needed
170         if (this._elements[i]) {
171           this.container.removeChild(this._elements[i]);
172           this._elements[i] = null;
173         }
174       }
175     }
176   }
177 };
178 
179 TKPositioningView.prototype.positionElements = function () {
180   var numElements = this.numberOfElements;
181 
182   for (var i=0; i < numElements; i++) {
183     
184     var element = this._elements[i];
185 
186     if (!element) {
187       // only layout elements we need to
188       continue;
189     }
190 
191     var position = this.dataSource[TKPositioningViewPositionAtIndex](this, i);
192     var translate = "translate3d(" + position.x + "px, " + position.y + "px, 0)";
193     element.style.webkitTransform = translate;
194   }
195 };
196 
197 TKPositioningView.prototype.positionContainer = function () {
198   var position = this.dataSource[TKPositioningViewPositionAtIndex](this, this._activeElementIndex);
199   var viewport = this.dataSource[TKPositioningViewViewportPositionAtIndex](this, this._activeElementIndex);
200   var scale = this.dataSource[TKPositioningViewViewportScaleAtIndex](this, this._activeElementIndex);
201   var transform = " scale(" + scale + ") translate3d(" + (-viewport.x - position.x) + "px, " + (-viewport.y - position.y) + "px, 0)";
202   this.container.style.webkitTransform = transform;
203 };
204 
205 TKPositioningView.prototype.applyClassesToElements = function () {
206   var numElements = this.numberOfElements;
207   for (var i=0; i < numElements; i++) {
208     var element = this._elements[i];
209 
210     if (!element) {
211       // only layout elements we need to
212       continue;
213     }
214 
215     var offset = this._activeElementIndex - i;
216     var absOffset = Math.abs(offset);
217     
218     if (offset == 0) {
219       this.applyClasses(element, TKPositioningViewCSSFocusedClass);
220     } else if (absOffset > this._nonActiveElementsVisible) {
221       this.applyClasses(element, TKPositioningViewCSSHiddenClass);
222     } else if (offset > 0) {
223       this.applyClasses(element, TKPositioningViewCSSAfterClass);
224     } else {
225       this.applyClasses(element, TKPositioningViewCSSBeforeClass);
226     }
227   }
228 };
229 
230 TKPositioningView.prototype.applyClasses = function (element, className) {
231   element.removeClassName(TKPositioningViewCSSFocusedClass);
232   element.removeClassName(TKPositioningViewCSSBeforeClass);
233   element.removeClassName(TKPositioningViewCSSAfterClass);
234   element.removeClassName(TKPositioningViewCSSHiddenClass);
235   
236   element.addClassName(className);
237 };
238 
239 TKPositioningView.prototype.handleEvent = function (event) {
240   switch (event.type) {
241     case "click":
242       this.handleClick(event);
243       break;
244     default:
245       debug("unhandled event type in TKPositioningView: " + event.type);
246   }
247 };
248 
249 TKPositioningView.prototype.handleClick = function (event) {
250   // The event.target should have an _positioningViewIndex property. If
251   // not, then go up to parent
252   var target = event.target;
253   while (target && TKUtils.objectIsUndefined(target._positioningViewIndex)) {
254     target = target.parentNode;
255   }
256   if (!target) {
257     return;
258   }
259   if (target._positioningViewIndex == this._activeElementIndex) {
260     if (TKUtils.objectHasMethod(this.delegate, TKPositioningViewDidSelectActiveElement)) {
261       this.delegate[TKPositioningViewDidSelectActiveElement](this, this._activeElementIndex);
262     }
263   } else {
264     this.activeElementIndex = target._positioningViewIndex;
265   }
266 };
267 
268 // delegate for page control
269 TKPositioningView.prototype.pageControlDidUpdateCurrentPage = function (control, newPageIndex) {
270   if (control === this._pageControl) {
271     this.activeElementIndex = newPageIndex;
272     this._pageControl.updateCurrentPageDisplay();
273   }
274 };
275 
276 
277 TKClass(TKPositioningView);
278 
279 /* ====================== Datasource helper ====================== */
280 
281 function TKPositioningViewDataSourceHelper(data, incrementalLoading) {
282   this.data = data;
283   this.incrementalLoading = incrementalLoading;
284   this.elements = [];
285   this.positions = [];
286   this.viewportPositions = [];
287   this.viewportScales = [];
288 };
289 
290 TKPositioningViewDataSourceHelper.prototype.positioningViewNumberOfElements = function(view) {
291   if (this.data) {
292     return this.data.length;
293   } else {
294     return 0;
295   }
296 };
297 
298 TKPositioningViewDataSourceHelper.prototype.positioningViewElementAtIndex = function(view, index) {
299   if (!this.data || index >= this.data.length) {
300     return null;
301   }
302   var element = this.elements[index];
303   if (!element) {
304     var source = this.data[index];
305     element = TKUtils.buildElement(source.element);
306   }
307   if (!this.incrementalLoading) {
308     this.elements[index] = element;
309   }
310   return element;
311 };
312 
313 TKPositioningViewDataSourceHelper.prototype.positioningViewPositionAtIndex = function(view, index) {
314   if (!this.data || index >= this.data.length) {
315     return null;
316   }
317   if (!this.positions[index]) {
318     var source = this.data[index];
319     this.positions[index] = { x: source.x, y: source.y };
320   }
321   return this.positions[index];
322 };
323 
324 TKPositioningViewDataSourceHelper.prototype.positioningViewViewportPositionAtIndex = function(view, index) {
325   if (!this.data || index >= this.data.length) {
326     return null;
327   }
328   if (!this.viewportPositions[index]) {
329     var source = this.data[index];
330     this.viewportPositions[index] = { };
331     this.viewportPositions[index].x = (TKUtils.objectIsUndefined(source.viewportx)) ? 0 : source.viewportx;
332     this.viewportPositions[index].y = (TKUtils.objectIsUndefined(source.viewporty)) ? 0 : source.viewporty;
333   }
334   return this.viewportPositions[index];
335 };
336 
337 TKPositioningViewDataSourceHelper.prototype.positioningViewViewportScaleAtIndex = function(view, index) {
338   if (!this.data || index >= this.data.length) {
339     return null;
340   }
341   if (!this.viewportScales[index]) {
342     var source = this.data[index];
343     this.viewportScales[index] = (TKUtils.objectIsUndefined(source.scale)) ? 1 : source.scale;
344   }
345   return this.viewportScales[index];
346 };
347 
348 /* ====================== Declarative helper ====================== */
349 
350 TKPositioningView.buildPositioningView = function(element, data) {
351   if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKPositioningView") {
352     return null;
353   }
354 
355   var positioningView = new TKPositioningView(element);
356 
357   if (!TKUtils.objectIsUndefined(data.elements)) {
358     positioningView.dataSource = new TKPositioningViewDataSourceHelper(data.elements, data.incrementalLoading);
359   }
360 
361   TKPositioningView.synthetizes.forEach(function(prop) {
362     if (prop != "dataSource" && prop != "delegate") {
363       if (!TKUtils.objectIsUndefined(data[prop])) {
364         positioningView[prop] = data[prop];
365       }
366     }
367   });
368 
369   return positioningView;
370 };
371 
372