<commit_before>function Authorize(opts){
    this.callback = opts.callback;
    this.service = opts.service;
    this.consumer =
        {
            'consumerKey': opts.key,
            'consumerSecret': opts.secret,
            'serviceProvider':
                {
                    'signatureMethod': "HMAC-SHA1",
                    'requestTokenURL': opts.requestTokenUrl,
    				'userAuthorizationURL': opts.userAuthorizationURL,
    				'accessTokenURL': opts.accessTokenURL,
    				'echoURL': opts.callbackUrl
    			}
        }
}

// Get a request token
Authorize.prototype.requestToken = function(){
    console.log('echo', this.consumer.serviceProvider.echoURL);
    var message =
        {
            'method': "post",
            'action': this.consumer.serviceProvider.requestTokenURL,
    		'parameters': [["oauth_callback", this.consumer.serviceProvider.echoURL]]
        }
    var requestBody = OAuth.formEncode(message.parameters);
    OAuth.completeRequest(message, this.consumer);
    var authorizationHeader = OAuth.getAuthorizationHeader("", message.parameters);
	$.ajax(
	    {
	        'url': message.action,
	        'type': message.method,
	        'beforeSend': function(x){
	            x.setRequestHeader("Authorization", authorizationHeader);
            },
            'complete': this.gotRequestToken.bind(this),
            'cache': false,
            'data': requestBody
        }
    );
}

// Got the request token
Authorize.prototype.gotRequestToken = function(xhr){
    console.log('gotRequestToken', xhr);
    if(xhr.status === 200){
        var results = OAuth.decodeForm(xhr.responseText);
        this.oauthToken = OAuth.getParameter(results, "oauth_token");
        this.oauthTokenSecret = OAuth.getParameter(results, "oauth_token_secret");
        this.bindedTabListener = this.tabListener.bind(this);
        chrome.tabs.onUpdated.addListener(this.bindedTabListener);
        chrome.tabs.create(
            {
                'url': this.consumer.serviceProvider.userAuthorizationURL+"?oauth_token="+this.oauthToken
            }
        );
    }
    else{
        this.callback(false);
    }
}

// listen for tab changes
// so we can figure out when
// user authed
Authorize.prototype.tabListener = function(tabId, obj, tab){
    var indexOf = tab.url.indexOf(this.consumer.serviceProvider.echoURL);
	if (indexOf !== -1 && obj.status === "complete"){
		chrome.tabs.onUpdated.removeListener(this.bindedListener);
		var results = OAuth.decodeForm(tab.url.split("?")[1]);
        this.oauthToken = OAuth.getParameter(results, "oauth_token");
        this.oauthVerifier = OAuth.getParameter(results, "oauth_verifier"),
        this.oauthVerifier = this.oauthVerifier.split('#_=_')[0];
		chrome.tabs.remove(tabId);
		this.requestAccessToken();
	}
}

// Get an access token
Authorize.prototype.requestAccessToken = function(){
    console.log('this.oauthVerifier', this.oauthVerifier);
    var message =
        {
		    'method': "post",
            'action': this.consumer.serviceProvider.accessTokenURL,
            'parameters': [["oauth_verifier", this.oauthVerifier]]
        }
    var requestBody = OAuth.formEncode(message.parameters);
    OAuth.completeRequest(
        message,
        {
            'consumerKey': this.consumer.consumerKey,
			'consumerSecret': this.consumer.consumerSecret,
			'token': this.oauthToken,
            'tokenSecret': this.oauthTokenSecret
        }
    );
    var authorizationHeader = OAuth.getAuthorizationHeader("", message.parameters);
    $.ajax(
        {
            'url': message.action,
            'type': message.method,
            'beforeSend': function(x){
                x.setRequestHeader("Authorization", authorizationHeader);
            },
            'complete': this.gotAccessToken.bind(this),
            'cache': false,
            'data': requestBody
        }
    );
}

// Got the access token
Authorize.prototype.gotAccessToken = function(xhr){
    console.log(xhr);
    if (xhr.status == 200){
        var results = OAuth.decodeForm(xhr.responseText);
        var obj = {
            "oauth_token" : OAuth.getParameter(results, "oauth_token"),
            "oauth_token_secret" : OAuth.getParameter(results, "oauth_token_secret")
            }
        this.callback(true, obj, this.service);
    } else {
        this.callback(false);
    }
}

// Oauth 2
// Open the auth dialog
Authorize.prototype.openAuthDialog = function(){
    var url = this.consumer.serviceProvider.userAuthorizationURL;
    url += "?client_id="+this.consumer.consumerKey;
    url += "&redirect_uri="+this.consumer.serviceProvider.echoURL;
    url += "&response_type=code_and_token&scope=non-expiring";
    chrome.tabs.create(
        {
            'url': url
        }
    );
    //https://soundcloud.com/connect?state=SoundCloud_Dialog_7dfac&client_id=leL50hzZ1H8tAdKCLSCnw&redirect_uri=http://oauth.extension.fm/soundcloud&response_type=code_and_token&scope=non-expiring
}<commit_msg>got rdio oauth working. seems like the oauth lib cant hadnle parameters correctly<commit_after>function Authorize(opts){
    this.callback = opts.callback;
    this.service = opts.service;
    this.consumer =
        {
            'consumerKey': opts.key,
            'consumerSecret': opts.secret,
            'serviceProvider':
                {
                    'signatureMethod': "HMAC-SHA1",
                    'requestTokenURL': opts.requestTokenUrl,
    				'userAuthorizationURL': opts.userAuthorizationURL,
    				'accessTokenURL': opts.accessTokenURL,
    				'echoURL': opts.callbackUrl
    			}
        }
}

// Get a request token
Authorize.prototype.requestToken = function(){
    var message =
        {
            'method': "POST",
            'action': this.consumer.serviceProvider.requestTokenURL + '?oauth_callback=' + this.consumer.serviceProvider.echoURL,
    		'parameters': []
        }
    //var requestBody = OAuth.formEncode(message.parameters);
    //console.log(requestBody);
    OAuth.completeRequest(message, this.consumer);
    var authorizationHeader = OAuth.getAuthorizationHeader("", message.parameters);
	$.ajax(
	    {
	        'url': message.action,
	        'type': message.method,
	        'beforeSend': function(x){
	            x.setRequestHeader("Authorization", authorizationHeader);
            },
            'complete': this.gotRequestToken.bind(this),
            'cache': false
        }
    );
}

// Got the request token
Authorize.prototype.gotRequestToken = function(xhr){
    console.log('gotRequestToken', xhr);
    if(xhr.status === 200){
        var results = OAuth.decodeForm(xhr.responseText);
        this.oauthToken = OAuth.getParameter(results, "oauth_token");
        this.oauthTokenSecret = OAuth.getParameter(results, "oauth_token_secret");
        this.bindedTabListener = this.tabListener.bind(this);
        chrome.tabs.onUpdated.addListener(this.bindedTabListener);
        chrome.tabs.create(
            {
                'url': this.consumer.serviceProvider.userAuthorizationURL+"?oauth_token="+this.oauthToken
            }
        );
    }
    else{
        this.callback(false);
    }
}

// listen for tab changes
// so we can figure out when
// user authed
Authorize.prototype.tabListener = function(tabId, obj, tab){
    var indexOf = tab.url.indexOf(this.consumer.serviceProvider.echoURL);
	if (indexOf !== -1 && obj.status === "complete"){
		chrome.tabs.onUpdated.removeListener(this.bindedListener);
		var results = OAuth.decodeForm(tab.url.split("?")[1]);
        this.oauthToken = OAuth.getParameter(results, "oauth_token");
        this.oauthVerifier = OAuth.getParameter(results, "oauth_verifier"),
        this.oauthVerifier = this.oauthVerifier.split('#_=_')[0];
		chrome.tabs.remove(tabId);
		this.requestAccessToken();
	}
}

// Get an access token
Authorize.prototype.requestAccessToken = function(){
    console.log('this.oauthVerifier', this.oauthVerifier);
    var message =
        {
		    'method': "POST",
            'action': this.consumer.serviceProvider.accessTokenURL + '?oauth_verifier=' + this.oauthVerifier,
            'parameters':[]
        }
    var requestBody = OAuth.formEncode(message.parameters);
    OAuth.completeRequest(
        message,
        {
            'consumerKey': this.consumer.consumerKey,
			'consumerSecret': this.consumer.consumerSecret,
			'token': this.oauthToken,
            'tokenSecret': this.oauthTokenSecret
        }
    );
    var authorizationHeader = OAuth.getAuthorizationHeader("", message.parameters);
    $.ajax(
        {
            'url': message.action,
            'type': message.method,
            'beforeSend': function(x){
                x.setRequestHeader("Authorization", authorizationHeader);
            },
            'complete': this.gotAccessToken.bind(this),
            'cache': false,
            'data': requestBody
        }
    );
}

// Got the access token
Authorize.prototype.gotAccessToken = function(xhr){
    console.log(xhr);
    if (xhr.status == 200){
        var results = OAuth.decodeForm(xhr.responseText);
        var obj = {
            "oauth_token" : OAuth.getParameter(results, "oauth_token"),
            "oauth_token_secret" : OAuth.getParameter(results, "oauth_token_secret")
            }
        this.callback(true, obj, this.service);
    } else {
        this.callback(false);
    }
}

// Oauth 2
// Open the auth dialog
Authorize.prototype.openAuthDialog = function(){
    var url = this.consumer.serviceProvider.userAuthorizationURL;
    url += "?client_id="+this.consumer.consumerKey;
    url += "&redirect_uri="+this.consumer.serviceProvider.echoURL;
    url += "&response_type=code_and_token&scope=non-expiring";
    chrome.tabs.create(
        {
            'url': url
        }
    );
    //https://soundcloud.com/connect?state=SoundCloud_Dialog_7dfac&client_id=leL50hzZ1H8tAdKCLSCnw&redirect_uri=http://oauth.extension.fm/soundcloud&response_type=code_and_token&scope=non-expiring
}<|endoftext|>


<commit_before>
'use strict';

var EventEmitter     = require('events'),
    interceptAnchors = require('./anchors'),
    StateWithParams  = require('./StateWithParams'),
    Transition       = require('./Transition'),
    util             = require('./util'),
    State            = require('./State'),
    api              = require('./api');

/*
* Create a new Router instance, passing any state defined declaratively.
* More states can be added using addState().
*
* Because a router manages global state (the URL), only one instance of Router
* should be used inside an application.
*/
function Router(declarativeStates) {
  var router = {},
      states = stateTrees(declarativeStates),
      firstTransition = true,
      options = {
        enableLogs: false,
        interceptAnchors: true,
        notFound: null,
        urlSync: true,
        hashPrefix: ''
      },
      ignoreNextURLChange = false,
      currentPathQuery,
      currentParamsDiff = {},
      currentState,
      previousState,
      transition,
      leafStates,
      urlChanged,
      initialized,
      hashSlashString;

  /*
  * Setting a new state will start a transition from the current state to the target state.
  * A successful transition will result in the URL being changed.
  * A failed transition will leave the router in its current state.
  */
  function setState(state, params, acc) {
    var fromState = transition
      ? StateWithParams(transition.currentState, transition.toParams)
      : currentState;

    var toState = StateWithParams(state, params);

    var diff = util.objectDiff(fromState && fromState.params, params);

    if (preventTransition(fromState, toState, diff)) {
      if (transition && transition.exiting) cancelTransition();
      return;
    }

    if (transition) cancelTransition();

    // While the transition is running, any code asking the router about the previous/current state should
    // get the end result state.
    previousState = currentState;
    currentState = toState;
    currentParamsDiff = diff;

    transition = Transition(
      fromState,
      toState,
      diff,
      acc,
      logger
    );

    startingTransition(fromState, toState);

    // In case of a redirect() called from 'startingTransition', the transition already ended.
    if (transition) transition.run();

    // In case of a redirect() called from the transition itself, the transition already ended
    if (transition) {
      if (transition.cancelled) currentState = fromState;
      else endingTransition(fromState, toState);
    }

    transition = null;
  }

  function cancelTransition() {
    logger.log('Cancelling existing transition from {0} to {1}',
      transition.from, transition.to);

    transition.cancel();

    firstTransition = false;
  }

  function startingTransition(fromState, toState) {
    logger.log('Starting transition from {0} to {1}', fromState, toState);

    var from = fromState ? fromState.asPublic : null;
    var to = toState.asPublic;

    router.transition.emit('started', to, from);
  }

  function endingTransition(fromState, toState) {
    if (!urlChanged && !firstTransition) {
      logger.log('Updating URL: {0}', currentPathQuery);
      updateURLFromState(currentPathQuery, document.title, currentPathQuery);
    }

    firstTransition = false;

    logger.log('Transition from {0} to {1} ended', fromState, toState);

    toState.state.lastParams = toState.params;

    var from = fromState ? fromState.asPublic : null;
    var to = toState.asPublic;
    router.transition.emit('ended', to, from);
  }

  function updateURLFromState(state, title, url) {
    if (isHashMode()) {
      ignoreNextURLChange = true;
      location.hash = options.hashPrefix + url;
    }
    else
      history.pushState(state, title, url);
  }

  /*
  * Return whether the passed state is the same as the current one;
  * in which case the router can ignore the change.
  */
  function preventTransition(current, newState, diff) {
    if (!current) return false;

    return (newState.state == current.state) && (util.objectSize(diff.all) == 0);
  }

  /*
  * The state wasn't found;
  * Transition to the 'notFound' state if the developer specified it or else throw an error.
  */
  function notFound(state) {
    logger.log('State not found: {0}', state);

    if (options.notFound)
      return setState(leafStates[options.notFound], {});
    else throw new Error ('State "' + state + '" could not be found');
  }

  /*
  * Configure the router before its initialization.
  * The available options are:
  *   enableLogs: Whether (debug and error) console logs should be enabled. Defaults to false.
  *   interceptAnchors: Whether anchor mousedown/clicks should be intercepted and trigger a state change. Defaults to true.
  *   notFound: The State to enter when no state matching the current path query or name could be found. Defaults to null.
  *   urlSync: How should the router maintain the current state and the url in sync. Defaults to true (history API).
  *   hashPrefix: Customize the hash separator. Set to '!' in order to have a hashbang like '/#!/'. Defaults to empty string.
  */
  function configure(withOptions) {
    util.mergeObjects(options, withOptions);
    return router;
  }

  /*
  * Initialize the router.
  * The router will immediately initiate a transition to, in order of priority:
  * 1) The init state passed as an argument
  * 2) The state captured by the current URL
  */
  function init(initState, initParams) {
    if (options.enableLogs)
      Router.enableLogs();

    if (options.interceptAnchors)
      interceptAnchors(router);

    hashSlashString = '#' + options.hashPrefix + '/';

    logger.log('Router init');

    initStates();
    logStateTree();

    initState = (initState !== undefined) ? initState : urlPathQuery();

    logger.log('Initializing to state {0}', initState || '""');
    transitionTo(initState, initParams);

    listenToURLChanges();

    initialized = true;
    return router;
  }

  /*
  * Remove any possibility of side effect this router instance might cause.
  * Used for testing purposes.
  */
  function terminate() {
    window.onhashchange = null;
    window.onpopstate = null;
  }

  function listenToURLChanges() {

    function onURLChange(evt) {
      if (ignoreNextURLChange) {
        ignoreNextURLChange = false;
        return;
      }

      var newState = evt.state || urlPathQuery();

      logger.log('URL changed: {0}', newState);
      urlChanged = true;
      setStateForPathQuery(newState);
    }

    window[isHashMode() ? 'onhashchange' : 'onpopstate'] = onURLChange;
  }

  function initStates() {
    var stateArray = util.objectToArray(states);

    eachRootState(function(name, state) {
      state.init(router, name);
    });

    assertPathUniqueness(stateArray);

    // Only leaf states can be transitioned to.
    leafStates = {};
    registerLeafStates(stateArray);
  }

  function assertPathUniqueness(states) {
    var paths = {};

    states.forEach(function(state) {
      if (paths[state.path]) {
        var fullPaths = states.map(function(s) { return s.fullPath() || 'empty' });
        throw new Error('Two sibling states have the same path (' + fullPaths + ')');
      }

      paths[state.path] = 1;
      assertPathUniqueness(state.children);
    });
  }

  function eachRootState(callback) {
    for (var name in states) callback(name, states[name]);
  }

  function registerLeafStates(states) {
    states.forEach(function(state) {
      if (state.children.length)
        registerLeafStates(state.children);
      else {
        leafStates[state.fullName] = state;
        state.paths = util.parsePaths(state.fullPath());
      }
    });
  }

  /*
  * Request a programmatic state change.
  *
  * Two notations are supported:
  * transitionTo('my.target.state', {id: 33, filter: 'desc'})
  * transitionTo('target/33?filter=desc')
  */
  function transitionTo(pathQueryOrName) {
    var isName = leafStates[pathQueryOrName] !== undefined;
    var params = (isName ? arguments[1] : null) || {};
    var acc = isName ? arguments[2] : arguments[1];

    logger.log('Changing state to {0}', pathQueryOrName || '""');

    urlChanged = false;

    if (isName)
      setStateByName(pathQueryOrName, params, acc);
    else
      setStateForPathQuery(pathQueryOrName, acc);
  }

  /*
  * Attempt to navigate to 'stateName' with its previous params or
  * fallback to the defaultParams parameter if the state was never entered.
  */
  function backTo(stateName, defaultParams, acc) {
    var params = leafStates[stateName].lastParams || defaultParams;
    transitionTo(stateName, params, acc);
  }

  function setStateForPathQuery(pathQuery, acc) {
    var state, params, _state, _params;

    currentPathQuery = util.normalizePathQuery(pathQuery);

    var pq = currentPathQuery.split('?');
    var path = pq[0];
    var query = pq[1];
    var paths = util.parsePaths(path);
    var queryParams = util.parseQueryParams(query);

    for (var name in leafStates) {
      _state = leafStates[name];
      _params = _state.matches(paths);

      if (_params) {
        state = _state;
        params = util.mergeObjects(_params, queryParams);
        break;
      }
    }

    if (state) setState(state, params, acc);
    else notFound(currentPathQuery);
  }

  function setStateByName(name, params, acc) {
    var state = leafStates[name];

    if (!state) return notFound(name);

    var pathQuery = interpolate(state, params);
    setStateForPathQuery(pathQuery, acc);
  }

  /*
  * Add a new root state to the router.
  * The name must be unique among root states.
  */
  function addState(name, state) {
    if (states[name])
      throw new Error('A state already exist in the router with the name ' + name);

    state = stateTree(state);

    states[name] = state;

    if (initialized) {
      state.init(router, name);
      registerLeafStates({ _: state });
    }

    return router;
  }

  /*
  * Read the path/query from the URL.
  */
  function urlPathQuery() {
    var hashSlash = location.href.indexOf(hashSlashString);
    var pathQuery;

    if (hashSlash > -1)
      pathQuery = location.href.slice(hashSlash + hashSlashString.length);
    else if (isHashMode())
      pathQuery = '/';
    else
      pathQuery = (location.pathname + location.search).slice(1);

    return util.normalizePathQuery(pathQuery);
  }

  function isHashMode() {
    return (options.urlSync == 'hash');
  }

  /*
  * Compute a link that can be used in anchors' href attributes
  * from a state name and a list of params, a.k.a reverse routing.
  */
  function link(stateName, params) {
    var state = leafStates[stateName];
    if (!state) throw new Error('Cannot find state ' + stateName);

    var interpolated = interpolate(state, params);

    return util.normalizePathQuery(interpolated);
  }

  function interpolate(state, params) {
    var encodedParams = {};

    for (var key in params) {
      encodedParams[key] = encodeURIComponent(params[key]);
    }

    return state.interpolate(encodedParams);
  }

  /*
  * Returns an object representing the current state of the router.
  */
  function getCurrent() {
    return currentState ? currentState.asPublic : null;
  }

  /*
  * Returns an object representing the previous state of the router
  * or null if the router is still in its initial state.
  */
  function getPrevious() {
    return previousState ? previousState.asPublic : null;
  }

  /*
  * Returns the diff between the current params and the previous ones.
  */
  function getParamsDiff() {
    return currentParamsDiff;
  }

  /*
  * Returns whether the router is executing its first transition.
  */
  function isFirstTransition() {
    return previousState == null;
  }

  function stateTrees(states) {
    return util.mapValues(states, stateTree);
  }

  /*
  * Creates an internal State object from a specification POJO.
  */
  function stateTree(state) {
    if (state.children) state.children = stateTrees(state.children);
    return State(state);
  }

  function logStateTree() {
    if (!logger.enabled) return;

    var indent = function(level) {
      if (level == 0) return '';
      return new Array(2 + (level - 1) * 4).join(' ') + '── ';
    }

    var stateTree = function(state) {
      var path = util.normalizePathQuery(state.fullPath());
      var pathStr = (state.children.length == 0)
        ? ' (@ path)'.replace('path', path)
        : '';
      var str = indent(state.parents.length) + state.name + pathStr + '\n';
      return str + state.children.map(stateTree).join('');
    }

    var msg = '\nState tree\n\n';
    msg += util.objectToArray(states).map(stateTree).join('');
    msg += '\n';

    logger.log(msg);
  }

  // Public methods

  router.configure = configure;
  router.init = init;
  router.transitionTo = transitionTo;
  router.backTo = backTo;
  router.addState = addState;
  router.link = link;
  router.current = getCurrent;
  router.previous = getPrevious;
  router.isFirstTransition = isFirstTransition;
  router.paramsDiff = getParamsDiff;

  router.transition = new EventEmitter();

  // Used for testing purposes only
  router.urlPathQuery = urlPathQuery;
  router.terminate = terminate;

  util.mergeObjects(api, router);

  return router;
}

// Logging

var logger = {
  log: util.noop,
  error: util.noop,
  enabled: false
};

Router.enableLogs = function() {
  logger.enabled = true;

  logger.log = function() {
    var message = util.makeMessage.apply(null, arguments);
    console.log(message);
  };

  logger.error = function() {
    var message = util.makeMessage.apply(null, arguments);
    console.error(message);
  };

};

module.exports = Router;<commit_msg>Quick refactoring: Less side effect<commit_after>
'use strict';

var EventEmitter     = require('events'),
    interceptAnchors = require('./anchors'),
    StateWithParams  = require('./StateWithParams'),
    Transition       = require('./Transition'),
    util             = require('./util'),
    State            = require('./State'),
    api              = require('./api');

/*
* Create a new Router instance, passing any state defined declaratively.
* More states can be added using addState().
*
* Because a router manages global state (the URL), only one instance of Router
* should be used inside an application.
*/
function Router(declarativeStates) {
  var router = {},
      states = stateTrees(declarativeStates),
      firstTransition = true,
      options = {
        enableLogs: false,
        interceptAnchors: true,
        notFound: null,
        urlSync: true,
        hashPrefix: ''
      },
      ignoreNextURLChange = false,
      currentPathQuery,
      currentParamsDiff = {},
      currentState,
      previousState,
      transition,
      leafStates,
      urlChanged,
      initialized,
      hashSlashString;

  /*
  * Setting a new state will start a transition from the current state to the target state.
  * A successful transition will result in the URL being changed.
  * A failed transition will leave the router in its current state.
  */
  function setState(state, params, acc) {
    var fromState = transition
      ? StateWithParams(transition.currentState, transition.toParams)
      : currentState;

    var toState = StateWithParams(state, params);

    var diff = util.objectDiff(fromState && fromState.params, params);

    if (preventTransition(fromState, toState, diff)) {
      if (transition && transition.exiting) cancelTransition();
      return;
    }

    if (transition) cancelTransition();

    // While the transition is running, any code asking the router about the previous/current state should
    // get the end result state.
    previousState = currentState;
    currentState = toState;
    currentParamsDiff = diff;

    transition = Transition(
      fromState,
      toState,
      diff,
      acc,
      logger
    );

    startingTransition(fromState, toState);

    // In case of a redirect() called from 'startingTransition', the transition already ended.
    if (transition) transition.run();

    // In case of a redirect() called from the transition itself, the transition already ended
    if (transition) {
      if (transition.cancelled) currentState = fromState;
      else endingTransition(fromState, toState);
    }

    transition = null;
  }

  function cancelTransition() {
    logger.log('Cancelling existing transition from {0} to {1}',
      transition.from, transition.to);

    transition.cancel();

    firstTransition = false;
  }

  function startingTransition(fromState, toState) {
    logger.log('Starting transition from {0} to {1}', fromState, toState);

    var from = fromState ? fromState.asPublic : null;
    var to = toState.asPublic;

    router.transition.emit('started', to, from);
  }

  function endingTransition(fromState, toState) {
    if (!urlChanged && !firstTransition) {
      logger.log('Updating URL: {0}', currentPathQuery);
      updateURLFromState(currentPathQuery, document.title, currentPathQuery);
    }

    firstTransition = false;

    logger.log('Transition from {0} to {1} ended', fromState, toState);

    toState.state.lastParams = toState.params;

    var from = fromState ? fromState.asPublic : null;
    var to = toState.asPublic;
    router.transition.emit('ended', to, from);
  }

  function updateURLFromState(state, title, url) {
    if (isHashMode()) {
      ignoreNextURLChange = true;
      location.hash = options.hashPrefix + url;
    }
    else
      history.pushState(state, title, url);
  }

  /*
  * Return whether the passed state is the same as the current one;
  * in which case the router can ignore the change.
  */
  function preventTransition(current, newState, diff) {
    if (!current) return false;

    return (newState.state == current.state) && (util.objectSize(diff.all) == 0);
  }

  /*
  * The state wasn't found;
  * Transition to the 'notFound' state if the developer specified it or else throw an error.
  */
  function notFound(state) {
    logger.log('State not found: {0}', state);

    if (options.notFound)
      return setState(leafStates[options.notFound], {});
    else throw new Error ('State "' + state + '" could not be found');
  }

  /*
  * Configure the router before its initialization.
  * The available options are:
  *   enableLogs: Whether (debug and error) console logs should be enabled. Defaults to false.
  *   interceptAnchors: Whether anchor mousedown/clicks should be intercepted and trigger a state change. Defaults to true.
  *   notFound: The State to enter when no state matching the current path query or name could be found. Defaults to null.
  *   urlSync: How should the router maintain the current state and the url in sync. Defaults to true (history API).
  *   hashPrefix: Customize the hash separator. Set to '!' in order to have a hashbang like '/#!/'. Defaults to empty string.
  */
  function configure(withOptions) {
    util.mergeObjects(options, withOptions);
    return router;
  }

  /*
  * Initialize the router.
  * The router will immediately initiate a transition to, in order of priority:
  * 1) The init state passed as an argument
  * 2) The state captured by the current URL
  */
  function init(initState, initParams) {
    if (options.enableLogs)
      Router.enableLogs();

    if (options.interceptAnchors)
      interceptAnchors(router);

    hashSlashString = '#' + options.hashPrefix + '/';

    logger.log('Router init');

    initStates();
    logStateTree();

    initState = (initState !== undefined) ? initState : urlPathQuery();

    logger.log('Initializing to state {0}', initState || '""');
    transitionTo(initState, initParams);

    listenToURLChanges();

    initialized = true;
    return router;
  }

  /*
  * Remove any possibility of side effect this router instance might cause.
  * Used for testing purposes.
  */
  function terminate() {
    window.onhashchange = null;
    window.onpopstate = null;
  }

  function listenToURLChanges() {

    function onURLChange(evt) {
      if (ignoreNextURLChange) {
        ignoreNextURLChange = false;
        return;
      }

      var newState = evt.state || urlPathQuery();

      logger.log('URL changed: {0}', newState);
      urlChanged = true;
      setStateForPathQuery(newState);
    }

    window[isHashMode() ? 'onhashchange' : 'onpopstate'] = onURLChange;
  }

  function initStates() {
    var stateArray = util.objectToArray(states);

    eachRootState(function(name, state) {
      state.init(router, name);
    });

    assertPathUniqueness(stateArray);

    leafStates = registerLeafStates(stateArray, {});
  }

  function assertPathUniqueness(states) {
    var paths = {};

    states.forEach(function(state) {
      if (paths[state.path]) {
        var fullPaths = states.map(function(s) { return s.fullPath() || 'empty' });
        throw new Error('Two sibling states have the same path (' + fullPaths + ')');
      }

      paths[state.path] = 1;
      assertPathUniqueness(state.children);
    });
  }

  function eachRootState(callback) {
    for (var name in states) callback(name, states[name]);
  }

  function registerLeafStates(states, leafStates) {
    return states.reduce(function(leafStates, state) {
      if (state.children.length)
        return registerLeafStates(state.children, leafStates);
      else {
        leafStates[state.fullName] = state;
        state.paths = util.parsePaths(state.fullPath());
        return leafStates;
      }
    }, leafStates);
  }

  /*
  * Request a programmatic state change.
  *
  * Two notations are supported:
  * transitionTo('my.target.state', {id: 33, filter: 'desc'})
  * transitionTo('target/33?filter=desc')
  */
  function transitionTo(pathQueryOrName) {
    var isName = leafStates[pathQueryOrName] !== undefined;
    var params = (isName ? arguments[1] : null) || {};
    var acc = isName ? arguments[2] : arguments[1];

    logger.log('Changing state to {0}', pathQueryOrName || '""');

    urlChanged = false;

    if (isName)
      setStateByName(pathQueryOrName, params, acc);
    else
      setStateForPathQuery(pathQueryOrName, acc);
  }

  /*
  * Attempt to navigate to 'stateName' with its previous params or
  * fallback to the defaultParams parameter if the state was never entered.
  */
  function backTo(stateName, defaultParams, acc) {
    var params = leafStates[stateName].lastParams || defaultParams;
    transitionTo(stateName, params, acc);
  }

  function setStateForPathQuery(pathQuery, acc) {
    var state, params, _state, _params;

    currentPathQuery = util.normalizePathQuery(pathQuery);

    var pq = currentPathQuery.split('?');
    var path = pq[0];
    var query = pq[1];
    var paths = util.parsePaths(path);
    var queryParams = util.parseQueryParams(query);

    for (var name in leafStates) {
      _state = leafStates[name];
      _params = _state.matches(paths);

      if (_params) {
        state = _state;
        params = util.mergeObjects(_params, queryParams);
        break;
      }
    }

    if (state) setState(state, params, acc);
    else notFound(currentPathQuery);
  }

  function setStateByName(name, params, acc) {
    var state = leafStates[name];

    if (!state) return notFound(name);

    var pathQuery = interpolate(state, params);
    setStateForPathQuery(pathQuery, acc);
  }

  /*
  * Add a new root state to the router.
  * The name must be unique among root states.
  */
  function addState(name, state) {
    if (states[name])
      throw new Error('A state already exist in the router with the name ' + name);

    state = stateTree(state);

    states[name] = state;

    if (initialized) {
      state.init(router, name);
      registerLeafStates({ _: state });
    }

    return router;
  }

  /*
  * Read the path/query from the URL.
  */
  function urlPathQuery() {
    var hashSlash = location.href.indexOf(hashSlashString);
    var pathQuery;

    if (hashSlash > -1)
      pathQuery = location.href.slice(hashSlash + hashSlashString.length);
    else if (isHashMode())
      pathQuery = '/';
    else
      pathQuery = (location.pathname + location.search).slice(1);

    return util.normalizePathQuery(pathQuery);
  }

  function isHashMode() {
    return (options.urlSync == 'hash');
  }

  /*
  * Compute a link that can be used in anchors' href attributes
  * from a state name and a list of params, a.k.a reverse routing.
  */
  function link(stateName, params) {
    var state = leafStates[stateName];
    if (!state) throw new Error('Cannot find state ' + stateName);

    var interpolated = interpolate(state, params);

    return util.normalizePathQuery(interpolated);
  }

  function interpolate(state, params) {
    var encodedParams = {};

    for (var key in params) {
      encodedParams[key] = encodeURIComponent(params[key]);
    }

    return state.interpolate(encodedParams);
  }

  /*
  * Returns an object representing the current state of the router.
  */
  function getCurrent() {
    return currentState ? currentState.asPublic : null;
  }

  /*
  * Returns an object representing the previous state of the router
  * or null if the router is still in its initial state.
  */
  function getPrevious() {
    return previousState ? previousState.asPublic : null;
  }

  /*
  * Returns the diff between the current params and the previous ones.
  */
  function getParamsDiff() {
    return currentParamsDiff;
  }

  /*
  * Returns whether the router is executing its first transition.
  */
  function isFirstTransition() {
    return previousState == null;
  }

  function stateTrees(states) {
    return util.mapValues(states, stateTree);
  }

  /*
  * Creates an internal State object from a specification POJO.
  */
  function stateTree(state) {
    if (state.children) state.children = stateTrees(state.children);
    return State(state);
  }

  function logStateTree() {
    if (!logger.enabled) return;

    var indent = function(level) {
      if (level == 0) return '';
      return new Array(2 + (level - 1) * 4).join(' ') + '── ';
    }

    var stateTree = function(state) {
      var path = util.normalizePathQuery(state.fullPath());
      var pathStr = (state.children.length == 0)
        ? ' (@ path)'.replace('path', path)
        : '';
      var str = indent(state.parents.length) + state.name + pathStr + '\n';
      return str + state.children.map(stateTree).join('');
    }

    var msg = '\nState tree\n\n';
    msg += util.objectToArray(states).map(stateTree).join('');
    msg += '\n';

    logger.log(msg);
  }

  // Public methods

  router.configure = configure;
  router.init = init;
  router.transitionTo = transitionTo;
  router.backTo = backTo;
  router.addState = addState;
  router.link = link;
  router.current = getCurrent;
  router.previous = getPrevious;
  router.isFirstTransition = isFirstTransition;
  router.paramsDiff = getParamsDiff;

  router.transition = new EventEmitter();

  // Used for testing purposes only
  router.urlPathQuery = urlPathQuery;
  router.terminate = terminate;

  util.mergeObjects(api, router);

  return router;
}

// Logging

var logger = {
  log: util.noop,
  error: util.noop,
  enabled: false
};

Router.enableLogs = function() {
  logger.enabled = true;

  logger.log = function() {
    var message = util.makeMessage.apply(null, arguments);
    console.log(message);
  };

  logger.error = function() {
    var message = util.makeMessage.apply(null, arguments);
    console.error(message);
  };

};

module.exports = Router;<|endoftext|>


<commit_before>/**
 * Views for the story viewer Backbone application
 */
Namespace('storybase.viewer');

// Container view for the viewer application.
// It delegates rendering and events for the entire app to views
// rendering more specific "widgets"
storybase.viewer.views.ViewerApp = Backbone.View.extend({
  // Initialize the view
  initialize: function() {
    this.navigationView = new storybase.viewer.views.StoryNavigation();
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.setSection(this.sections.at(0), {showActiveSection: false});
    _.bindAll(this, 'handleScroll');
    $(window).scroll(this.handleScroll);
  },

  // Add the view's container element to the DOM and render the sub-views
  render: function() {
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    return this;
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
  },

  // Show the active section
  showActiveSection: function() {
    throw "showActiveSection() is not implemented";
  },

  // Set the active story section
  setSection: function(section, options) {
    options = typeof options !== 'undefined' ? options : {};
    var showActiveSection = options.hasOwnProperty('showActiveSection') ? options.showActiveSection : true;
    this.activeSection = section;
    if (showActiveSection) {
      this.showActiveSection();
    }
    this.updateSubviewSections();
  },

  // Like setSection(), but takes a section ID as an argument instead of
  // a Section model instance object
  setSectionById: function(id) {
    this.setSection(this.sections.get(id));
  },

  // Convenience method to get the element for the active
  activeSectionEl: function() {
    return this.$('#' + this.activeSection.id);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    // Do nothing. Subclasses might want to implement this to do some work
  }
});

// View for the story viewer header.
// Updates the section heading title when the active section changes
storybase.viewer.views.StoryHeader = Backbone.View.extend({
  el: 'header',

  render: function() {
    var $titleEl = this.$el.find('.section-title').first();
    if ($titleEl.length == 0) {
      $titleEl = $('
');
      this.$el.append($titleEl);
    }
    $titleEl.text(this.section.get('title'));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// View to provide previous/next buttons to navigate between sections
storybase.viewer.views.StoryNavigation = Backbone.View.extend({
  tagName: 'nav',

  className: 'story-nav',

  initialize: function() {
    this.section = null;
    if (this.options.hasOwnProperty('addlLinks')) {
      this.addlLinks = this.options.addlLinks.map(function(link) {
        return {
         text: link.text,
         id: link.id,
         href: link.hasOwnProperty('href') ? link.href: '#'
       }
      });
    }
    else {
      this.addlLinks = [];
    }
  },

  render: function() {
    var context = {};
    if (this.section) {
      context.next_section = this.section.collection.get(
        this.section.get('next_section_id'));
      context.previous_section = this.section.collection.get(
        this.section.get('previous_section_id'));
    }
    context.addl_links = this.addlLinks;

    this.$el.html(ich.navigationTemplate(context));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// Interative visualization of a spider story structure
storybase.viewer.views.Spider = Backbone.View.extend({
  events: {
    "hover g.node": "hoverSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.activeSection = null;
    // The id for the visualization's wrapper element
    this.visId = 'spider-vis';
  },

  // Return the visualization wrapper element
  visEl: function() {
    return this.$('#' + this.visId).first();
  },

  setSection: function(section) {
    this.activeSection = section;
    this.highlightSectionNode(this.activeSection.id);
  },

  highlightSectionNode: function(sectionId) {
    d3.selectAll('g.node').classed('active', false);
    d3.select('g.node.section-' + sectionId).classed('active', true);
  },

  render: function() {
    var elId = this.$el.attr('id');
    var width = this.$el.width();
    var height = this.$el.height();
    var vis = d3.select("#" + elId).insert("svg", "section")
        .attr("id", this.visId)
        .attr("width", width)
        .attr("height", height)
      .append("g")
      .attr("transform", "translate(40, 100)");
    var rootSection = this.sections.at(0).populateChildren();
    var tree = d3.layout.tree().size([300, 150]);
    var diagonal = d3.svg.diagonal();
    var nodes = tree.nodes(rootSection);
    var links = tree.links(nodes);

    var link = vis.selectAll("path.link")
        .data(links)
      .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    var node = vis.selectAll("g.node")
      .data(nodes)
      .enter().append("g")
      .attr('class', function(d) {
        return "node section-" + d.id;
      })
      .attr("transform",function(d) { return "translate(" + d.x + "," + d.y + ")"; });

    node.append("circle")
        .attr("r", 10);

    node.append("text")
      .attr("dx", function(d) { return d.children ? -20 : 20; })
      .attr("dy", 3)
      .attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
      .text(function(d) { return d.get('title'); })

  },

  // Event handler for hovering over a section node
  // Changes the cursor to a "pointer" style icon to indicate that
  // the nodes are clickable.
  hoverSectionNode: function(e) {
    e.currentTarget.style.cursor = 'pointer';
  },
});

// Master view that shows a story in a linear fashion
storybase.viewer.views.LinearViewerApp = storybase.viewer.views.ViewerApp.extend({
  // Get the position, from the top of the winder, of the lower edge of the
  // header
  headerBottom: function() {
    return this.$('header').offset().top + this.$('header').outerHeight();
  },

  footerTop: function() {
    return this.$('footer').offset().top;
  },

  // Show the active section
  showActiveSection: function() {
    var sectionTop = this.$('#' + this.activeSection.id).offset().top;
    $(window).scrollTop(sectionTop - this.headerBottom());
  },

  // Return the element of the last visible story section
  getLastVisibleSectionEl: function() {
    var visibleSections = [];
    var numSections = this.$('section').length;
    for (var i = 0; i < numSections; i++) {
      var $section = this.$('section').eq(i);
      var sectionTop = $section.offset().top;
      var sectionBottom = sectionTop + $section.outerHeight();
      if (sectionTop >= this.headerBottom() &&
          sectionBottom <= this.footerTop()) {
        visibleSections.push($section);
      }
    }
    return _.last(visibleSections);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    var $lastVisibleSectionEl = this.getLastVisibleSectionEl();
    if ($lastVisibleSectionEl) {
      var lastVisibleSection = this.sections.get($lastVisibleSectionEl.attr('id'));
      if (lastVisibleSection != this.activeSection) {
        this.setSection(lastVisibleSection, {showActiveSection: false});
      }
    }
  }
});

// Master view that shows the story structure visualization initially
storybase.viewer.views.SpiderViewerApp = storybase.viewer.views.ViewerApp.extend({
  events: {
    "click #topic-map": "clickTopicMapLink",
    "click g.node": "clickSectionNode"
  },

  initialize: function() {
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      addlLinks: [{text: "Topic Map", id: 'topic-map'}]
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.initialView = new storybase.viewer.views.Spider({
      el: this.$('#body'),
      sections: this.options.sections
    });
    this.sections = this.options.sections;
    this.story = this.options.story;
  },

  render: function() {
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.initialView.render();
    // Hide all the section content initially
    this.$('section').hide();
    return this;
  },

  // Show the active section
  showActiveSection: function() {
    // Hide all sections other than the active one
    this.$('section').hide();
    this.$('#' + this.activeSection.id).show();
    // Hide the visualization
    this.initialView.$('svg').hide();
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
    this.initialView.setSection(this.activeSection);
  },

  // Event handler for clicks on a section node
  clickSectionNode: function(e) {
    var node = d3.select(e.currentTarget);
    var sectionId = node.data()[0].id;
    this.initialView.visEl().hide();
    // TODO: I'm not sure if this is the best way to access the router
    // I don't like global variables, but passing it as an argument
    // makes for a weird circular dependency between the master view/
    // router.
    storybase.viewer.router.navigate("sections/" + sectionId,
                                     {trigger: true});
  },

  // Event handler for clicking the "Topic Map" link
  clickTopicMapLink: function(e) {
    this.activeSectionEl().toggle();
    this.initialView.visEl().show();
  }

});

// Get the appropriate master view based on the story structure type
storybase.viewer.views.getViewerApp = function(structureType, options) {
  if (structureType == 'linear') {
    return new storybase.viewer.views.LinearViewerApp(options);
  }
  else if (structureType == 'spider') {
    return new storybase.viewer.views.SpiderViewerApp(options);
  }
  else {
    throw "Unknown story structure type '" + structureType + "'";
  }
};
<commit_msg>Prevent the scroll event handler logic if we invoke the scroll using scrollTop()<commit_after>/**
 * Views for the story viewer Backbone application
 */
Namespace('storybase.viewer');

// Container view for the viewer application.
// It delegates rendering and events for the entire app to views
// rendering more specific "widgets"
storybase.viewer.views.ViewerApp = Backbone.View.extend({
  // Initialize the view
  initialize: function() {
    this.navigationView = new storybase.viewer.views.StoryNavigation();
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.setSection(this.sections.at(0), {showActiveSection: false});
    _.bindAll(this, 'handleScroll');
    $(window).scroll(this.handleScroll);
  },

  // Add the view's container element to the DOM and render the sub-views
  render: function() {
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    return this;
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
  },

  // Show the active section
  showActiveSection: function() {
    throw "showActiveSection() is not implemented";
  },

  // Set the active story section
  setSection: function(section, options) {
    options = typeof options !== 'undefined' ? options : {};
    var showActiveSection = options.hasOwnProperty('showActiveSection') ? options.showActiveSection : true;
    this.activeSection = section;
    if (showActiveSection) {
      this.showActiveSection();
    }
    this.updateSubviewSections();
  },

  // Like setSection(), but takes a section ID as an argument instead of
  // a Section model instance object
  setSectionById: function(id) {
    this.setSection(this.sections.get(id));
  },

  // Convenience method to get the element for the active
  activeSectionEl: function() {
    return this.$('#' + this.activeSection.id);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    // Do nothing. Subclasses might want to implement this to do some work
  }
});

// View for the story viewer header.
// Updates the section heading title when the active section changes
storybase.viewer.views.StoryHeader = Backbone.View.extend({
  el: 'header',

  render: function() {
    var $titleEl = this.$el.find('.section-title').first();
    if ($titleEl.length == 0) {
      $titleEl = $('
');
      this.$el.append($titleEl);
    }
    $titleEl.text(this.section.get('title'));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// View to provide previous/next buttons to navigate between sections
storybase.viewer.views.StoryNavigation = Backbone.View.extend({
  tagName: 'nav',

  className: 'story-nav',

  initialize: function() {
    this.section = null;
    if (this.options.hasOwnProperty('addlLinks')) {
      this.addlLinks = this.options.addlLinks.map(function(link) {
        return {
         text: link.text,
         id: link.id,
         href: link.hasOwnProperty('href') ? link.href: '#'
       }
      });
    }
    else {
      this.addlLinks = [];
    }
  },

  render: function() {
    var context = {};
    if (this.section) {
      context.next_section = this.section.collection.get(
        this.section.get('next_section_id'));
      context.previous_section = this.section.collection.get(
        this.section.get('previous_section_id'));
    }
    context.addl_links = this.addlLinks;

    this.$el.html(ich.navigationTemplate(context));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// Interative visualization of a spider story structure
storybase.viewer.views.Spider = Backbone.View.extend({
  events: {
    "hover g.node": "hoverSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.activeSection = null;
    // The id for the visualization's wrapper element
    this.visId = 'spider-vis';
  },

  // Return the visualization wrapper element
  visEl: function() {
    return this.$('#' + this.visId).first();
  },

  setSection: function(section) {
    this.activeSection = section;
    this.highlightSectionNode(this.activeSection.id);
  },

  highlightSectionNode: function(sectionId) {
    d3.selectAll('g.node').classed('active', false);
    d3.select('g.node.section-' + sectionId).classed('active', true);
  },

  render: function() {
    var elId = this.$el.attr('id');
    var width = this.$el.width();
    var height = this.$el.height();
    var vis = d3.select("#" + elId).insert("svg", "section")
        .attr("id", this.visId)
        .attr("width", width)
        .attr("height", height)
      .append("g")
      .attr("transform", "translate(40, 100)");
    var rootSection = this.sections.at(0).populateChildren();
    var tree = d3.layout.tree().size([300, 150]);
    var diagonal = d3.svg.diagonal();
    var nodes = tree.nodes(rootSection);
    var links = tree.links(nodes);

    var link = vis.selectAll("path.link")
        .data(links)
      .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    var node = vis.selectAll("g.node")
      .data(nodes)
      .enter().append("g")
      .attr('class', function(d) {
        return "node section-" + d.id;
      })
      .attr("transform",function(d) { return "translate(" + d.x + "," + d.y + ")"; });

    node.append("circle")
        .attr("r", 10);

    node.append("text")
      .attr("dx", function(d) { return d.children ? -20 : 20; })
      .attr("dy", 3)
      .attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
      .text(function(d) { return d.get('title'); })

  },

  // Event handler for hovering over a section node
  // Changes the cursor to a "pointer" style icon to indicate that
  // the nodes are clickable.
  hoverSectionNode: function(e) {
    e.currentTarget.style.cursor = 'pointer';
  },
});

// Master view that shows a story in a linear fashion
storybase.viewer.views.LinearViewerApp = storybase.viewer.views.ViewerApp.extend({
  // Get the position, from the top of the winder, of the lower edge of the
  // header
  headerBottom: function() {
    return this.$('header').offset().top + this.$('header').outerHeight();
  },

  footerTop: function() {
    return this.$('footer').offset().top;
  },

  // Show the active section
  showActiveSection: function() {
    var sectionTop = this.$('#' + this.activeSection.id).offset().top;
    this._preventScrollEvent = true;
    $(window).scrollTop(sectionTop - this.headerBottom());
  },

  // Return the element of the last visible story section
  getLastVisibleSectionEl: function() {
    var visibleSections = [];
    var numSections = this.$('section').length;
    for (var i = 0; i < numSections; i++) {
      var $section = this.$('section').eq(i);
      var sectionTop = $section.offset().top;
      var sectionBottom = sectionTop + $section.outerHeight();
      if (sectionTop >= this.headerBottom() &&
          sectionBottom <= this.footerTop()) {
        visibleSections.push($section);
      }
    }
    return _.last(visibleSections);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    var newSection = this.activeSection;
    if (this._preventScrollEvent !== true) {
      if ($(window).scrollTop() == 0) {
	// At the top of the window.  Set the active section to the
	// first section
	newSection = this.sections.first();
      }
      else {
	var $lastVisibleSectionEl = this.getLastVisibleSectionEl();
	  if ($lastVisibleSectionEl) {
	  var lastVisibleSection = this.sections.get($lastVisibleSectionEl.attr('id'));
	  if (lastVisibleSection != this.activeSection) {
	    newSection = lastVisibleSection;
	  }
	}
      }
      this.setSection(newSection, {showActiveSection: false});
    }
    this._preventScrollEvent = false;
  }
});

// Master view that shows the story structure visualization initially
storybase.viewer.views.SpiderViewerApp = storybase.viewer.views.ViewerApp.extend({
  events: {
    "click #topic-map": "clickTopicMapLink",
    "click g.node": "clickSectionNode"
  },

  initialize: function() {
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      addlLinks: [{text: "Topic Map", id: 'topic-map'}]
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.initialView = new storybase.viewer.views.Spider({
      el: this.$('#body'),
      sections: this.options.sections
    });
    this.sections = this.options.sections;
    this.story = this.options.story;
  },

  render: function() {
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.initialView.render();
    // Hide all the section content initially
    this.$('section').hide();
    return this;
  },

  // Show the active section
  showActiveSection: function() {
    // Hide all sections other than the active one
    this.$('section').hide();
    this.$('#' + this.activeSection.id).show();
    // Hide the visualization
    this.initialView.$('svg').hide();
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
    this.initialView.setSection(this.activeSection);
  },

  // Event handler for clicks on a section node
  clickSectionNode: function(e) {
    var node = d3.select(e.currentTarget);
    var sectionId = node.data()[0].id;
    this.initialView.visEl().hide();
    // TODO: I'm not sure if this is the best way to access the router
    // I don't like global variables, but passing it as an argument
    // makes for a weird circular dependency between the master view/
    // router.
    storybase.viewer.router.navigate("sections/" + sectionId,
                                     {trigger: true});
  },

  // Event handler for clicking the "Topic Map" link
  clickTopicMapLink: function(e) {
    this.activeSectionEl().toggle();
    this.initialView.visEl().show();
  }

});

// Get the appropriate master view based on the story structure type
storybase.viewer.views.getViewerApp = function(structureType, options) {
  if (structureType == 'linear') {
    return new storybase.viewer.views.LinearViewerApp(options);
  }
  else if (structureType == 'spider') {
    return new storybase.viewer.views.SpiderViewerApp(options);
  }
  else {
    throw "Unknown story structure type '" + structureType + "'";
  }
};
<|endoftext|>


<commit_before>/**
 * Views for the story viewer Backbone application
 */
Namespace('storybase.viewer');

// Translations for interface text
Globalize.addCultureInfo('en', {
  messages: {
    "Topic Map": "Topic Map",
    "How Can You Help?": "How Can You Help?"
  }
});

Globalize.addCultureInfo('es', {
  messages: {
    "Topic Map": "Mapa Temático",
    "How Can You Help?": "How Can You Help?"
  }
});

// Container view for the viewer application.
// It delegates rendering and events for the entire app to views
// rendering more specific "widgets"
storybase.viewer.views.ViewerApp = Backbone.View.extend({
  // Initialize the view
  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.setSection(this.sections.at(0), {showActiveSection: false});
    _.bindAll(this, 'handleScroll');
    $(window).scroll(this.handleScroll);
  },

  // Add the view's container element to the DOM and render the sub-views
  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.$('.summary').show();
    this.$('.section').show();
    return this;
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
  },

  // Show the active section
  showActiveSection: function() {
    throw "showActiveSection() is not implemented";
  },

  // Set the active story section
  setSection: function(section, options) {
    options = typeof options !== 'undefined' ? options : {};
    var showActiveSection = options.hasOwnProperty('showActiveSection') ? options.showActiveSection : true;
    this.activeSection = section;
    if (showActiveSection) {
      this.showActiveSection();
    }
    this.updateSubviewSections();
  },

  // Like setSection(), but takes a section ID as an argument instead of
  // a Section model instance object
  setSectionById: function(id) {
    this.setSection(this.sections.get(id));
  },

  // Convenience method to get the element for the active section
  activeSectionEl: function() {
      return this.$('#' + this.activeSection.id);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    // Do nothing. Subclasses might want to implement this to do some work
  }
});

// View for the story viewer header.
// Updates the section heading title when the active section changes
storybase.viewer.views.StoryHeader = Backbone.View.extend({
  el: 'header',

  render: function() {
    var $titleEl = this.$el.find('.section-title').first();
    if ($titleEl.length == 0) {
      $titleEl = $('
');
      this.$el.append($titleEl);
    }
    $titleEl.text(this.section.get('title'));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// View to provide previous/next buttons to navigate between sections
storybase.viewer.views.StoryNavigation = Backbone.View.extend({
  tagName: 'nav',

  className: 'story-nav',

  initialize: function() {
    this.section = null;
    this.sections = this.options.sections;
    if (this.options.hasOwnProperty('addlLinks')) {
      this.addlLinks = this.options.addlLinks.map(function(link) {
        return {
         text: link.text,
         id: link.id,
         href: link.hasOwnProperty('href') ? link.href: '#'
       }
      });
    }
    else {
      this.addlLinks = [];
    }
  },

  render: function() {
    var context = {};
    if (this.section) {
      context.next_section = this.sections.get(
	this.section.get('next_section_id'));
      context.previous_section = this.sections.get(
	this.section.get('previous_section_id'));
    }
    context.addl_links = this.addlLinks;

    this.$el.html(ich.navigationTemplate(context));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// Interative visualization of a spider story structure
storybase.viewer.views.Spider = Backbone.View.extend({
  events: {
    "hover g.node": "hoverSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.insertBefore = this.options.insertBefore;
    this.subtractHeight = this.options.subtractHeight;
    this.subtractWidth = this.options.subtractWidth;
    this.activeSection = null;
    // The id for the visualization's wrapper element
    this.visId = 'spider-vis';
    this.maxDepth = null;
    this.sectionsAtDepth = [];
    this.maxSectionsAtDepth = 0;
    this.walkSectionHierarchy(0, this.sections.at(0));
  },

  // Walk the section hierarchy to build a sense of its "shape"
  walkSectionHierarchy: function(depth, section) {
    if (this.maxDepth == null ||
        depth > this.maxDepth) {
      this.maxDepth = depth;
    }

    if (this.sectionsAtDepth[depth] === undefined) {
      this.sectionsAtDepth[depth] = 1;
    }
    else {
      this.sectionsAtDepth[depth]++;
    }

    if (this.sectionsAtDepth[depth] > this.maxSectionsAtDepth) {
      this.maxSectionsAtDepth = this.sectionsAtDepth[depth];
    }

    var $this = this;
    _.each(section.get('children'), function(childId) {
      $this.walkSectionHierarchy(depth + 1, $this.sections.get(childId));
    });
  },

  // Return the visualization wrapper element
  visEl: function() {
    return this.$('#' + this.visId).first();
  },

  setSection: function(section) {
    this.activeSection = section;
    this.highlightSectionNode(this.activeSection.id);
  },

  highlightSectionNode: function(sectionId) {
    d3.selectAll('g.node').classed('active', false);
    d3.select('g.node.section-' + sectionId).classed('active', true);
  },

  // Get the dimensions of the visualization's wrapper element
  getVisDimensions: function() {
    // We have to calculate the dimensions relative to the window rather than
    // the parent element because the parent element is really tall as it
    // also contains the (hidden) section content.  It seems there should be
    // a better way to do this, but I don't know it.
    width = $(window).width();
    height = $(window).height();
    _.each(this.subtractWidth, function(selector) {
      width -= $(selector).outerWidth();
    });
    _.each(this.subtractHeight, function(selector) {
      height -= $(selector).outerHeight();
    });
    return {
      width: width,
      height: height
    };
  },

  // Get the radius of the tree
  getTreeRadius: function(width, height) {
    return (this.maxSectionsAtDepth == 1 ? _.max([width, height]) : _.min([width, height])) * .66;
  },

  render: function() {
    var $this = this;
    var elId = this.$el.attr('id');
    var dimensions = this.getVisDimensions();
    var treeRadius = this.getTreeRadius(dimensions.width, dimensions.height);
    var vis = d3.select("#" + elId).insert("svg", this.insertBefore)
        .attr("id", this.visId)
        .attr("width", dimensions.width)
        .attr("height", dimensions.height)
	.attr("style", "float:left")
      .append("g");
    var rootSection = this.sections.at(0).populateChildren();
    var tree = d3.layout.tree()
      .size([360, treeRadius - 120])
      .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
    var diagonal = d3.svg.diagonal.radial()
      .projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
    var nodes = tree.nodes(rootSection);
    // Fix x coordinate (angle) when each level has only one child
    // In this case d.x = NaN which breakins things
    nodes = _.map(nodes, function(node) {
      node.x = $this.maxSectionsAtDepth == 1 ? 90 : node.x;
      return node;
    });
    var links = tree.links(nodes);

    var link = vis.selectAll("path.link")
        .data(links)
      .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    var node = vis.selectAll("g.node")
      .data(nodes)
      .enter().append("g")
      .attr('class', function(d) {
        return "node section-" + d.id;
      })
      .attr("transform", function(d) {
	var transform = "";
	if (d.depth > 0) {
	  transform += "rotate(" + (d.x - 90) + ")";
	}
        transform += "translate(" + d.y + ")";
	return transform;
      });

    node.append("circle")
        .attr("r", 15);

    node.append("text")
      .attr("x", function(d) {
	if (d.depth == 0) { return 20; }
        return d.x < 180 ? 20 : -20;
      })
      .attr("y", ".31em")
      .attr("text-anchor", function(d) {
	if (d.depth == 0) { return "start"; }
        return d.x < 180 ? "start" : "end"; })
      .attr("transform", function(d) {
	var rotation = 0;
	if ($this.maxSectionsAtDepth == 1) {
	  rotation = 315;
	}
	else if (d.depth > 0) {
          rotation = 90 - d.x;
	}
	return "rotate(" + rotation + ")";
      })
      .text(function(d) { return d.get('title'); });

    // Center the tree within the viewport
    var treeBBox = vis[0][0].getBBox();
    var translateX = (0 - treeBBox.x) + ((dimensions.width - treeBBox.width) / 2);
    var translateY = (0 - treeBBox.y) + ((dimensions.height - treeBBox.height) / 2);
    vis.attr("transform", "translate(" + translateX + ", " + translateY + ")");
  },

  // Event handler for hovering over a section node
  // Changes the cursor to a "pointer" style icon to indicate that
  // the nodes are clickable.
  hoverSectionNode: function(e) {
    e.currentTarget.style.cursor = 'pointer';
  },
});

// Master view that shows a story in a linear fashion
storybase.viewer.views.LinearViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'linear',

  // Get the position, from the top of the winder, of the lower edge of the
  // header
  headerBottom: function() {
    return headerHeight = this.$('header').outerHeight()
  },

  footerTop: function() {
    return this.$('footer').offset().top;
  },

  // Show the active section
  showActiveSection: function() {
    var sectionTop = this.$('#' + this.activeSection.id).offset().top;
    this._preventScrollEvent = true;
    var headerBottom = this.headerBottom();
    var scrollBy = Math.ceil(sectionTop - this.headerBottom());
    $(window).scrollTop(scrollBy);
  },

  // Return the element of the last visible story section
  getLastVisibleSectionEl: function() {
    var visibleSections = [];
    var numSections = this.$('section').length;
    for (var i = 0; i < numSections; i++) {
      var $section = this.$('section').eq(i);
      var sectionTop = $section.offset().top;
      var sectionBottom = sectionTop + $section.outerHeight();
      if (sectionTop >= this.headerBottom() &&
          sectionBottom <= this.footerTop()) {
        visibleSections.push($section);
      }
    }
    return _.last(visibleSections);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    var newSection = this.activeSection;
    if (this._preventScrollEvent !== true) {
      if ($(window).scrollTop() == 0) {
	// At the top of the window.  Set the active section to the
	// first section
	newSection = this.sections.first();
      }
      else {
	var $lastVisibleSectionEl = this.getLastVisibleSectionEl();
	if ($lastVisibleSectionEl) {
	  var lastVisibleSection = this.sections.get($lastVisibleSectionEl.attr('id'));
	  if (lastVisibleSection != this.activeSection) {
	    newSection = lastVisibleSection;
	  }
	}
      }
      this.setSection(newSection, {showActiveSection: false});
      storybase.viewer.router.navigate("sections/" + newSection.id,
                                     {trigger: false});
    }
    this._preventScrollEvent = false;
  }
});

// Master view that shows the story structure visualization initially
storybase.viewer.views.SpiderViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'spider',

  events: {
    "click #topic-map": "clickTopicMapLink",
    "click g.node": "clickSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections,
      addlLinks: [{text: Globalize.localize("Topic Map"), id: 'topic-map'}]
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.initialView = new storybase.viewer.views.Spider({
      el: this.$('#body'),
      sections: this.options.sections,
      insertBefore: '.section',
      subtractWidth: ['.summary'],
      subtractHeight: ['header', 'footer']
    });
  },

  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.initialView.render();
    this.$('.summary').show();
    // Hide all the section content initially
    this.$('.section').hide();
    return this;
  },

  // Show the active section
  showActiveSection: function() {
    // Hide the summary
    this.$('.summary').hide();
    // Hide all sections other than the active one
    this.$('.section').hide();
    this.$('#' + this.activeSection.id).show();
    // Hide the visualization
    this.initialView.$('svg').hide();
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
    this.initialView.setSection(this.activeSection);
  },

  // Event handler for clicks on a section node
  clickSectionNode: function(e) {
    var node = d3.select(e.currentTarget);
    var sectionId = node.data()[0].id;
    this.initialView.visEl().hide();
    // TODO: I'm not sure if this is the best way to access the router
    // I don't like global variables, but passing it as an argument
    // makes for a weird circular dependency between the master view/
    // router.
    storybase.viewer.router.navigate("sections/" + sectionId,
                                     {trigger: true});
  },

  // Event handler for clicking the "Topic Map" link
  clickTopicMapLink: function(e) {
    var activeSectionEl = this.activeSectionEl();
    if (activeSectionEl !== null) {
      // Only toggle the topic map if an active section has been set
      // The only time there should not be one set is when the viewer
      // first loads
      activeSectionEl.toggle();
      var visEl = this.initialView.visEl();
      if (visEl.css('display') == 'none') {
	this.initialView.visEl().show();
      }
      else {
	this.initialView.visEl().hide();
      }
      this.$('.summary').toggle();
    }
  }

});

// Get the appropriate master view based on the story structure type
storybase.viewer.views.getViewerApp = function(structureType, options) {
  if (structureType == 'linear') {
    return new storybase.viewer.views.LinearViewerApp(options);
  }
  else if (structureType == 'spider') {
    return new storybase.viewer.views.SpiderViewerApp(options);
  }
  else {
    throw "Unknown story structure type '" + structureType + "'";
  }
};
<commit_msg>s/this\.section/this\.activeSection/ to match other views<commit_after>/**
 * Views for the story viewer Backbone application
 */
Namespace('storybase.viewer');

// Translations for interface text
Globalize.addCultureInfo('en', {
  messages: {
    "Topic Map": "Topic Map",
    "How Can You Help?": "How Can You Help?"
  }
});

Globalize.addCultureInfo('es', {
  messages: {
    "Topic Map": "Mapa Temático",
    "How Can You Help?": "How Can You Help?"
  }
});

// Container view for the viewer application.
// It delegates rendering and events for the entire app to views
// rendering more specific "widgets"
storybase.viewer.views.ViewerApp = Backbone.View.extend({
  // Initialize the view
  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.setSection(this.sections.at(0), {showActiveSection: false});
    _.bindAll(this, 'handleScroll');
    $(window).scroll(this.handleScroll);
  },

  // Add the view's container element to the DOM and render the sub-views
  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.$('.summary').show();
    this.$('.section').show();
    return this;
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
  },

  // Show the active section
  showActiveSection: function() {
    throw "showActiveSection() is not implemented";
  },

  // Set the active story section
  setSection: function(section, options) {
    options = typeof options !== 'undefined' ? options : {};
    var showActiveSection = options.hasOwnProperty('showActiveSection') ? options.showActiveSection : true;
    this.activeSection = section;
    if (showActiveSection) {
      this.showActiveSection();
    }
    this.updateSubviewSections();
  },

  // Like setSection(), but takes a section ID as an argument instead of
  // a Section model instance object
  setSectionById: function(id) {
    this.setSection(this.sections.get(id));
  },

  // Convenience method to get the element for the active section
  activeSectionEl: function() {
      return this.$('#' + this.activeSection.id);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    // Do nothing. Subclasses might want to implement this to do some work
  }
});

// View for the story viewer header.
// Updates the section heading title when the active section changes
storybase.viewer.views.StoryHeader = Backbone.View.extend({
  el: 'header',

  render: function() {
    var $titleEl = this.$el.find('.section-title').first();
    if ($titleEl.length == 0) {
      $titleEl = $('
');
      this.$el.append($titleEl);
    }
    $titleEl.text(this.section.get('title'));
    return this;
  },

  setSection: function(section) {
    this.section = section;
    this.render();
  }
});

// View to provide previous/next buttons to navigate between sections
storybase.viewer.views.StoryNavigation = Backbone.View.extend({
  tagName: 'nav',

  className: 'story-nav',

  initialize: function() {
    this.activeSection = null;
    this.sections = this.options.sections;
    if (this.options.hasOwnProperty('addlLinks')) {
      this.addlLinks = this.options.addlLinks.map(function(link) {
        return {
         text: link.text,
         id: link.id,
         href: link.hasOwnProperty('href') ? link.href: '#'
       }
      });
    }
    else {
      this.addlLinks = [];
    }
  },

  render: function() {
    var context = {};
    if (this.activeSection) {
      context.next_section = this.sections.get(
	this.activeSection.get('next_section_id'));
      context.previous_section = this.sections.get(
	this.activeSection.get('previous_section_id'));
    }
    context.addl_links = this.addlLinks;

    this.$el.html(ich.navigationTemplate(context));
    return this;
  },

  setSection: function(section) {
    this.activeSection = section;
    this.render();
  }
});

// Interative visualization of a spider story structure
storybase.viewer.views.Spider = Backbone.View.extend({
  events: {
    "hover g.node": "hoverSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.insertBefore = this.options.insertBefore;
    this.subtractHeight = this.options.subtractHeight;
    this.subtractWidth = this.options.subtractWidth;
    this.activeSection = null;
    // The id for the visualization's wrapper element
    this.visId = 'spider-vis';
    this.maxDepth = null;
    this.sectionsAtDepth = [];
    this.maxSectionsAtDepth = 0;
    this.walkSectionHierarchy(0, this.sections.at(0));
  },

  // Walk the section hierarchy to build a sense of its "shape"
  walkSectionHierarchy: function(depth, section) {
    if (this.maxDepth == null ||
        depth > this.maxDepth) {
      this.maxDepth = depth;
    }

    if (this.sectionsAtDepth[depth] === undefined) {
      this.sectionsAtDepth[depth] = 1;
    }
    else {
      this.sectionsAtDepth[depth]++;
    }

    if (this.sectionsAtDepth[depth] > this.maxSectionsAtDepth) {
      this.maxSectionsAtDepth = this.sectionsAtDepth[depth];
    }

    var $this = this;
    _.each(section.get('children'), function(childId) {
      $this.walkSectionHierarchy(depth + 1, $this.sections.get(childId));
    });
  },

  // Return the visualization wrapper element
  visEl: function() {
    return this.$('#' + this.visId).first();
  },

  setSection: function(section) {
    this.activeSection = section;
    this.highlightSectionNode(this.activeSection.id);
  },

  highlightSectionNode: function(sectionId) {
    d3.selectAll('g.node').classed('active', false);
    d3.select('g.node.section-' + sectionId).classed('active', true);
  },

  // Get the dimensions of the visualization's wrapper element
  getVisDimensions: function() {
    // We have to calculate the dimensions relative to the window rather than
    // the parent element because the parent element is really tall as it
    // also contains the (hidden) section content.  It seems there should be
    // a better way to do this, but I don't know it.
    width = $(window).width();
    height = $(window).height();
    _.each(this.subtractWidth, function(selector) {
      width -= $(selector).outerWidth();
    });
    _.each(this.subtractHeight, function(selector) {
      height -= $(selector).outerHeight();
    });
    return {
      width: width,
      height: height
    };
  },

  // Get the radius of the tree
  getTreeRadius: function(width, height) {
    return (this.maxSectionsAtDepth == 1 ? _.max([width, height]) : _.min([width, height])) * .66;
  },

  render: function() {
    var $this = this;
    var elId = this.$el.attr('id');
    var dimensions = this.getVisDimensions();
    var treeRadius = this.getTreeRadius(dimensions.width, dimensions.height);
    var vis = d3.select("#" + elId).insert("svg", this.insertBefore)
        .attr("id", this.visId)
        .attr("width", dimensions.width)
        .attr("height", dimensions.height)
	.attr("style", "float:left")
      .append("g");
    var rootSection = this.sections.at(0).populateChildren();
    var tree = d3.layout.tree()
      .size([360, treeRadius - 120])
      .separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
    var diagonal = d3.svg.diagonal.radial()
      .projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });
    var nodes = tree.nodes(rootSection);
    // Fix x coordinate (angle) when each level has only one child
    // In this case d.x = NaN which breakins things
    nodes = _.map(nodes, function(node) {
      node.x = $this.maxSectionsAtDepth == 1 ? 90 : node.x;
      return node;
    });
    var links = tree.links(nodes);

    var link = vis.selectAll("path.link")
        .data(links)
      .enter().append("path")
        .attr("class", "link")
        .attr("d", diagonal);

    var node = vis.selectAll("g.node")
      .data(nodes)
      .enter().append("g")
      .attr('class', function(d) {
        return "node section-" + d.id;
      })
      .attr("transform", function(d) {
	var transform = "";
	if (d.depth > 0) {
	  transform += "rotate(" + (d.x - 90) + ")";
	}
        transform += "translate(" + d.y + ")";
	return transform;
      });

    node.append("circle")
        .attr("r", 15);

    node.append("text")
      .attr("x", function(d) {
	if (d.depth == 0) { return 20; }
        return d.x < 180 ? 20 : -20;
      })
      .attr("y", ".31em")
      .attr("text-anchor", function(d) {
	if (d.depth == 0) { return "start"; }
        return d.x < 180 ? "start" : "end"; })
      .attr("transform", function(d) {
	var rotation = 0;
	if ($this.maxSectionsAtDepth == 1) {
	  rotation = 315;
	}
	else if (d.depth > 0) {
          rotation = 90 - d.x;
	}
	return "rotate(" + rotation + ")";
      })
      .text(function(d) { return d.get('title'); });

    // Center the tree within the viewport
    var treeBBox = vis[0][0].getBBox();
    var translateX = (0 - treeBBox.x) + ((dimensions.width - treeBBox.width) / 2);
    var translateY = (0 - treeBBox.y) + ((dimensions.height - treeBBox.height) / 2);
    vis.attr("transform", "translate(" + translateX + ", " + translateY + ")");
  },

  // Event handler for hovering over a section node
  // Changes the cursor to a "pointer" style icon to indicate that
  // the nodes are clickable.
  hoverSectionNode: function(e) {
    e.currentTarget.style.cursor = 'pointer';
  },
});

// Master view that shows a story in a linear fashion
storybase.viewer.views.LinearViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'linear',

  // Get the position, from the top of the winder, of the lower edge of the
  // header
  headerBottom: function() {
    return headerHeight = this.$('header').outerHeight()
  },

  footerTop: function() {
    return this.$('footer').offset().top;
  },

  // Show the active section
  showActiveSection: function() {
    var sectionTop = this.$('#' + this.activeSection.id).offset().top;
    this._preventScrollEvent = true;
    var headerBottom = this.headerBottom();
    var scrollBy = Math.ceil(sectionTop - this.headerBottom());
    $(window).scrollTop(scrollBy);
  },

  // Return the element of the last visible story section
  getLastVisibleSectionEl: function() {
    var visibleSections = [];
    var numSections = this.$('section').length;
    for (var i = 0; i < numSections; i++) {
      var $section = this.$('section').eq(i);
      var sectionTop = $section.offset().top;
      var sectionBottom = sectionTop + $section.outerHeight();
      if (sectionTop >= this.headerBottom() &&
          sectionBottom <= this.footerTop()) {
        visibleSections.push($section);
      }
    }
    return _.last(visibleSections);
  },

  // Event handler for scroll event
  handleScroll: function(e) {
    var newSection = this.activeSection;
    if (this._preventScrollEvent !== true) {
      if ($(window).scrollTop() == 0) {
	// At the top of the window.  Set the active section to the
	// first section
	newSection = this.sections.first();
      }
      else {
	var $lastVisibleSectionEl = this.getLastVisibleSectionEl();
	if ($lastVisibleSectionEl) {
	  var lastVisibleSection = this.sections.get($lastVisibleSectionEl.attr('id'));
	  if (lastVisibleSection != this.activeSection) {
	    newSection = lastVisibleSection;
	  }
	}
      }
      this.setSection(newSection, {showActiveSection: false});
      storybase.viewer.router.navigate("sections/" + newSection.id,
                                     {trigger: false});
    }
    this._preventScrollEvent = false;
  }
});

// Master view that shows the story structure visualization initially
storybase.viewer.views.SpiderViewerApp = storybase.viewer.views.ViewerApp.extend({
  elClass: 'spider',

  events: {
    "click #topic-map": "clickTopicMapLink",
    "click g.node": "clickSectionNode"
  },

  initialize: function() {
    this.sections = this.options.sections;
    this.story = this.options.story;
    this.navigationView = new storybase.viewer.views.StoryNavigation({
      sections: this.options.sections,
      addlLinks: [{text: Globalize.localize("Topic Map"), id: 'topic-map'}]
    });
    this.headerView = new storybase.viewer.views.StoryHeader();
    this.initialView = new storybase.viewer.views.Spider({
      el: this.$('#body'),
      sections: this.options.sections,
      insertBefore: '.section',
      subtractWidth: ['.summary'],
      subtractHeight: ['header', 'footer']
    });
  },

  render: function() {
    this.$el.addClass(this.elClass);
    this.$('footer').append(this.navigationView.el);
    this.navigationView.render();
    this.initialView.render();
    this.$('.summary').show();
    // Hide all the section content initially
    this.$('.section').hide();
    return this;
  },

  // Show the active section
  showActiveSection: function() {
    // Hide the summary
    this.$('.summary').hide();
    // Hide all sections other than the active one
    this.$('.section').hide();
    this.$('#' + this.activeSection.id).show();
    // Hide the visualization
    this.initialView.$('svg').hide();
  },

  // Update the active story section in the sub-views
  updateSubviewSections: function() {
    this.navigationView.setSection(this.activeSection);
    this.headerView.setSection(this.activeSection);
    this.initialView.setSection(this.activeSection);
  },

  // Event handler for clicks on a section node
  clickSectionNode: function(e) {
    var node = d3.select(e.currentTarget);
    var sectionId = node.data()[0].id;
    this.initialView.visEl().hide();
    // TODO: I'm not sure if this is the best way to access the router
    // I don't like global variables, but passing it as an argument
    // makes for a weird circular dependency between the master view/
    // router.
    storybase.viewer.router.navigate("sections/" + sectionId,
                                     {trigger: true});
  },

  // Event handler for clicking the "Topic Map" link
  clickTopicMapLink: function(e) {
    var activeSectionEl = this.activeSectionEl();
    if (activeSectionEl !== null) {
      // Only toggle the topic map if an active section has been set
      // The only time there should not be one set is when the viewer
      // first loads
      activeSectionEl.toggle();
      var visEl = this.initialView.visEl();
      if (visEl.css('display') == 'none') {
	this.initialView.visEl().show();
      }
      else {
	this.initialView.visEl().hide();
      }
      this.$('.summary').toggle();
    }
  }

});

// Get the appropriate master view based on the story structure type
storybase.viewer.views.getViewerApp = function(structureType, options) {
  if (structureType == 'linear') {
    return new storybase.viewer.views.LinearViewerApp(options);
  }
  else if (structureType == 'spider') {
    return new storybase.viewer.views.SpiderViewerApp(options);
  }
  else {
    throw "Unknown story structure type '" + structureType + "'";
  }
};
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** @module */

let player;

export function init(aPlayer) {
    player = aPlayer;

    window.addEventListener("hashchange", onHashChange, false);

    if (player.presentation.updateURLOnFrameChange) {
        player.addListener("frameChange", onFrameChange);
    }
}

export function getFrame() {
    if (window.location.hash) {
        const indexOrId = window.location.hash.slice(1);
        const frame = player.presentation.getFrameWithId(indexOrId);
        if (frame) {
            return frame;
        }
        else {
            const index = parseInt(indexOrId);
            return !isNaN(index) && index > 0 && index <= player.presentation.frames.length ?
                player.presentation.frames[index - 1] :
                player.currentFrame;
        }
    }
    else {
        return player.currentFrame;
    }
}

function onHashChange() {
    const frame = getFrame();
    if (player.currentFrame !== frame) {
        player.moveToFrame(frame);
    }
}

function onFrameChange() {
    window.location.hash = "#" + player.currentFrame.frameId;
}
<commit_msg>Completed documentation in FrameURL<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** Manage the browser's location bar while playing a presentation.
 *
 * This module is part of the Sozi player embedded in each presentation.
 *
 * @module
 */

/** The current Sozi player.
 *
 * @type {module:player/Player.Player} */
let player;

/** Initialize the location bar management.
 *
 * This function registers a `hashchange` and a
 * {@linkcode module:player/Viewport.frameChange|frameChange} event handlers
 * to reflect the current frame ID in the current URL.
 *
 * @param {module:player/Player.Player} p - The current Sozi player.
 */
export function init(p) {
    player = p;

    window.addEventListener("hashchange", onHashChange, false);

    if (player.presentation.updateURLOnFrameChange) {
        player.addListener("frameChange", onFrameChange);
    }
}

/** Get the frame for the current URL.
 *
 * This function parses the current URL hash as a frame ID or a frame number.
 * It returns the corresponding frame, or the current frame if no match was found.
 *
 * @returns {module:model/Presentation.Frame} - The frame that corresponds to the current URL hash.
 */
export function getFrame() {
    if (window.location.hash) {
        const indexOrId = window.location.hash.slice(1);
        const frame = player.presentation.getFrameWithId(indexOrId);
        if (frame) {
            return frame;
        }
        else {
            const index = parseInt(indexOrId);
            return !isNaN(index) && index > 0 && index <= player.presentation.frames.length ?
                player.presentation.frames[index - 1] :
                player.currentFrame;
        }
    }
    else {
        return player.currentFrame;
    }
}

/** Process the `hashchange` event.
 *
 * Move the presentation to the frame that corresponds to the current URL.
 */
function onHashChange() {
    const frame = getFrame();
    if (player.currentFrame !== frame) {
        player.moveToFrame(frame);
    }
}

/** Process the {@linkcode module:player/Viewport.frameChange|frameChange} event.
 *
 * Update the URL hash in the location bar with the current frame ID.
 */
function onFrameChange() {
    window.location.hash = "#" + player.currentFrame.frameId;
}
<|endoftext|>


<commit_before><commit_msg>- remove unnecessary required package<commit_after><|endoftext|>