@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,1183 @@
1
+
2
+ import $ from '$qui/lib/jquery.module.js'
3
+
4
+ import {AssertionError} from '$qui/base/errors.js'
5
+ import {gettext} from '$qui/base/i18n.js'
6
+ import {mix} from '$qui/base/mixwith.js'
7
+ import Icon from '$qui/icons/icon.js'
8
+ import StockIcon from '$qui/icons/stock-icon.js'
9
+ import * as Theme from '$qui/theme.js'
10
+ import {asap} from '$qui/utils/misc.js'
11
+ import VisibilityManager from '$qui/utils/visibility-manager.js'
12
+ import ViewMixin from '$qui/views/view.js'
13
+ import {STATE_NORMAL} from '$qui/views/view.js'
14
+ import * as Window from '$qui/window.js'
15
+
16
+ import {STATE_APPLIED} from './forms.js'
17
+ import {ValidationError} from './forms.js'
18
+
19
+
20
+ /**
21
+ * A form field.
22
+ * @alias qui.forms.FormField
23
+ * @mixes qui.views.ViewMixin
24
+ */
25
+ class FormField extends mix().with(ViewMixin) {
26
+
27
+ /**
28
+ * @constructs
29
+ * @param {String} name field name
30
+ * @param {String} [initialValue] an optional initial value for the field
31
+ * @param {String} [label] field label
32
+ * @param {String} [description] an optional field description
33
+ * @param {String} [unit] an optional unit of measurement
34
+ * @param {Boolean} [separator] if set to `true`, a separator will be drawn above the field (defaults to `false`)
35
+ * @param {Boolean} [required] if set to `true`, the field value must be present for a successful validation
36
+ * (defaults to `false`)
37
+ * @param {Boolean} [readonly] if set to `true`, the field widget will not allow user input
38
+ * @param {Boolean} [disabled] if set to `true`, the field widget will be completely disabled (defaults to `false`)
39
+ * @param {Boolean} [hidden] if set to `true`, the field will be initially hidden (defaults to `false`)
40
+ * @param {Boolean} [forceOneLine] if set to `true`, label and value will be shown on one single line, ignoring
41
+ * form's `compact` attribute
42
+ * @param {?Number} [valueWidth] sets the width of the value column, in percents, relative to the form body; if
43
+ * set to `null` (the default), the form's `valuesWidth` setting will be used; if set to `0`, no width constraints
44
+ * will be imposed on value or label; this attribute only works for fields displayed on a single line
45
+ * @param {Function} [onChange] called whenever the field value changes (see {@link qui.forms.FormField#onChange})
46
+ * @param {Function} [validate] a validator function (see {@link qui.forms.FormField#validate})
47
+ * @param {...*} args parent class parameters
48
+ */
49
+ constructor({
50
+ name,
51
+ initialValue = null,
52
+ label = '',
53
+ description = '',
54
+ unit = '',
55
+ separator = false,
56
+ required = false,
57
+ readonly = false,
58
+ disabled = false,
59
+ hidden = false,
60
+ forceOneLine = false,
61
+ valueWidth = null,
62
+ onChange = null,
63
+ validate = null,
64
+ ...args
65
+ }) {
66
+ super(args)
67
+
68
+ this._name = name
69
+ this._initialValue = initialValue
70
+ this._label = label
71
+ this._description = description
72
+ this._unit = unit
73
+ this._separator = separator
74
+ this._required = required
75
+ this._readonly = readonly
76
+ this._disabled = disabled
77
+ this._initiallyHidden = hidden
78
+ this._visibilityManager = null
79
+ this._forceOneLine = forceOneLine
80
+ this._valueWidth = valueWidth
81
+
82
+ if (onChange) {
83
+ this.onChange = onChange
84
+ }
85
+ if (validate) {
86
+ this.validate = validate
87
+ }
88
+
89
+ this._form = null
90
+ this._widget = null
91
+ this._descriptionDiv = null
92
+ this._warningDiv = null
93
+ this._errorDiv = null
94
+ this._labelDiv = null
95
+ this._valueDiv = null
96
+ this._sideIcon = null
97
+ this._sideIconDiv = null
98
+ this._focused = false
99
+ this._origValue = null
100
+ this._changed = false
101
+ }
102
+
103
+ makeHTML() {
104
+ if (!this._form) {
105
+ throw new AssertionError('makeHTML() called before assigning to a form')
106
+ }
107
+
108
+ let html = $('<div></div>', {'class': 'qui-form-field', 'data-name': this._name})
109
+
110
+ if (this._forceOneLine) {
111
+ html.addClass('force-one-line')
112
+ }
113
+
114
+ let valueWidth = this._valueWidth
115
+ if (valueWidth == null) {
116
+ if (this._label) {
117
+ valueWidth = this._form._valuesWidth
118
+ }
119
+ else {
120
+ valueWidth = 0
121
+ }
122
+ }
123
+
124
+ let useOneLine = !this._form.isCompact() || this._forceOneLine
125
+
126
+ /* Description */
127
+ this._descriptionDiv = this.makeDescriptionHTML()
128
+ html.append(this._descriptionDiv)
129
+ if (this._description) {
130
+ html.addClass('has-description')
131
+ }
132
+
133
+ /* Warning */
134
+ this._warningDiv = this.makeWarningHTML()
135
+ html.append(this._warningDiv)
136
+
137
+ /* Error */
138
+ this._errorDiv = this.makeErrorHTML()
139
+ html.append(this._errorDiv)
140
+
141
+ /* Label */
142
+ this._labelDiv = this.makeLabelHTML()
143
+ html.append(this._labelDiv)
144
+ if (useOneLine) {
145
+ if (valueWidth) {
146
+ this._labelDiv.css('width', `${(100 - valueWidth)}%`)
147
+ }
148
+ else {
149
+ html.addClass('auto-width')
150
+ }
151
+ }
152
+
153
+ if (this._label) {
154
+ this._labelDiv.children('span.qui-form-field-label-caption').html(this._label)
155
+ html.addClass('has-label')
156
+ }
157
+
158
+ /* Unit */
159
+ if (this._unit) {
160
+ this._labelDiv.find('span.qui-form-field-unit').text(`(${this._unit})`)
161
+ html.addClass('has-unit')
162
+ }
163
+
164
+ /* Value */
165
+ this._valueDiv = this.makeValueHTML()
166
+ html.append(this._valueDiv)
167
+ if (valueWidth && useOneLine) {
168
+ this._valueDiv.css('width', `${valueWidth}%`)
169
+ }
170
+
171
+ /* Side icon */
172
+ this._sideIconDiv = this.makeSideIconHTML()
173
+ html.append(this._sideIconDiv)
174
+
175
+ /* Other attributes */
176
+ if (this._required) {
177
+ html.addClass('required')
178
+ }
179
+
180
+ if (this._readonly) {
181
+ html.addClass('readonly')
182
+ this.setWidgetReadonly(true)
183
+ }
184
+
185
+ if (this._disabled) {
186
+ this.disableWidget()
187
+ }
188
+
189
+ if (this._separator) {
190
+ html.addClass('separator')
191
+ }
192
+
193
+ /* Initial value */
194
+ if (this._initialValue != null) {
195
+ this.setValue(this._initialValue)
196
+ }
197
+
198
+ this._visibilityManager = new VisibilityManager({element: html})
199
+
200
+ if (this._initiallyHidden) {
201
+ this._visibilityManager.hideElement()
202
+ }
203
+
204
+ return html
205
+ }
206
+
207
+ /**
208
+ * Create the description HTML element.
209
+ * @returns {jQuery}
210
+ */
211
+ makeDescriptionHTML() {
212
+ let descDiv = $('<div></div>', {class: 'qui-form-field-description'})
213
+ let descLabel = $('<div></div>', {class: 'qui-form-field-description-label'})
214
+ let descIcon = $('<span></span>', {class: 'qui-form-field-description-icon'})
215
+ let descText = $('<span></span>', {class: 'qui-form-field-description-text'})
216
+
217
+ descLabel.append(descIcon).append(descText)
218
+ descDiv.append(descLabel)
219
+
220
+ if (this._description) {
221
+ descText.html(this._description)
222
+ }
223
+
224
+ return descDiv
225
+ }
226
+
227
+ /**
228
+ * Create the warning HTML element.
229
+ * @returns {jQuery}
230
+ */
231
+ makeWarningHTML() {
232
+ let warningDiv = $('<div></div>', {class: 'qui-form-field-warning'})
233
+ let warningLabel = $('<div></div>', {class: 'qui-form-field-warning-label'})
234
+ let warningText = $('<span></span>', {class: 'qui-form-field-warning-text'})
235
+
236
+ warningLabel.append(warningText)
237
+ warningDiv.append(warningLabel)
238
+
239
+ return warningDiv
240
+ }
241
+
242
+ /**
243
+ * Create the error HTML element.
244
+ * @returns {jQuery}
245
+ */
246
+ makeErrorHTML() {
247
+ let errorDiv = $('<div></div>', {class: 'qui-form-field-error'})
248
+ let errorLabel = $('<div></div>', {class: 'qui-form-field-error-label'})
249
+ let errorText = $('<span></span>', {class: 'qui-form-field-error-text'})
250
+
251
+ errorLabel.append(errorText)
252
+ errorDiv.append(errorLabel)
253
+
254
+ return errorDiv
255
+ }
256
+
257
+ /**
258
+ * Create the label HTML element.
259
+ * @returns {jQuery}
260
+ */
261
+ makeLabelHTML() {
262
+ let labelDiv = $('<div></div>', {class: 'qui-form-field-label'})
263
+
264
+ let captionSpan = $('<span></span>', {class: 'qui-form-field-label-caption'})
265
+
266
+ let descriptionIcon = new StockIcon({
267
+ name: 'qmark',
268
+ variant: 'disabled',
269
+ scale: 0.5
270
+ })
271
+
272
+ let unitSpan = $('<span></span>', {class: 'qui-form-field-unit'})
273
+
274
+ let descriptionIconDiv = $('<div></div>', {class: 'qui-base-button qui-form-field-description-icon'})
275
+ descriptionIcon.applyTo(descriptionIconDiv)
276
+
277
+ descriptionIconDiv.on('click', function () {
278
+ if (this._description) {
279
+ this.setDescriptionVisible(!this.isDescriptionVisible())
280
+ }
281
+ }.bind(this))
282
+
283
+ labelDiv.append(captionSpan)
284
+ labelDiv.append(unitSpan)
285
+ labelDiv.append(descriptionIconDiv)
286
+
287
+ return labelDiv
288
+ }
289
+
290
+ /**
291
+ * Create the value HTML element.
292
+ * @returns {jQuery}
293
+ */
294
+ makeValueHTML() {
295
+ let valueDiv = $('<div></div>', {class: 'qui-form-field-value'})
296
+
297
+ /* Value widget */
298
+ let widget = this.getWidget()
299
+
300
+ widget.on('change', () => this.handleChange(this.getValue()))
301
+ widget.on('focus', () => this.handleFocus())
302
+ widget.on('blur', () => this.handleBlur())
303
+
304
+ /* Add widget to value div, but only if it hasn't already been added to another container; this allows fields
305
+ * like CompositeField to use the widget directly inside their container. */
306
+ if (!widget.parent().length) {
307
+ valueDiv.append(widget)
308
+ }
309
+
310
+ return valueDiv
311
+ }
312
+
313
+ /**
314
+ * Create the side icon HTML element.
315
+ * @returns {jQuery}
316
+ */
317
+ makeSideIconHTML() {
318
+ let sideIconDiv = $('<div></div>', {class: 'qui-base-button qui-form-field-side-icon'})
319
+
320
+ this._sideIcon = new StockIcon({name: 'success', scale: 0.75})
321
+ this._sideIcon.applyTo(sideIconDiv)
322
+
323
+ return sideIconDiv
324
+ }
325
+
326
+
327
+ /* Value */
328
+
329
+ /**
330
+ * Return the current field value.
331
+ * @returns {*}
332
+ */
333
+ getValue() {
334
+ return this.widgetToValue()
335
+ }
336
+
337
+ /**
338
+ * Update the current field value.
339
+ * @param {*} value
340
+ */
341
+ setValue(value) {
342
+ this._origValue = value
343
+ this._changed = false
344
+
345
+ this.valueToWidget(value)
346
+ }
347
+
348
+ /**
349
+ * Handle change events.
350
+ * @param {*} value new value
351
+ */
352
+ handleChange(value) {
353
+ let form = this.getForm()
354
+
355
+ this.clearApplied()
356
+ this._changed = true
357
+ this.onChange(value, form)
358
+ form._handleFieldChange(this)
359
+ }
360
+
361
+ /**
362
+ * Tell if field has been changed since the last time it was applied.
363
+ * @returns {boolean}
364
+ */
365
+ isChanged() {
366
+ return this._changed
367
+ }
368
+
369
+ /**
370
+ * Clear the changed flag.
371
+ */
372
+ clearChanged() {
373
+ this._origValue = this.getValue()
374
+ this._changed = false
375
+ }
376
+
377
+ /**
378
+ * Return the original field value (before user changes). The original value is always the last value that has been
379
+ * applied.
380
+ * @returns {*}
381
+ */
382
+ getOrigValue() {
383
+ return this._origValue
384
+ }
385
+
386
+
387
+ /* Name */
388
+
389
+ /**
390
+ * Return the field name.
391
+ * @returns {String}
392
+ */
393
+ getName() {
394
+ return this._name
395
+ }
396
+
397
+
398
+ /* Label */
399
+
400
+ /**
401
+ * Return the field label.
402
+ * @returns {String}
403
+ */
404
+ getLabel() {
405
+ return this._label
406
+ }
407
+
408
+ /**
409
+ * Update the field label.
410
+ * @param {String} label
411
+ */
412
+ setLabel(label) {
413
+ this._label = label
414
+
415
+ if (!this.hasHTML()) {
416
+ return
417
+ }
418
+
419
+ this._labelDiv.children('div.qui-form-field-label-caption').html(label || '')
420
+ this.getHTML().toggleClass('has-label', Boolean(label))
421
+ }
422
+
423
+
424
+ /* Description */
425
+
426
+ /**
427
+ * Return the field description.
428
+ * @returns {String}
429
+ */
430
+ getDescription() {
431
+ return this._description
432
+ }
433
+
434
+ /**
435
+ * Update the field description.
436
+ * @param {String} description
437
+ */
438
+ setDescription(description) {
439
+ this._description = description
440
+
441
+ if (!this.hasHTML()) {
442
+ return
443
+ }
444
+
445
+ let descText = this.getHTML().find('div.qui-form-field-description div.qui-form-field-description-text')
446
+
447
+ if (description) {
448
+ descText.html(description)
449
+ this.getHTML().addClass('has-description')
450
+ }
451
+ else {
452
+ descText.html('')
453
+ this.getHTML().removeClass('has-description description-visible')
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Tell if the field description is visible or not.
459
+ * @returns {Boolean}
460
+ */
461
+ isDescriptionVisible() {
462
+ return this.getHTML().hasClass('description-visible')
463
+ }
464
+
465
+ setDescriptionVisible(visible) {
466
+ if (visible) {
467
+ this.getHTML().addClass('description-visible')
468
+
469
+ let field = this
470
+ let descDiv = this.getHTML().children('div.qui-form-field-description')
471
+
472
+ /* Close description when clicking anywhere */
473
+ Window.$body.on('click', function f(e) {
474
+ if (field.getHTML() === e.target ||
475
+ field.getHTML().has(e.target).length ||
476
+ descDiv.has(e.target).length) {
477
+
478
+ /* Clicked on the description itself */
479
+ return
480
+ }
481
+
482
+ Window.$body.off('click', f)
483
+ field.setDescriptionVisible(false)
484
+ })
485
+ }
486
+ else {
487
+ this.getHTML().removeClass('description-visible')
488
+ }
489
+ }
490
+
491
+
492
+ /* Unit */
493
+
494
+ /**
495
+ * Return the field unit.
496
+ * @returns {String}
497
+ */
498
+ getUnit() {
499
+ return this._unit
500
+ }
501
+
502
+ /**
503
+ * Update the field unit.
504
+ * @param {String} unit
505
+ */
506
+ setUnit(unit) {
507
+ this._unit = unit
508
+
509
+ if (!this.hasHTML()) {
510
+ return
511
+ }
512
+
513
+ this._labelDiv.find('span.qui-form-field-unit').text(`(${this._unit || ''})`)
514
+ this.getHTML().toggleClass('has-unit', Boolean(unit))
515
+ }
516
+
517
+
518
+ /* Separator */
519
+
520
+ /**
521
+ * Tell if the field has separator above.
522
+ * @returns {Boolean}
523
+ */
524
+ hasSeparator() {
525
+ return this._separator
526
+ }
527
+
528
+ /**
529
+ * Set the field separator.
530
+ * @param {Boolean} separator
531
+ */
532
+ setSeparator(separator) {
533
+ this._separator = separator
534
+
535
+ if (!this.hasHTML()) {
536
+ return
537
+ }
538
+
539
+ this.getHTML().toggleClass('separator', separator)
540
+ }
541
+
542
+
543
+ /* Required */
544
+
545
+ /**
546
+ * Tell if the field is required.
547
+ * @returns {Boolean}
548
+ */
549
+ isRequired() {
550
+ return this._required
551
+ }
552
+
553
+ /**
554
+ * Update the required state.
555
+ * @param {Boolean} required
556
+ */
557
+ setRequired(required) {
558
+ this._required = required
559
+
560
+ if (!this.hasHTML()) {
561
+ return
562
+ }
563
+
564
+ this.getHTML().toggleClass('required', required)
565
+ }
566
+
567
+
568
+ /* Read-only state */
569
+
570
+ /**
571
+ * Tell if the field is read-only.
572
+ * @returns {Boolean}
573
+ */
574
+ isReadonly() {
575
+ return this._readonly
576
+ }
577
+
578
+ /**
579
+ * Update the read-only state.
580
+ * @param {Boolean} readonly
581
+ */
582
+ setReadonly(readonly) {
583
+ this._readonly = readonly
584
+ this.setWidgetReadonly(readonly)
585
+
586
+ if (!this.hasHTML()) {
587
+ return
588
+ }
589
+
590
+ this.getHTML().toggleClass('readonly', readonly)
591
+ }
592
+
593
+
594
+ /* Disabled state */
595
+
596
+ /**
597
+ * Tell if the field is disabled.
598
+ * @returns {Boolean}
599
+ */
600
+ isDisabled() {
601
+ return this._disabled
602
+ }
603
+
604
+ /**
605
+ * Enable the field.
606
+ */
607
+ enable() {
608
+ if (!this._disabled) {
609
+ return
610
+ }
611
+
612
+ this._disabled = false
613
+
614
+ this.enableWidget()
615
+ }
616
+
617
+ /**
618
+ * Disable the field.
619
+ */
620
+ disable() {
621
+ if (this._disabled) {
622
+ return
623
+ }
624
+
625
+ this._disabled = true
626
+
627
+ this.disableWidget()
628
+ }
629
+
630
+
631
+ /* Visibility */
632
+
633
+ /**
634
+ * Tell if the field is hidden.
635
+ * @returns {Boolean}
636
+ */
637
+ isHidden() {
638
+ return !this._visibilityManager.isElementVisible()
639
+ }
640
+
641
+ /**
642
+ * Show the field.
643
+ */
644
+ show() {
645
+ if (this._visibilityManager.isElementVisible()) {
646
+ return
647
+ }
648
+
649
+ this._visibilityManager.showElement()
650
+
651
+ if (this._form._continuousValidation) {
652
+ /* We need to revalidate the form upon field show, since hidden fields are considered always valid */
653
+ this._form._clearValidationCache(this._name)
654
+ this._form.updateValidationStateASAP()
655
+ }
656
+
657
+ this._form.updateFieldsState()
658
+ }
659
+
660
+ /**
661
+ * Hide the field.
662
+ */
663
+ hide() {
664
+ if (!this._visibilityManager.isElementVisible()) {
665
+ return
666
+ }
667
+
668
+ this._visibilityManager.hideElement()
669
+
670
+ if (this._form._continuousValidation) {
671
+ /* We need to revalidate the form upon field hide, since hidden fields are considered always valid */
672
+ this._form._clearValidationCache(this._name)
673
+ this._form.updateValidationStateASAP()
674
+ }
675
+
676
+ this._form.updateFieldsState()
677
+ }
678
+
679
+
680
+ /* Focus */
681
+
682
+ /**
683
+ * Called whenever the field is focused.
684
+ */
685
+ onFocus() {
686
+ }
687
+
688
+ /**
689
+ * Called whenever the field loses focus.
690
+ */
691
+ onBlur(value) {
692
+ }
693
+
694
+ /**
695
+ * Tell if the field is focused or not.
696
+ * @returns {Boolean}
697
+ */
698
+ isFocused() {
699
+ return this._focused
700
+ }
701
+
702
+ /**
703
+ * Focus the field.
704
+ */
705
+ focus() {
706
+ this.getWidget().focus()
707
+ }
708
+
709
+ /**
710
+ * Handle focus events.
711
+ */
712
+ handleFocus() {
713
+ this._focused = true
714
+ this.onFocus()
715
+ }
716
+
717
+ /**
718
+ * Handle blur events.
719
+ */
720
+ handleBlur() {
721
+ this._focused = false
722
+ this.onBlur()
723
+ }
724
+
725
+
726
+ /* Progress state */
727
+
728
+ showProgress(percent) {
729
+ this.setSideIcon('progress')
730
+ }
731
+
732
+ hideProgress() {
733
+ this._updateSideIcon()
734
+ }
735
+
736
+
737
+ /* Warning state */
738
+
739
+ showWarning(message) {
740
+ this.getHTML().find('span.qui-form-field-warning-text').html(message)
741
+ this.getHTML().addClass('has-warning')
742
+ if (message) {
743
+ this.getHTML().addClass('has-warning-message')
744
+ }
745
+
746
+ this._updateSideIcon()
747
+ }
748
+
749
+ hideWarning() {
750
+ this.getHTML().removeClass('has-warning has-warning-message')
751
+ Theme.afterTransition(function () {
752
+ if (this.hasWarning()) {
753
+ return /* Warning has been reshown */
754
+ }
755
+
756
+ this.getHTML().find('span.qui-form-field-warning-text').html('')
757
+ }.bind(this), this.getHTML())
758
+
759
+ this._updateSideIcon()
760
+ }
761
+
762
+
763
+ /* Error state */
764
+
765
+ showError(message) {
766
+ this.getHTML().find('span.qui-form-field-error-text').html(message)
767
+ this.getHTML().addClass('has-error')
768
+ if (message) {
769
+ this.getHTML().addClass('has-error-message')
770
+ }
771
+
772
+ this._updateSideIcon()
773
+ }
774
+
775
+ hideError() {
776
+ this.getHTML().removeClass('has-error has-error-message')
777
+ Theme.afterTransition(function () {
778
+ if (this.hasError()) {
779
+ return /* Error has been reshown */
780
+ }
781
+
782
+ this.getHTML().find('span.qui-form-field-error-text').html('')
783
+ }.bind(this), this.getHTML())
784
+
785
+ this._updateSideIcon()
786
+ }
787
+
788
+
789
+ /* Applied state */
790
+
791
+ /**
792
+ * Optionally put the field in the applied state {@link qui.forms.STATE_APPLIED}.
793
+ */
794
+ setApplied() {
795
+ this._origValue = this.getValue()
796
+ this._changed = false
797
+ this.setState(STATE_APPLIED)
798
+ }
799
+
800
+ /**
801
+ * Put the field in the normal state, but only if the current state is {@link qui.forms.STATE_APPLIED}.
802
+ */
803
+ clearApplied() {
804
+ if (this.getState() === STATE_APPLIED) {
805
+ this.setState(STATE_NORMAL)
806
+ }
807
+ }
808
+
809
+ enterState(oldState, newState) {
810
+ switch (newState) {
811
+ case STATE_APPLIED:
812
+ this._updateSideIcon()
813
+ break
814
+
815
+ default:
816
+ super.enterState(oldState, newState)
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Tell if the field data has been applied (and thus there are no pending changes).
822
+ * @returns {Boolean}
823
+ */
824
+ isApplied() {
825
+ return this.getState() === STATE_APPLIED
826
+ }
827
+
828
+
829
+ /* Value */
830
+
831
+ /**
832
+ * Called whenever the field value is changed by the user.
833
+ * @param {*} value the new (unvalidated) field value
834
+ * @param {qui.forms.Form} form the owning form
835
+ */
836
+ onChange(value, form) {
837
+ }
838
+
839
+ /**
840
+ * Tell if a given value is valid or not.
841
+ *
842
+ * Override this method to implement custom validation for this field.
843
+ *
844
+ * @param {*} value the value to validate
845
+ * @param {Object} data the form data
846
+ * @returns {?Promise<*,qui.forms.ValidationError>} `null` or a promise that resolves, if the value is valid; a
847
+ * promise that rejects with a validation error otherwise
848
+ * @throws qui.forms.ValidationError the validation error can also be thrown instead of being returned in a rejected
849
+ * response
850
+ */
851
+ validate(value, data) {
852
+ return null
853
+ }
854
+
855
+ /**
856
+ * Wrap validate() into a try/catch and return a validation promise. Also apply required and widget validation.
857
+ * @private
858
+ * @param {*} value the value to validate
859
+ * @param {Object} data
860
+ * @returns {Promise}
861
+ */
862
+ _validate(value, data) {
863
+ /* Required validation */
864
+ if (this._required && !this.isHidden()) {
865
+ if (value == null || value === false || value === '') {
866
+ return Promise.reject(new ValidationError(gettext('This field is required.')))
867
+ }
868
+ }
869
+
870
+ /* Widget validation */
871
+ let error = this.validateWidget(value)
872
+ if (error) {
873
+ return Promise.reject(new ValidationError(error))
874
+ }
875
+
876
+ /* Custom validation */
877
+ try {
878
+ return this.validate(value, data) || Promise.resolve()
879
+ }
880
+ catch (e) {
881
+ return Promise.reject(e)
882
+ }
883
+ }
884
+
885
+
886
+ /* Side icon */
887
+
888
+ /**
889
+ * Show or hide the side icon, updating it according to the supplied icon.
890
+ * @param {?String|qui.icons.Icon} icon an icon or an icon type; passing `null` hides the side icon; known icon
891
+ * types are:
892
+ * * `"success"`
893
+ * * `"warning"`
894
+ * * `"error"`
895
+ * * `"progress"`
896
+ * @param {Function} [clickCallback] an optional function to be called when the icon is clicked; the function will
897
+ * be called with the field as `this` argument
898
+ */
899
+ setSideIcon(icon, clickCallback) {
900
+ /* Make sure the HTML is created */
901
+ this.getHTML()
902
+
903
+ if (icon && (typeof icon === 'string') && this._sideIconDiv.hasClass(icon)) {
904
+ return /* Side icon already set with given icon type */
905
+ }
906
+
907
+ if (!this.hasHTML()) {
908
+ return
909
+ }
910
+
911
+ this.getHTML().addClass('side-icon-visible')
912
+ this._sideIconDiv.removeClass('success warning error progress custom')
913
+
914
+ if (typeof icon === 'string') {
915
+ this._sideIconDiv.addClass(icon)
916
+
917
+ if (!(this._sideIcon instanceof StockIcon)) {
918
+ this._sideIcon = new StockIcon({name: 'success', scale: 0.75})
919
+ }
920
+
921
+ switch (icon) {
922
+ case 'success':
923
+ this._sideIcon = this._sideIcon.alter({
924
+ name: 'check', variant: 'green',
925
+ activeName: 'check', activeVariant: 'green'
926
+ })
927
+
928
+ break
929
+
930
+ case 'warning':
931
+ this._sideIcon = this._sideIcon.alter({
932
+ name: 'exclam', variant: 'warning',
933
+ activeName: 'exclam', activeVariant: 'warning-active'
934
+ })
935
+
936
+ /* Toggle warning visibility */
937
+ clickCallback = clickCallback || function () {
938
+ this.getHTML().toggleClass('warning-visible')
939
+ }.bind(this)
940
+
941
+ break
942
+
943
+ case 'error':
944
+ this._sideIcon = this._sideIcon.alter({
945
+ name: 'exclam', variant: 'error',
946
+ activeName: 'exclam', activeVariant: 'error-active'
947
+ })
948
+
949
+ /* Toggle error visibility */
950
+ clickCallback = clickCallback || function () {
951
+ this.getHTML().toggleClass('error-visible')
952
+ }.bind(this)
953
+
954
+ break
955
+
956
+ case 'progress':
957
+ this._sideIcon = this._sideIcon.alter({
958
+ name: 'sync', variant: 'green',
959
+ activeName: 'sync', activeVariant: 'green'
960
+ })
961
+
962
+ break
963
+
964
+ default:
965
+ throw new AssertionError(`Unknown icon type ${icon}`)
966
+ }
967
+
968
+ this._sideIcon.applyTo(this._sideIconDiv)
969
+ }
970
+ else if (icon instanceof Icon) {
971
+ this._sideIconDiv.addClass('custom')
972
+ if (icon instanceof StockIcon) {
973
+ icon = icon.alterDefault({scale: 0.75})
974
+ }
975
+ icon.applyTo(this._sideIconDiv)
976
+
977
+ this._sideIcon = icon
978
+ }
979
+ else { /* Assuming null */
980
+ this.getHTML().removeClass('side-icon-visible')
981
+ this._sideIcon = null
982
+ }
983
+
984
+ this._sideIconDiv.off('click')
985
+ if (clickCallback) {
986
+ this._sideIconDiv.on('click', clickCallback.bind(this))
987
+ this._sideIconDiv.css('cursor', 'pointer')
988
+ }
989
+ else {
990
+ this._sideIconDiv.css('cursor', 'default')
991
+ }
992
+ }
993
+
994
+ _updateSideIcon() {
995
+ /* Use asap() here so that we can rely on the the final state when deciding what side icon to display */
996
+ asap(function () {
997
+ if (this.getHTML().hasClass('has-error-message')) {
998
+ this.setSideIcon('error')
999
+ }
1000
+ else if (this.getHTML().hasClass('has-warning-message')) {
1001
+ this.setSideIcon('warning')
1002
+ }
1003
+ else if (this.isApplied()) {
1004
+ this.setSideIcon('success')
1005
+ }
1006
+ else {
1007
+ this.setSideIcon(null)
1008
+ }
1009
+ }.bind(this))
1010
+ }
1011
+
1012
+
1013
+ /* Widget */
1014
+
1015
+ /**
1016
+ * Return the fields's widget.
1017
+ * @returns {jQuery}
1018
+ */
1019
+ getWidget() {
1020
+ if (!this._widget) {
1021
+ this._widget = this.makeWidget()
1022
+ this.initWidget(this._widget)
1023
+ }
1024
+
1025
+ return this._widget
1026
+ }
1027
+
1028
+ /**
1029
+ * Create the widget's HTML element.
1030
+ *
1031
+ * Override this to implement how the widget element is created.
1032
+ *
1033
+ * @abstract
1034
+ * @returns {jQuery}
1035
+ */
1036
+ makeWidget() {
1037
+ }
1038
+
1039
+ /**
1040
+ * Initialize the widget's HTML element.
1041
+ *
1042
+ * Override this to implement how the widget element is set up, after it has been created.
1043
+ *
1044
+ * @param {jQuery} widget the widget's HTML element
1045
+ */
1046
+ initWidget(widget) {
1047
+ }
1048
+
1049
+ /**
1050
+ * Implement widget value validation.
1051
+ *
1052
+ * Override this to implement validation for your widget.
1053
+ *
1054
+ * @param {*} value the value to validate
1055
+ * @returns {?String} `null` if the value is valid, or an error message otherwise; if an empty string is returned,
1056
+ * the value is considered *invalid* but no error message will be shown
1057
+ */
1058
+ validateWidget(value) {
1059
+ return null
1060
+ }
1061
+
1062
+ /**
1063
+ * Read value from the widget and adapt it to the field.
1064
+ *
1065
+ * Override this to implement how a value is read from the widget.
1066
+ *
1067
+ * @abstract
1068
+ * @returns {*} the field value
1069
+ */
1070
+ widgetToValue() {
1071
+ }
1072
+
1073
+ /**
1074
+ * Adapt and write a value to the widget.
1075
+ *
1076
+ * Override this to implement how a value is set to the widget.
1077
+ *
1078
+ * @abstract
1079
+ * @param {*} value the field value
1080
+ */
1081
+ valueToWidget(value) {
1082
+ }
1083
+
1084
+ /**
1085
+ * Update the read-only state of the widget.
1086
+ *
1087
+ * Override this to implement how the widget's readonly state is modified.
1088
+ *
1089
+ * @abstract
1090
+ * @param {Boolean} readonly
1091
+ */
1092
+ setWidgetReadonly(readonly) {
1093
+ }
1094
+
1095
+ /**
1096
+ * Enable the widget.
1097
+ *
1098
+ * Override this to implement how the widget is enabled.
1099
+ *
1100
+ * @abstract
1101
+ */
1102
+ enableWidget() {
1103
+ }
1104
+
1105
+ /**
1106
+ * Disable the widget.
1107
+ *
1108
+ * Override this to implement how the widget is disabled.
1109
+ *
1110
+ * @abstract
1111
+ */
1112
+ disableWidget() {
1113
+ }
1114
+
1115
+ /**
1116
+ * Return the value of the forceOneLine flag.
1117
+ * @returns {Boolean}
1118
+ */
1119
+ isForceOneLine() {
1120
+ return this._forceOneLine
1121
+ }
1122
+
1123
+ /**
1124
+ * Return the width percent of the value element.
1125
+ * @returns {?Number}
1126
+ */
1127
+ getValueWidth() {
1128
+ return this._valueWidth
1129
+ }
1130
+
1131
+ /**
1132
+ * Set the width percent of the value element.
1133
+ * @param {?Number} width
1134
+ */
1135
+ setValueWidth(width) {
1136
+ this._valueWidth = width
1137
+ if (!this.hasHTML()) {
1138
+ return
1139
+ }
1140
+
1141
+ this.getHTML().removeClass('auto-width')
1142
+ this._labelDiv.css('width', '')
1143
+ this._valueDiv.css('width', '')
1144
+
1145
+ let valueWidth = this._valueWidth
1146
+ if (valueWidth == null) {
1147
+ valueWidth = this._form._valuesWidth
1148
+ }
1149
+
1150
+ let useOneLine = !this._form.isCompact() || this._forceOneLine
1151
+ if (useOneLine) {
1152
+ if (valueWidth) {
1153
+ this._labelDiv.css('width', `${(100 - valueWidth)}%`)
1154
+ }
1155
+ else {
1156
+ this.getHTML().addClass('auto-width')
1157
+ }
1158
+ }
1159
+ if (valueWidth && useOneLine) {
1160
+ this._valueDiv.css('width', `${valueWidth}%`)
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Return the owning form.
1166
+ * @returns {qui.forms.Form}
1167
+ */
1168
+ getForm() {
1169
+ return this._form
1170
+ }
1171
+
1172
+ /**
1173
+ * Set the owning form.
1174
+ * @param {qui.forms.Form} form
1175
+ */
1176
+ setForm(form) {
1177
+ this._form = form
1178
+ }
1179
+
1180
+ }
1181
+
1182
+
1183
+ export default FormField