1 /* 2 * Copyright © 2009 Apple Inc. All rights reserved. 3 */ 4 5 const TKPageSliderControllerContainerCSSClass = 'tk-page-slider-controller-view'; 6 7 TKPageSliderController.inherits = TKController; 8 TKPageSliderController.synthetizes = ['slidingViewData', 'pageControlData', 'previousPageButton', 'nextPageButton', 'highlightedPageIndex']; 9 10 /** 11 * @class 12 * 13 * <p>A page slider controller adds the ability to browse through a collection of elements, often images, with nice and smooth transitions 14 * set up in CSS. Using this controller type, you can easily track when a new page is highlighted or activated. Optionally, you can also 15 * set up a series of indicators giving the user an overview of the number of images and arrows to navigate through elements, the connection 16 * between the various components being automatically handled behind the scenes for you.</p> 17 * 18 * @extends TKController 19 * @since TuneKit 1.0 20 * 21 * @param {Object} data A hash of properties to use as this object is initialized. 22 */ 23 function TKPageSliderController (data) { 24 this._previousPageButton = null; 25 this._nextPageButton = null; 26 /** 27 * Indicates whether the pages managed by the controller are navigable with keys. Defaults to <code>true</code>. 28 * @type bool 29 */ 30 this.highlightsFocusedPage = true; 31 /** 32 * Indicates whether navigation of pages is done strictly with paging buttons. Defaults to <code>false</code>, allowing the Apple remote to 33 * be used to navigate between pages. 34 * @type bool 35 */ 36 this.navigatesWithPagingButtonsOnly = false; 37 /** 38 * Indicates whether the focused page can get activated. Defaults to <code>true</code>, setting this to <code>false</code> plays the limit sound when 39 * the user attempts to activate the focused page. 40 * @type bool 41 */ 42 this.activatesFocusedPage = true; 43 /** 44 * Provides the list of directions that the user can navigate out of the list of pages. By default, this array is empty, meaning that if the 45 * {@link #navigatesWithPagingButtonsOnly} property is set to <code>false</code> and pages can be navigated with arrow keys, then the user will not 46 * be able to move focus out of the pages from either ends. The directions allowed are the <code>TKSpatialNavigationManagerDirection</code> family 47 * of constants. 48 * @type Array 49 */ 50 this.allowedOutwardNavigationDirections = []; 51 // set up the sliding view 52 /** 53 * The backing sliding view hosting the pages. 54 * @type TKSlidingView 55 * @private 56 */ 57 this.slidingView = new TKSlidingView(); 58 this.slidingView.ready = false; 59 this.slidingView.delegate = this; 60 // set up the page control 61 /** 62 * The backing page control hosting the page indicators. 63 * @type TKPageControl 64 * @private 65 */ 66 this.pageControl = new TKPageControl(); 67 this.pageControl.delegate = this; 68 // 69 this._slidingViewData = null; 70 this._pageControlData = null; 71 // 72 this.callSuper(data); 73 }; 74 75 TKPageSliderController.prototype.processView = function () { 76 this.callSuper(); 77 // restore properties that have not been set yet since construction 78 this.restoreProperty('previousPageButton'); 79 this.restoreProperty('nextPageButton'); 80 this.restoreProperty('slidingViewData'); 81 this.restoreProperty('pageControlData'); 82 this.restoreProperty('highlightedPageIndex'); 83 // wire up actions if we have a previous and next button wired 84 // add the sliding view and page control containers 85 this.container = this._view.appendChild(document.createElement('div')); 86 this.container.addClassName(TKPageSliderControllerContainerCSSClass); 87 this.container.appendChild(this.slidingView.element); 88 this.container.appendChild(this.pageControl.element); 89 // ensure our first page gets told about its being highlighted 90 this.pageWasHighlighted(this.highlightedPageIndex); 91 this.syncPageButtons(); 92 // highlight the first page in case we have no explicit highlighted element 93 if ((this.highlightedElement === null || !this.highlightedElement.isNavigable()) && this.slidingView.ready && this.highlightsFocusedPage) { 94 this.highlightedElement = this.slidingView.activeElement; 95 } 96 }; 97 98 /** 99 * @name TKPageSliderController.prototype 100 * @property {int} highlightedPageIndex The index of the page currently selected within the collection of pages. 101 */ 102 TKPageSliderController.prototype.getHighlightedPageIndex = function () { 103 return this.slidingView.activeElementIndex; 104 }; 105 106 TKPageSliderController.prototype.setHighlightedPageIndex = function (index) { 107 if (index === null) { 108 return; 109 } 110 // apply to the sliding view if it's ready 111 if (this.slidingView.ready) { 112 this.slidingView.activeElementIndex = index; 113 if (this.highlightedElement.hasClassName(TKSlidingViewCSSElementClass)) { 114 this.registerNavigablePages(); 115 this.highlightedElement = this.slidingView.activeElement; 116 } 117 } 118 }; 119 120 /** 121 * @name TKPageSliderController.prototype 122 * @property {String} previousPageButton A CSS selector matching a button to be used as the button to decrement the {@link #highlightedPageIndex}. 123 */ 124 TKPageSliderController.prototype.setPreviousPageButton = function (previousPageButton) { 125 if (previousPageButton === null) { 126 return; 127 } 128 // forget old button 129 if (this._previousPageButton) { 130 this.removeNavigableElement(this._previousPageButton); 131 } 132 // process new one 133 if (previousPageButton !== null) { 134 this._previousPageButton = this.view.querySelector(previousPageButton); 135 if (this._previousPageButton !== null) { 136 if (IS_APPLE_TV && !this.navigatesWithPagingButtonsOnly) { 137 this._previousPageButton.style.display = 'none'; 138 } 139 else { 140 this.addNavigableElement(this._previousPageButton); 141 } 142 } 143 } 144 }; 145 146 /** 147 * @name TKPageSliderController.prototype 148 * @property {String} nextPageButton A CSS selector matching a button to be used as the button to increment the {@link #highlightedPageIndex}. 149 */ 150 TKPageSliderController.prototype.setNextPageButton = function (nextPageButton) { 151 if (nextPageButton === null) { 152 return; 153 } 154 // forget old button 155 if (this._nextPageButton) { 156 this.removeNavigableElement(this._nextPageButton); 157 } 158 // process new one 159 if (nextPageButton !== null) { 160 this._nextPageButton = this.view.querySelector(nextPageButton); 161 if (this._nextPageButton !== null) { 162 if (IS_APPLE_TV && !this.navigatesWithPagingButtonsOnly) { 163 this._nextPageButton.style.display = 'none'; 164 } 165 else { 166 this.addNavigableElement(this._nextPageButton); 167 } 168 } 169 } 170 }; 171 172 /** 173 * @name TKPageSliderController.prototype 174 * @property {TKSlidingViewData} slidingViewData The set of properties used to set up the contents of the page slider. 175 */ 176 TKPageSliderController.prototype.setSlidingViewData = function (data) { 177 if (data === null) { 178 return; 179 } 180 // set up the data source if we have .elements on the data object 181 if (!TKUtils.objectIsUndefined(data.elements)) { 182 this.slidingView.dataSource = new TKSlidingViewDataSourceHelper(data.elements); 183 delete data.element; 184 } 185 // see if we have some intersting bits to pass through 186 var archived_page_index = this.getArchivedProperty('highlightedPageIndex'); 187 if (archived_page_index !== undefined) { 188 data.activeElementIndex = archived_page_index; 189 } 190 // copy properties 191 TKUtils.copyPropertiesFromSourceToTarget(data, this.slidingView); 192 // init our view 193 this.slidingView.init(); 194 // 195 this.slidingView.ready = true; 196 // 197 this.syncPageButtons(); 198 // add the currently focused element to the list of keyboard elements and highlight it 199 if (this.highlightsFocusedPage) { 200 this.registerNavigablePages(); 201 this.highlightedElement = this.slidingView.activeElement; 202 if (this.viewWasProcessed) { 203 TKSpatialNavigationManager.sharedManager.highlightElement(this.slidingView.activeElement); 204 } 205 } 206 }; 207 208 /** 209 * @name TKPageSliderController.prototype 210 * @property {TKPageControlData} pageControlData The set of properties used to set up the contents of the optional page indicators. 211 */ 212 TKPageSliderController.prototype.setPageControlData = function (data) { 213 if (data === null) { 214 return; 215 } 216 // set up the data source 217 this.pageControl.dataSource = new TKPageControlDataSourceHelper(data); 218 // copy properties 219 TKUtils.copyPropertiesFromSourceToTarget(data, this.pageControl); 220 // init our control 221 this.pageControl.init(); 222 // get the current page from the sliding view if we have it set only after 223 if (this.slidingView.ready) { 224 this.pageControl.currentPage = this.highlightedPageIndex; 225 } 226 }; 227 228 /* ==================== Event Handling ==================== */ 229 230 // private method meant to be over-ridden by a controller sub-classes to provide a custom element 231 // to highlight, returning null means there's nothing custom to report 232 TKPageSliderController.prototype._preferredElementToHighlightInDirection = function (currentElement, direction) { 233 var can_exit_in_direction = (this.allowedOutwardNavigationDirections.indexOf(direction) != -1); 234 var element = this.callSuper(currentElement, direction); 235 if (!this.navigatesWithPagingButtonsOnly && currentElement.hasClassName(TKSlidingViewCSSElementClass)) { 236 if (direction == KEYBOARD_LEFT) { 237 if (this.slidingView.activeElementIndex <= 0 && !this.slidingView.loops) { 238 if (!can_exit_in_direction) { 239 element = null; 240 } 241 } 242 else { 243 element = this.slidingView.getElementAtIndex((this.slidingView.activeElementIndex + this.slidingView.numberOfElements - 1) % this.slidingView.numberOfElements); 244 } 245 } 246 else if (direction == KEYBOARD_RIGHT) { 247 if (this.slidingView.activeElementIndex >= this.slidingView.numberOfElements - 1 && !this.slidingView.loops) { 248 if (!can_exit_in_direction) { 249 element = null; 250 } 251 } 252 else { 253 element = this.slidingView.getElementAtIndex((this.slidingView.activeElementIndex + 1) % this.slidingView.numberOfElements); 254 } 255 } 256 } 257 return element; 258 }; 259 260 TKPageSliderController.prototype.elementWasActivated = function (element) { 261 // previous page button pressed 262 if (element === this._previousPageButton) { 263 var can_navigate = (this.slidingView.activeElementIndex > 0 || this.slidingView.loops); 264 TKSpatialNavigationManager.soundToPlay = (can_navigate) ? SOUND_MOVED : SOUND_LIMIT; 265 if (can_navigate) { 266 this.unregisterNavigablePages(); 267 } 268 this.slidingView.activeElementIndex--; 269 } 270 // next page button pressed 271 else if (element === this._nextPageButton) { 272 var can_navigate = (this.slidingView.activeElementIndex < this.slidingView.numberOfElements - 1 || this.slidingView.loops); 273 TKSpatialNavigationManager.soundToPlay = (can_navigate) ? SOUND_MOVED : SOUND_LIMIT; 274 if (can_navigate) { 275 this.unregisterNavigablePages(); 276 } 277 this.slidingView.activeElementIndex++; 278 } 279 // focused element in the sliding view 280 else if (element.hasClassName(TKSlidingViewCSSFocusedClass)) { 281 if (this.activatesFocusedPage) { 282 this.pageWasSelected(this.slidingView.activeElementIndex); 283 } 284 else { 285 TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; 286 } 287 } 288 // fall back to default behavior 289 else { 290 this.callSuper(element); 291 } 292 }; 293 294 TKPageSliderController.prototype.elementWasHighlighted = function (element, previouslyHighlightedElement) { 295 if (element.hasClassName(TKSlidingViewCSSElementClass)) { 296 this.slidingView.activeElementIndex = element._slidingViewIndex; 297 // track navigation on all elements if we're getting highlight for the first time 298 if (previouslyHighlightedElement !== null && 299 (!previouslyHighlightedElement.hasClassName(TKSlidingViewCSSElementClass) || 300 previouslyHighlightedElement._controller !== this)) { 301 this.registerNavigablePages(); 302 } 303 } 304 }; 305 306 TKPageSliderController.prototype.elementWasUnhighlighted = function (element, nextHighlightedElement) { 307 // are we focusing an element outside of our sliding view? 308 if (element.hasClassName(TKSlidingViewCSSElementClass) && 309 (!nextHighlightedElement.hasClassName(TKSlidingViewCSSElementClass) || 310 nextHighlightedElement._controller !== this)) { 311 // make all the other elements non-navigable 312 this.unregisterNavigablePages(); 313 // save for the active one 314 this.addNavigableElement(this.slidingView.activeElement); 315 } 316 }; 317 318 TKPageSliderController.prototype.syncPageButtons = function () { 319 // nothing to do if the sliding view is looping 320 if (this.slidingView.loops || !this.isViewLoaded()) { 321 return; 322 } 323 // check if the previous page button needs hiding 324 if (this._previousPageButton instanceof Element) { 325 this._previousPageButton[(this.slidingView.activeElementIndex <= 0 ? 'add' : 'remove') + 'ClassName']('inactive'); 326 } 327 // check if the next page button needs hiding 328 if (this._nextPageButton instanceof Element) { 329 this._nextPageButton[(this.slidingView.activeElementIndex >= this.slidingView.numberOfElements - 1 ? 'add' : 'remove') + 'ClassName']('inactive'); 330 } 331 }; 332 333 TKPageSliderController.prototype.registerNavigablePages = function () { 334 if (this.navigatesWithPagingButtonsOnly) { 335 this.addNavigableElement(this.slidingView.activeElement); 336 } 337 else { 338 var elements = this.slidingView.element.querySelectorAll('.' + TKSlidingViewCSSElementClass); 339 for (var i = 0; i < elements.length; i++) { 340 this.addNavigableElement(elements[i]); 341 } 342 } 343 }; 344 345 TKPageSliderController.prototype.unregisterNavigablePages = function () { 346 var elements = this.slidingView.element.querySelectorAll('.' + TKSlidingViewCSSElementClass); 347 for (var i = 0; i < elements.length; i++) { 348 this.removeNavigableElement(elements[i]); 349 } 350 }; 351 352 /* ==================== TKSlidingView Protocol ==================== */ 353 354 TKPageSliderController.prototype.slidingViewDidFocusElementAtIndex = function (view, index) { 355 if (this.highlightsFocusedPage && this.slidingView.ready) { 356 if (this.highlightedElement.hasClassName(TKSlidingViewCSSElementClass)) { 357 this.registerNavigablePages(); 358 } 359 else { 360 this.unregisterNavigablePages(); 361 this.addNavigableElement(this.slidingView.activeElement); 362 } 363 // make sure the element has focus 364 if (this.viewWasProcessed) { 365 TKSpatialNavigationManager.sharedManager.highlightElement(this.slidingView.activeElement); 366 } 367 } 368 // 369 this.pageControl.currentPage = index; 370 this.pageWasHighlighted(index); 371 // update the states of previous and next buttons 372 this.syncPageButtons(); 373 }; 374 375 TKPageSliderController.prototype.slidingViewDidBlurElementAtIndex = function (view, index) { 376 if (this.highlightsFocusedPage) { 377 this.unregisterNavigablePages(); 378 } 379 }; 380 381 TKPageSliderController.prototype.slidingViewDidSelectActiveElement = function (view, index) { 382 this.pageWasSelected(index); 383 }; 384 385 TKPageSliderController.prototype.slidingViewStyleForItemAtIndex = function (view, index) { 386 return this.styleForPageAtIndex(index); 387 }; 388 389 TKPageSliderController.prototype.slidingViewDidHoverElementAtIndex = function (view, index) { 390 this.pageWasHovered(index); 391 }; 392 393 TKPageSliderController.prototype.slidingViewDidUnhoverElementAtIndex = function (view, index) { 394 this.pageWasUnhovered(index); 395 }; 396 397 /* ==================== Placeholder Methods ==================== */ 398 399 /** 400 * Triggered as the {@link #highlightedPageIndex} property has changed when a new page became focused. 401 * 402 * @param {int} index The index of the newly focused page 403 */ 404 TKPageSliderController.prototype.pageWasHighlighted = function (index) {}; 405 406 /** 407 * Triggered as the focused page was selected by the user, either from clicking on the page or using the play/pause remote key. 408 * 409 * @param {int} index The index of the activated page 410 */ 411 TKPageSliderController.prototype.pageWasSelected = function (index) {}; 412 413 TKPageSliderController.prototype.pageWasHovered = function (index) {}; 414 415 TKPageSliderController.prototype.pageWasUnhovered = function (index) {}; 416 417 /** 418 * This method allows to provide custom style rules for a page programatically any time the {@link #highlightedPageIndex} property changes. The values in this 419 * array are expected to be individual two-value arrays, where the first index holds the CSS property name, and the second index its value. 420 * 421 * @param {Array} index The index of the page for which we are trying to obtain custom styles. 422 */ 423 TKPageSliderController.prototype.styleForPageAtIndex = function (index) { 424 return []; 425 }; 426 427 /* ==================== TKPageControl Protocol ==================== */ 428 429 TKPageSliderController.prototype.pageControlDidUpdateCurrentPage = function (control, newPageIndex) { 430 this.slidingView.activeElementIndex = newPageIndex; 431 this.pageControl.updateCurrentPageDisplay(); 432 }; 433 434 /* ==================== Archival ==================== */ 435 436 TKPageSliderController.prototype.archive = function () { 437 var archive = this.callSuper(); 438 archive.highlightedPageIndex = this.highlightedPageIndex; 439 return archive; 440 }; 441 442 TKClass(TKPageSliderController); 443