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