@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,1018 @@
1
+
2
+ import $ from '$qui/lib/jquery.module.js'
3
+
4
+ import {AssertionError} from '$qui/base/errors.js'
5
+ import {Mixin} from '$qui/base/mixwith.js'
6
+ import * as GlobalGlass from '$qui/global-glass.js'
7
+ import * as OptionsBar from '$qui/main-ui/options-bar.js'
8
+ import * as Navigation from '$qui/navigation.js'
9
+ import * as Theme from '$qui/theme.js'
10
+ import {asap} from '$qui/utils/misc.js'
11
+ import ViewMixin from '$qui/views/view.js'
12
+ import * as Sections from '$qui/sections/sections.js'
13
+ import * as Window from '$qui/window.js'
14
+
15
+ import {getPagesContainer} from './pages.js'
16
+ import {updateUI} from './pages.js'
17
+
18
+
19
+ const viewMixinPrototype = ViewMixin().prototype
20
+
21
+
22
+ /** @lends qui.pages.PageMixin */
23
+ const PageMixin = Mixin((superclass = Object, rootclass) => {
24
+
25
+ let rootPrototype = rootclass.prototype
26
+
27
+
28
+ /**
29
+ * A mixin that offers page behavior.
30
+ * @alias qui.pages.PageMixin
31
+ * @mixin
32
+ * @extends qui.views.ViewMixin
33
+ */
34
+ class PageMixin extends ViewMixin(superclass) {
35
+
36
+ /**
37
+ * @constructs
38
+ * @param {?String} [title] page title
39
+ * @param {String} [pathId] identifies the page in the URL; leave this unset if the page should not be part
40
+ * of the URL or is the main page of a section
41
+ * @param {Boolean} [columnLayout] indicates that the page layout is a column and does not expand horizontally
42
+ * (defaults to `false`)
43
+ * @param {Boolean} [keepPrevVisible] indicates that the previous page should be kept visible while this page
44
+ * is the current one, to the extent possible (defaults to `false`)
45
+ * @param {Boolean} [popup] indicates that the page should be shown as a popup container, on top of the existing
46
+ * content (defaults to `false`)
47
+ * @param {Boolean} [modal] indicates that the page should be modal, not allowing any external interaction
48
+ * (defaults to `false`, implies `popup` as `true`)
49
+ * @param {Boolean} [transparent] indicates that the page should be transparent (defaults to `true`)
50
+ * @param {...*} args parent class parameters
51
+ */
52
+ constructor({
53
+ title = null,
54
+ pathId = null,
55
+ columnLayout = false,
56
+ keepPrevVisible = false,
57
+ popup = false,
58
+ modal = false,
59
+ transparent = true,
60
+ ...args
61
+ } = {}) {
62
+ super(args)
63
+
64
+ this._title = title
65
+ this._pathId = pathId
66
+ this._columnLayout = columnLayout
67
+ this._keepPrevVisible = keepPrevVisible
68
+ this._transparent = transparent
69
+ this._popup = popup || modal
70
+ this._modal = modal
71
+
72
+ this._pageHTML = null
73
+ this._closingPromise = null
74
+ this._closed = false
75
+ this._attached = false
76
+ this._context = null
77
+ this._optionsBarContent = null
78
+ this._optionsBarOpen = false /* Flag indicating the last known options bar status for this page */
79
+ this._whenLoaded = null
80
+ }
81
+
82
+ /**
83
+ * Override this method to customize navigation beyond this page. By default, it returns `null`, preventing
84
+ * further navigation.
85
+ *
86
+ * It is safe to assume that this page is visible and loaded when this method is called.
87
+ *
88
+ * @param {String} pathId the next path id
89
+ * @returns {?qui.pages.PageMixin|Promise<qui.pages.PageMixin>} the next page or `null` if navigation to given
90
+ * path id is not possible; a promise that resolves to a page can also be returned
91
+ */
92
+ navigate(pathId) {
93
+ return null
94
+ }
95
+
96
+ /**
97
+ * Return the page title.
98
+ * @returns {?String}
99
+ */
100
+ getTitle() {
101
+ if (rootPrototype.getTitle) {
102
+ return rootPrototype.getTitle.call(this)
103
+ }
104
+
105
+ return this._title
106
+ }
107
+
108
+ /**
109
+ * Set the page title.
110
+ * @param {?String} title the new title
111
+ */
112
+ setTitle(title) {
113
+ if (rootPrototype.getTitle) {
114
+ rootPrototype.setTitle.call(this, title)
115
+ }
116
+
117
+ this._title = title
118
+ if (this.getContext() && this.getContext().isCurrent()) {
119
+ updateUI()
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Return the path id of the page.
125
+ * @returns {?String}
126
+ */
127
+ getPathId() {
128
+ return this._pathId
129
+ }
130
+
131
+ /**
132
+ * Update the path id of the page.
133
+ * @param {?String} pathId
134
+ */
135
+ setPathId(pathId) {
136
+ this._pathId = pathId
137
+ if (this.getContext() && this.getContext().isCurrent()) {
138
+ Navigation.updateHistoryEntry()
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Override this method to specify page state to be saved when page is saved into browser history.
144
+ *
145
+ * This state will later be restored by calling {@link qui.pages.PageMixin#restoreHistoryState}.
146
+ *
147
+ * @returns {*} the state
148
+ */
149
+ getHistoryState() {
150
+ return {}
151
+ }
152
+
153
+ /**
154
+ * Override this method to implement restoring page state from history.
155
+ *
156
+ * This method will be given as argument the state that has been previously created by
157
+ * {@link qui.pages.PageMixin#getHistoryState}.
158
+ *
159
+ * This method must be prepared to receive a `null` history state.
160
+ *
161
+ * @param {*} state
162
+ */
163
+ restoreHistoryState(state) {
164
+ }
165
+
166
+ /**
167
+ * Call this whenever the content of the history state changes.
168
+ */
169
+ updateHistoryState() {
170
+ if (this.getContext() && this.getContext().isCurrent()) {
171
+ Navigation.updateHistoryEntry()
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Ad a new entry to the browser history. This is just a convenience wrapper around
177
+ * {@link qui.navigation.addHistoryEntry}.
178
+ */
179
+ addHistoryEntry() {
180
+ Navigation.addHistoryEntry()
181
+ }
182
+
183
+ /**
184
+ * Override this to implement how the page is loaded.
185
+ *
186
+ * Does nothing by default, returning a resolved promise.
187
+ *
188
+ * @returns {Promise}
189
+ */
190
+ load() {
191
+ return Promise.resolve()
192
+ }
193
+
194
+ /**
195
+ * Return a promise that settles as soon as the page is loaded.
196
+ *
197
+ * This method calls {@link qui.pages.PageMixin#load} once per page instance.
198
+ *
199
+ * @returns {Promise}
200
+ */
201
+ whenLoaded() {
202
+ if (!this._whenLoaded) {
203
+ this.setProgress()
204
+ this._whenLoaded = this.load()
205
+ this._whenLoaded.then(function () {
206
+ this.clearProgress()
207
+ }.bind(this)).catch(function (error) {
208
+ this.setError(error)
209
+ }.bind(this))
210
+ }
211
+
212
+ return this._whenLoaded
213
+ }
214
+
215
+ /**
216
+ * Create the page HTML wrapper. This method is called only once per page instance.
217
+ * @returns {jQuery}
218
+ */
219
+ makePageHTML() {
220
+ let html = $('<div></div>', {class: 'qui-page'})
221
+
222
+ if (this._columnLayout) {
223
+ html.addClass('column-layout')
224
+ }
225
+
226
+ if (this._transparent) {
227
+ html.addClass('transparent')
228
+ }
229
+
230
+ if (this._popup) {
231
+ html.addClass('popup')
232
+ }
233
+
234
+ if (this._modal) {
235
+ html.addClass('modal')
236
+ }
237
+
238
+ /* Add a reference from HTML element to the page object */
239
+ html.data('page', this)
240
+
241
+ html.append(this.getHTML())
242
+
243
+ return html
244
+ }
245
+
246
+ /**
247
+ * Override this to further initialize the Page HTML wrapper.
248
+ * @param {jQuery} html the HTML wrapper to be initialized
249
+ */
250
+ initPageHTML(html) {
251
+ }
252
+
253
+ /**
254
+ * Return the page HTML wrapper. Calls {@link qui.pages.PageMixin#makePageHTML} at first invocation.
255
+ * @returns {jQuery}
256
+ */
257
+ getPageHTML() {
258
+ if (this._pageHTML == null) {
259
+ this._pageHTML = this.makePageHTML()
260
+ this.initPageHTML(this._pageHTML)
261
+ }
262
+
263
+ return this._pageHTML
264
+ }
265
+
266
+ /**
267
+ * Attach the page to the page container.
268
+ */
269
+ attach() {
270
+ if (this._attached) {
271
+ throw new AssertionError('Attempt to attach an already attached page')
272
+ }
273
+ // TODO following condition breaks sticky modal pages
274
+ // if (!this._context || !this._context.isCurrent()) {
275
+ // throw new AssertionError('Attempt to attach a page belonging to a non-current context')
276
+ // }
277
+
278
+ let html = this.getPageHTML()
279
+ if (this.isPopup()) {
280
+ GlobalGlass.addContent(html)
281
+ GlobalGlass.setModal(this.isModal())
282
+ }
283
+ else {
284
+ getPagesContainer().append(html)
285
+ }
286
+
287
+ asap(function () {
288
+ this.getPageHTML().addClass('attached')
289
+ }.bind(this))
290
+
291
+ this._attached = true
292
+ }
293
+
294
+ /**
295
+ * Detach the page from the page container.
296
+ */
297
+ detach() {
298
+ if (!this._attached) {
299
+ throw new AssertionError('Attempt to detach an already detached page')
300
+ }
301
+
302
+ this._attached = false
303
+
304
+ this.getPageHTML().removeClass('attached')
305
+ if (this.isPopup()) {
306
+ GlobalGlass.setModal(true) /* default */
307
+ }
308
+
309
+ Theme.afterTransition(function () {
310
+
311
+ if (this._attached) {
312
+ /* Detaching cancelled by subsequent attach */
313
+ return
314
+ }
315
+
316
+ this.getPageHTML().detach()
317
+
318
+ }.bind(this), this.getPageHTML())
319
+ }
320
+
321
+ /**
322
+ * Tell if the page layout is a column and does not expand horizontally.
323
+ * @returns {Boolean}
324
+ */
325
+ isColumnLayout() {
326
+ return this._columnLayout
327
+ }
328
+
329
+ /**
330
+ * Set the page column layout.
331
+ * @param {Boolean} columnLayout
332
+ */
333
+ setColumnLayout(columnLayout) {
334
+ if (columnLayout !== this._columnLayout) {
335
+ this._columnLayout = columnLayout
336
+ this.getPageHTML().toggleClass('column-layout', columnLayout)
337
+
338
+ updateUI()
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Return the current vertical scroll parameters.
344
+ * @returns {{offset: Number, maxOffset: Number}} `offset` represents the current scroll offset and `maxOffset`
345
+ * is the maximum scroll offset (`0` if no scrolling is possible)
346
+ */
347
+ getVertScrollParams() {
348
+ let pageHTML = this.getPageHTML()
349
+
350
+ return {
351
+ offset: pageHTML[0].scrollTop,
352
+ maxOffset: pageHTML[0].scrollHeight - pageHTML[0].clientHeight
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Called when the page is scrolled vertically.
358
+ * @param {Number} offset the vertical scroll offset
359
+ * @param {Number} maxOffset the maximum vertical scroll offset
360
+ */
361
+ onVertScroll(offset, maxOffset) {
362
+ }
363
+
364
+ /**
365
+ * Handle vertical scroll events. Internally calls {@link qui.pages.PageMixin#onVertScroll}.
366
+ */
367
+ handleVertScroll() {
368
+ let params = this.getVertScrollParams()
369
+
370
+ this.onVertScroll(params.offset, params.maxOffset)
371
+ }
372
+
373
+ /**
374
+ * Called when the page is resized.
375
+ */
376
+ onResize() {
377
+ }
378
+
379
+ /**
380
+ * Handle the resize events. Internally calls {@link qui.pages.PageMixin#onResize}.
381
+ */
382
+ handleResize() {
383
+ this.onResize()
384
+ }
385
+
386
+ /**
387
+ * Return the index of this page in its context. If page has no context, `-1` is returned.
388
+ * @returns {Number}
389
+ */
390
+ getContextIndex() {
391
+ if (!this._context) {
392
+ return -1
393
+ }
394
+
395
+ return this._context.getPages().indexOf(this)
396
+ }
397
+
398
+ /**
399
+ * Return the associated pages context.
400
+ * @returns {?qui.pages.PagesContext}
401
+ */
402
+ getContext() {
403
+ return this._context
404
+ }
405
+
406
+ /**
407
+ * Tell if this page is the current page within its context.
408
+ * @returns {Boolean}
409
+ */
410
+ isCurrent() {
411
+ if (!this._context) {
412
+ return false
413
+ }
414
+
415
+ return this._context.getCurrentPage() === this
416
+ }
417
+
418
+ /**
419
+ * Tell if the page is visible.
420
+ * @returns {Boolean}
421
+ */
422
+ isVisible() {
423
+ if (!this._context) {
424
+ return false
425
+ }
426
+
427
+ if (!this._context.isCurrent()) {
428
+ return false
429
+ }
430
+
431
+ if (!this._context.getPages().includes(this)) {
432
+ return false /* Not part of current context */
433
+ }
434
+
435
+ if (Window.isSmallScreen() && this.getContextIndex() < this._context.getSize() - 1) {
436
+ return false /* On small screens, only the last page is actually visible */
437
+ }
438
+
439
+ return this._context.getVisiblePages().includes(this)
440
+ }
441
+
442
+ /**
443
+ * Tells if the page has a context, effectively indicating whether the page is currently added to a context, or
444
+ * not.
445
+ * @returns {Boolean}
446
+ */
447
+ hasContext() {
448
+ return !!this._context
449
+ }
450
+
451
+ /**
452
+ * Tell if the page is kept visible while the next page is current.
453
+ * @returns {Boolean}
454
+ */
455
+ isPrevKeptVisible() {
456
+ return this._keepPrevVisible
457
+ }
458
+
459
+ /**
460
+ * Set the *keep-prev-visible* flag, controlling if the page is kept visible while the next page is current.
461
+ * @param {Boolean} keepPrevVisible
462
+ */
463
+ setKeepPrevVisible(keepPrevVisible) {
464
+ this._keepPrevVisible = keepPrevVisible
465
+
466
+ if (this._context && this._context.isCurrent()) {
467
+ updateUI()
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Tell if the page is popup.
473
+ * @returns {Boolean}
474
+ */
475
+ isPopup() {
476
+ return this._popup
477
+ }
478
+
479
+ /**
480
+ * Set the popup flag.
481
+ * @param {Boolean} popup
482
+ */
483
+ setPopup(popup) {
484
+ if (this._modal) {
485
+ popup = true /* Modals are always popups */
486
+ }
487
+
488
+ let needsReattach = false
489
+ if (this._popup !== popup && this._attached) {
490
+ needsReattach = true
491
+ this.detach()
492
+ }
493
+
494
+ this._popup = popup
495
+
496
+ if (needsReattach) {
497
+ this.attach()
498
+ }
499
+
500
+ this.getPageHTML().toggleClass('popup', popup)
501
+
502
+ if (this.getContext() && this.getContext().isCurrent()) {
503
+ updateUI()
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Tell if the page is modal.
509
+ * @returns {Boolean}
510
+ */
511
+ isModal() {
512
+ return this._modal
513
+ }
514
+
515
+ /**
516
+ * Set the modal flag.
517
+ * @param {Boolean} modal
518
+ */
519
+ setModal(modal) {
520
+ let needsReattach = false
521
+ if (this._modal !== modal && this._attached) {
522
+ needsReattach = true
523
+ this.detach()
524
+ }
525
+
526
+ this._modal = modal
527
+ if (modal) {
528
+ this._popup = true /* Modals are always popups */
529
+ }
530
+
531
+ if (needsReattach) {
532
+ this.attach()
533
+ }
534
+
535
+ this.getPageHTML().toggleClass('modal', this._modal)
536
+ this.getPageHTML().toggleClass('popup', this._popup) /* Popup might have also changed */
537
+
538
+ if (this.getContext() && this.getContext().isCurrent()) {
539
+ updateUI()
540
+ }
541
+ }
542
+
543
+ /**
544
+ * Called when the page is closed.
545
+ */
546
+ onClose() {
547
+ }
548
+
549
+ /**
550
+ * Called when the next page is closed.
551
+ * @param {qui.pages.PageMixin} next the next page that has just been closed
552
+ */
553
+ onCloseNext(next) {
554
+ }
555
+
556
+ /**
557
+ * Close the page. Calls {@link qui.pages.PageMixin#canClose} to determine if the page can be closed.
558
+ * @param {Boolean} [force] set to `true` to force page close without calling
559
+ * {@link qui.pages.PageMixin#canClose}
560
+ * @returns {Promise} a promise that is resolved as soon as the page is closed and is rejected if the page close
561
+ * was rejected.
562
+ */
563
+ close(force = false) {
564
+ if (this._closed) {
565
+ throw new AssertionError('Attempt to close an already closed page')
566
+ }
567
+
568
+ /* If page is already in the process of being closed, return the closing promise */
569
+ if (this._closingPromise) {
570
+ return this._closingPromise
571
+ }
572
+
573
+ /* Close all following pages */
574
+ let promise = Promise.resolve()
575
+ let index = this.getContextIndex()
576
+ let context = this.getContext()
577
+ if (index >= 0 && context && context.getSize() > index + 1) {
578
+ promise = context.getPageAt(index + 1).close(force)
579
+ }
580
+
581
+ this._closingPromise = promise.then(() => force || this.canClose()).then(function () {
582
+ if (rootPrototype.close) {
583
+ rootPrototype.close.call(this)
584
+ }
585
+
586
+ /* Mark as closed */
587
+ this._closed = true
588
+ this._closingPromise = null
589
+
590
+ /* Pop this page from context */
591
+ if (context) {
592
+ let currentPage = context.getCurrentPage()
593
+ if (currentPage !== this) {
594
+ throw new AssertionError('New page added to context while current page closing')
595
+ }
596
+
597
+ if (context.isCurrent()) {
598
+ this.handleLeaveCurrent()
599
+ }
600
+
601
+ context.pop()
602
+ }
603
+
604
+ this.onClose()
605
+ this._context = null
606
+
607
+ /* Detach from DOM */
608
+ if (this._attached) {
609
+ this.detach()
610
+ }
611
+
612
+ updateUI()
613
+
614
+ if (context) {
615
+ let currentPage = context.getCurrentPage()
616
+ if (currentPage) {
617
+ currentPage.onCloseNext(this)
618
+
619
+ if (context.isCurrent()) {
620
+ currentPage.handleBecomeCurrent()
621
+ Navigation.updateHistoryEntry()
622
+ }
623
+ }
624
+ else {
625
+ if (context.isCurrent()) {
626
+ OptionsBar.setContent(null)
627
+ }
628
+ }
629
+ }
630
+ }.bind(this)).catch(function (e) {
631
+
632
+ /* Clear closing promise if close cancelled */
633
+ this._closingPromise = null
634
+ throw e
635
+
636
+ }.bind(this))
637
+
638
+ return this._closingPromise
639
+
640
+ }
641
+
642
+ /**
643
+ * Tell if the page has been closed.
644
+ * @returns {Boolean}
645
+ */
646
+ isClosed() {
647
+ /* Prefer root isClosed unless it's the one inherited from ViewMixin */
648
+ if (rootPrototype.isClosed &&
649
+ (rootPrototype.isClosed !== viewMixinPrototype.isClosed) &&
650
+ (rootPrototype.isClosed.toString() !== viewMixinPrototype.isClosed.toString())) {
651
+
652
+ return rootPrototype.isClosed.call(this)
653
+ }
654
+
655
+ return this._closed
656
+ }
657
+
658
+ /**
659
+ * Override this method to prevent accidental closing of the page, to the possible extent. Pages can be closed
660
+ * by default.
661
+ * @returns {Promise} a promise that, if rejected, will prevent the page close
662
+ */
663
+ canClose() {
664
+ return Promise.resolve()
665
+ }
666
+
667
+ /**
668
+ * Called when the page becomes the current page on the current context.
669
+ */
670
+ onBecomeCurrent() {
671
+ }
672
+
673
+ /**
674
+ * Handle the event of becoming the current page of the current context.
675
+ */
676
+ handleBecomeCurrent() {
677
+ this.onBecomeCurrent()
678
+ let context = this.getContext()
679
+ if (!context || !context.isCurrent()) {
680
+ throw new AssertionError('Attempt to call handleBecomeCurrent() on non-current context')
681
+ }
682
+
683
+ OptionsBar.setContent(this._prepareOptionsBarContent())
684
+ if (this._optionsBarOpen) {
685
+ OptionsBar.open()
686
+ }
687
+ else {
688
+ OptionsBar.close()
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Called when the page is no longer the current page on the current context.
694
+ */
695
+ onLeaveCurrent() {
696
+ }
697
+
698
+ /**
699
+ * Handle the event of no longer being the current page of the current context.
700
+ */
701
+ handleLeaveCurrent() {
702
+ this.onLeaveCurrent()
703
+ }
704
+
705
+ /**
706
+ * Called when the section to which page belongs is shown.
707
+ */
708
+ onSectionShow() {
709
+ }
710
+
711
+ /**
712
+ * Handle the event of owning section becoming visible.
713
+ */
714
+ handleSectionShow() {
715
+ this.onSectionShow()
716
+ }
717
+
718
+ /**
719
+ * Called when the section to which page belongs is hidden.
720
+ */
721
+ onSectionHide() {
722
+ }
723
+
724
+ /**
725
+ * Handle the event of owning section becoming hidden.
726
+ */
727
+ handleSectionHide() {
728
+ this.onSectionHide()
729
+ }
730
+
731
+ /**
732
+ * Returns the section to which the page currently belongs (may be `null`).
733
+ * @returns {?qui.sections.Section}
734
+ */
735
+ getSection() {
736
+ return Sections.all().find(s => s.getPagesContext() === this.getContext()) || null
737
+ }
738
+
739
+ /**
740
+ * Override this method to enable the options bar for this page.
741
+ * @returns {?jQuery|qui.views.ViewMixin}
742
+ */
743
+ makeOptionsBarContent() {
744
+ return null
745
+ }
746
+
747
+ /**
748
+ * Return the options bar content of this page.
749
+ * @returns {?jQuery|qui.views.ViewMixin}
750
+ */
751
+ getOptionsBarContent() {
752
+ if (!this._optionsBarContent) {
753
+ this._optionsBarContent = this.makeOptionsBarContent()
754
+ }
755
+
756
+ return this._optionsBarContent
757
+ }
758
+
759
+ /**
760
+ * Called when the page options change; the page options are defined by the options bar content.
761
+ *
762
+ * This currently works only when using an {@link qui.forms.OptionsForm} for the options bar content.
763
+ *
764
+ * @param {Object} options
765
+ */
766
+ onOptionsChange(options) {
767
+ }
768
+
769
+ _prepareOptionsBarContent() {
770
+ let content = this.getOptionsBarContent()
771
+ if (content) {
772
+ if (content instanceof ViewMixin) {
773
+ content = content.getHTML()
774
+ }
775
+ }
776
+
777
+ return content
778
+ }
779
+
780
+ /**
781
+ * If this is the current page, open the options bar right away. Otherwise, the options bar will be
782
+ * automatically opened as soon as this page becomes current.
783
+ */
784
+ openOptionsBar() {
785
+ if (this.isCurrent()) {
786
+ OptionsBar.open()
787
+ }
788
+ else {
789
+ this._optionsBarOpen = true
790
+ }
791
+ }
792
+
793
+ /**
794
+ * If this is the current page, close the options bar right away. Otherwise, the options bar will remain closed
795
+ * as soon as this page becomes current.
796
+ */
797
+ closeOptionsBar() {
798
+ if (this.isCurrent()) {
799
+ OptionsBar.close()
800
+ }
801
+ else {
802
+ this._optionsBarOpen = false
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Called when the page is pushed to a context.
808
+ */
809
+ onPush() {
810
+ }
811
+
812
+ /**
813
+ * Push a new page after this one. Any following pages will be closed. The new page is not guaranteed to be
814
+ * pushed by the time the function exists.
815
+ * @param {qui.pages.PageMixin} page the page to be pushed
816
+ * @param {Boolean} [historyEntry] whether to create a new history entry for current page before adding the new
817
+ * page, or not (determined automatically by default, using new page's `pathId`)
818
+ * @returns {Promise} a promise that resolves as soon as the page is pushed, or rejected if the page cannot be
819
+ * pushed
820
+ */
821
+ pushPage(page, historyEntry = null) {
822
+ let index = this.getContextIndex()
823
+ if (index < 0) {
824
+ throw new AssertionError('Attempt to push from a contextless page')
825
+ }
826
+
827
+ /* By default, pages with pathId will add a new history entry */
828
+ if (historyEntry == null) {
829
+ historyEntry = !!page.getPathId()
830
+ }
831
+
832
+ let context = this.getContext()
833
+ if (!context.isCurrent()) {
834
+ historyEntry = false
835
+ }
836
+
837
+ /* Preserve current state for after potential next page close */
838
+ let state = null
839
+ if (historyEntry) {
840
+ state = Navigation.getCurrentHistoryEntryState()
841
+ }
842
+
843
+ /* Close any following page */
844
+ let promise
845
+ let nextPage = context.getPageAt(index + 1)
846
+ if (nextPage) {
847
+ promise = nextPage.close()
848
+ }
849
+ else {
850
+ promise = Promise.resolve()
851
+ }
852
+
853
+ return promise.then(function () {
854
+ if (historyEntry) {
855
+ Navigation.updateHistoryEntry(state)
856
+ }
857
+
858
+ if (context.isCurrent()) {
859
+ this.handleLeaveCurrent()
860
+ }
861
+
862
+ page.pushSelf(context)
863
+ page.whenLoaded() /* Start loading the page automatically when pushed */
864
+
865
+ if (historyEntry) {
866
+ Navigation.addHistoryEntry()
867
+ }
868
+ else if (context.isCurrent()) {
869
+ Navigation.updateHistoryEntry()
870
+ }
871
+ }.bind(this))
872
+ }
873
+
874
+ /**
875
+ * Push this page to a context.
876
+ * @param {qui.pages.PagesContext} context
877
+ */
878
+ pushSelf(context) {
879
+ if (this._context) {
880
+ throw new AssertionError('Attempt to push page with context')
881
+ }
882
+
883
+ this._closed = false
884
+ this._closingPromise = null
885
+
886
+ /* Attach the page to context */
887
+ context.push(this)
888
+ this._context = context
889
+
890
+ if (context.isCurrent()) {
891
+ this.attach()
892
+ updateUI()
893
+ }
894
+
895
+ this.onPush()
896
+
897
+ if (context.isCurrent()) {
898
+ this.handleBecomeCurrent()
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Return the previous page in context.
904
+ * @returns {?qui.pages.PageMixin}
905
+ */
906
+ getPrev() {
907
+ let index = this.getContextIndex()
908
+ if (index < 0) {
909
+ return null
910
+ }
911
+
912
+ return this._context.getPageAt(index - 1)
913
+ }
914
+
915
+ /**
916
+ * Return the next page in context.
917
+ * @returns {?qui.pages.PageMixin}
918
+ */
919
+ getNext() {
920
+ let index = this.getContextIndex()
921
+ if (index < 0) {
922
+ return null
923
+ }
924
+
925
+ return this._context.getPageAt(index + 1)
926
+ }
927
+
928
+
929
+ /* Following methods are overridden so that versions from the rootclass are also taken into consideration */
930
+
931
+ makeHTML() {
932
+ if (rootPrototype.makeHTML) {
933
+ return rootPrototype.makeHTML.call(this)
934
+ }
935
+
936
+ return super.makeHTML()
937
+ }
938
+
939
+ initHTML(html) {
940
+ if (rootPrototype.initHTML) {
941
+ rootPrototype.initHTML.call(this, html)
942
+ }
943
+ else {
944
+ super.initHTML(html)
945
+ }
946
+ }
947
+
948
+ init() {
949
+ if (rootPrototype.init) {
950
+ rootPrototype.init.call(this)
951
+ }
952
+ else {
953
+ super.init()
954
+ }
955
+ }
956
+
957
+ showProgress(percent) {
958
+ if (rootPrototype.showProgress) {
959
+ rootPrototype.showProgress.call(this, percent)
960
+ }
961
+ else {
962
+ super.showProgress(percent)
963
+ }
964
+ }
965
+
966
+ hideProgress() {
967
+ if (rootPrototype.hideProgress) {
968
+ rootPrototype.hideProgress.call(this)
969
+ }
970
+ else {
971
+ super.hideProgress()
972
+ }
973
+ }
974
+
975
+ showWarning(message) {
976
+ if (rootPrototype.showWarning) {
977
+ rootPrototype.showWarning.call(this, message)
978
+ }
979
+ else {
980
+ super.showWarning(message)
981
+ }
982
+ }
983
+
984
+ hideWarning() {
985
+ if (rootPrototype.hideWarning) {
986
+ rootPrototype.hideWarning.call(this)
987
+ }
988
+ else {
989
+ super.hideWarning()
990
+ }
991
+ }
992
+
993
+ showError(message) {
994
+ if (rootPrototype.showError) {
995
+ rootPrototype.showError.call(this, message)
996
+ }
997
+ else {
998
+ super.showError(message)
999
+ }
1000
+ }
1001
+
1002
+ hideError() {
1003
+ if (rootPrototype.hideError) {
1004
+ rootPrototype.hideError.call(this)
1005
+ }
1006
+ else {
1007
+ super.hideError()
1008
+ }
1009
+ }
1010
+
1011
+ }
1012
+
1013
+ return PageMixin
1014
+
1015
+ })
1016
+
1017
+
1018
+ export default PageMixin