OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @typedef {{ | |
7 * dialog: (string|undefined), | |
8 * page: string, | |
9 * section: string, | |
10 * subpage: !Array<string>, | |
11 * }} | |
12 */ | |
13 var SettingsRoute; | |
14 | |
15 /** @typedef {SettingsRoute|{url: string}} */ | |
16 var CanonicalRoute; | |
17 | |
18 /** @typedef {SettingsRoute|{inHistory: boolean}} */ | |
19 var HistoricRoute; | |
20 | |
21 /** | |
22 * @fileoverview | 6 * @fileoverview |
23 * 'settings-router' is a simple router for settings. Its responsibilities: | 7 * 'settings-router' is a simple router for settings. Its responsibilities: |
24 * - Update the URL when the routing state changes. | 8 * - Update the URL when the routing state changes. |
25 * - Initialize the routing state with the initial URL. | 9 * - Initialize the routing state with the initial URL. |
26 * - Process and validate all routing state changes. | 10 * - Process and validate all routing state changes. |
27 * | 11 * |
28 * Example: | 12 * Example: |
29 * | 13 * |
30 * <settings-router current-route="{{currentRoute}}"> | 14 * <settings-router current-route="{{currentRoute}}"> |
31 * </settings-router> | 15 * </settings-router> |
32 */ | 16 */ |
33 Polymer({ | 17 Polymer({ |
34 is: 'settings-router', | 18 is: 'settings-router', |
35 | 19 |
36 properties: { | 20 properties: { |
37 /** | 21 /** |
38 * The current active route. This is reflected to the URL. Updates to this | 22 * The current active route. This may only be updated via the global |
39 * property should replace the whole object. | 23 * function settings.navigateTo. |
40 * | 24 * |
41 * currentRoute.page refers to top-level pages such as Basic and Advanced. | 25 * currentRoute.page refers to top-level pages such as Basic and Advanced. |
42 * | 26 * |
43 * currentRoute.section is only non-empty when the user is on a subpage. If | 27 * currentRoute.section is only non-empty when the user is on a subpage. If |
44 * the user is on Basic, for instance, this is an empty string. | 28 * the user is on Basic, for instance, this is an empty string. |
45 * | 29 * |
46 * currentRoute.subpage is an Array. The last element is the actual subpage | 30 * currentRoute.subpage is an Array. The last element is the actual subpage |
47 * the user is on. The previous elements are the ancestor subpages. This | 31 * the user is on. The previous elements are the ancestor subpages. This |
48 * enables support for multiple paths to the same subpage. This is used by | 32 * enables support for multiple paths to the same subpage. This is used by |
49 * both the Back button and the Breadcrumb to determine ancestor subpages. | 33 * both the Back button and the Breadcrumb to determine ancestor subpages. |
50 * @type {SettingsRoute} | 34 * @type {!settings.Route} |
51 */ | 35 */ |
52 currentRoute: { | 36 currentRoute: { |
53 notify: true, | 37 notify: true, |
54 observer: 'currentRouteChanged_', | |
55 type: Object, | 38 type: Object, |
56 value: function() { | 39 value: function() { |
57 var initialRoute = this.canonicalRoutes_[0]; | 40 return this.getRouteFor_(window.location.pathname); |
58 | |
59 // Take the current URL, find a matching pre-defined route, and | |
60 // initialize the currentRoute to that pre-defined route. | |
61 for (var i = 0; i < this.canonicalRoutes_.length; ++i) { | |
62 var canonicalRoute = this.canonicalRoutes_[i]; | |
63 if (canonicalRoute.url == window.location.pathname) { | |
64 initialRoute = canonicalRoute; | |
65 break; | |
66 } | |
67 } | |
68 | |
69 return { | |
70 page: initialRoute.page, | |
71 section: initialRoute.section, | |
72 subpage: initialRoute.subpage, | |
73 dialog: initialRoute.dialog, | |
74 }; | |
75 }, | |
76 }, | |
77 | |
78 /** | |
79 * Page titles for the currently active route. Updated by the currentRoute | |
80 * property observer. | |
81 * @type {{pageTitle: string}} | |
82 */ | |
83 currentRouteTitles: { | |
84 notify: true, | |
85 type: Object, | |
86 value: function() { | |
87 return { | |
88 pageTitle: '', | |
89 }; | |
90 }, | 41 }, |
91 }, | 42 }, |
92 }, | 43 }, |
93 | 44 |
94 | |
95 /** | |
96 * @private {!Array<!CanonicalRoute>} | |
97 * The 'url' property is not accessible to other elements. | |
98 */ | |
99 canonicalRoutes_: Object.keys(settings.Route).map(function(key) { | |
100 return settings.Route[key]; | |
101 }), | |
102 | |
103 /** | 45 /** |
104 * Sets up a history popstate observer. | 46 * Sets up a history popstate observer. |
| 47 * @override |
105 */ | 48 */ |
106 created: function() { | 49 created: function() { |
107 window.addEventListener('popstate', function(event) { | 50 window.addEventListener('popstate', function(event) { |
108 if (event.state && event.state.page) | 51 // On pop state, do not push the state onto the window.history again. |
109 this.currentRoute = event.state; | 52 this.currentRoute = this.getRouteFor_(window.location.pathname); |
110 }.bind(this)); | 53 }.bind(this)); |
| 54 |
| 55 settings.navigateTo = this.navigateTo_.bind(this); |
111 }, | 56 }, |
112 | 57 |
113 /** | 58 /** |
114 * Is called when another element modifies the route. This observer validates | 59 * Returns the matching canonical route, or the default route if none matches. |
115 * the route change against the pre-defined list of routes, and updates the | 60 * @param {string} path |
116 * URL appropriately. | 61 * @return {!settings.Route} |
117 * @param {!SettingsRoute} newRoute Where we're headed. | |
118 * @param {!SettingsRoute|undefined} oldRoute Where we've been. | |
119 * @private | 62 * @private |
120 */ | 63 */ |
121 currentRouteChanged_: function(newRoute, oldRoute) { | 64 getRouteFor_: function(path) { |
122 for (var i = 0; i < this.canonicalRoutes_.length; ++i) { | 65 // TODO(tommycli): Use Object.values once Closure compilation supports it. |
123 var canonicalRoute = this.canonicalRoutes_[i]; | 66 var matchingKey = Object.keys(settings.Route).find(function(key) { |
124 if (canonicalRoute.page == newRoute.page && | 67 return settings.Route[key].path == path; |
125 canonicalRoute.section == newRoute.section && | 68 }); |
126 canonicalRoute.dialog == newRoute.dialog && | |
127 canonicalRoute.subpage.length == newRoute.subpage.length && | |
128 canonicalRoute.subpage.every(function(value, index) { | |
129 return value == newRoute.subpage[index]; | |
130 })) { | |
131 // Update the property containing the titles for the current route. | |
132 this.currentRouteTitles = { | |
133 pageTitle: loadTimeData.getString(canonicalRoute.page + 'PageTitle'), | |
134 }; | |
135 | 69 |
136 // If we are restoring a state from history, don't push it again. | 70 if (!matchingKey) |
137 if (/** @type {HistoricRoute} */(newRoute).inHistory) | 71 return settings.Route.BASIC; |
138 return; | |
139 | 72 |
140 // Mark routes persisted in history as already stored in history. | 73 return settings.Route[matchingKey]; |
141 var historicRoute = /** @type {HistoricRoute} */({ | 74 }, |
142 inHistory: true, | |
143 page: newRoute.page, | |
144 section: newRoute.section, | |
145 subpage: newRoute.subpage, | |
146 dialog: newRoute.dialog, | |
147 }); | |
148 | 75 |
149 // Push the current route to the history state, so when the user | 76 /** |
150 // navigates with the browser back button, we can recall the route. | 77 * Navigates to a canonical route. |
151 if (oldRoute) { | 78 * @param {!settings.Route} route |
152 window.history.pushState(historicRoute, document.title, | 79 * @private |
153 canonicalRoute.url); | 80 */ |
154 } else { | 81 navigateTo_: function(route) { |
155 // For the very first route (oldRoute will be undefined), we replace | 82 assert(!!route); |
156 // the existing state instead of pushing a new one. This is to allow | |
157 // the user to use the browser back button to exit Settings entirely. | |
158 window.history.replaceState(historicRoute, document.title); | |
159 } | |
160 | 83 |
161 return; | 84 if (route == this.currentRoute) |
162 } | 85 return; |
163 } | |
164 | 86 |
165 assertNotReached('Route not found: ' + JSON.stringify(newRoute)); | 87 window.history.pushState(undefined, document.title, route.path); |
| 88 this.currentRoute = route; |
166 }, | 89 }, |
167 }); | 90 }); |
OLD | NEW |