blob: 7457f60fa54992b41202cae4560fcdec621b7ba5 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('settings', function() {
/**
* Class for navigable routes. May only be instantiated within this file.
* @constructor
* @param {string} path
* @private
*/
var Route = function(path) {
this.path = path;
/** @type {?settings.Route} */
this.parent = null;
/** @type {number} */
this.depth = 0;
// Below are all legacy properties to provide compatibility with the old
// routing system. TODO(tommycli): Remove once routing refactor complete.
this.section = '';
};
Route.prototype = {
/**
* Returns a new Route instance that's a child of this route.
* @param {string} path Extends this route's path if it doesn't contain a
* leading slash.
* @return {!settings.Route}
* @private
*/
createChild: function(path) {
assert(path);
// |path| extends this route's path if it doesn't have a leading slash.
// If it does have a leading slash, it's just set as the new route's URL.
var newUrl = path[0] == '/' ? path : this.path + '/' + path;
var route = new Route(newUrl);
route.parent = this;
route.section = this.section;
route.depth = this.depth + 1;
return route;
},
/**
* Returns a new Route instance that's a child section of this route.
* TODO(tommycli): Remove once we've obsoleted the concept of sections.
* @param {string} path
* @param {string} section
* @return {!settings.Route}
* @private
*/
createSection: function(path, section) {
var route = this.createChild(path);
route.section = section;
return route;
},
/**
* Returns true if this route matches or is an ancestor of the parameter.
* @param {!settings.Route} route
* @return {boolean}
*/
contains: function(route) {
for (var r = route; r != null; r = r.parent) {
if (this == r)
return true;
}
return false;
},
/**
* Returns true if this route is a subpage of a section.
* @return {boolean}
*/
isSubpage: function() {
return !!this.parent && !!this.section &&
this.parent.section == this.section;
},
};
// Abbreviated variable for easier definitions.
var r = Route;
// Root pages.
r.BASIC = new Route('/');
r.ADVANCED = new Route('/advanced');
r.ABOUT = new Route('/help');
// Navigable dialogs. These are the only non-section children of root pages.
// These are disfavored. If we add anymore, we should add explicit support.
r.IMPORT_DATA = r.BASIC.createChild('/importData');
r.SIGN_OUT = r.BASIC.createChild('/signOut');
r.CLEAR_BROWSER_DATA = r.ADVANCED.createChild('/clearBrowserData');
r.RESET_DIALOG = r.ADVANCED.createChild('/resetProfileSettings');
r.TRIGGERED_RESET_DIALOG =
r.ADVANCED.createChild('/triggeredResetProfileSettings');
// <if expr="chromeos">
r.INTERNET = r.BASIC.createSection('/internet', 'internet');
r.NETWORK_DETAIL = r.INTERNET.createChild('/networkDetail');
r.KNOWN_NETWORKS = r.INTERNET.createChild('/knownNetworks');
r.BLUETOOTH = r.BASIC.createSection('/bluetooth', 'bluetooth');
r.BLUETOOTH_DEVICES = r.BLUETOOTH.createChild('/bluetoothDevices');
// </if>
r.APPEARANCE = r.BASIC.createSection('/appearance', 'appearance');
r.FONTS = r.APPEARANCE.createChild('/fonts');
r.DEFAULT_BROWSER =
r.BASIC.createSection('/defaultBrowser', 'defaultBrowser');
r.SEARCH = r.BASIC.createSection('/search', 'search');
r.SEARCH_ENGINES = r.SEARCH.createChild('/searchEngines');
// <if expr="chromeos">
r.ANDROID_APPS = r.BASIC.createSection('/androidApps', 'androidApps');
// </if>
r.ON_STARTUP = r.BASIC.createSection('/onStartup', 'onStartup');
r.PEOPLE = r.BASIC.createSection('/people', 'people');
r.SYNC = r.PEOPLE.createChild('/syncSetup');
// <if expr="not chromeos">
r.MANAGE_PROFILE = r.PEOPLE.createChild('/manageProfile');
// </if>
// <if expr="chromeos">
r.CHANGE_PICTURE = r.PEOPLE.createChild('/changePicture');
r.ACCOUNTS = r.PEOPLE.createChild('/accounts');
r.LOCK_SCREEN = r.PEOPLE.createChild('/lockScreen');
r.FINGERPRINT = r.LOCK_SCREEN.createChild('/lockScreen/fingerprint');
r.DEVICE = r.BASIC.createSection('/device', 'device');
r.POINTERS = r.DEVICE.createChild('/pointer-overlay');
r.KEYBOARD = r.DEVICE.createChild('/keyboard-overlay');
r.DISPLAY = r.DEVICE.createChild('/display');
r.STYLUS = r.DEVICE.createChild('/stylus');
r.STORAGE = r.DEVICE.createChild('/storage');
// </if>
r.PRIVACY = r.ADVANCED.createSection('/privacy', 'privacy');
r.CERTIFICATES = r.PRIVACY.createChild('/certificates');
r.SITE_SETTINGS = r.PRIVACY.createChild('/content');
r.SITE_SETTINGS_ALL = r.SITE_SETTINGS.createChild('all');
r.SITE_SETTINGS_SITE_DETAILS =
r.SITE_SETTINGS_ALL.createChild('/content/siteDetails');
r.SITE_SETTINGS_HANDLERS = r.SITE_SETTINGS.createChild('/handlers');
// TODO(tommycli): Find a way to refactor these repetitive category routes.
r.SITE_SETTINGS_AUTOMATIC_DOWNLOADS =
r.SITE_SETTINGS.createChild('automaticDownloads');
r.SITE_SETTINGS_BACKGROUND_SYNC =
r.SITE_SETTINGS.createChild('backgroundSync');
r.SITE_SETTINGS_CAMERA = r.SITE_SETTINGS.createChild('camera');
r.SITE_SETTINGS_COOKIES = r.SITE_SETTINGS.createChild('cookies');
r.SITE_SETTINGS_DATA_DETAILS =
r.SITE_SETTINGS_COOKIES.createChild('/cookies/detail');
r.SITE_SETTINGS_IMAGES = r.SITE_SETTINGS.createChild('images');
r.SITE_SETTINGS_JAVASCRIPT = r.SITE_SETTINGS.createChild('javascript');
r.SITE_SETTINGS_LOCATION = r.SITE_SETTINGS.createChild('location');
r.SITE_SETTINGS_MICROPHONE = r.SITE_SETTINGS.createChild('microphone');
r.SITE_SETTINGS_NOTIFICATIONS = r.SITE_SETTINGS.createChild('notifications');
r.SITE_SETTINGS_FLASH = r.SITE_SETTINGS.createChild('flash');
r.SITE_SETTINGS_POPUPS = r.SITE_SETTINGS.createChild('popups');
r.SITE_SETTINGS_UNSANDBOXED_PLUGINS =
r.SITE_SETTINGS.createChild('unsandboxedPlugins');
r.SITE_SETTINGS_USB_DEVICES = r.SITE_SETTINGS.createChild('usbDevices');
r.SITE_SETTINGS_ZOOM_LEVELS = r.SITE_SETTINGS.createChild('zoomLevels');
r.SITE_SETTINGS_PDF_DOCUMENTS = r.SITE_SETTINGS.createChild('pdfDocuments');
r.SITE_SETTINGS_PROTECTED_CONTENT =
r.SITE_SETTINGS.createChild('protectedContent');
// <if expr="chromeos">
r.DATETIME = r.ADVANCED.createSection('/dateTime', 'dateTime');
// </if>
r.PASSWORDS =
r.ADVANCED.createSection('/passwordsAndForms', 'passwordsAndForms');
r.AUTOFILL = r.PASSWORDS.createChild('/autofill');
r.MANAGE_PASSWORDS = r.PASSWORDS.createChild('/passwords');
r.LANGUAGES = r.ADVANCED.createSection('/languages', 'languages');
// <if expr="chromeos">
r.INPUT_METHODS = r.LANGUAGES.createChild('/inputMethods');
// </if>
// <if expr="not is_macosx">
r.EDIT_DICTIONARY = r.LANGUAGES.createChild('/editDictionary');
// </if>
r.DOWNLOADS = r.ADVANCED.createSection('/downloads', 'downloads');
r.PRINTING = r.ADVANCED.createSection('/printing', 'printing');
r.CLOUD_PRINTERS = r.PRINTING.createChild('/cloudPrinters');
// <if expr="chromeos">
r.CUPS_PRINTERS = r.PRINTING.createChild('/cupsPrinters');
r.CUPS_PRINTER_DETAIL = r.CUPS_PRINTERS.createChild('/cupsPrinterDetails');
// </if>
r.ACCESSIBILITY = r.ADVANCED.createSection('/accessibility', 'a11y');
r.MANAGE_ACCESSIBILITY = r.ACCESSIBILITY.createChild('/manageAccessibility');
r.SYSTEM = r.ADVANCED.createSection('/system', 'system');
r.RESET = r.ADVANCED.createSection('/reset', 'reset');
// <if expr="chromeos">
// "About" is the only section in About, but we still need to create the route
// in order to show the subpage on Chrome OS.
r.ABOUT_ABOUT = r.ABOUT.createSection('/help/about', 'about');
r.DETAILED_BUILD_INFO = r.ABOUT_ABOUT.createChild('/help/details');
// </if>
var routeObservers_ = new Set();
/** @polymerBehavior */
var RouteObserverBehavior = {
/** @override */
attached: function() {
assert(!routeObservers_.has(this));
routeObservers_.add(this);
// Emulating Polymer data bindings, the observer is called when the
// element starts observing the route.
this.currentRouteChanged(currentRoute_, undefined);
},
/** @override */
detached: function() {
assert(routeObservers_.delete(this));
},
/**
* @param {!settings.Route|undefined} opt_newRoute
* @param {!settings.Route|undefined} opt_oldRoute
* @abstract
*/
currentRouteChanged: function(opt_newRoute, opt_oldRoute) {
assertNotReached();
},
};
/**
* Regular expression that captures the leading slash, the content and the
* trailing slash in three different groups.
* @const {!RegExp}
*/
var CANONICAL_PATH_REGEX = /(^\/)([\/-\w]+)(\/$)/;
/**
* @param {string} path
* @return {?settings.Route} The matching canonical route, or null if none
* matches.
*/
var getRouteForPath = function(path) {
// Allow trailing slash in paths.
var canonicalPath = path.replace(CANONICAL_PATH_REGEX, '$1$2');
// TODO(tommycli): Use Object.values once Closure compilation supports it.
var matchingKey = Object.keys(Route).find(function(key) {
return Route[key].path == canonicalPath;
});
return !!matchingKey ? Route[matchingKey] : null;
};
/**
* The current active route. This updated is only by settings.navigateTo or
* settings.initializeRouteFromUrl.
* @private {!settings.Route}
*/
var currentRoute_ = Route.BASIC;
/**
* The current query parameters. This is updated only by settings.navigateTo
* or settings.initializeRouteFromUrl.
* @private {!URLSearchParams}
*/
var currentQueryParameters_ = new URLSearchParams();
/** @private {boolean} */
var lastRouteChangeWasPopstate_ = false;
/** @private */
var initializeRouteFromUrlCalled_ = false;
/**
* Initialize the route and query params from the URL.
*/
var initializeRouteFromUrl = function() {
assert(!initializeRouteFromUrlCalled_);
initializeRouteFromUrlCalled_ = true;
var route = getRouteForPath(window.location.pathname);
// Never allow direct navigation to ADVANCED.
if (route && route != Route.ADVANCED) {
currentRoute_ = route;
currentQueryParameters_ = new URLSearchParams(window.location.search);
} else {
window.history.replaceState(undefined, '', Route.BASIC.path);
}
};
function resetRouteForTesting() {
initializeRouteFromUrlCalled_ = false;
lastRouteChangeWasPopstate_ = false;
currentRoute_ = Route.BASIC;
currentQueryParameters_ = new URLSearchParams();
}
/**
* Helper function to set the current route and notify all observers.
* @param {!settings.Route} route
* @param {!URLSearchParams} queryParameters
* @param {boolean} isPopstate
*/
var setCurrentRoute = function(route, queryParameters, isPopstate) {
var oldRoute = currentRoute_;
currentRoute_ = route;
currentQueryParameters_ = queryParameters;
lastRouteChangeWasPopstate_ = isPopstate;
routeObservers_.forEach(function(observer) {
observer.currentRouteChanged(currentRoute_, oldRoute);
});
};
/** @return {!settings.Route} */
var getCurrentRoute = function() { return currentRoute_; };
/** @return {!URLSearchParams} */
var getQueryParameters = function() {
return new URLSearchParams(currentQueryParameters_); // Defensive copy.
};
/** @return {boolean} */
var lastRouteChangeWasPopstate = function() {
return lastRouteChangeWasPopstate_;
};
/**
* Navigates to a canonical route and pushes a new history entry.
* @param {!settings.Route} route
* @param {URLSearchParams=} opt_dynamicParameters Navigations to the same
* URL parameters in a different order will still push to history.
* @param {boolean=} opt_removeSearch Whether to strip the 'search' URL
* parameter during navigation. Defaults to false.
*/
var navigateTo = function(route, opt_dynamicParameters, opt_removeSearch) {
var params = opt_dynamicParameters || new URLSearchParams();
var removeSearch = !!opt_removeSearch;
var oldSearchParam = getQueryParameters().get('search') || '';
var newSearchParam = params.get('search') || '';
if (!removeSearch && oldSearchParam && !newSearchParam)
params.append('search', oldSearchParam);
var url = route.path;
var queryString = params.toString();
if (queryString)
url += '?' + queryString;
// History serializes the state, so we don't push the actual route object.
window.history.pushState(currentRoute_.path, '', url);
setCurrentRoute(route, params, false);
};
/**
* Navigates to the previous route if it has an equal or lesser depth.
* If there is no previous route in history meeting those requirements,
* this navigates to the immediate parent. This will never exit Settings.
*/
var navigateToPreviousRoute = function() {
var previousRoute =
window.history.state &&
assert(getRouteForPath(/** @type {string} */ (window.history.state)));
if (previousRoute && previousRoute.depth <= currentRoute_.depth)
window.history.back();
else
navigateTo(currentRoute_.parent || Route.BASIC);
};
window.addEventListener('popstate', function(event) {
// On pop state, do not push the state onto the window.history again.
setCurrentRoute(getRouteForPath(window.location.pathname) || Route.BASIC,
new URLSearchParams(window.location.search), true);
});
return {
Route: Route,
RouteObserverBehavior: RouteObserverBehavior,
getRouteForPath: getRouteForPath,
initializeRouteFromUrl: initializeRouteFromUrl,
resetRouteForTesting: resetRouteForTesting,
getCurrentRoute: getCurrentRoute,
getQueryParameters: getQueryParameters,
lastRouteChangeWasPopstate: lastRouteChangeWasPopstate,
navigateTo: navigateTo,
navigateToPreviousRoute: navigateToPreviousRoute,
};
});