1 /* 2 * Copyright © 2009 Apple Inc. All rights reserved. 3 */ 4 5 TKController.inherits = TKObject; 6 TKController.synthetizes = ['view', 'navigableElements', 'actions', 'outlets', 'scrollable', 'backButton', 'navigatesTo']; 7 8 /** 9 * The hash in which we keep references to all controller instantiated throughout the lifecycle of the booklet. Use a controller's {@link #id} to 10 * access the controller for that id. 11 * @type Object 12 */ 13 TKController.controllers = {}; 14 15 /** 16 * The fraction of the scrollable element's width or height that is being scrolled by in order to increment or decrement the scroll offset. 17 * @constant 18 * @type float 19 * @private 20 */ 21 const TKControllerScrollIncrementFraction = 0.75; 22 /** 23 * The time in miliseconds that the scrolling animation lasts. 24 * @constant 25 * @type int 26 * @private 27 */ 28 const TKControllerScrollDuration = 500; 29 /** 30 * The spline used for the scrolling animation. 31 * @constant 32 * @type Array 33 * @private 34 */ 35 const TKControllerScrollSpline = [0.211196, 0.811224, 0.641221, 0.979592]; 36 const TKControllerScrollDirectionUp = 0; 37 const TKControllerScrollDirectionDown = 1; 38 const TKControllerScrollDirectionLeft = 0; 39 const TKControllerScrollDirectionRight = 1; 40 41 /** 42 * @class 43 * 44 * <p>The TKController class is the base class for all TuneKit controllers. Controllers are useful objects that control all the core functionalities of 45 * a screen or sub-screen: view-loading, interaction, navigation, etc.</p> 46 * 47 * @extends TKObject 48 * @since TuneKit 1.0 49 * 50 * @param {Object} data A hash of properties to use as this object is initialized. 51 */ 52 function TKController (data) { 53 this.callSuper(); 54 // 55 this.propertiesToRestoreOnLoad = []; 56 // synthetized property 57 this._view = null; 58 /** 59 * @name TKController.prototype 60 * @property {Array} navigableElements The complete list of all elements that can be navigated to within this controller and all of its sub-controllers. 61 * The contents of this array should not be directly manipulated, instead use the {@link #addNavigableElement} and {@link #removeNavigableElement} methods. 62 */ 63 this._navigableElements = []; 64 /** 65 * The controller directly containing this controller instance, <code>null</code> if the controller is not attached to any yet. 66 * @type TKController 67 */ 68 this.parentController = null; 69 // default transition styles for navigation 70 this.enforcesCustomTransitions = false; // whether we should use the custom transitions on ATV only 71 /** 72 * The animated transition to use for this controller's view when the controller becomes inactive. 73 * @type TKTransitionDefinition 74 */ 75 this.becomesInactiveTransition = TKViewTransitionDissolveOut; 76 /** 77 * The animated transition to use for this controller's view when the controller becomes active. 78 * @type TKTransitionDefinition 79 */ 80 this.becomesActiveTransition = TKViewTransitionDissolveIn; 81 // default properties 82 /** 83 * The unique id for this controller's view. This is the same string that will be used for the {@link #view}'s HTML <code>id</code> attribute, as well 84 * as a key in the {@link TKController.controllers} hash, and thus must be adequate for both uses. The controller's id is used in the view-loading mechanism, 85 * such that if there is an HTML file in the booklet's <code>views/</code> directory that shares the same name, it is that file that is loaded to provide 86 * the view's content. 87 * @type String 88 */ 89 this.id = data.id; 90 /** 91 * A DOM element to be used as the view for this controller, which overrides the default view-loading mechanism in case it's set before the view is loaded. 92 * @type Element 93 * @private 94 */ 95 this.explicitView = null; 96 /** 97 * The name of the template to be used to create the view's content. If there is an HTML file in the <code>templates/</code> directory with that name, the 98 * view is loaded by cloning the content of that file and replacing the ID with that provided by the {@link #id} property. 99 * @type String 100 */ 101 this.template = null; 102 /** 103 * A list of image URIs to preload. 104 * @type Array 105 */ 106 this.preloads = []; 107 this._navigatesTo = null; 108 this._actions = null; 109 this._outlets = null; 110 this._scrollable = null; 111 this._backButton = null; 112 /** 113 * The highlighted element within that controller. This is only unique to the view managed by this controller, and not to the entire booklet or even any 114 * controllers that might be contained within this controller. 115 * @type Element 116 * @private 117 */ 118 this.highlightedElement = null; 119 /** 120 * Indicates that the view has not appeared on screen yet. 121 * @type bool 122 * @private 123 */ 124 this.viewNeverAppeared = true; 125 /** 126 * Indicates that the view was fully processed. 127 * @type bool 128 * @private 129 */ 130 this.viewWasProcessed = false; 131 /** 132 * The CSS selector for the default scrollable element. If this value is non-<code>null</code> then the up and down keys scroll the element specified 133 * by the selector. 134 * @type String 135 */ 136 this.scrollableElement = null; 137 /** 138 * The animator managing the currently scrolling element. 139 * @type TKAnimator 140 * @private 141 */ 142 this.animator = new TKAnimator(TKControllerScrollDuration, null, TKControllerScrollSpline); 143 this.upScrollData = { 144 direction: TKControllerScrollDirectionUp, 145 animator: this.animator 146 } 147 this.downScrollData = { 148 direction: TKControllerScrollDirectionDown, 149 animator: this.animator 150 } 151 // copy properties 152 this.copyNonSynthetizedProperties(data); 153 // register controller 154 TKController.controllers[this.id] = this; 155 }; 156 157 /** 158 * A utility method to get the controller from an Object that is either the {@link TKController#id} of a controller or a controller directly. 159 * 160 * @param {Object} stringOrControllerReference Either the {@link TKController#id} of a controller or a controller directly 161 * @returns {TKController} The controller. 162 */ 163 TKController.resolveController = function (stringOrControllerReference) { 164 return (TKUtils.objectIsString(stringOrControllerReference)) ? TKController.controllers[stringOrControllerReference] : stringOrControllerReference; 165 }; 166 167 /** 168 * A utility method to copy all properties from another object onto the controller, ignoring any property that is synthetized. 169 * 170 * @param {Object} properties An object containing a set of properties to be copied across to the receiver. 171 * @private 172 */ 173 TKController.prototype.copyNonSynthetizedProperties = function (properties) { 174 for (var property in properties) { 175 // don't copy synthetized properties but track them for later 176 if (this.__lookupSetter__(property)) { 177 this.propertiesToRestoreOnLoad[property] = properties[property]; 178 continue; 179 } 180 this[property] = properties[property]; 181 } 182 }; 183 184 /* ==================== Managing the View ==================== */ 185 186 TKController.prototype.getView = function () { 187 // create the view if it's not set yet 188 if (this._view === null) { 189 this.loadView(); 190 } 191 return this._view; 192 }; 193 194 TKController.prototype.setView = function (view) { 195 this.explicitView = view; 196 }; 197 198 TKController.prototype.loadView = function () { 199 this.viewNeverAppeared = false; 200 // first, check if we have an element already defined 201 var view; 202 if (this.explicitView !== null) { 203 view = this.explicitView; 204 // check if node is already in the document 205 this.viewNeverAppeared = !TKUtils.isNodeChildOfOtherNode(view, document); 206 } 207 // check if our view already exists in the DOM 208 else { 209 view = document.getElementById(this.id); 210 } 211 // if not, load it from the views directory 212 if (view === null) { 213 this.viewNeverAppeared = true; 214 view = this.loadFragment('views', this.id); 215 // there was no such view available, try and see if we have a template available 216 if (view === null) { 217 if (this.template !== null) { 218 view = this.loadFragment('templates', this.template); 219 } 220 // no template, just create an empty <div> then 221 if (view === null) { 222 view = document.createElement('div'); 223 } 224 } 225 } 226 // make sure we know when the view is added to the document if it 227 // wasn't part of the DOM already 228 if (this.viewNeverAppeared) { 229 view.addEventListener('DOMNodeInsertedIntoDocument', this, false); 230 } 231 // set up the correct id on our view 232 view.id = this.id; 233 // link the view to our controller 234 view._controller = this; 235 // and remember our view 236 this._view = view; 237 // let our object perform more setup code 238 this.viewDidLoad(); 239 // do post-loading processing 240 this.processView(); 241 // 242 this.viewWasProcessed = true; 243 }; 244 245 TKController.prototype.loadFragment = function (directory, id) { 246 var imported_fragment = null; 247 // 248 var request = new XMLHttpRequest(); 249 var failed = false; 250 request.open('GET', directory + '/' + id + '.html', false); 251 try { 252 request.send(); 253 } catch (err) { 254 // iTunes will throw an error if the request doesn't exist 255 // when using the booklet:// scheme 256 // Mark the error here so we can take the FAIL path below, which 257 // is actually what we want. 258 failed = true; 259 } 260 // everything went well 261 // XXX: we should do more work to differentitate between http:// and file:// URLs here 262 if (!failed && ((request.status <= 0 && request.responseText !== '') || request.status == 200)) { 263 // XXX: this is way dirty 264 var loaded_fragment = document.implementation.createHTMLDocument(); 265 loaded_fragment.write(request.responseText); 266 imported_fragment = document.importNode(loaded_fragment.getElementById(id), true); 267 } 268 return imported_fragment; 269 }; 270 271 /** 272 * This method is called once the view has been loaded and allows the controller to post-process it. 273 */ 274 TKController.prototype.processView = function () { 275 var view = this._view; 276 // restore properties that have not been set yet since construction 277 this.restoreProperty('navigatesTo'); 278 this.restoreProperty('actions'); 279 this.restoreProperty('outlets'); 280 this.restoreProperty('backButton'); 281 this.restoreProperty('scrollable'); 282 // process highlightedElement 283 if (this.highlightedElement !== null && !(this.highlightedElement instanceof Element)) { 284 this.highlightedElement = this._view.querySelector(this.highlightedElement); 285 } 286 // process links 287 var links = view.querySelectorAll('a'); 288 for (var i = 0; i < links.length; i++) { 289 this.addNavigableElement(links[i]); 290 } 291 // process assets to pre-load 292 for (var i = 0; i < this.preloads.length; i++) { 293 new Image().src = this.preloads[i]; 294 } 295 }; 296 297 TKController.prototype.restoreProperty = function (property) { 298 var value = this.propertiesToRestoreOnLoad[property]; 299 if (value !== undefined && this['_' + property] === null) { 300 this[property] = value; 301 } 302 }; 303 304 TKController.prototype.getArchivedProperty = function (property) { 305 var archived_value; 306 try { 307 var archived_value = bookletController.archive.controllers[this.id][property]; 308 } 309 catch (e) {} 310 return archived_value; 311 }; 312 313 /** 314 * Indicates whether the view the controller manages was loaded yet. 315 * 316 * @returns {bool} Whether the view was loaded yet. 317 */ 318 TKController.prototype.isViewLoaded = function () { 319 return (this._view !== null); 320 }; 321 322 /* ==================== Dealing with the various properties ==================== */ 323 324 /** 325 * @name TKController.prototype 326 * @property {Array} navigatesTo A list of objects defining elements to act as anchors to navigate to other controllers. Each object in the array is an 327 * ad-hoc object with <code>selector</code> and <code>controller</code> properties. The <code>selector</code> is a string describing a CSS selector used 328 * to match the element within the {@link #view} that will act as an anchor to navigate to the controller. The <code>controller</code> is either a string matching 329 * the {@link #id} of an existing controller or an reference to a {@link TKController}. 330 */ 331 TKController.prototype.setNavigatesTo = function (navigatesTo) { 332 if (navigatesTo === null) { 333 return; 334 } 335 // unregister the previous elements if we have any 336 if (this._navigatesTo !== null) { 337 for (var i = 0; i < this._navigatesTo.length; i++) { 338 var element = this._navigatesTo[i]; 339 element._navigationData = undefined; 340 this.removeNavigableElement(element); 341 } 342 } 343 // register the new elements 344 this._navigatesTo = []; 345 for (var i = 0; i < navigatesTo.length; i++) { 346 var item = navigatesTo[i]; 347 var element = this._view.querySelector(item.selector); 348 if (element) { 349 element._navigationData = item; 350 this._navigatesTo.push(element); 351 this.addNavigableElement(element); 352 } 353 } 354 }; 355 356 /** 357 * @name TKController.prototype 358 * @property {Array} actions A list of objects defining elements to act as anchors to trigger a JavaScript callback. Each object in the array is an 359 * ad-hoc object with <code>selector</code>, <code>action</code> and, optionally, <code>arguments</code> properties. The <code>selector</code> is a 360 * string describing a CSS selector used to match the element within the {@link #view} that will act as a trigger for the action. The <code>action</code> 361 * specifies the function to call when the action is triggered, which is either a string matching the name of a method on this controller instance or 362 * a direct reference to a function. Optionally, the <code>arguments</code> property can be specified in order to provide a list of arguments to be passed 363 * to the callback when the action is triggered. 364 */ 365 TKController.prototype.setActions = function (actions) { 366 if (actions === null || this._actions === actions) { 367 return; 368 } 369 // unregister the previous elements if we have any 370 if (this._actions !== null) { 371 for (var i = 0; i < this._actions.length; i++) { 372 var element = this._actions[i]; 373 element._actionData = undefined; 374 this.removeNavigableElement(element); 375 } 376 } 377 // register the new elements 378 this._actions = []; 379 for (var i = 0; i < actions.length; i++) { 380 var item = actions[i]; 381 var element = this._view.querySelector(item.selector); 382 if (element) { 383 element._actionData = item; 384 this._actions.push(element); 385 this.addNavigableElement(element); 386 } 387 } 388 }; 389 390 /** 391 * @name TKController.prototype 392 * @property {Array} outlets A list of objects defining elements to which we want to create an automatic reference on the controller instance. Each object in 393 * the array has a <code>selector</code> and a <code>name</code> property. The <code>selector</code> is a string describing a CSS selector used to match the 394 * element within the {@link #view} to which we want to create a reference. The <code>name</code> specifies the name of the JavaScript property that will be holding 395 * that reference on the controller instance. 396 */ 397 TKController.prototype.setOutlets = function (outlets) { 398 if (outlets === null) { 399 return; 400 } 401 // unregister the previous outlets if we have any 402 if (this._outlets !== null) { 403 for (var i = 0; i < this._outlets.length; i++) { 404 this[this._outlets[i]] = undefined; 405 } 406 } 407 // register the new outlets 408 for (var i = 0; i < outlets.length; i++) { 409 var item = outlets[i]; 410 this[item.name] = this._view.querySelector(item.selector); 411 } 412 this._outlets = outlets; 413 }; 414 415 /** 416 * @name TKController.prototype 417 * @property {String} backButton A CSS selector that matches an element in the {@link #view} that acts as the back button. 418 */ 419 TKController.prototype.setBackButton = function (backButton) { 420 if (backButton === null) { 421 return; 422 } 423 // forget the old back button if we have one 424 if (this._backButton !== null) { 425 this._backButton._backButton = undefined; 426 // restore display type on ATV 427 if (IS_APPLE_TV) { 428 this._backButton.style.display = this._backButton._previousDisplayStyle; 429 } 430 this.removeNavigableElement(this._backButton); 431 } 432 // set up the new one 433 if (backButton !== null) { 434 var element = this._view.querySelector(backButton); 435 element._backButton = true; 436 // hide it on ATV 437 if (IS_APPLE_TV) { 438 element._previousDisplayStyle = element.style.display; 439 element.style.display = 'none'; 440 } 441 this.addNavigableElement(element); 442 this._backButton = element; 443 } 444 }; 445 446 /* ==================== Notification Methods ==================== */ 447 448 /** 449 * This method is called when the view has been fully loaded but not yet processed. Override this method in order to customize the content of the view. 450 */ 451 TKController.prototype.viewDidLoad = function () {}; 452 TKController.prototype.viewDidUnload = function () {}; 453 454 TKController.prototype._viewWillAppear = function () {}; 455 /** 456 * This method is called when the view managed by this controller is about to appear on screen, probably after an animated transition. 457 */ 458 TKController.prototype.viewWillAppear = function () {}; 459 460 TKController.prototype._viewDidAppear = function () {}; 461 /** 462 * This method is called when the view managed by this controller appeared on screen, probably after an animated transition. 463 */ 464 TKController.prototype.viewDidAppear = function () {}; 465 466 TKController.prototype._viewWillDisappear = function () {}; 467 /** 468 * This method is called when the view managed by this controller is about to disappear from the screen, probably after an animated transition. 469 */ 470 TKController.prototype.viewWillDisappear = function () {}; 471 472 TKController.prototype._viewDidDisappear = function () {}; 473 /** 474 * This method is called when the view managed by this controller has disappeared from the screen, probably after an animated transition. 475 */ 476 TKController.prototype.viewDidDisappear = function () {}; 477 478 /* ==================== Event Handling ==================== */ 479 480 /** 481 * Entry point for all event handling, since <code>TKController</code> implements the DOM <code>EventListener</code> protocol. This method may be subclassed 482 * but it is important to call the superclass's implementation of this method as essential event routing happens there. 483 * 484 * @param {Event} event The event. 485 */ 486 TKController.prototype.handleEvent = function (event) { 487 switch (event.type) { 488 case 'click' : 489 this.elementWasActivated(event.currentTarget); 490 break; 491 case 'highlight' : 492 this.elementWasHighlighted(event.currentTarget, event.relatedTarget); 493 break; 494 case 'unhighlight' : 495 this.elementWasUnhighlighted(event.currentTarget, event.relatedTarget); 496 break; 497 case 'mouseover' : 498 this.elementWasHovered(event.currentTarget); 499 break; 500 case 'mouseout' : 501 this.elementWasUnhovered(event.currentTarget); 502 break; 503 case 'DOMNodeInsertedIntoDocument' : 504 this.viewWasInsertedIntoDocument(event); 505 break; 506 } 507 }; 508 509 /** 510 * Triggered when an element is activated by the user, no matter what the host's input methods are as TuneKit abstracts the interaction that yields 511 * an element's activation. This method may be subclassed but it is important to call the superclass's implementation of this method as navigation anchors, 512 * actions, etc. are handled in that method directly. 513 * 514 * @param {Element} element The element that was just activated. 515 */ 516 TKController.prototype.elementWasActivated = function (element) { 517 if (element._navigationData !== undefined) { 518 // pointer to the controller 519 var controller = TKController.resolveController(element._navigationData.controller); 520 // error if we have an undefined object 521 if (controller === undefined) { 522 console.error('TKController.elementWasActivated: trying to push an undefined controller'); 523 return; 524 } 525 // otherwise, navigate to it 526 TKSpatialNavigationManager.sharedManager.highlightElement(element); 527 TKNavigationController.sharedNavigation.pushController(controller); 528 } 529 else if (element._actionData !== undefined) { 530 TKSpatialNavigationManager.sharedManager.highlightElement(element); 531 // get the callback for this action 532 var callback = element._actionData.action; 533 // see if it's a string in which case we need to get the function dynamically 534 if (TKUtils.objectIsString(callback) && TKUtils.objectHasMethod(this, callback)) { 535 callback = this[element._actionData.action]; 536 } 537 // see if we have custom arguments 538 if (TKUtils.objectIsArray(element._actionData.arguments)) { 539 callback.apply(this, element._actionData.arguments); 540 } 541 // otherwise just call the callback 542 else { 543 callback.apply(this); 544 } 545 } 546 else if (element._backButton !== undefined) { 547 TKSpatialNavigationManager.soundToPlay = SOUND_EXIT; 548 TKSpatialNavigationManager.sharedManager.highlightElement(element); 549 TKNavigationController.sharedNavigation.popController(); 550 } 551 else if (element.localName == 'a' && IS_APPLE_TV) { 552 element.dispatchEvent(TKUtils.createEvent('click', null)); 553 } 554 else if (element._scrollableData !== undefined) { 555 this.scrollWithData(element._scrollableData); 556 } 557 }; 558 559 /** 560 * Triggered when an element receives highlight. 561 * 562 * @param {Element} element The element that is newly being highlighted. 563 * @param {Element} previouslyHighlightedElement The element that previously was highlighted, or <code>null</code> if there was none. 564 */ 565 TKController.prototype.elementWasHighlighted = function (element, previouslyHighlightedElement) { 566 }; 567 568 /** 569 * Triggered when an element loses highlight. 570 * 571 * @param {Element} element The element that is newly being highlighted. 572 * @param {Element} nextHighlightedElement The element that is going to be highlighted next, or <code>null</code> if there is none. 573 */ 574 TKController.prototype.elementWasUnhighlighted = function (element, nextHighlightedElement) { 575 }; 576 577 /** 578 * Triggered when an element is hovered, which only happens when a mouse is present. 579 * 580 * @param {Element} element The element that is being hovered. 581 */ 582 TKController.prototype.elementWasHovered = function (element) { 583 }; 584 585 /** 586 * Triggered when an element is not hovered any longer, which only happens when a mouse is present. 587 * 588 * @param {Element} element The element that is not hovered any longer. 589 */ 590 TKController.prototype.elementWasUnhovered = function (element) { 591 }; 592 593 /** 594 * Triggered when the view is first inserted into the document. 595 */ 596 TKController.prototype.viewWasInsertedIntoDocument = function () { 597 this.viewNeverAppeared = false; 598 }; 599 600 /** 601 * Indicates whether the receiver is a descendent of the controller passed as an argument. 602 * 603 * @param {TKController} purportedParentController The controller that we assume is a parent controller to the receiver. 604 * @returns {bool} Whether the controller is a descendent of the other controller passed as a parameter. 605 */ 606 TKController.prototype.isDescendentOfController = function (purportedParentController) { 607 var is_descendent = false; 608 var parent = this.parentController; 609 while (parent !== null) { 610 if (parent === purportedParentController) { 611 is_descendent = true; 612 break; 613 } 614 parent = parent.parentController; 615 } 616 return is_descendent; 617 }; 618 619 /* ==================== Keyboard Navigation ==================== */ 620 621 /** 622 * Adds an element within the controller's view to the list of navigable elements. Any element that is interactive should be registered as navigable, even 623 * when a mouse is available. 624 * 625 * @param {Element} element The element we wish to make navigable. 626 */ 627 TKController.prototype.addNavigableElement = function (element) { 628 // nothing to do if we already know about this element 629 if (this._navigableElements.indexOf(element) > -1) { 630 return; 631 } 632 // 633 if (!IS_APPLE_TV) { 634 element.addEventListener('click', this, false); 635 } 636 element.addEventListener('highlight', this, false); 637 element.addEventListener('unhighlight', this, false); 638 element._controller = this; 639 // 640 this._navigableElements.push(element); 641 // 642 TKSpatialNavigationManager.sharedManager.addNavigableElement(element); 643 }; 644 645 /** 646 * Removes an element within the controller's view from the list of navigable elements. 647 * 648 * @param {Element} element The element we wish to remove from the navigable elements list. 649 */ 650 TKController.prototype.removeNavigableElement = function (element) { 651 // find the index for this element 652 var index = this._navigableElements.indexOf(element); 653 if (index < 0) { 654 return; 655 } 656 // 657 element.removeEventListener('click', this, false); 658 element.removeEventListener('highlight', this, false); 659 element.removeEventListener('unhighlight', this, false); 660 element._controller = undefined; 661 // remove elements from the tracking arrays 662 this._navigableElements.splice(index, 1); 663 // 664 TKSpatialNavigationManager.sharedManager.removeNavigableElement(element); 665 }; 666 667 /** 668 * Allows to specify custom metrics for an element displayed on screen. This method is called by the spatial navigation manager when determining what the 669 * element to highlight is after the uses presses a directional key on the Apple remote. By default, the CSS metrics for the elements are used, but in 670 * certain cases the author may wish to use different metrics that are more logical for the navigation. Return <code>null</code> in order to specify that 671 * the element has no custom metrics. 672 * 673 * @param {Element} element The element which the spatial navigation manager is inspecting. 674 * @returns {TKRect} The custom metrics for the given element. 675 */ 676 TKController.prototype.customMetricsForElement = function (element) { 677 return null; 678 }; 679 680 /** 681 * Allows the controller to provide the spatial navigation manager with a prefered element to highlight, overriding the default behavior of using CSS metrics. 682 * The default implementation returns <code>undefined</code>, which indicates that the automatic behavior should be used, while returning <code>null</code> 683 * means that there should be no element highlighted in the provided direction. 684 * 685 * @param {Element} currentElement The element that is currently highlighted. 686 * @param {int} direction The direction the user is navigation towards. 687 * @returns {Element} The preferred element to highlight in the provided direction. 688 */ 689 TKController.prototype.preferredElementToHighlightInDirection = function (currentElement, direction) { 690 return undefined; 691 }; 692 693 // private method meant to be over-ridden by a controller sub-classes to provide a custom element 694 // to highlight, returning null means there's nothing custom to report 695 // XXX: we really need a better mechanism to do this stuff, having a private method for subclasses and one for instances is way dirty 696 TKController.prototype._preferredElementToHighlightInDirection = function (currentElement, direction) { 697 return undefined; 698 }; 699 700 /* ==================== Scrolling ==================== */ 701 702 TKController.prototype.setScrollable = function (scrollable) { 703 // remove all scrollable regions if we already had some set up 704 if (this._scrollable !== null) { 705 for (var i = 0; i < this._scrollable.length; i++) { 706 var element = this._scrollable[i]; 707 element._scrollableData = undefined; 708 this.removeNavigableElement(element); 709 } 710 } 711 // process scrollable regions 712 this._scrollable = []; 713 for (var i = 0; i < scrollable.length; i++) { 714 var scrollable_data = scrollable[i]; 715 // create an animator for this scrollable element 716 scrollable_data.animator = new TKAnimator(TKControllerScrollDuration, null, TKControllerScrollSpline); 717 // check if we have an up element 718 if (scrollable_data.up !== undefined) { 719 var up_button = this._view.querySelector(scrollable_data.up); 720 up_button._scrollableData = { 721 direction: TKControllerScrollDirectionUp, 722 target: scrollable_data.target, 723 animator: scrollable_data.animator 724 }; 725 this._scrollable.push(up_button); 726 this.addNavigableElement(up_button); 727 } 728 // check if we have a down element 729 if (scrollable_data.down !== undefined) { 730 var down_button = this._view.querySelector(scrollable_data.down); 731 down_button._scrollableData = { 732 direction: TKControllerScrollDirectionDown, 733 target: scrollable_data.target, 734 animator: scrollable_data.animator 735 }; 736 this._scrollable.push(down_button); 737 this.addNavigableElement(down_button); 738 } 739 // check if we have a left element 740 if (scrollable_data.left !== undefined) { 741 var left_button = this._view.querySelector(scrollable_data.left); 742 left_button._scrollableData = { 743 direction: TKControllerScrollDirectionLeft, 744 target: scrollable_data.target, 745 animator: scrollable_data.animator 746 }; 747 this._scrollable.push(left_button); 748 this.addNavigableElement(left_button); 749 } 750 // check if we have a right element 751 if (scrollable_data.right !== undefined) { 752 var right_button = this._view.querySelector(scrollable_data.right); 753 right_button._scrollableData = { 754 direction: TKControllerScrollDirectionRight, 755 target: scrollable_data.target, 756 animator: scrollable_data.animator 757 }; 758 this._scrollable.push(right_button); 759 this.addNavigableElement(right_button); 760 } 761 } 762 }; 763 764 TKController.prototype.scrollWithData = function (scrollData) { 765 // stop any running animation for this scrollable region 766 scrollData.animator.stop(); 767 // get a pointer to the target element 768 var element = this._view.querySelector(scrollData.target); 769 // stop right there if there's no such element 770 if (!(element instanceof Element)) { 771 TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; 772 return; 773 } 774 // figure out which direction we're scrolling 775 var vertical_scrolling = (scrollData.direction == TKControllerScrollDirectionUp || scrollData.direction == TKControllerScrollDirectionDown); 776 // get the increment for this scroll 777 var increment = element[vertical_scrolling ? 'offsetHeight' : 'offsetWidth'] * TKControllerScrollIncrementFraction; 778 // get the start value 779 var start_value = element[vertical_scrolling ? 'scrollTop' : 'scrollLeft']; 780 // get the target value 781 var target_value; 782 if (scrollData.direction == TKControllerScrollDirectionUp) { 783 target_value = Math.max(element.scrollTop - increment, 0); 784 } 785 else if (scrollData.direction == TKControllerScrollDirectionDown) { 786 target_value = Math.min(element.scrollTop + increment, element.scrollHeight - element.offsetHeight); 787 } 788 else if (scrollData.direction == TKControllerScrollDirectionLeft) { 789 target_value = Math.max(element.scrollLeft - increment, 0); 790 } 791 else if (scrollData.direction == TKControllerScrollDirectionRight) { 792 target_value = Math.min(element.scrollLeft + increment, element.scrollWidth - element.offsetWidth); 793 } 794 // only run if we have different values 795 if (start_value == target_value) { 796 TKSpatialNavigationManager.soundToPlay = SOUND_LIMIT; 797 return; 798 } 799 // set the delegate 800 scrollData.animator.delegate = { 801 animationDidIterate : function (fraction) { 802 element[vertical_scrolling ? 'scrollTop' : 'scrollLeft'] = start_value + fraction * (target_value - start_value); 803 } 804 } 805 // start the animation 806 scrollData.animator.start(); 807 // play the move sound since we're scrolling 808 TKSpatialNavigationManager.soundToPlay = SOUND_MOVED; 809 }; 810 811 TKController.prototype.scrollUp = function () { 812 this.upScrollData.target = this.scrollableElement; 813 this.scrollWithData(this.upScrollData); 814 }; 815 816 TKController.prototype.scrollDown = function () { 817 this.downScrollData.target = this.scrollableElement; 818 this.scrollWithData(this.downScrollData); 819 }; 820 821 /* ==================== Keyboard Navigation ==================== */ 822 823 /** 824 * Indicates whether the controller is interested in providing event handling for the key with the given identifier. By default, this method returns 825 * <code>false</code>, letting the spatial navigation manager take care of the event in order to perform navigation. In case the method returns 826 * <code>true</code>, the {@link #keyWasPressed} method is called to let the controller provide its own custom key event handling. 827 * 828 * @param {int} key The identifier for the key that was pressed. 829 * @returns {bool} Whether the controller wants to provide its own custom key event handling. 830 */ 831 TKController.prototype.wantsToHandleKey = function (key) { 832 return (this.scrollableElement !== null && (key == KEYBOARD_UP || key == KEYBOARD_DOWN)); 833 }; 834 835 /** 836 * Triggered when a key was pressed and the receiver has expressed explicit interest in providing custom key event handling by returning <code>true</code> in 837 * the {@link #wantsToHandleKey} method. 838 * 839 * @param {int} key The identifier for the key that was pressed. 840 */ 841 TKController.prototype.keyWasPressed = function (key) { 842 // up should scroll 843 if (key == KEYBOARD_UP) { 844 this.scrollUp(); 845 } 846 // down should scroll 847 else if (key == KEYBOARD_DOWN) { 848 this.scrollDown(); 849 } 850 // done, play the sound 851 TKUtils.playSound(TKSpatialNavigationManager.soundToPlay); 852 }; 853 854 /* ==================== Archival ==================== */ 855 856 /** 857 * Called when the booklet's state is being archived. This method needs to return a list of properties reflecting the current state for the receiver. 858 * 859 * @returns {Object} The list of properties to archive as a hash-like object. 860 * @private 861 */ 862 TKController.prototype.archive = function () { 863 var archive = { 864 id: this.id 865 }; 866 // see if we can add a highlighted element index 867 if (this.highlightedElement !== null) { 868 archive.highlightedElementIndex = this.navigableElements.indexOf(this.highlightedElement); 869 } 870 // 871 return archive; 872 }; 873 874 TKClass(TKController); 875