@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
package/js/theme.js ADDED
@@ -0,0 +1,304 @@
1
+ /**
2
+ * @namespace qui.theme
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 Signal from '$qui/base/signal.js'
10
+ import Config from '$qui/config.js'
11
+ import * as Colors from '$qui/utils/colors.js'
12
+ import * as CSS from '$qui/utils/css.js'
13
+ import {asap} from '$qui/utils/misc.js'
14
+ import * as ObjectUtils from '$qui/utils/object.js'
15
+ import * as PromiseUtils from '$qui/utils/promise.js'
16
+ import * as StringUtils from '$qui/utils/string.js'
17
+ import * as Window from '$qui/window.js'
18
+
19
+
20
+ const STORAGE_BACKGROUND_COLOR_KEY = 'theme.background-color'
21
+
22
+ const logger = Logger.get('qui.theme')
23
+
24
+ let currentTheme = null
25
+ let themeVars = null
26
+ let transitionDuration = null
27
+ let effectsDisabled = null
28
+
29
+
30
+ /**
31
+ * Emitted whenever the theme is changed. Handlers are called with the following parameters:
32
+ * * `theme: String`, the new theme
33
+ * @alias qui.theme.changeSignal
34
+ */
35
+ export const changeSignal = new Signal()
36
+
37
+
38
+ /**
39
+ * Tell the current theme.
40
+ * @alias qui.theme.getCurrent
41
+ * @returns {String}
42
+ */
43
+ export function getCurrent() {
44
+ return currentTheme
45
+ }
46
+
47
+ /**
48
+ * Change the theme.
49
+ * @alias qui.theme.setCurrent
50
+ * @param {String} theme
51
+ * @returns {Promise} a promise that resolves as soon as the theme has been set
52
+ */
53
+ export function setCurrent(theme) {
54
+ if (currentTheme === theme) {
55
+ return Promise.resolve()
56
+ }
57
+
58
+ currentTheme = theme
59
+
60
+ logger.debug(`setting theme to ${theme}`)
61
+
62
+ if (Window.$body == null) {
63
+ return Promise.resolve() /* Not initialized yet */
64
+ }
65
+
66
+ return updateCurrent()
67
+ }
68
+
69
+ function updateCurrent() {
70
+ /* Fade out body */
71
+ Window.$body.css('opacity', '')
72
+
73
+ function isLoaded() {
74
+ return CSS.findRules('^br.-theme-name$').some(function (rule) {
75
+ let parts = rule.declaration.split(':', 2)
76
+ if (parts.length < 2) {
77
+ return
78
+ }
79
+
80
+ let value = parts[1].split(';')[0].trim()
81
+ value = value.replace(/"/g, '') /* Remove quotation marks */
82
+
83
+ return value === currentTheme
84
+ })
85
+ }
86
+
87
+ function loadedOrLater() {
88
+ if (isLoaded()) {
89
+ return Promise.resolve()
90
+ }
91
+ else {
92
+ return PromiseUtils.later(10).then(() => loadedOrLater())
93
+ }
94
+ }
95
+
96
+ /* Allow 500ms for fading-out */
97
+ return PromiseUtils.later(500).then(function () {
98
+
99
+ /* Update disabled attribute of CSS link elements */
100
+ $('link[theme]').each(function () {
101
+ let $link = $(this)
102
+ let linkTheme = $link.attr('theme')
103
+ if (linkTheme === currentTheme) {
104
+ $link.removeAttr('disabled')
105
+ }
106
+ else {
107
+ $link.attr('disabled', '')
108
+ }
109
+ })
110
+
111
+ return loadedOrLater().then(function () {
112
+
113
+ logger.debug(`theme set to ${currentTheme}`)
114
+
115
+ /* Invalidate theme vars */
116
+ themeVars = null
117
+
118
+ /* Finally, emit change signal */
119
+ changeSignal.emit(currentTheme)
120
+
121
+ /* Consider the theme updated, but allow another 500ms for section soft reload */
122
+ setTimeout(function () {
123
+ Window.$body.css('opacity', '1')
124
+ }, 500)
125
+
126
+ /* Set current background color in local storage, to be used at next app reload */
127
+ window.localStorage.setItem(STORAGE_BACKGROUND_COLOR_KEY, getVar('background-color'))
128
+ })
129
+
130
+ })
131
+ }
132
+
133
+ /**
134
+ * Return the available themes.
135
+ * @alias qui.theme.getAvailable
136
+ * @returns {Object<String,String>} a dictionary with theme names as keys and display names as values
137
+ */
138
+ export function getAvailable() {
139
+ return ObjectUtils.fromEntries(Config.themes.split(',').map(function (theme) {
140
+ return [theme, gettext(StringUtils.title(theme))]
141
+ }))
142
+ }
143
+
144
+ /**
145
+ * Return the value of a theme variable.
146
+ * @alias qui.theme.getVar
147
+ * @param {String} name the variable name
148
+ * @param {String} [def] a default value if the variable is not found or not set
149
+ * @returns {String}
150
+ */
151
+ export function getVar(name, def) {
152
+ if (!themeVars) {
153
+ themeVars = {}
154
+ CSS.findRules('^br.-theme-').forEach(function (rule) {
155
+ let name = rule.selector.substring(10)
156
+ let parts = rule.declaration.split(':', 2)
157
+ if (parts.length < 2) {
158
+ return
159
+ }
160
+
161
+ themeVars[name] = parts[1].split(';')[0].trim()
162
+ })
163
+ }
164
+
165
+ return themeVars[name] || def
166
+ }
167
+
168
+ /**
169
+ * Resolve a color name and normalize it using {@link qui.utils.colors.normalize}.
170
+ *
171
+ * A color name can be an HTML color (e.g. `teal`) or a color theme variable name starting with an `@` (e.g.
172
+ * `@background-color`).
173
+ *
174
+ * If a color is given, it will be normalized and returned right away. If the given color name cannot be resolved, the
175
+ * `@foreground-color` is returned.
176
+ *
177
+ * @alias qui.theme.getColor
178
+ * @param {String} color a color or a color name
179
+ * @returns {String}
180
+ */
181
+ export function getColor(color) {
182
+ if (color.startsWith('@')) {
183
+ color = getVar(color.substring(1))
184
+ }
185
+
186
+ if (!color) {
187
+ color = getVar('foreground-color')
188
+ }
189
+
190
+ return Colors.normalize(color)
191
+ }
192
+
193
+ /**
194
+ * Return the default transition duration, in milliseconds.
195
+ * @alias qui.theme.getTransitionDuration
196
+ * @returns {Number}
197
+ */
198
+ export function getTransitionDuration() {
199
+ if (transitionDuration == null) {
200
+ transitionDuration = parseFloat(getVar('transition-duration')) * 1000
201
+ }
202
+
203
+ return transitionDuration
204
+ }
205
+
206
+ /**
207
+ * Call a function after a timeout equal to a transition duration,
208
+ * @alias qui.theme.afterTransition
209
+ * @param {Function} func function to run
210
+ * @param {?jQuery} [element] an optional HTML element whose visibility will be tested; if element is not currently
211
+ * visible, `func` will be called asap; if supplied, will be used as `this` argument for `func`
212
+ * @returns {Number} a timeout handle
213
+ */
214
+ export function afterTransition(func, element = null) {
215
+ let thisArg = element || window
216
+
217
+ if (element && !element.is(':visible')) {
218
+ return asap(function () {
219
+ func.call(thisArg)
220
+ })
221
+ }
222
+
223
+ return setTimeout(function () {
224
+ func.call(thisArg)
225
+ }, getTransitionDuration())
226
+ }
227
+
228
+ /**
229
+ * Create a promise that resolves after a timeout equal to a transition duration,
230
+ * @alias qui.theme.afterTransitionPromise
231
+ * @param {?jQuery} [element] an optional HTML element whose visibility will be tested; if element is not currently
232
+ * visible, promise is resolved asap
233
+ * @returns {Promise}
234
+ */
235
+ export function afterTransitionPromise(element = null) {
236
+ return new Promise(function (resolve, reject) {
237
+
238
+ if (element && !element.is(':visible')) {
239
+ resolve()
240
+ }
241
+ else {
242
+ afterTransition(function () {
243
+ resolve()
244
+ }, element)
245
+ }
246
+ })
247
+ }
248
+
249
+ /**
250
+ * Enable transitions, animations, blur filters and other effects. Use this function to re-enable effects disabled by
251
+ * {@link qui.theme.disableEffects}.
252
+ * @alias qui.theme.enableEffects
253
+ */
254
+ export function enableEffects() {
255
+ if (!effectsDisabled) {
256
+ return
257
+ }
258
+ effectsDisabled = false
259
+ logger.debug('enabling effects')
260
+
261
+ if (Window.$body != null) {
262
+ Window.$body.removeClass('effects-disabled')
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Disable transitions, animations, blur filters and other effects. Use {@link qui.theme.enableEffects} to re-enable
268
+ * effects.
269
+ * @alias qui.theme.disableEffects
270
+ */
271
+ export function disableEffects() {
272
+ if (effectsDisabled) {
273
+ return
274
+ }
275
+ effectsDisabled = true
276
+ logger.debug('disabling effects')
277
+
278
+ if (Window.$body != null) {
279
+ Window.$body.addClass('effects-disabled')
280
+ }
281
+ }
282
+
283
+
284
+ /**
285
+ * Initialize the theme subsystem.
286
+ * @alias qui.theme.init
287
+ * @returns {Promise} a promise that is resolved when theme subsystem has been initialized
288
+ */
289
+ export function init() {
290
+ if (effectsDisabled == null) {
291
+ effectsDisabled = Config.defaultEffectsDisabled
292
+ }
293
+ if (currentTheme == null) {
294
+ currentTheme = Config.defaultTheme
295
+ }
296
+
297
+ Window.$body.toggleClass('effects-disabled', effectsDisabled)
298
+
299
+ return updateCurrent().then(function () {
300
+ /* Normally updateCurrent() will take care of fading in body, adds extra 500ms for section soft reload.
301
+ * This being the first call, we want the body visible as soon as theme is loaded */
302
+ Window.$body.css('opacity', '1')
303
+ })
304
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @namespace qui.utils.ajax
3
+ */
4
+
5
+ import $ from '$qui/lib/jquery.module.js'
6
+
7
+ import * as ObjectUtils from '$qui/utils/object.js'
8
+ import * as Window from '$qui/window.js'
9
+
10
+ import {asap} from './misc.js'
11
+ import URL from './url.js'
12
+
13
+
14
+ const DEFAULT_REQUEST_TIMEOUT = 60 /* Seconds */
15
+
16
+ let pendingRequests = []
17
+
18
+
19
+ function prepareResponseHeaders(response) {
20
+ let headersStr = response.getAllResponseHeaders()
21
+ let headers = headersStr.split('\r\n').filter(h => h.length > 0)
22
+
23
+ return ObjectUtils.fromEntries(headers.map(function (h) {
24
+ let parts = h.split(':')
25
+ let name = parts[0].trim()
26
+ let value = parts.slice(1).join(':').trim()
27
+
28
+ return [name, value]
29
+ }))
30
+ }
31
+
32
+
33
+ /**
34
+ * Return a list of currently pending requests
35
+ * @alias qui.utils.ajax.getPendingRequests
36
+ * @returns {jqXHR[]}
37
+ */
38
+ export function getPendingRequests() {
39
+ return pendingRequests.slice()
40
+ }
41
+
42
+ /**
43
+ * Perform an AJAX JSON HTTP request and decode response as JSON.
44
+ * @alias qui.utils.ajax.requestJSON
45
+ * @param {String} method the HTTP method
46
+ * @param {String} path the path or a URL to request
47
+ * @param {Object} [query]
48
+ * @param {*} [data] data to transmit in request body
49
+ * @param {Function} [success] successful callback; will be called with decoded response and headers as parameters
50
+ * @param {Function} [failure] failure callback; will be called with decoded response, status code, a result message and
51
+ * headers as parameters
52
+ * @param {Object} [headers] optional request headers
53
+ * @param {Number} [timeout] optional request timeout, in seconds
54
+ * @returns {jqXHR}
55
+ */
56
+ export function requestJSON(
57
+ method, path, query = null, data = null, success = null, failure = null, headers = null,
58
+ timeout = DEFAULT_REQUEST_TIMEOUT
59
+ ) {
60
+ let contentType = null
61
+
62
+ if (data != null) {
63
+ data = JSON.stringify(data)
64
+ contentType = 'application/json'
65
+ }
66
+
67
+ let url = URL.parse(URL.qualify(path)).alter({query: query || {}}).toString()
68
+
69
+ let request = $.ajax({
70
+ url: url,
71
+ type: method,
72
+ data: data,
73
+ processData: false,
74
+ cache: false,
75
+ contentType: contentType,
76
+ timeout: timeout * 1000,
77
+ beforeSend: function (xhr) {
78
+ if (headers) {
79
+ ObjectUtils.forEach(headers, function (k, v) {
80
+ xhr.setRequestHeader(k, v)
81
+ })
82
+ }
83
+ },
84
+ success: function (data, status, response) {
85
+ let headers = prepareResponseHeaders(response)
86
+
87
+ /* Delay calling the success handler a bit so that complete handler below gets called */
88
+ asap(function () {
89
+ if (success) {
90
+ success(data, headers)
91
+ }
92
+ })
93
+ },
94
+ error: function (request, msg) {
95
+ let headers = prepareResponseHeaders(request)
96
+
97
+ /* Defer error handling a bit, it seems to fix call order when window closes */
98
+ asap(function () {
99
+ if (failure) {
100
+ failure(request.responseJSON || {}, request.status, msg, headers)
101
+ }
102
+ })
103
+ },
104
+ complete: function (req, status) {
105
+ /* Remove the request from pending list */
106
+ let index = pendingRequests.indexOf(request)
107
+ if (index >= 0) {
108
+ pendingRequests.splice(index, 1)
109
+ }
110
+ }
111
+ })
112
+
113
+ request.details = {
114
+ method: method,
115
+ path: path,
116
+ query: query,
117
+ data: data,
118
+ sent: new Date()
119
+ }
120
+
121
+ if (!Window.isClosing()) {
122
+ pendingRequests.push(request)
123
+ }
124
+
125
+ return request
126
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @namespace qui.utils.array
3
+ */
4
+
5
+ function makeSortFunc(extractFunc, desc, thisArg) {
6
+ return function (e1, e2) {
7
+ let k1 = extractFunc.call(thisArg, e1)
8
+ let k2 = extractFunc.call(thisArg, e2)
9
+
10
+ if ((desc && k1 > k2) || (!desc && k1 < k2)) {
11
+ return -1
12
+ }
13
+ if ((desc && k1 < k2) || (!desc && k1 > k2)) {
14
+ return 1
15
+ }
16
+ else {
17
+ return 0
18
+ }
19
+ }
20
+ }
21
+
22
+ function merge(left, right, compareFunc) {
23
+ let result = []
24
+
25
+ while (left.length > 0 || right.length > 0) {
26
+ if (left.length > 0 && right.length > 0) {
27
+ if (compareFunc(left[0], right[0]) <= 0) {
28
+ result.push(left[0])
29
+ left = left.slice(1)
30
+ }
31
+ else {
32
+ result.push(right[0])
33
+ right = right.slice(1)
34
+ }
35
+ }
36
+ else if (left.length > 0) {
37
+ result.push(left[0])
38
+ left = left.slice(1)
39
+ }
40
+ else if (right.length > 0) {
41
+ result.push(right[0])
42
+ right = right.slice(1)
43
+ }
44
+ }
45
+
46
+ return result
47
+ }
48
+
49
+ function sorted(array, compareFunc) {
50
+ let length = array.length
51
+ let middle = Math.floor(length / 2)
52
+
53
+ compareFunc = compareFunc || function (left, right) {
54
+ if (left < right) {
55
+ return -1
56
+ }
57
+ else if (left === right) {
58
+ return 0
59
+ }
60
+ else {
61
+ return 1
62
+ }
63
+ }
64
+
65
+ if (length < 2) {
66
+ return array
67
+ }
68
+
69
+ return merge(
70
+ sorted(array.slice(0, middle), compareFunc),
71
+ sorted(array.slice(middle, length), compareFunc),
72
+ compareFunc
73
+ )
74
+ }
75
+
76
+
77
+ /**
78
+ * Perform a stable sort on an array, *in place*, using a comparison function.
79
+ * @alias qui.utils.array.stableSort
80
+ * @param {Array} array
81
+ * @param {Function} compareFunc comparison function; takes two elements as parameters and returns `-1, `0` or `1`
82
+ * @returns {Array} the array
83
+ */
84
+ export function stableSort(array, compareFunc) {
85
+ /* Slower, but such is life */
86
+ let result = sorted(array, compareFunc).slice()
87
+
88
+ /* Replace all the elements in array with the sorted ones */
89
+ array.length = 0
90
+ Array.prototype.splice.apply(array, [0, 0].concat(result))
91
+
92
+ return array
93
+ }
94
+
95
+ /**
96
+ * Perform a sort on an array, *in place*, using a key extraction function.
97
+ * @alias qui.utils.array.sortKey
98
+ * @param {Array} array
99
+ * @param {Function} func key extraction function; will be called with each element as parameter and is expected to
100
+ * return the comparison key
101
+ * @param {Boolean} [desc] whether to do a descending sort (defaults to `false`)
102
+ * @param {*} [thisArg] optional argument to use as `this` when calling the key extraction function.
103
+ * @returns {Array} the array
104
+ */
105
+ export function sortKey(array, func, desc = false, thisArg = null) {
106
+ return array.sort(makeSortFunc(func, desc, thisArg))
107
+ }
108
+
109
+ /**
110
+ * Perform a stable sort on an array, *in place*, using a key extraction function.
111
+ * @alias qui.utils.array.stableSortKey
112
+ * @param {Array} array
113
+ * @param {Function} func key extraction function; will be called with each element as parameter and is expected to
114
+ * return the comparison key
115
+ * @param {Boolean} [desc] whether to do a descending sort (defaults to `false`)
116
+ * @param {*} [thisArg] optional argument to use as `this` when calling the key extraction function.
117
+ * @returns {Array} the array
118
+ */
119
+ export function stableSortKey(array, func, desc = false, thisArg = null) {
120
+ return stableSort(array, makeSortFunc(func, desc, thisArg))
121
+ }
122
+
123
+ /**
124
+ * Generate an array based on a range of numbers.
125
+ * @alias qui.utils.array.range
126
+ * @param {Number} start inclusive range start
127
+ * @param {Number} stop exclusive range stop
128
+ * @param {Number} [step] range step (defaults to `1`)
129
+ * @returns {Number[]}
130
+ */
131
+ export function range(start, stop, step = 1) {
132
+ let array = []
133
+ for (let i = start; i < stop; i += step) {
134
+ array.push(i)
135
+ }
136
+
137
+ return array
138
+ }
139
+
140
+ /**
141
+ * Return an array of distinct elements found in an input array.
142
+ * @alias qui.utils.array.distinct
143
+ * @param {Array} array
144
+ * @param {Function} [equalsFunc] a comparison function (defaults to the `===` operator)
145
+ * @param {*} [thisArg] optional argument to be used as `this` when calling `equalsFunc`
146
+ * @returns {Array} the distinct elements array
147
+ */
148
+ export function distinct(array, equalsFunc = null, thisArg = null) {
149
+ let uniqueElements = []
150
+
151
+ if (!equalsFunc) {
152
+ equalsFunc = (a, b) => a === b
153
+ }
154
+
155
+ array.forEach(function (element) {
156
+ let found = uniqueElements.find(function (elem) {
157
+ return equalsFunc.call(thisArg, elem, element)
158
+ })
159
+
160
+ if (found === undefined) {
161
+ uniqueElements.push(element)
162
+ }
163
+ })
164
+
165
+ return uniqueElements
166
+ }
167
+
168
+ /**
169
+ * Remove all occurrences of an element from an array, in place.
170
+ * @alias qui.utils.array.remove
171
+ * @param {Array} array
172
+ * @param {*} element the element to remove
173
+ */
174
+ export function remove(array, element) {
175
+ let index
176
+ while ((index = array.indexOf(element)) >= 0) {
177
+ array.splice(index, 1)
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Remove all occurrences of a set of elements from an array, in place.
183
+ * @alias qui.utils.array.removeMany
184
+ * @param {Array} array
185
+ * @param {Array} elements the set of elements to remove
186
+ */
187
+ export function removeMany(array, elements) {
188
+ let index
189
+ elements.forEach(function (element) {
190
+ while ((index = array.indexOf(element)) >= 0) {
191
+ array.splice(index, 1)
192
+ }
193
+ })
194
+ }