Controller.js

Summary

No overview generated for 'Controller.js'


Class Summary
TKController  

/**
 *  Copyright © 2009 Apple Inc.  All rights reserved.
 *
 *  @class
 **/
 
TKController.inherits = TKObject;
TKController.synthetizes = ['view', 'metricsView'];

const TKControllerHighlightCSSClass = 'tk-highlighted';
const TKControllerInactiveCSSClass = 'tk-inactive';

TKController.busyControllers = 0;

TKController.controllers = {};

function TKController (data) {
  this.callSuper();
  // synthetized property
  this._view = null;
  this._metricsView = null;
  // parent controller
  this.parentController = null;
  // default transition styles for navigation
  this.becomesInactiveTransition = TKViewTransitionDissolveOut;
  this.becomesActiveTransition = TKViewTransitionDissolveIn;
  // default properties
  this.explicitView = null;
  this.navigatesTo = [];
  this.actions = [];
  this.outlets = [];
  this.backButton = null;
  this.highlightedElement = null;
  this.viewNeverAppeared = true;
  // copy properties from data over to object directly
  TKUtils.copyPropertiesFromSourceToTarget(data, this);
  // this is the array that'll contain all elements
  // that can be navigated to using the keyboard
  this.keyboardElements = [];
  // register controller
  TKController.controllers[this.id] = this;
};

/* ==================== Managing the View ==================== */

TKController.prototype.getView = function () {
  // create the view if it's not set yet
  if (this._view === null) {
    // load the markup
    this.loadView();
    // let our object perform more setup code
    this.viewDidLoad();
    //
    this.processView();
  }
  return this._view;
};

TKController.prototype.getMetricsView = function () {
  return (this._metricsView != null) ? this._metricsView : this.view;
};

TKController.prototype.setView = function (view) {
  this.explicitView = view;
};

TKController.prototype.loadView = function () {
  this.viewNeverAppeared = false;
  // first, check if we have an element already defined
  var view;
  if (this.explicitView !== null) {
    view = this.explicitView;
    // check if node is already in the document
    this.viewNeverAppeared = !TKUtils.isNodeChildOfOtherNode(view, document);
  }
  // check if our view already exists in the DOM
  else {
    view = document.getElementById(this.id);
  }
  // if not, load it from the views directory
  if (view === null) {
    this.viewNeverAppeared = true;
    var request = new XMLHttpRequest();
    var requestFailed = false;
    request.open('GET', 'views/' + this.id + ".html", false);
    try {
      request.send();
    } catch (err) {
      // iTunes will throw an error if the request doesn't exist
      // when using the booklet:// scheme
      // Mark the error here so we can take the FAIL path below, which
      // is actually what we want.
      requestFailed = true;
    }
    // everything went well
    // XXX: we should do more work to differentitate between http:// and file:// URLs here
    if (!requestFailed && ((request.status <= 0 && request.responseText !== '') || request.status == 200)) {
      // XXX: this is way dirty
      var fragment = document.implementation.createHTMLDocument();
      fragment.write(request.responseText);
      view = document.importNode(fragment.getElementById(this.id), true);
    }
    // FAIL
    else {
      view = document.createElement('div');
      view.id = this.id;
    }
  }
  // make sure we know when the view is added to the document if it
  // wasn't part of the DOM already
  if (this.viewNeverAppeared) {
    view.addEventListener('DOMNodeInsertedIntoDocument', this, false);
  }
  // otherwise, process its keyboard regions
  else {
    this.highlightTopElement();
  }
  // link the view to our controller
  view._controller = this;
  // and remember our view
  this._view = view;
};

TKController.prototype.processView = function () {
  var view = this._view;
  // process navigation controllers
  for (var i = 0; i < this.navigatesTo.length; i++) {
    var item = this.navigatesTo[i];
    var element = view.querySelector(item.selector);
    if (element) {
      element._navigationData = item;
      element.addEventListener('click', this, false);
      this.addKeyboardElement(element);
    }
  }
  // process actions
  for (var i = 0; i < this.actions.length; i++) {
    var item = this.actions[i];
    var element = view.querySelector(item.selector);
    if (element) {
      element._actionData = item;
      element.addEventListener('click', this, false);
      this.addKeyboardElement(element);
    }
  }
  // process outlets
  for (var i = 0; i < this.outlets.length; i++) {
    var item = this.outlets[i];
    this[item.name] = view.querySelector(item.selector);
  }
  // process back button
  if (this.backButton !== null) {
    var element = view.querySelector(this.backButton);
    element._backButton = true;
    element.addEventListener('click', this, false);
    this.addKeyboardElement(element);
  }
  // process highlightedElement
  if (this.highlightedElement !== null) {
    var element = this._view.querySelector(this.highlightedElement);
    this.highlightedElement = null;
    this.highlightElement(element);
  }
  // process links
  var links = view.querySelectorAll('a');
  for (var i = 0; i < links.length; i++) {
    this.addKeyboardElement(links[i]);
  }
};

TKController.prototype.viewDidLoad = function () {};

TKController.prototype.viewDidUnload = function () {};

TKController.prototype.isViewLoaded = function () {
  return (this._view !== null);
};

/* ==================== Responding to View Events ==================== */

TKController.prototype.viewWillAppear = function () {};

TKController.prototype.viewDidAppear = function () {};

TKController.prototype.viewWillDisappear = function () {};

TKController.prototype.viewDidDisappear = function () {};

/* ==================== Event Handling ==================== */

TKController.prototype.handleEvent = function (event) {
  switch (event.type) {
    case 'click' : 
      this.elementWasActivated(event.currentTarget);
      break;
    case 'mouseover' : 
      this.elementWasHovered(event.currentTarget);
      break;
    case 'mouseout' : 
      this.elementWasUnhovered(event.currentTarget);
      break;
    case 'DOMNodeInsertedIntoDocument' : 
      this.viewWasInsertedIntoDocument(event);
      break;
  }
};

TKController.prototype.elementWasActivated = function (element) {
  if (element._navigationData !== undefined) {
    // pointer to the controller
    var controller = element._navigationData.controller;
    // if it's a string, try to find it in the controllers hash
    if (TKUtils.objectIsString(controller)) {
      controller = TKController.controllers[controller];
    }
    // error if we have an undefined object
    if (controller === undefined) {
      console.error('TKController.elementWasActivated: trying to push an undefined controller');
      return;
    }
    // otherwise, navigate to it
    this.highlightElement(element);
    TKNavigationController.sharedNavigation.pushController(controller);
  }
  else if (element._actionData !== undefined) {
    this.highlightElement(element);
    // get the callback for this action
    var callback = element._actionData.action;
    // see if it's a string in which case we need to get the function dynamically
    if (TKUtils.objectIsString(callback) && TKUtils.objectHasMethod(this, callback)) {
      callback = this[element._actionData.action];
    }
    // see if we have custom arguments
    if (TKUtils.objectIsArray(element._actionData.arguments)) {
      callback.apply(this, element._actionData.arguments);
    }
    // otherwise just call the callback
    else {
      callback.apply(this);
    }
  }
  else if (element._backButton !== undefined) {
    this.highlightElement(element);
    TKNavigationController.sharedNavigation.popController();
  }
  else if (element.localName == 'a') {
    element.dispatchEvent(TKUtils.createEvent('click', null));
  }
};

TKController.prototype.elementWasHovered = function (element) {
};

TKController.prototype.elementWasUnhovered = function (element) {
};

TKController.prototype.mouseWasMoved = function () {
};

TKController.prototype.viewWasInsertedIntoDocument = function (event) {
  this.viewNeverAppeared = false;
  // need to do this on a delay to actually have metrics
  var _this = this;
  setTimeout(function () {
    _this.highlightTopElement();
  }, 0);
};

/* ==================== Keyboard Navigation ==================== */

const TKControllerKnownDirections = [KEYBOARD_UP, KEYBOARD_RIGHT, KEYBOARD_DOWN, KEYBOARD_LEFT];

TKController.prototype.handleKeydown = function (event) {
  var key = event.keyCode;
  // check if we're busy doing other things
  if (TKController.busyControllers > 0) {
    return;
  }
  // check if we want to activate an element
  if (key == KEYBOARD_RETURN) {
    if (this.highlightedElement !== null) {
      this.elementWasActivated(this.highlightedElement);
    }
  }
  // keyboard nav
  else {
    // do nothing if we don't have any highlightable elements or don't know about this navigation direction
    if (this.keyboardElements.length == 0 || TKControllerKnownDirections.indexOf(key) == -1) {
      return;
    }
    // prevent the default action from happening
    event.preventDefault();
    // create the array of resigning controllers here, which are scoped to this event, if it does not already exist.
    // we use this array to track what controllers have specifically already said they don't have an element that
    // they manage that they can move the highlight too, thus asking for the parent controller if they'd rather handle it.
    if (event._resigningControllers === undefined) {
      event._resigningControllers = [];
    }
    // see if the current highlight item is owned by a controller so that this controller can take care of it right away
    // only if it has not already resigned from managing the event
    if (this.highlightedElement !== null && this.highlightedElement._controller !== undefined &&
        event._resigningControllers.indexOf(this.highlightedElement._controller) == -1) {
      this.highlightedElement._controller.handleKeydown(event);
    }
    else {
      // figure the index of the element to highlight
      var index = this.nearestElementIndexInDirection(key);
      // only highlight if we know what element to highlight
      if (index != -1) {
        this.highlightElement(this.keyboardElements[index]);
        // consider this element for focus since it just got explicit highlight
        // from the user
        TKFocusManager.focusElement(this.highlightedElement);
      }
      // otherwise track that we've resigned the wish to handle this
      // event for navigation purposes and try to push navigation to
      // a parent controller
      else {
        event._resigningControllers.push(this);
        if (this.parentController !== null) {
          this.parentController.handleKeydown(event);
        }
      }
    }
  }
};

TKController.prototype.wantsToHandleKeyboardEvent = function (event) {
  return false;
};

const TKControllerDirectionUp = KEYBOARD_UP;
const TKControllerDirectionRight = KEYBOARD_RIGHT;
const TKControllerDirectionDown = KEYBOARD_DOWN;
const TKControllerDirectionLeft = KEYBOARD_LEFT;

TKController.prototype.nearestElementIndexInDirection = function (direction) {
  // nothing to do if we don't have a next element
  if (this.highlightedElement === null) {
    if (direction == TKControllerDirectionUp) {
      return this.bottomMostIndex();
    }
    else if (direction == TKControllerDirectionRight) {
      return this.leftMostIndex();
    }
    else if (direction == TKControllerDirectionDown) {
      return this.topMostIndex();
    }
    else if (direction == TKControllerDirectionLeft) {
      return this.rightMostIndex();
    }    
  }
  // figure out parameters
  var ref_position, target_position;
  if (direction == TKControllerDirectionUp) {
    ref_position = TKRectMiddleOfTopEdge;
    target_position = TKRectMiddleOfBottomEdge;
  }
  else if (direction == TKControllerDirectionRight) {
    ref_position = TKRectMiddleOfRightEdge;
    target_position = TKRectMiddleOfLeftEdge;
  }
  else if (direction == TKControllerDirectionDown) {
    ref_position = TKRectMiddleOfBottomEdge;
    target_position = TKRectMiddleOfTopEdge;
  }
  else if (direction == TKControllerDirectionLeft) {
    ref_position = TKRectMiddleOfLeftEdge;
    target_position = TKRectMiddleOfRightEdge;
  }
  // look for the closest element now
  var index = -1;
  var min_d = 10000000;
  var highlight_index = this.keyboardElements.indexOf(this.highlightedElement);
  var ref_metrics = this.metricsForElement(this.highlightedElement);
  var ref_point = ref_metrics.pointAtPosition(ref_position);
  var ref_center = ref_metrics.pointAtPosition(TKRectCenter);
  for (var i = 0; i < this.keyboardElements.length; i++) {
    // skip this element if it has display : none
    if (!this.isElementAtIndexNavigatable(i)) {
      continue;
    }
    var metrics = this.metricsForElement(this.keyboardElements[i]);
    // go to next item if it's not in the right direction or already has highlight
    if ((direction == TKControllerDirectionUp && metrics.pointAtPosition(TKRectBottomLeftCorner).y > ref_center.y) ||
        (direction == TKControllerDirectionRight && metrics.pointAtPosition(TKRectTopLeftCorner).x < ref_center.x) ||
        (direction == TKControllerDirectionDown && metrics.pointAtPosition(TKRectTopLeftCorner).y < ref_center.y) ||
        (direction == TKControllerDirectionLeft && metrics.pointAtPosition(TKRectTopRightCorner).x > ref_center.x) ||
        i == highlight_index) {
      continue;
    }
    var d = ref_point.distanceToPoint(metrics.pointAtPosition(target_position));
    if (d < min_d) {
      min_d = d;
      index = i;
    }
  }
  // return the index, if any
  return index;
};

TKController.prototype.topMostIndex = function () {
  var index = 0;
  var min_y = 10000;
  for (var i = 0; i < this.keyboardElements.length; i++) {
    if (!this.isElementAtIndexNavigatable(i)) {
      continue;
    }
    var y = this.metricsForElementAtIndex(i).y;
    if (y < min_y) {
      min_y = y;
      index = i;
    }
  }
  return index;
};

TKController.prototype.rightMostIndex = function () {
  var index = 0;
  var max_x = 0;
  for (var i = 0; i < this.keyboardElements.length; i++) {
    if (!this.isElementAtIndexNavigatable(i)) {
      continue;
    }
    var x = this.metricsForElementAtIndex(i).pointAtPosition(TKRectTopRightCorner).x;
    if (x > max_x) {
      max_x = x;
      index = i;
    }
  }
  return index;
};

TKController.prototype.bottomMostIndex = function () {
  var index = 0;
  var max_y = 0;
  for (var i = 0; i < this.keyboardElements.length; i++) {
    if (!this.isElementAtIndexNavigatable(i)) {
      continue;
    }
    var y = this.metricsForElementAtIndex(i).pointAtPosition(TKRectBottomRightCorner).y;
    if (y > max_y) {
      max_y = y;
      index = i;
    }
  }
  return index;
};

TKController.prototype.leftMostIndex = function () {
  var index = 0;
  var min_x = 10000;
  for (var i = 0; i < this.keyboardElements.length; i++) {
    if (!this.isElementAtIndexNavigatable(i)) {
      continue;
    }
    var y = this.metricsForElementAtIndex(i).x;
    if (y < min_x) {
      min_x = y;
      index = i;
    }
  }
  return index;
};

TKController.prototype.highlightElement = function (element) {
  // nothing to do if we don't really have an element to highlight
  if (!(element instanceof Element)) {
    return;
  }
  //
  if (this.highlightedElement !== null) {
    this.highlightedElement.dispatchEvent(TKUtils.createEvent('unhighlight', element));
    if (SUPPORTS_KEYBOARD_NAVIGATION) {
      this.highlightedElement.removeClassName(TKControllerHighlightCSSClass);
    }
  }
  //
  element.dispatchEvent(TKUtils.createEvent('highlight', this.highlightedElement));
  if (SUPPORTS_KEYBOARD_NAVIGATION) {
    element.addClassName(TKControllerHighlightCSSClass);
  }
  this.highlightedElement = element;
};

TKController.prototype.addKeyboardElement = function (element) {
  // nothing to do if we already know about this element
  if (this.keyboardElements.indexOf(element) > -1) {
    return;
  }
  //
  element.addEventListener('highlight', this, false);
  //
  this.keyboardElements.push(element);
};

TKController.prototype.removeKeyboardElement = function (element) {
  // find the index for this element
  var index = this.keyboardElements.indexOf(element);
  if (index < 0) {
    return;
  }
  // remove elements from the tracking arrays
  this.keyboardElements.splice(index, 1);
};

TKController.prototype.metricsForElement = function (element) {
  var metrics_element = element;
  if (element._controller !== undefined) {
    metrics_element = element._controller.metricsView;
  }
  return metrics_element.getBounds();
};

TKController.prototype.metricsForElementAtIndex = function (index) {
  return this.metricsForElement(this.keyboardElements[index]);
};

TKController.prototype.highlightTopElement = function () {
  // now see if we need to enforce some default element
  if (this.highlightedElement === null && this.keyboardElements.length > 0) {
    this.highlightElement(this.keyboardElements[this.topMostIndex()]);
  }
};

TKController.prototype.isElementAtIndexNavigatable = function (index) {
  return !this.keyboardElements[index].hasClassName(TKControllerInactiveCSSClass);
};

TKClass(TKController);


Documentation generated by JSDoc on Tue Sep 15 21:24:36 2009