@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.
Files changed (202) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +492 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
  6. package/.github/workflows/main.yml +74 -0
  7. package/.pre-commit-config.yaml +8 -0
  8. package/LICENSE.txt +177 -0
  9. package/README.md +4 -0
  10. package/font/dejavusans-bold.woff +0 -0
  11. package/font/dejavusans-bolditalic.woff +0 -0
  12. package/font/dejavusans-italic.woff +0 -0
  13. package/font/dejavusans-regular.woff +0 -0
  14. package/img/qui-icons.svg +1937 -0
  15. package/js/base/base.js +47 -0
  16. package/js/base/condition-variable.js +92 -0
  17. package/js/base/errors.js +36 -0
  18. package/js/base/i18n.js +20 -0
  19. package/js/base/mixwith.js +135 -0
  20. package/js/base/require-js-compat.js +78 -0
  21. package/js/base/signal.js +91 -0
  22. package/js/base/singleton.js +66 -0
  23. package/js/base/timer.js +126 -0
  24. package/js/config.js +184 -0
  25. package/js/forms/common-fields/check-field.js +42 -0
  26. package/js/forms/common-fields/choice-buttons-field.js +30 -0
  27. package/js/forms/common-fields/color-combo-field.js +37 -0
  28. package/js/forms/common-fields/combo-field.js +108 -0
  29. package/js/forms/common-fields/common-fields.js +23 -0
  30. package/js/forms/common-fields/composite-field.js +132 -0
  31. package/js/forms/common-fields/custom-html-field.js +51 -0
  32. package/js/forms/common-fields/email-field.js +30 -0
  33. package/js/forms/common-fields/file-picker-field.js +46 -0
  34. package/js/forms/common-fields/jquery-ui-field.js +111 -0
  35. package/js/forms/common-fields/labels-field.js +69 -0
  36. package/js/forms/common-fields/numeric-field.js +39 -0
  37. package/js/forms/common-fields/password-field.js +28 -0
  38. package/js/forms/common-fields/phone-field.js +26 -0
  39. package/js/forms/common-fields/progress-disk-field.js +69 -0
  40. package/js/forms/common-fields/push-button-field.js +138 -0
  41. package/js/forms/common-fields/slider-field.js +51 -0
  42. package/js/forms/common-fields/text-area-field.js +34 -0
  43. package/js/forms/common-fields/text-field.js +89 -0
  44. package/js/forms/common-fields/up-down-field.js +85 -0
  45. package/js/forms/common-forms/common-forms.js +16 -0
  46. package/js/forms/common-forms/options-form.js +77 -0
  47. package/js/forms/common-forms/page-form.js +115 -0
  48. package/js/forms/form-button.js +202 -0
  49. package/js/forms/form-field.js +1183 -0
  50. package/js/forms/form.js +1181 -0
  51. package/js/forms/forms.js +68 -0
  52. package/js/global-glass.js +100 -0
  53. package/js/icons/default-stock.js +173 -0
  54. package/js/icons/icon.js +64 -0
  55. package/js/icons/icons.js +16 -0
  56. package/js/icons/multi-state-sprites-icon.js +362 -0
  57. package/js/icons/stock-icon.js +219 -0
  58. package/js/icons/stock.js +98 -0
  59. package/js/icons/stocks.js +57 -0
  60. package/js/index.js +232 -0
  61. package/js/lib/jquery.longpress.js +79 -0
  62. package/js/lib/jquery.module.js +4 -0
  63. package/js/lib/logger.module.js +4 -0
  64. package/js/lib/pep.module.js +4 -0
  65. package/js/lists/common-items/common-items.js +5 -0
  66. package/js/lists/common-items/icon-label-list-item.js +86 -0
  67. package/js/lists/common-lists/common-lists.js +5 -0
  68. package/js/lists/common-lists/page-list.js +53 -0
  69. package/js/lists/list-item.js +147 -0
  70. package/js/lists/list.js +636 -0
  71. package/js/lists/lists.js +26 -0
  72. package/js/main-ui/main-ui.js +64 -0
  73. package/js/main-ui/menu-bar.js +144 -0
  74. package/js/main-ui/options-bar.js +181 -0
  75. package/js/main-ui/status.js +185 -0
  76. package/js/main-ui/top-bar.js +59 -0
  77. package/js/messages/common-message-forms/common-message-forms.js +7 -0
  78. package/js/messages/common-message-forms/confirm-message-form.js +81 -0
  79. package/js/messages/common-message-forms/simple-message-form.js +67 -0
  80. package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
  81. package/js/messages/message-form.js +107 -0
  82. package/js/messages/messages.js +21 -0
  83. package/js/messages/sticky-modal-page.js +98 -0
  84. package/js/messages/sticky-modal-progress-message.js +27 -0
  85. package/js/messages/toast.js +164 -0
  86. package/js/navigation.js +654 -0
  87. package/js/pages/breadcrumbs.js +124 -0
  88. package/js/pages/common-pages/common-pages.js +6 -0
  89. package/js/pages/common-pages/modal-progress-page.js +83 -0
  90. package/js/pages/common-pages/structured-page.js +46 -0
  91. package/js/pages/page.js +1018 -0
  92. package/js/pages/pages-context.js +154 -0
  93. package/js/pages/pages.js +252 -0
  94. package/js/pwa.js +337 -0
  95. package/js/sections/section.js +612 -0
  96. package/js/sections/sections.js +300 -0
  97. package/js/tables/common-cells/common-cells.js +7 -0
  98. package/js/tables/common-cells/icon-label-table-cell.js +68 -0
  99. package/js/tables/common-cells/push-button-table-cell.js +133 -0
  100. package/js/tables/common-cells/simple-table-cell.js +37 -0
  101. package/js/tables/common-tables/common-tables.js +5 -0
  102. package/js/tables/common-tables/page-table.js +55 -0
  103. package/js/tables/table-cell.js +198 -0
  104. package/js/tables/table-row.js +126 -0
  105. package/js/tables/table.js +492 -0
  106. package/js/tables/tables.js +36 -0
  107. package/js/theme.js +304 -0
  108. package/js/utils/ajax.js +126 -0
  109. package/js/utils/array.js +194 -0
  110. package/js/utils/colors.js +445 -0
  111. package/js/utils/cookies.js +65 -0
  112. package/js/utils/crypto.js +439 -0
  113. package/js/utils/css.js +234 -0
  114. package/js/utils/date.js +300 -0
  115. package/js/utils/files.js +27 -0
  116. package/js/utils/gestures.js +165 -0
  117. package/js/utils/html.js +76 -0
  118. package/js/utils/misc.js +81 -0
  119. package/js/utils/object.js +324 -0
  120. package/js/utils/promise.js +49 -0
  121. package/js/utils/string.js +192 -0
  122. package/js/utils/url.js +187 -0
  123. package/js/utils/utils.js +3 -0
  124. package/js/utils/visibility-manager.js +211 -0
  125. package/js/views/common-views/common-views.js +7 -0
  126. package/js/views/common-views/icon-label-view.js +210 -0
  127. package/js/views/common-views/progress-view.js +89 -0
  128. package/js/views/common-views/structured-view.js +368 -0
  129. package/js/views/view.js +467 -0
  130. package/js/views/views.js +3 -0
  131. package/js/widgets/base-widget.js +23 -0
  132. package/js/widgets/common-widgets/check-button.js +109 -0
  133. package/js/widgets/common-widgets/choice-buttons.js +322 -0
  134. package/js/widgets/common-widgets/color-combo.js +104 -0
  135. package/js/widgets/common-widgets/combo.js +645 -0
  136. package/js/widgets/common-widgets/common-widgets.js +17 -0
  137. package/js/widgets/common-widgets/email-input.js +7 -0
  138. package/js/widgets/common-widgets/file-picker.js +133 -0
  139. package/js/widgets/common-widgets/labels.js +132 -0
  140. package/js/widgets/common-widgets/numeric-input.js +49 -0
  141. package/js/widgets/common-widgets/password-input.js +91 -0
  142. package/js/widgets/common-widgets/phone-input.js +7 -0
  143. package/js/widgets/common-widgets/progress-disk.js +174 -0
  144. package/js/widgets/common-widgets/push-button.js +155 -0
  145. package/js/widgets/common-widgets/slider.js +455 -0
  146. package/js/widgets/common-widgets/text-area.js +52 -0
  147. package/js/widgets/common-widgets/text-input.js +174 -0
  148. package/js/widgets/common-widgets/up-down.js +351 -0
  149. package/js/widgets/widgets.js +57 -0
  150. package/js/window.js +557 -0
  151. package/jsdoc.conf.json +20 -0
  152. package/less/base.less +123 -0
  153. package/less/forms/common-fields.less +101 -0
  154. package/less/forms/common-forms.less +5 -0
  155. package/less/forms/form-button.less +21 -0
  156. package/less/forms/form-field.less +266 -0
  157. package/less/forms/form.less +131 -0
  158. package/less/global-glass.less +64 -0
  159. package/less/icon-label-view.less +82 -0
  160. package/less/icons.less +144 -0
  161. package/less/lists.less +105 -0
  162. package/less/main-ui.less +328 -0
  163. package/less/messages.less +189 -0
  164. package/less/no-effects.less +24 -0
  165. package/less/pages/breadcrumbs.less +98 -0
  166. package/less/pages/common-pages.less +36 -0
  167. package/less/pages/page.less +70 -0
  168. package/less/progress-view.less +51 -0
  169. package/less/stock-icons.less +43 -0
  170. package/less/structured-view.less +245 -0
  171. package/less/tables.less +84 -0
  172. package/less/theme-dark.less +133 -0
  173. package/less/theme-light.less +132 -0
  174. package/less/theme.less +419 -0
  175. package/less/visibility-manager.less +11 -0
  176. package/less/widgets/check-button.less +96 -0
  177. package/less/widgets/choice-buttons.less +160 -0
  178. package/less/widgets/color-combo.less +33 -0
  179. package/less/widgets/combo.less +230 -0
  180. package/less/widgets/common-buttons.less +120 -0
  181. package/less/widgets/common.less +24 -0
  182. package/less/widgets/input.less +258 -0
  183. package/less/widgets/labels.less +81 -0
  184. package/less/widgets/progress-disk.less +70 -0
  185. package/less/widgets/slider.less +199 -0
  186. package/less/widgets/updown.less +115 -0
  187. package/less/widgets/various.less +36 -0
  188. package/package.json +47 -0
  189. package/pyproject.toml +45 -0
  190. package/qui/__init__.py +110 -0
  191. package/qui/constants.py +1 -0
  192. package/qui/exceptions.py +2 -0
  193. package/qui/j2template.py +71 -0
  194. package/qui/settings.py +60 -0
  195. package/qui/templates/manifest.json +25 -0
  196. package/qui/templates/qui.html +126 -0
  197. package/qui/templates/service-worker.js +188 -0
  198. package/qui/web/__init__.py +0 -0
  199. package/qui/web/tornado.py +220 -0
  200. package/scripts/postinstall.sh +10 -0
  201. package/webpack/webpack-adjust-css-urls-loader.js +36 -0
  202. package/webpack/webpack-common.js +384 -0
@@ -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
+ }