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