@qtoggle/qui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/.eslintrc.json +492 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
- package/.github/workflows/main.yml +74 -0
- package/.pre-commit-config.yaml +8 -0
- package/LICENSE.txt +177 -0
- package/README.md +4 -0
- package/font/dejavusans-bold.woff +0 -0
- package/font/dejavusans-bolditalic.woff +0 -0
- package/font/dejavusans-italic.woff +0 -0
- package/font/dejavusans-regular.woff +0 -0
- package/img/qui-icons.svg +1937 -0
- package/js/base/base.js +47 -0
- package/js/base/condition-variable.js +92 -0
- package/js/base/errors.js +36 -0
- package/js/base/i18n.js +20 -0
- package/js/base/mixwith.js +135 -0
- package/js/base/require-js-compat.js +78 -0
- package/js/base/signal.js +91 -0
- package/js/base/singleton.js +66 -0
- package/js/base/timer.js +126 -0
- package/js/config.js +184 -0
- package/js/forms/common-fields/check-field.js +42 -0
- package/js/forms/common-fields/choice-buttons-field.js +30 -0
- package/js/forms/common-fields/color-combo-field.js +37 -0
- package/js/forms/common-fields/combo-field.js +108 -0
- package/js/forms/common-fields/common-fields.js +23 -0
- package/js/forms/common-fields/composite-field.js +132 -0
- package/js/forms/common-fields/custom-html-field.js +51 -0
- package/js/forms/common-fields/email-field.js +30 -0
- package/js/forms/common-fields/file-picker-field.js +46 -0
- package/js/forms/common-fields/jquery-ui-field.js +111 -0
- package/js/forms/common-fields/labels-field.js +69 -0
- package/js/forms/common-fields/numeric-field.js +39 -0
- package/js/forms/common-fields/password-field.js +28 -0
- package/js/forms/common-fields/phone-field.js +26 -0
- package/js/forms/common-fields/progress-disk-field.js +69 -0
- package/js/forms/common-fields/push-button-field.js +138 -0
- package/js/forms/common-fields/slider-field.js +51 -0
- package/js/forms/common-fields/text-area-field.js +34 -0
- package/js/forms/common-fields/text-field.js +89 -0
- package/js/forms/common-fields/up-down-field.js +85 -0
- package/js/forms/common-forms/common-forms.js +16 -0
- package/js/forms/common-forms/options-form.js +77 -0
- package/js/forms/common-forms/page-form.js +115 -0
- package/js/forms/form-button.js +202 -0
- package/js/forms/form-field.js +1183 -0
- package/js/forms/form.js +1181 -0
- package/js/forms/forms.js +68 -0
- package/js/global-glass.js +100 -0
- package/js/icons/default-stock.js +173 -0
- package/js/icons/icon.js +64 -0
- package/js/icons/icons.js +16 -0
- package/js/icons/multi-state-sprites-icon.js +362 -0
- package/js/icons/stock-icon.js +219 -0
- package/js/icons/stock.js +98 -0
- package/js/icons/stocks.js +57 -0
- package/js/index.js +232 -0
- package/js/lib/jquery.longpress.js +79 -0
- package/js/lib/jquery.module.js +4 -0
- package/js/lib/logger.module.js +4 -0
- package/js/lib/pep.module.js +4 -0
- package/js/lists/common-items/common-items.js +5 -0
- package/js/lists/common-items/icon-label-list-item.js +86 -0
- package/js/lists/common-lists/common-lists.js +5 -0
- package/js/lists/common-lists/page-list.js +53 -0
- package/js/lists/list-item.js +147 -0
- package/js/lists/list.js +636 -0
- package/js/lists/lists.js +26 -0
- package/js/main-ui/main-ui.js +64 -0
- package/js/main-ui/menu-bar.js +144 -0
- package/js/main-ui/options-bar.js +181 -0
- package/js/main-ui/status.js +185 -0
- package/js/main-ui/top-bar.js +59 -0
- package/js/messages/common-message-forms/common-message-forms.js +7 -0
- package/js/messages/common-message-forms/confirm-message-form.js +81 -0
- package/js/messages/common-message-forms/simple-message-form.js +67 -0
- package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
- package/js/messages/message-form.js +107 -0
- package/js/messages/messages.js +21 -0
- package/js/messages/sticky-modal-page.js +98 -0
- package/js/messages/sticky-modal-progress-message.js +27 -0
- package/js/messages/toast.js +164 -0
- package/js/navigation.js +654 -0
- package/js/pages/breadcrumbs.js +124 -0
- package/js/pages/common-pages/common-pages.js +6 -0
- package/js/pages/common-pages/modal-progress-page.js +83 -0
- package/js/pages/common-pages/structured-page.js +46 -0
- package/js/pages/page.js +1018 -0
- package/js/pages/pages-context.js +154 -0
- package/js/pages/pages.js +252 -0
- package/js/pwa.js +337 -0
- package/js/sections/section.js +612 -0
- package/js/sections/sections.js +300 -0
- package/js/tables/common-cells/common-cells.js +7 -0
- package/js/tables/common-cells/icon-label-table-cell.js +68 -0
- package/js/tables/common-cells/push-button-table-cell.js +133 -0
- package/js/tables/common-cells/simple-table-cell.js +37 -0
- package/js/tables/common-tables/common-tables.js +5 -0
- package/js/tables/common-tables/page-table.js +55 -0
- package/js/tables/table-cell.js +198 -0
- package/js/tables/table-row.js +126 -0
- package/js/tables/table.js +492 -0
- package/js/tables/tables.js +36 -0
- package/js/theme.js +304 -0
- package/js/utils/ajax.js +126 -0
- package/js/utils/array.js +194 -0
- package/js/utils/colors.js +445 -0
- package/js/utils/cookies.js +65 -0
- package/js/utils/crypto.js +439 -0
- package/js/utils/css.js +234 -0
- package/js/utils/date.js +300 -0
- package/js/utils/files.js +27 -0
- package/js/utils/gestures.js +165 -0
- package/js/utils/html.js +76 -0
- package/js/utils/misc.js +81 -0
- package/js/utils/object.js +324 -0
- package/js/utils/promise.js +49 -0
- package/js/utils/string.js +192 -0
- package/js/utils/url.js +187 -0
- package/js/utils/utils.js +3 -0
- package/js/utils/visibility-manager.js +211 -0
- package/js/views/common-views/common-views.js +7 -0
- package/js/views/common-views/icon-label-view.js +210 -0
- package/js/views/common-views/progress-view.js +89 -0
- package/js/views/common-views/structured-view.js +368 -0
- package/js/views/view.js +467 -0
- package/js/views/views.js +3 -0
- package/js/widgets/base-widget.js +23 -0
- package/js/widgets/common-widgets/check-button.js +109 -0
- package/js/widgets/common-widgets/choice-buttons.js +322 -0
- package/js/widgets/common-widgets/color-combo.js +104 -0
- package/js/widgets/common-widgets/combo.js +645 -0
- package/js/widgets/common-widgets/common-widgets.js +17 -0
- package/js/widgets/common-widgets/email-input.js +7 -0
- package/js/widgets/common-widgets/file-picker.js +133 -0
- package/js/widgets/common-widgets/labels.js +132 -0
- package/js/widgets/common-widgets/numeric-input.js +49 -0
- package/js/widgets/common-widgets/password-input.js +91 -0
- package/js/widgets/common-widgets/phone-input.js +7 -0
- package/js/widgets/common-widgets/progress-disk.js +174 -0
- package/js/widgets/common-widgets/push-button.js +155 -0
- package/js/widgets/common-widgets/slider.js +455 -0
- package/js/widgets/common-widgets/text-area.js +52 -0
- package/js/widgets/common-widgets/text-input.js +174 -0
- package/js/widgets/common-widgets/up-down.js +351 -0
- package/js/widgets/widgets.js +57 -0
- package/js/window.js +557 -0
- package/jsdoc.conf.json +20 -0
- package/less/base.less +123 -0
- package/less/forms/common-fields.less +101 -0
- package/less/forms/common-forms.less +5 -0
- package/less/forms/form-button.less +21 -0
- package/less/forms/form-field.less +266 -0
- package/less/forms/form.less +131 -0
- package/less/global-glass.less +64 -0
- package/less/icon-label-view.less +82 -0
- package/less/icons.less +144 -0
- package/less/lists.less +105 -0
- package/less/main-ui.less +328 -0
- package/less/messages.less +189 -0
- package/less/no-effects.less +24 -0
- package/less/pages/breadcrumbs.less +98 -0
- package/less/pages/common-pages.less +36 -0
- package/less/pages/page.less +70 -0
- package/less/progress-view.less +51 -0
- package/less/stock-icons.less +43 -0
- package/less/structured-view.less +245 -0
- package/less/tables.less +84 -0
- package/less/theme-dark.less +133 -0
- package/less/theme-light.less +132 -0
- package/less/theme.less +419 -0
- package/less/visibility-manager.less +11 -0
- package/less/widgets/check-button.less +96 -0
- package/less/widgets/choice-buttons.less +160 -0
- package/less/widgets/color-combo.less +33 -0
- package/less/widgets/combo.less +230 -0
- package/less/widgets/common-buttons.less +120 -0
- package/less/widgets/common.less +24 -0
- package/less/widgets/input.less +258 -0
- package/less/widgets/labels.less +81 -0
- package/less/widgets/progress-disk.less +70 -0
- package/less/widgets/slider.less +199 -0
- package/less/widgets/updown.less +115 -0
- package/less/widgets/various.less +36 -0
- package/package.json +47 -0
- package/pyproject.toml +45 -0
- package/qui/__init__.py +110 -0
- package/qui/constants.py +1 -0
- package/qui/exceptions.py +2 -0
- package/qui/j2template.py +71 -0
- package/qui/settings.py +60 -0
- package/qui/templates/manifest.json +25 -0
- package/qui/templates/qui.html +126 -0
- package/qui/templates/service-worker.js +188 -0
- package/qui/web/__init__.py +0 -0
- package/qui/web/tornado.py +220 -0
- package/scripts/postinstall.sh +10 -0
- package/webpack/webpack-adjust-css-urls-loader.js +36 -0
- package/webpack/webpack-common.js +384 -0
package/js/navigation.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @namespace qui.navigation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import $ from '$qui/lib/jquery.module.js'
|
|
6
|
+
import Logger from '$qui/lib/logger.module.js'
|
|
7
|
+
|
|
8
|
+
import {gettext} from '$qui/base/i18n.js'
|
|
9
|
+
import ConditionVariable from '$qui/base/condition-variable.js'
|
|
10
|
+
import Config from '$qui/config.js'
|
|
11
|
+
import * as Toast from '$qui/messages/toast.js'
|
|
12
|
+
import PageMixin from '$qui/pages/page.js'
|
|
13
|
+
import {getCurrentContext} from '$qui/pages/pages.js'
|
|
14
|
+
import * as Sections from '$qui/sections/sections.js'
|
|
15
|
+
import * as ObjectUtils from '$qui/utils/object.js'
|
|
16
|
+
import * as StringUtils from '$qui/utils/string.js'
|
|
17
|
+
import URL from '$qui/utils/url.js'
|
|
18
|
+
import * as Window from '$qui/window.js'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @alias qui.navigation.BACK_MODE_HISTORY
|
|
23
|
+
*/
|
|
24
|
+
export const BACK_MODE_HISTORY = 'history'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @alias qui.navigation.BACK_MODE_CLOSE
|
|
28
|
+
*/
|
|
29
|
+
export const BACK_MODE_CLOSE = 'close'
|
|
30
|
+
|
|
31
|
+
const logger = Logger.get('qui.navigation')
|
|
32
|
+
|
|
33
|
+
let basePath = ''
|
|
34
|
+
let initialURLPath = null
|
|
35
|
+
let initialURLQuery = null
|
|
36
|
+
let currentURLPath = null
|
|
37
|
+
let currentURLQuery = null
|
|
38
|
+
let backMode = BACK_MODE_CLOSE
|
|
39
|
+
let historyIndex = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A condition that is fulfilled as soon as the initial navigation is completed.
|
|
44
|
+
* @alias qui.navigation.whenInitialNavigationReady
|
|
45
|
+
* @type {qui.base.ConditionVariable}
|
|
46
|
+
*/
|
|
47
|
+
export let whenInitialNavigationReady = new ConditionVariable()
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An error indicating that navigation could not be done beyond a certain path.
|
|
51
|
+
* @alias qui.navigation.PageNotFoundError
|
|
52
|
+
* @extends Error
|
|
53
|
+
*/
|
|
54
|
+
export class PageNotFoundError extends Error {
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @constructs
|
|
58
|
+
* @param {String[]} path the full path that could not be navigated
|
|
59
|
+
* @param {String} pathId the path id to which the navigation could not be done
|
|
60
|
+
* @param {qui.sections.Section} section the section in which the navigation error occurred
|
|
61
|
+
* @param {qui.navigation.PageMixin} page the page where the navigation stopped
|
|
62
|
+
*/
|
|
63
|
+
constructor(path, pathId, section, page) {
|
|
64
|
+
super(gettext(`Page not found: /${path.join('/')}`))
|
|
65
|
+
|
|
66
|
+
this.path = path
|
|
67
|
+
this.pathId = pathId
|
|
68
|
+
this.section = section
|
|
69
|
+
this.page = page
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An error indicating that navigation could not be done due to a page load error.
|
|
76
|
+
* @alias qui.navigation.PageLoadError
|
|
77
|
+
* @extends Error
|
|
78
|
+
*/
|
|
79
|
+
export class PageLoadError extends Error {
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @constructs
|
|
83
|
+
* @param {String[]} path the full path that could not be navigated
|
|
84
|
+
* @param {String} pathId the path id to which the navigation could not be done
|
|
85
|
+
* @param {qui.sections.Section} section the section in which the navigation error occurred
|
|
86
|
+
* @param {qui.navigation.PageMixin} page the page where the navigation stopped
|
|
87
|
+
* @param {Error} error the error that occurred
|
|
88
|
+
*/
|
|
89
|
+
constructor(path, pathId, section, page, error) {
|
|
90
|
+
let msg
|
|
91
|
+
if (error) {
|
|
92
|
+
msg = StringUtils.formatPercent(
|
|
93
|
+
gettext('Page could not be loaded: %(error)s'),
|
|
94
|
+
{error: error.message}
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
msg = gettext('Page could not be loaded')
|
|
99
|
+
}
|
|
100
|
+
super(msg)
|
|
101
|
+
|
|
102
|
+
this.path = path
|
|
103
|
+
this.pathId = pathId
|
|
104
|
+
this.section = section
|
|
105
|
+
this.page = page
|
|
106
|
+
this.error = error
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* An error indicating that navigation could not be done due to a section load error.
|
|
113
|
+
* @alias qui.navigation.SectionLoadError
|
|
114
|
+
* @extends Error
|
|
115
|
+
*/
|
|
116
|
+
export class SectionLoadError extends Error {
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @constructs
|
|
120
|
+
* @param {String[]} path the full path that could not be navigated
|
|
121
|
+
* @param {String} pathId the path id to which the navigation could not be done
|
|
122
|
+
* @param {qui.sections.Section} section the section in which the navigation error occurred
|
|
123
|
+
* @param {Error} error the error that occurred
|
|
124
|
+
*/
|
|
125
|
+
constructor(path, pathId, section, error) {
|
|
126
|
+
let msg
|
|
127
|
+
if (error) {
|
|
128
|
+
msg = StringUtils.formatPercent(
|
|
129
|
+
gettext('Page could not be loaded: %(error)s'),
|
|
130
|
+
{error: error.message}
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
msg = gettext('Page could not be loaded')
|
|
135
|
+
}
|
|
136
|
+
super(msg)
|
|
137
|
+
|
|
138
|
+
this.path = path
|
|
139
|
+
this.pathId = pathId
|
|
140
|
+
this.section = section
|
|
141
|
+
this.error = error
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
function updateCurrentURL() {
|
|
148
|
+
let details
|
|
149
|
+
if (Config.navigationUsesFragment) {
|
|
150
|
+
details = URL.parse(window.location.hash.substring(1))
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
details = URL.parse(window.location.href)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
currentURLPath = details.path.substring(basePath.length)
|
|
157
|
+
currentURLQuery = details.queryStr
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Set the function of the back button:
|
|
163
|
+
* * {@link qui.navigation.BACK_MODE_CLOSE} makes back button close the current page (default on small screens)
|
|
164
|
+
* * {@link qui.navigation.BACK_MODE_HISTORY} makes back button go back through history (default on large screens)
|
|
165
|
+
* @alias qui.navigation.setBackMode
|
|
166
|
+
* @param {String} mode
|
|
167
|
+
*/
|
|
168
|
+
export function setBackMode(mode) {
|
|
169
|
+
backMode = mode
|
|
170
|
+
logger.debug(`back mode set to "${backMode}"`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Return the function of the back button.
|
|
175
|
+
* @alias qui.navigation.getBackMode
|
|
176
|
+
* @returns {String} one of:
|
|
177
|
+
* * {@link qui.navigation.BACK_MODE_CLOSE}
|
|
178
|
+
* * {@link qui.navigation.BACK_MODE_HISTORY}
|
|
179
|
+
*/
|
|
180
|
+
export function getBackMode() {
|
|
181
|
+
return backMode
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Navigate to initial browser path. Call this function after all sections have been registered.
|
|
186
|
+
* @returns {Promise} a promise that settles as soon as the navigation ends, being rejected in case of any error
|
|
187
|
+
*/
|
|
188
|
+
export function navigateInitial() {
|
|
189
|
+
let promise
|
|
190
|
+
|
|
191
|
+
if (initialURLPath) {
|
|
192
|
+
logger.debug(`initial navigation to ${initialURLPath}`)
|
|
193
|
+
promise = navigate({path: initialURLPath, historyEntry: false})
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
logger.debug('initial navigation to home')
|
|
197
|
+
promise = Sections.showHome()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return promise.then(function () {
|
|
201
|
+
whenInitialNavigationReady.fulfill()
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Return the current path as an array of path ids.
|
|
207
|
+
* @alias qui.navigation.getCurrentPath
|
|
208
|
+
* @returns {String[]}
|
|
209
|
+
*/
|
|
210
|
+
export function getCurrentPath() {
|
|
211
|
+
let path = []
|
|
212
|
+
let context = getCurrentContext()
|
|
213
|
+
|
|
214
|
+
if (!context) {
|
|
215
|
+
return []
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
context.getPages().forEach(function (page) {
|
|
219
|
+
if (!page.getPathId()) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
path = path.concat(page.getPathId().split('/').filter(p => Boolean(p)))
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return path
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Return the current URL query as a dictionary.
|
|
232
|
+
* @alias qui.navigation.getCurrentQuery
|
|
233
|
+
* @returns {Object<String,String>}
|
|
234
|
+
*/
|
|
235
|
+
export function getCurrentQuery() {
|
|
236
|
+
if (!currentURLQuery) {
|
|
237
|
+
return {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Transform query string into key-value pairs */
|
|
241
|
+
return URL.parse(`?${currentURLQuery}`).query
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Transform a QUI path into a fully qualified URL.
|
|
246
|
+
* @alias qui.navigation.pathToURL
|
|
247
|
+
* @param {String|String[]} path
|
|
248
|
+
* @returns {String}
|
|
249
|
+
*/
|
|
250
|
+
export function pathToURL(path) {
|
|
251
|
+
if (Array.isArray(path)) {
|
|
252
|
+
path = `/${path.join('/')}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return Config.navigationBasePrefix + path
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create an anchor element that represents a link to an internal path.
|
|
260
|
+
* @alias qui.navigation.makeInternalAnchor
|
|
261
|
+
* @param {String|String[]} path
|
|
262
|
+
* @param {String|jQuery} content
|
|
263
|
+
* @returns {jQuery}
|
|
264
|
+
*/
|
|
265
|
+
export function makeInternalAnchor(path, content) {
|
|
266
|
+
let url = pathToURL(path)
|
|
267
|
+
|
|
268
|
+
let anchor = $('<a></a>', {href: url})
|
|
269
|
+
anchor.html(content)
|
|
270
|
+
|
|
271
|
+
anchor.on('click', function (e) {
|
|
272
|
+
/* Prevent browser navigation, handle navigation internally */
|
|
273
|
+
e.preventDefault()
|
|
274
|
+
navigate({path})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
return anchor
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Navigate the given path.
|
|
282
|
+
* @alias qui.navigation.navigate
|
|
283
|
+
* @param {String|String[]} path the path to navigate
|
|
284
|
+
* @param {Boolean} [handleErrors] set to `false` to pass errors to the caller instead of handling them internally
|
|
285
|
+
* (defaults to `true`)
|
|
286
|
+
* @param {*} [pageState] optional history state to pass to {@link qui.navigation.PageMixin#restoreHistoryState}
|
|
287
|
+
* @param {Boolean} [historyEntry] whether to create a new history entry for current page before navigating (defaults
|
|
288
|
+
* to `true`)
|
|
289
|
+
* @returns {Promise} a promise that settles as soon as the navigation ends, being rejected in case of any error
|
|
290
|
+
*/
|
|
291
|
+
export function navigate({path, handleErrors = true, pageState = null, historyEntry = true}) {
|
|
292
|
+
/* Normalize path */
|
|
293
|
+
if (typeof path === 'string') {
|
|
294
|
+
path = path.split('/')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
path = path.filter(id => Boolean(id))
|
|
298
|
+
|
|
299
|
+
let origPath = path.slice()
|
|
300
|
+
let pathStr = `/${path.join('/')}`
|
|
301
|
+
let oldPath = getCurrentPath()
|
|
302
|
+
let currIndex = 1 /* Starts from 1, skipping the section id */
|
|
303
|
+
let section
|
|
304
|
+
let sectionRedirected = false
|
|
305
|
+
|
|
306
|
+
function handleError(error) {
|
|
307
|
+
if (handleErrors) {
|
|
308
|
+
Toast.error(error.message)
|
|
309
|
+
logger.errorStack('navigation error', error)
|
|
310
|
+
|
|
311
|
+
// TODO this is a good place to add custom navigation error handling function
|
|
312
|
+
Sections.showHome(/* reset = */ true)
|
|
313
|
+
|
|
314
|
+
return Promise.resolve()
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
throw error
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Don't do anything if requested path is actually current path */
|
|
322
|
+
if (ObjectUtils.deepEquals(path, oldPath) && path.length > 0) {
|
|
323
|
+
return Promise.resolve(getCurrentContext().getCurrentPage())
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (historyEntry) {
|
|
327
|
+
addHistoryEntry()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
logger.debug(`navigating to "${pathStr}", pageState = "${JSON.stringify(pageState)}"`)
|
|
331
|
+
|
|
332
|
+
if (!path.length) { /* Empty path means home */
|
|
333
|
+
section = Sections.getHome()
|
|
334
|
+
if (!section) {
|
|
335
|
+
logger.warn('no home section')
|
|
336
|
+
return Promise.reject(new Error('No home section'))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
let sectionId = path.shift() /* path[0] is always the section id */
|
|
341
|
+
section = Sections.get(sectionId)
|
|
342
|
+
if (!section) {
|
|
343
|
+
logger.error(`cannot find section with id "${sectionId}"`)
|
|
344
|
+
return handleError(new PageNotFoundError(origPath, sectionId, /* section = */ null, /* page = */ null))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* One step navigation function */
|
|
349
|
+
function navigateNext(currentPage) {
|
|
350
|
+
if (!path.length) { /* Navigation done */
|
|
351
|
+
/* Pop everything beyond given path */
|
|
352
|
+
let promise
|
|
353
|
+
let page = getCurrentContext().getPageAt(currIndex)
|
|
354
|
+
if (page) {
|
|
355
|
+
promise = page.close()
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
promise = Promise.resolve()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return promise.then(function () {
|
|
362
|
+
updateHistoryEntry()
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let pathId = path.shift()
|
|
367
|
+
|
|
368
|
+
logger.debug(`navigating from "${currentPage.getPathId()}" to "${pathId}"`)
|
|
369
|
+
|
|
370
|
+
let promiseOrPage = currentPage.navigate(pathId)
|
|
371
|
+
let promise = promiseOrPage
|
|
372
|
+
if (promiseOrPage == null || (promiseOrPage instanceof PageMixin) /* Page passed directly */) {
|
|
373
|
+
promise = Promise.resolve(promiseOrPage)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return promise.then(function (nextPage) {
|
|
377
|
+
|
|
378
|
+
if (nextPage == null) {
|
|
379
|
+
logger.error(`could not navigate from "${currentPage.getPathId()}" to "${pathId}"`)
|
|
380
|
+
throw new PageNotFoundError(origPath, pathId, section, currentPage)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let index = nextPage.getContextIndex()
|
|
384
|
+
if (index >= 0) {
|
|
385
|
+
logger.debug('page with context, detected navigation flow stop')
|
|
386
|
+
return nextPage.whenLoaded().then(function () {
|
|
387
|
+
return nextPage
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return currentPage.pushPage(nextPage, /* historyEntry = */ false).then(function () {
|
|
392
|
+
currIndex++
|
|
393
|
+
|
|
394
|
+
return nextPage.whenLoaded().then(function () {
|
|
395
|
+
return navigateNext(nextPage)
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Call section.navigate() to determine possible redirects */
|
|
402
|
+
if (Sections.getCurrent() !== section) {
|
|
403
|
+
let s = section.navigate(origPath)
|
|
404
|
+
if (s !== section) {
|
|
405
|
+
logger.debug(`section "${section.getId()}" redirected to section "${s.getId()}"`)
|
|
406
|
+
sectionRedirected = true
|
|
407
|
+
section = s
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return Sections.switchTo(section, /* source = */ 'navigate').catch(function (error) {
|
|
412
|
+
|
|
413
|
+
logger.errorStack(`could not navigate to section "${section.getId()}"`, error)
|
|
414
|
+
|
|
415
|
+
throw new SectionLoadError(origPath, section.getId(), section, error)
|
|
416
|
+
|
|
417
|
+
}).then(function () {
|
|
418
|
+
|
|
419
|
+
/* Section redirection can also occur during Sections.switchTo() */
|
|
420
|
+
if (Sections.getCurrent() !== section) {
|
|
421
|
+
sectionRedirected = true
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* If we've been redirected by Section.navigate() to another section, discard the rest of the path, since it
|
|
425
|
+
* doesn't make sense anymore. */
|
|
426
|
+
if (sectionRedirected) {
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* Count the number of path elements that are common between old and new paths.
|
|
431
|
+
* commonPathLen doesn't take into account section id.
|
|
432
|
+
* We also have to make sure we're on the same section. */
|
|
433
|
+
let commonPathLen = 0
|
|
434
|
+
if (oldPath[0] === section.getId()) {
|
|
435
|
+
while ((oldPath.length > commonPathLen + 1) &&
|
|
436
|
+
(path.length > commonPathLen) &&
|
|
437
|
+
(oldPath[commonPathLen + 1] === path[commonPathLen])) {
|
|
438
|
+
|
|
439
|
+
commonPathLen++
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let currentContext = getCurrentContext()
|
|
444
|
+
let promise
|
|
445
|
+
if (commonPathLen) { /* We have a common path part */
|
|
446
|
+
path = path.slice(commonPathLen)
|
|
447
|
+
currIndex += commonPathLen
|
|
448
|
+
|
|
449
|
+
/* Context size also contains the section id, while commonPathLen does not */
|
|
450
|
+
if (currentContext.getSize() > commonPathLen + 1) {
|
|
451
|
+
let page = currentContext.getPageAt(commonPathLen + 1)
|
|
452
|
+
promise = page.close().then(() => currentContext.getCurrentPage())
|
|
453
|
+
promise.catch(function () {
|
|
454
|
+
logger.debug('page close rejected')
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
else { /* Common path, but no page to close */
|
|
458
|
+
promise = Promise.resolve(currentContext.getCurrentPage())
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else { /* No common path */
|
|
462
|
+
promise = Promise.resolve(section.getMainPage())
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return promise.then(function (currentPage) {
|
|
466
|
+
|
|
467
|
+
return currentPage.whenLoaded().then(function () {
|
|
468
|
+
return currentPage
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
}).then(function (currentPage) {
|
|
474
|
+
|
|
475
|
+
if (sectionRedirected) {
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return navigateNext(currentPage)
|
|
480
|
+
|
|
481
|
+
}).then(function () {
|
|
482
|
+
|
|
483
|
+
if (sectionRedirected) {
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (pageState) {
|
|
488
|
+
getCurrentContext().getPages().forEach(function (page, i) {
|
|
489
|
+
page.restoreHistoryState(pageState[i])
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
}).catch(handleError)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
function setHistoryEntry(addUpdate, state) {
|
|
498
|
+
let pathStr = state.pathStr
|
|
499
|
+
|
|
500
|
+
let msg = `${addUpdate === 'add' ? 'adding' : 'updating'} history entry`
|
|
501
|
+
msg = `${msg}: path = "${pathStr}", pageState = "${JSON.stringify(state.pageState)}"`
|
|
502
|
+
|
|
503
|
+
logger.debug(msg)
|
|
504
|
+
|
|
505
|
+
if (addUpdate === 'add') {
|
|
506
|
+
currentURLQuery = null
|
|
507
|
+
state.historyIndex = ++historyIndex
|
|
508
|
+
window.history.pushState(state, '', basePath + pathStr)
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
currentURLPath = pathStr
|
|
512
|
+
state.historyIndex = historyIndex
|
|
513
|
+
window.history.replaceState(state, '', basePath + pathStr)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Capture a snapshot of the state of the current history entry.
|
|
519
|
+
* @alias qui.navigation.getCurrentHistoryEntryState
|
|
520
|
+
* @returns {Object}
|
|
521
|
+
*/
|
|
522
|
+
export function getCurrentHistoryEntryState() {
|
|
523
|
+
let path = getCurrentPath()
|
|
524
|
+
let pathStr = `/${path.join('/')}`
|
|
525
|
+
let pageState = []
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
pageState = getCurrentContext().getPages().map(page => page.getHistoryState())
|
|
529
|
+
}
|
|
530
|
+
catch (e) {
|
|
531
|
+
logger.errorStack('failed to gather history state', e)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* Preserve query, if present */
|
|
535
|
+
if (currentURLQuery) {
|
|
536
|
+
pathStr += `?${currentURLQuery}`
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (Config.navigationUsesFragment) {
|
|
540
|
+
pathStr = `#${pathStr}`
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
pageState: pageState,
|
|
545
|
+
pathStr: pathStr
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Add a history entry corresponding to the current path, or optionally to a given state.
|
|
551
|
+
* @alias qui.navigation.addHistoryEntry
|
|
552
|
+
* @see qui.navigation.getCurrentPath
|
|
553
|
+
* @param {Object} [state] the history entry state to add (will use {@link qui.navigation.getCurrentHistoryEntryState}
|
|
554
|
+
* by default)
|
|
555
|
+
*/
|
|
556
|
+
export function addHistoryEntry(state = null) {
|
|
557
|
+
state = state || getCurrentHistoryEntryState()
|
|
558
|
+
setHistoryEntry(/* addUpdate = */ 'add', state)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Update the current history entry from the current path, or optionally from a given state.
|
|
563
|
+
* @alias qui.navigation.updateHistoryEntry
|
|
564
|
+
* @see qui.navigation.getCurrentPath
|
|
565
|
+
* @param {Object} [state] the history entry state to add (will use {@link qui.navigation.getCurrentHistoryEntryState}
|
|
566
|
+
* by default)
|
|
567
|
+
*/
|
|
568
|
+
export function updateHistoryEntry(state = null) {
|
|
569
|
+
state = state || getCurrentHistoryEntryState()
|
|
570
|
+
setHistoryEntry(/* addUpdate = */ 'update', state)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function initHistory() {
|
|
574
|
+
Window.$window.on('hashchange', function () {
|
|
575
|
+
if (!Config.navigationUsesFragment) {
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
updateCurrentURL()
|
|
580
|
+
logger.debug(`hash-change: navigating to "${currentURLPath}"`)
|
|
581
|
+
navigate({path: currentURLPath, historyEntry: false})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
Window.$window.on('popstate', function (e) {
|
|
585
|
+
let oe = e.originalEvent
|
|
586
|
+
if (oe.state == null || oe.state.pageState == null || oe.state.historyIndex == null) { /* Not ours */
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let forward = oe.state.historyIndex > historyIndex
|
|
591
|
+
if (forward) {
|
|
592
|
+
logger.debug('pop-state: forward history move detected')
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
logger.debug('pop-state: back history move detected')
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
historyIndex = oe.state.historyIndex
|
|
599
|
+
|
|
600
|
+
if (backMode === BACK_MODE_CLOSE) {
|
|
601
|
+
let context = getCurrentContext()
|
|
602
|
+
let currentPage = context.getCurrentPage()
|
|
603
|
+
|
|
604
|
+
if (forward) {
|
|
605
|
+
logger.debug('pop-state: ignoring forward history move')
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (context.getSize() === 1) {
|
|
610
|
+
/* A context size of 1 indicates that only current section's main page is present; instead of closing
|
|
611
|
+
* the main page, we close the app, by going back through history for as long as we have control */
|
|
612
|
+
logger.debug('pop-state: closing app')
|
|
613
|
+
|
|
614
|
+
let goBack = function () {
|
|
615
|
+
window.history.back()
|
|
616
|
+
setTimeout(goBack, 100)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
goBack()
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
logger.debug('pop-state: closing current page')
|
|
623
|
+
currentPage.close()
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
updateCurrentURL()
|
|
628
|
+
logger.debug(`pop-state: going through history to "${currentURLPath}"`)
|
|
629
|
+
|
|
630
|
+
navigate({path: currentURLPath, pageState: oe.state.pageState, historyEntry: false}).catch(function () {
|
|
631
|
+
addHistoryEntry(oe.state)
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
export function init() {
|
|
639
|
+
/* Deduce base path from base URL */
|
|
640
|
+
if (Config.navigationBasePrefix) {
|
|
641
|
+
basePath = URL.parse(Config.navigationBasePrefix).path
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* On desktop, use the browser history when going back, by default */
|
|
645
|
+
if (!Window.isSmallScreen()) {
|
|
646
|
+
backMode = BACK_MODE_HISTORY
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
updateCurrentURL()
|
|
650
|
+
initialURLPath = currentURLPath
|
|
651
|
+
initialURLQuery = currentURLQuery
|
|
652
|
+
|
|
653
|
+
initHistory()
|
|
654
|
+
}
|