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