1 /*
  2  *  Copyright © 2009 Apple Inc. All rights reserved.
  3  */
  4 
  5 // ---------------------------------------------------
  6 // A menu grid implementation
  7 // ---------------------------------------------------
  8 
  9 // data source method names
 10 const TKGridViewNumberOfElements = 'gridViewNumberOfElements';
 11 const TKGridViewElementAtIndex = 'gridViewElementAtIndex';
 12 const TKGridViewHighlightElement = 'gridViewHighlightElement';
 13 
 14 // delegate method names
 15 const TKGridViewDidFocusElementAtIndex = 'gridViewDidFocusElementAtIndex';
 16 const TKGridViewDidBlurElementAtIndex = 'gridViewDidBlurElementAtIndex';
 17 
 18 // css protocol
 19 const TKGridViewCSSClass = 'grid-view';
 20 const TKGridViewCSSContainerClass = 'grid-view-container';
 21 const TKGridViewCSSElementClass = 'grid-view-element';
 22 const TKGridViewCSSHighlightClass = 'grid-view-highlight-element';
 23 const TKGridViewCSSFocusedClass = 'grid-view-element-focused';
 24 
 25 // fill order
 26 // This defines the way the elements are inserted into the grid, and
 27 // also the direction in which they will overflow
 28 const TKGridViewFillOrderColumnFirst = 'gridViewFillOrderColumnFirst';
 29 const TKGridViewFillOrderRowFirst = 'gridViewFillOrderRowFirst';
 30 
 31 TKGridView.synthetizes = ['dataSource', 'delegate',
 32                           'activeElementIndex', 
 33                           'numRows', 'numColumns',
 34                           'elementWidth', 'elementHeight',
 35                           'visibleRows', 'visibleColumns',
 36                           'drawHighlightAbove', 'fillOrder'];
 37 
 38 function TKGridView (element) {
 39   this.callSuper();
 40   //
 41   this._numRows = 0;
 42   this._numColumns = 0;
 43   // these defaults look ok for 4x2 elements 180 square
 44   this._elementWidth = 200;
 45   this._elementHeight = 200;
 46   this._visibleRows = 2;
 47   this._visibleColumns = 2;
 48   this._drawHighlightAbove = true;
 49   this._activeElementIndex = 0;
 50   this._fillOrder = TKGridViewFillOrderColumnFirst;
 51   
 52   this.currentTopRow = 0;
 53   this.currentRow = 0;
 54   this.currentLeftColumn = 0;
 55   this.currentColumn = 0;
 56   
 57   if (element) {
 58     this.element = element;
 59   } else {
 60     // create the element we'll use as a container
 61     this.element = document.createElement("div");
 62   }
 63   this.element.addClassName(TKGridViewCSSClass);
 64 }
 65 
 66 TKGridView.prototype.init = function () {
 67 
 68   if (!this.dataSource ||
 69       !TKUtils.objectHasMethod(this.dataSource, TKGridViewNumberOfElements) ||
 70       !TKUtils.objectHasMethod(this.dataSource, TKGridViewElementAtIndex)) {
 71     return;
 72   }
 73 
 74   // create the grid container
 75   this.gridContainer = document.createElement("div");
 76   this.gridContainer.addClassName(TKGridViewCSSContainerClass);
 77   this.element.appendChild(this.gridContainer);
 78 
 79   var numElements = this.dataSource[TKGridViewNumberOfElements](this);
 80 
 81   for (var i=0; i < numElements; i++) {
 82     var el = this.dataSource[TKGridViewElementAtIndex](this, i);
 83     el.addClassName(TKGridViewCSSElementClass);
 84     this.gridContainer.appendChild(el);
 85   }
 86   
 87   // FIXME: this will fail later. Need to clean it up.
 88   if (TKUtils.objectHasMethod(this.dataSource, TKGridViewHighlightElement)) {
 89     // add the highlight (last, so it shows above other things)
 90     el = this.dataSource[TKGridViewHighlightElement](this);
 91     el.addClassName(TKGridViewCSSHighlightClass);
 92     if (this._drawHighlightAbove) {
 93       this.element.appendChild(el);
 94     } else {
 95       this.element.insertBefore(el, this.element.firstChild);
 96     }
 97   }
 98   
 99   this.layout();
100 };
101 
102 TKGridView.prototype.setNumRows = function (newNumRows) {
103   this._numRows = newNumRows;
104   this.layout();
105 };
106 
107 TKGridView.prototype.setNumColumns = function (newNumColumns) {
108   this._numColumns = newNumColumns;
109   this.layout();
110 };
111 
112 TKGridView.prototype.setElementWidth = function (newElementWidth) {
113   this._elementWidth = newElementWidth;
114   this.layout();
115 };
116 
117 TKGridView.prototype.setElementHeight = function (newElementHeight) {
118   this._elementHeight = newElementHeight;
119   this.layout();
120 };
121 
122 TKGridView.prototype.setActiveElementIndex = function (newActiveElementIndex) {
123   
124   if (newActiveElementIndex >= 0 &&
125       newActiveElementIndex < this.dataSource[TKGridViewNumberOfElements](this) &&
126       newActiveElementIndex != this._activeElementIndex) {
127 
128     // call delegate to inform blur of current active element
129     if (TKUtils.objectHasMethod(this.delegate, TKGridViewDidBlurElementAtIndex)) {
130       this.delegate[TKGridViewDidBlurElementAtIndex](this, this._activeElementIndex);
131     }
132 
133     // remove focused class from current active element
134     var activeElement = this.dataSource[TKGridViewElementAtIndex](this, this._activeElementIndex);
135     activeElement.removeClassName(TKGridViewCSSFocusedClass);
136     
137     this._activeElementIndex = newActiveElementIndex;
138     
139     // call delegate to inform focus of new active element
140     if (TKUtils.objectHasMethod(this.delegate, TKGridViewDidFocusElementAtIndex)) {
141       this.delegate[TKGridViewDidFocusElementAtIndex](this, this._activeElementIndex);
142     }
143 
144     // add focused class to new active element
145     activeElement = this.dataSource[TKGridViewElementAtIndex](this, this._activeElementIndex);
146     activeElement.addClassName(TKGridViewCSSFocusedClass);
147 
148     this.positionContainer();
149     this.positionHighlight();
150   }
151 };
152 
153 TKGridView.prototype.moveLeft = function () {
154   if (this.canMoveLeft()) {
155     if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
156       this.activeElementIndex--;
157     } else {
158       this.activeElementIndex -= this.numRows;
159     }
160   }
161 };
162 
163 TKGridView.prototype.canMoveLeft = function () {
164   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
165     return (this._activeElementIndex > 0 &&
166             this._activeElementIndex % this._numColumns != 0);
167   } else {
168     return (this._activeElementIndex >= this._numRows);
169   }
170 };
171 
172 TKGridView.prototype.moveRight = function () {
173   if (this.canMoveRight()) {
174     if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
175       this.activeElementIndex++;
176     } else {
177       this.activeElementIndex += this.numRows;
178     }
179   }
180 };
181 
182 TKGridView.prototype.canMoveRight = function () {
183   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
184     return (this._activeElementIndex < (this.dataSource[TKGridViewNumberOfElements](this) - 1) &&
185             this._activeElementIndex % this._numColumns != (this._numColumns - 1));
186   } else {
187     return (this._activeElementIndex < (this.dataSource[TKGridViewNumberOfElements](this) - this._numRows));
188   }
189 };
190 
191 TKGridView.prototype.moveUp = function () {
192   if (this.canMoveUp()) {
193     if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
194       this.activeElementIndex -= this.numColumns;
195     } else {
196       this.activeElementIndex--;
197     }
198   }
199 };
200 
201 TKGridView.prototype.canMoveUp = function () {
202   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
203     return (this._activeElementIndex >= this._numColumns);
204   } else {
205     return (this._activeElementIndex > 0 &&
206             this._activeElementIndex % this._numRows != 0);
207   }
208 };
209 
210 TKGridView.prototype.moveDown = function () {
211   if (this.canMoveDown()) {
212     if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
213       this.activeElementIndex += this.numColumns;
214     } else {
215       this.activeElementIndex++;
216     }
217   }
218 };
219 
220 TKGridView.prototype.canMoveDown = function () {
221   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
222     return (this._activeElementIndex < (this.dataSource[TKGridViewNumberOfElements](this) - this._numColumns));
223   } else {
224     return (this._activeElementIndex < (this.dataSource[TKGridViewNumberOfElements](this) - 1) &&
225             this._activeElementIndex % this._numRows != (this._numRows - 1));
226   }
227 };
228 
229 TKGridView.prototype.layout = function () {
230   this.positionCells();
231   this.positionContainer();
232   this.positionHighlight();
233 };
234 
235 TKGridView.prototype.positionCells = function () {
236   if (!this.dataSource) {
237     return;
238   }
239   
240   var numElements = this.dataSource[TKGridViewNumberOfElements](this);
241 
242   for (var i=0; i < numElements; i++) {
243     var x = this.gridXForElement(i);
244     var y = this.gridYForElement(i);
245     this.dataSource[TKGridViewElementAtIndex](this, i).style.webkitTransform = "translate(" + x + "px, " + y + "px)";
246   }
247 };
248 
249 TKGridView.prototype.positionContainer = function () {
250   // if the highlight is outside the current viewable selection
251   // then adjust the position of the internal container (ie. scroll)
252   var offsetX = 0;
253   var offsetY = 0;
254 
255   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
256     this.currentColumn = this._activeElementIndex % this._numColumns;
257     this.currentRow = Math.floor(this._activeElementIndex / this._numColumns);
258     if (this.currentTopRow <= (this.currentRow - this._visibleRows)) {
259       this.currentTopRow = this.currentRow - this._visibleRows + 1;
260     } else if (this.currentTopRow > this.currentRow) {
261       this.currentTopRow = this.currentRow;
262     }
263     offsetY = this.currentTopRow * this._elementHeight;
264   } else {
265     this.currentColumn = Math.floor(this._activeElementIndex / this._numRows);
266     this.currentRow = this._activeElementIndex % this._numRows;
267     if (this.currentLeftColumn <= (this.currentColumn - this._visibleColumns)) {
268       this.currentLeftColumn = this.currentColumn - this._visibleColumns + 1;
269     } else if (this.currentLeftColumn > this.currentColumn) {
270       this.currentLeftColumn = this.currentColumn;
271     }
272     offsetX = this.currentLeftColumn * this._elementWidth;
273   }
274   if (this.gridContainer) {
275     this.gridContainer.style.webkitTransform = "translate(-" + offsetX + "px, -" + offsetY + "px)";
276   }
277 };
278 
279 TKGridView.prototype.positionHighlight = function () {
280   if (!this.dataSource) {
281     return;
282   }
283   
284   var x;
285   var y;
286 
287   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
288     x = this.gridXForElement(this._activeElementIndex);
289     y = (Math.floor(this._activeElementIndex / this._numColumns) - this.currentTopRow) * this._elementHeight;
290   } else {
291     x = (Math.floor(this._activeElementIndex / this._numRows) - this.currentLeftColumn) * this._elementWidth;
292     y = this.gridYForElement(this._activeElementIndex);
293   }
294 
295   this.dataSource[TKGridViewHighlightElement](this).style.webkitTransform = "translate(" + x + "px, " + y + "px)";
296 };
297 
298 TKGridView.prototype.gridXForElement = function (index) {
299   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
300     return (index % this._numColumns) * this._elementWidth;
301   } else {
302     return Math.floor(index / this._numRows) * this._elementWidth;
303   }
304 };
305 
306 TKGridView.prototype.gridYForElement = function (index) {
307   if (this._fillOrder == TKGridViewFillOrderColumnFirst) {
308     return Math.floor(index / this._numColumns) * this._elementHeight;
309   } else {
310     return (index % this._numRows) * this._elementHeight;
311   }
312 };
313 
314 TKClass(TKGridView);
315 
316 /* ====================== Datasource helper ====================== */
317 
318 function TKGridViewDataSourceHelper(data, highlightData) {
319   this.data = data;
320   this.highlightData = highlightData;
321   this.elements = [];
322   this.highlight = null;
323 };
324 
325 TKGridViewDataSourceHelper.prototype.gridViewNumberOfElements = function(view) {
326   if (this.data) {
327     return this.data.length;
328   } else {
329     return 0;
330   }
331 };
332 
333 TKGridViewDataSourceHelper.prototype.gridViewElementAtIndex = function(view, index) {
334   if (!this.data || index >= this.data.length) {
335     return null;
336   }
337   if (!this.elements[index]) {
338     var source = this.data[index];
339     var element = TKUtils.buildElement(source);
340     this.elements[index] = element;
341   }
342   return this.elements[index];
343 };
344 
345 TKGridViewDataSourceHelper.prototype.gridViewHighlightElement = function(view) {
346   if (!this.highlightData) {
347     return null;
348   }
349   if (!this.highlight) {
350     var element = TKUtils.buildElement(this.highlightData);
351     this.highlight = element;
352   }
353   return this.highlight;
354 };
355 
356 /* ====================== Declarative helper ====================== */
357 
358 TKGridView.buildGridView = function(element, data) {
359   if (TKUtils.objectIsUndefined(data) || !data || data.type != "TKGridView") {
360     return null;
361   }
362 
363   var gridView = new TKGridView(element);
364 
365   if (!TKUtils.objectIsUndefined(data.elements)) {
366     gridView.dataSource = new TKGridViewDataSourceHelper(data.elements, data.highlight);
367   }
368 
369   TKGridView.synthetizes.forEach(function(prop) {
370     if (prop != "dataSource" && prop != "delegate") {
371       if (!TKUtils.objectIsUndefined(data[prop])) {
372         gridView[prop] = data[prop];
373       }
374     }
375   });
376 
377   return gridView;
378 };
379 
380