Source: src/includes/go.inc.js

// Holds onto any query string during the page go process.
var _drupalgap_goto_query_string = null;

/**
 * Given a path, this will change the current page in the app.
 * @param {String} path
 * @return {*}
 */
function drupalgap_goto(path) {
  try {

    // Extract any incoming options, set any defaults that weren't provided,
    // then populate the global page options variable.
    var options = {};
    if (arguments[1]) {
      options = arguments[1];
      if (typeof options.form_submission === 'undefined') {
        options.form_submission = false;
      }
    }
    drupalgap.page.options = options;

    // Prepare the path.
    path = _drupalgap_goto_prepare_path(path, true);
    if (!path) { return false; }

    // Invoke all implementations of hook_drupalgap_goto_preprocess().
    module_invoke_all('drupalgap_goto_preprocess', path);

    // Determine the router path.
    var router_path = drupalgap_get_menu_link_router_path(path);

    // Make sure we have a menu link item that can handle this router path,
    // otherwise we'll goto the 404 page.
    if (!drupalgap.menu_links[router_path]) {
      // Is anyone trying to handle this 404?
      var new_path = false;
      var invocation_results = module_invoke_all('404', router_path);
      if (invocation_results) {
        for (var index in invocation_results) {
            if (!invocation_results.hasOwnProperty(index)) { continue; }
            var result = invocation_results[index];
            if (result !== false) {
              new_path = result;
              break;
            }
        }
      }
      // If a 404 handler provided a new path use it, otherwise just use the
      // system 404 page. Either way, update the router path before continuing
      // with a normal page build.
      if (new_path) { path = new_path; }
      else { path = '404'; }
      router_path = drupalgap_get_menu_link_router_path(path);
    }

    // Make sure the user has access to this router path, if they don't send
    // them to the 401 page.
    // @TODO - for now we're going to skip access checks on local tasks, since
    // they are covered by menu_block_view(), but if someone were to navigate
    // directly to e.g. a node's edit page, they would be able to see the page.
    // Of course Drupal would actually prevent them from updating the node on
    // the live site, but nonetheless this needs to be fixed. It's a tough issue
    // though and related to https://github.com/signalpoint/DrupalGap/issues/257
    if (
      drupalgap.menu_links[router_path].type != 'MENU_DEFAULT_LOCAL_TASK' &&
      drupalgap.menu_links[router_path].type != 'MENU_LOCAL_TASK' &&
      !drupalgap_menu_access(router_path, path)
    ) {
      path = '401';
      router_path = drupalgap_get_menu_link_router_path(path);
    }

    // If the new router path is the same as the current router path and the new
    // path is the same as the current path, we may need to cancel the
    // navigation attempt (i.e. don't go anywhere), unless it is a
    // form submission, then continue.
    if (
      router_path == drupalgap_router_path_get() &&
      path == drupalgap_path_get()
    ) {
      // If it's a form submission, we'll continue onward...
      if (options.form_submission) { }

      // If we're reloading the current page, we need to set aside this path
      // and navigate to the system's _reload page, which will then handle the
      // actual reloading of the page.
      // @see system_drupalgap_goto_post_process()
      else if (options.reloadPage) {
        _system_reload_page = path;
        path = '_reload';
        router_path = drupalgap_get_menu_link_router_path(path);
      }

      // Otherwise, just stop the navigation attempt.
      else { return false; }

    }

    // Grab the page id.
    var page_id = drupalgap_get_page_id(path);

    // Return if we are trying to go to the path we are already on, unless this
    // was a form submission, then we'll let the page rebuild itself. For
    // accuracy we compare the jQM active page url with the destination page
    // id.
    // @todo - this boolean doesn't match the comment description of the code
    // block, i.e. the form_submission check is opposite of what it says
    if (drupalgap_jqm_active_page_url() == page_id && options.form_submission) {
      // Clear any messages from the page before returning.
      drupalgap_clear_messages();
      return false;
    }

    // @TODO when drupalgap_goto() is called during the bootstrap, we shouldn't
    // put the path onto the back_path array.

    // Save the back path.
    var back_paths_to_ignore = ['user/logout', '_reload'];
    if (!in_array(drupalgap_path_get())) {
      drupalgap.back_path.push(drupalgap_path_get());
    }

    // Set the current menu path to the path input.
    drupalgap_path_set(path);

    // Set the drupalgap router path.
    drupalgap_router_path_set(router_path);

    // If the page is already in the DOM and we're asked to reload it, then
    // remove the page and let it rebuild itself. If we're not reloading the
    // page and we're not in the middle of a form submission, prevent the page
    // from processing then change to it.
    if (drupalgap_page_in_dom(page_id)) {

      // If there are any hook_menu() item options for this router path, bring
      // them into the current options without overwriting any existing values.
      if (drupalgap.menu_links[router_path].options) {
        options = $.extend(
          {},
          drupalgap.menu_links[router_path].options,
          options
        );
      }

      // Reload the page? If so, remove the page from the DOM, delete the
      // reloadPage option, then set the reloadingPage option to true so others
      // down the line will know the page is reloading. We can't pass along the
      // actual reloadPage option since it may collide with jQM later on. We
      // have to use 'force' when removing the page from the DOM since DG won't
      // remove it since it thinks we are already on the page, so it won't
      // remove it.
      if (typeof options.reloadPage !== 'undefined' && options.reloadPage) {
        // @TODO we may need to leave the query ONLY if the above call to
        // _drupalgap_goto_prepare_path() yielded a query string, otherwise we
        // may be leaving unwanted queries in _GET()!
        var leaveQuery = _drupalgap_goto_query_string ? true : false;
        drupalgap_remove_page_from_dom(page_id, {
          force: true,
          leaveQuery: leaveQuery
        });
        delete options.reloadPage;
        options.reloadingPage = true;
      }
      else if (!options.form_submission) {
        drupalgap_clear_messages();
        _drupalgap_goto_query_string = null;
        drupalgap.page.process = false;
        // @TODO changePage has been deprecated as of jQM 1.4.0 and will be
        // removed in 1.50.
        // @see https://api.jquerymobile.com/jQuery.mobile.changePage/
        $.mobile.changePage('#' + page_id, options);
        module_invoke_all('drupalgap_goto_post_process', path);
        return;
      }
    }
    else if (typeof options.reloadPage !== 'undefined' && options.reloadPage) {
      // The page is not in the DOM, and we're being asked to reload it, so
      // we'll just delete the reloadPage option.
      delete options.reloadPage;
    }

    // Generate the page.
    drupalgap_goto_generate_page_and_go(path, page_id, options, drupalgap.menu_links[router_path]);
  }
  catch (error) { console.log('drupalgap_goto - ' + error); }
}

/**
 * Generate a JQM page by running it through the theme then attach the
 * page to the <body> of the document, then change to the page. Remember,
 * the rendering of the page does not take place here, that is covered by
 * the pagebeforechange event in theme.inc.js which happens after we change
 * the page here.
 * @param {String} path
 * @param {String} page_id
 * @param {Object} options
 * @param {Object} menu_link The menu link object from drupalgap.menu_links.
 */
function drupalgap_goto_generate_page_and_go(path, page_id, options, menu_link) {
  try {

    // First see if the theme implements hook_page_tpl_html() and use its string for the page placeholders,
    // otherwise use the file read method from DrupalGap's early days, and notify the developer to upgrade
    // their theme.
    var html = '';
    var themeName = drupalgap.settings.theme;
    var pageTplHtmlFunctionName = themeName + '_page_tpl_html';
    if (function_exists(pageTplHtmlFunctionName)) { html = window[pageTplHtmlFunctionName](); }
    else {
      var pageTemplatePath = path_to_theme() + '/page.tpl.html';
      console.log('@deprecated: ' + pageTemplatePath + ' - use ' + pageTplHtmlFunctionName + '() in ' +
          themeName + '.js instead, see: http://docs.drupalgap.org/7/Themes/Create_a_Custom_Theme');
      html = drupalgap_file_get_contents(pageTemplatePath);
    }

    // Reset the internal query string.
    _drupalgap_goto_query_string = null;

    // If options wasn't set, set it as an empty JSON object.
    if (typeof options === 'undefined') { options = {}; }

    // Add page to DOM.
    drupalgap_add_page_to_dom({
      page_id: page_id,
      html: html,
      menu_link: menu_link
    });

    // Setup change page options if necessary.
    if (drupalgap_path_get() == path && options.form_submission) {
      options.allowSamePageTransition = true;
    }

    // Let's change to the page. Web apps and the ripple emulator do not
    // seem to like the 'index.html' prefix, so we'll remove that.
    var destination = 'index.html#' + page_id;
    if (
        drupalgap.settings.mode != 'phonegap' ||
        typeof parent.window.ripple === 'function'
    ) { destination = '#' + page_id; }
    $.mobile.changePage(destination, options); // @see the pagebeforechange handler in page.inc.js

    // Invoke all implementations of hook_drupalgap_goto_post_process().
    module_invoke_all('drupalgap_goto_post_process', path);
  }
  catch (error) { console.log('drupalgap_goto_generate_page_and_go - ' + error); }
}

/**
 * Prepares a drupalgap page path.
 * @deprecated Use _drupalgap_goto_prepare_path() instead.
 * @param {String} path
 * @return {String}
 */
function drupalgap_goto_prepare_path(path) {
  try {
    console.log('WARNING - drupalgap_goto_prepare_path() is deprecated, ' +
      'use _drupalgap_goto_prepare_path() instead!');
    return _drupalgap_goto_prepare_path(path);
  }
  catch (error) { console.log('drupalgap_goto_prepare_path - ' + error); }
}

/**
 * An internal function used to prepare the path for menu routing. An optional
 * second parameter (boolean) may be passed in, and if it is set to true it will
 * process any _GET query string parameters.
 * @param {String} path
 * @return {String}
 */
function _drupalgap_goto_prepare_path(path) {
  try {

    // Pull out any query string parameters and populate them into _GET, if we
    // were instructed to do so. Hold onto a global copy of the query string
    // for page go processing.
    if (typeof arguments[1] !== 'undefined' && arguments[1]) {
      var pos = path.indexOf('?');
      if (pos != -1 && pos != path.length - 1) {
        var query = path.substr(pos + 1, path.length - pos);
        _drupalgap_goto_query_string = query;
        path = path.substr(0, pos);
        var parts = query.split('&');
        for (var i = 0; i < parts.length; i++) {
          pos = parts[i].indexOf('=');
          if (pos == -1) { continue; }
          query = parts[i].split('=');
          if (query.length != 2) { continue; }
          _GET(
            decodeURIComponent(query[0]),
            decodeURIComponent(query[1]),
            path
          );
        }
      }
    }

    // If the path is an empty string, change it to the front page path.
    if (path == '') {
      if (!drupalgap.settings.front) {
        drupalgap_alert(
          'drupalgap_goto_prepare_path - ' +
          t('no front page specified in settings.js!')
        );
        return false;
      }
      else { path = drupalgap.settings.front; }
    }

    // Change 'user' to 'user/login' for anonymous users, or change it to e.g.
    // 'user/123' for authenticated users.
    else if (path == 'user') {
      if (Drupal.user.uid != 0) { path = 'user/' + Drupal.user.uid; }
      else { path = 'user/login'; }
    }

    // Finally return the path.
    return path;

  }
  catch (error) { console.log('drupalgap_goto_prepare_path - ' + error); }
}

/**
 * Change the page to the previous page.
 */
function drupalgap_back() {
  try {
    var active_page_id = $('.ui-page-active').attr('id');
    if (active_page_id == drupalgap.settings.front) {
      var msg = t('Exit') + ' ' + drupalgap.settings.title + '?';
      if (drupalgap.settings.exit_message) {
        msg = drupalgap.settings.exit_message;
      }
      drupalgap_confirm(msg, {
          confirmCallback: _drupalgap_back_exit
      });
    }
    else if (active_page_id == '_drupalgap_splash') { return; }
    else { _drupalgap_back(); }
  }
  catch (error) { console.log('drupalgap_back - ' + error); }
}

/**
 * The DrupalGap "Back to the Future" function goes forward to the previous page by first
 * removing that page from the DOM, then going "forward" to that page using drupalgap_goto()
 * with a reloadPage option set to true.
 */
function drupalgap_back_2tf() {
  var back = drupalgap_back_path();
  drupalgap_remove_page_from_dom(drupalgap_get_page_id(back));
  drupalgap_goto(back, { reloadPage: true });
}

/**
 * Returns the previous page path, or the front page if there is none.
 * @returns {String}
 */
function drupalgap_back_path() {
  return drupalgap.back_path.length ?
      drupalgap.back_path[drupalgap.back_path.length - 1] :
      drupalgap.settings.front;
}

/**
 * An internal function used to change the page to the previous page.
 */
function _drupalgap_back() {
  try {
    // @WARNING - any changes here (except the history.back() call) need to be
    // reflected into the window "navigate" handler below
    drupalgap.back = true;

    // Properly handle iOS9 back button clicks, and default back button clicks.
    if (typeof device !== 'undefined' && device.platform === "iOS" && parseInt(device.version) === 9) {
      $.mobile.back();
    }
    else { history.back(); }

    // Update the path and router path.
    var from = drupalgap_path_get();
    drupalgap_path_set(drupalgap.back_path.pop());
    var to = drupalgap_path_get();
    drupalgap_router_path_set(drupalgap_get_menu_link_router_path(to));
    module_invoke_all('drupalgap_back', from, to);

  }
  catch (error) { console.log('_drupalgap_back - ' + error); }
}

/**
 * An internal function used to exit the app when the back button is clicked.
 * @param {Number} button Which button was pressed.
 */
function _drupalgap_back_exit(button) {
  try {
    button === 1 ? navigator.app.exitApp() : '';
  }
  catch (error) { console.log('_drupalgap_back_exit - ' + error); }
}

$(window).on('navigate', function(event, data) {

    // If we're already moving backwards (aka from a "soft" back button click),
    // then don't do anything.
    if (drupalgap.back) { return; }

    // In web-app mode, clicking the back button on your browser (or Android
    // device browser), does not actually fire drupalgap_back(), so we mimic
    // it here, but skip the history.back() call because that's already
    // happened via the hardware button.
    if (drupalgap.settings.mode == 'web-app') {
      // Grab the direction we're travelling.
      // back, forward (or undefined, aka moving from splash to front page)
      var direction = data.state.direction;
      if (direction == 'back' && drupalgap.back_path.length > 0) {
        // @WARNING - any changes here should be reflected into _drupalgap_back().
        drupalgap.back = true;
        // Update the path and router path.
        var from = drupalgap_path_get();
        drupalgap_path_set(drupalgap.back_path.pop());
        var to = drupalgap_path_get();
        drupalgap_router_path_set(drupalgap_get_menu_link_router_path(to));
        module_invoke_all('drupalgap_back', from, to);
      }
    }

});