@qtoggle/qui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/.eslintrc.json +492 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
- package/.github/workflows/main.yml +74 -0
- package/.pre-commit-config.yaml +8 -0
- package/LICENSE.txt +177 -0
- package/README.md +4 -0
- package/font/dejavusans-bold.woff +0 -0
- package/font/dejavusans-bolditalic.woff +0 -0
- package/font/dejavusans-italic.woff +0 -0
- package/font/dejavusans-regular.woff +0 -0
- package/img/qui-icons.svg +1937 -0
- package/js/base/base.js +47 -0
- package/js/base/condition-variable.js +92 -0
- package/js/base/errors.js +36 -0
- package/js/base/i18n.js +20 -0
- package/js/base/mixwith.js +135 -0
- package/js/base/require-js-compat.js +78 -0
- package/js/base/signal.js +91 -0
- package/js/base/singleton.js +66 -0
- package/js/base/timer.js +126 -0
- package/js/config.js +184 -0
- package/js/forms/common-fields/check-field.js +42 -0
- package/js/forms/common-fields/choice-buttons-field.js +30 -0
- package/js/forms/common-fields/color-combo-field.js +37 -0
- package/js/forms/common-fields/combo-field.js +108 -0
- package/js/forms/common-fields/common-fields.js +23 -0
- package/js/forms/common-fields/composite-field.js +132 -0
- package/js/forms/common-fields/custom-html-field.js +51 -0
- package/js/forms/common-fields/email-field.js +30 -0
- package/js/forms/common-fields/file-picker-field.js +46 -0
- package/js/forms/common-fields/jquery-ui-field.js +111 -0
- package/js/forms/common-fields/labels-field.js +69 -0
- package/js/forms/common-fields/numeric-field.js +39 -0
- package/js/forms/common-fields/password-field.js +28 -0
- package/js/forms/common-fields/phone-field.js +26 -0
- package/js/forms/common-fields/progress-disk-field.js +69 -0
- package/js/forms/common-fields/push-button-field.js +138 -0
- package/js/forms/common-fields/slider-field.js +51 -0
- package/js/forms/common-fields/text-area-field.js +34 -0
- package/js/forms/common-fields/text-field.js +89 -0
- package/js/forms/common-fields/up-down-field.js +85 -0
- package/js/forms/common-forms/common-forms.js +16 -0
- package/js/forms/common-forms/options-form.js +77 -0
- package/js/forms/common-forms/page-form.js +115 -0
- package/js/forms/form-button.js +202 -0
- package/js/forms/form-field.js +1183 -0
- package/js/forms/form.js +1181 -0
- package/js/forms/forms.js +68 -0
- package/js/global-glass.js +100 -0
- package/js/icons/default-stock.js +173 -0
- package/js/icons/icon.js +64 -0
- package/js/icons/icons.js +16 -0
- package/js/icons/multi-state-sprites-icon.js +362 -0
- package/js/icons/stock-icon.js +219 -0
- package/js/icons/stock.js +98 -0
- package/js/icons/stocks.js +57 -0
- package/js/index.js +232 -0
- package/js/lib/jquery.longpress.js +79 -0
- package/js/lib/jquery.module.js +4 -0
- package/js/lib/logger.module.js +4 -0
- package/js/lib/pep.module.js +4 -0
- package/js/lists/common-items/common-items.js +5 -0
- package/js/lists/common-items/icon-label-list-item.js +86 -0
- package/js/lists/common-lists/common-lists.js +5 -0
- package/js/lists/common-lists/page-list.js +53 -0
- package/js/lists/list-item.js +147 -0
- package/js/lists/list.js +636 -0
- package/js/lists/lists.js +26 -0
- package/js/main-ui/main-ui.js +64 -0
- package/js/main-ui/menu-bar.js +144 -0
- package/js/main-ui/options-bar.js +181 -0
- package/js/main-ui/status.js +185 -0
- package/js/main-ui/top-bar.js +59 -0
- package/js/messages/common-message-forms/common-message-forms.js +7 -0
- package/js/messages/common-message-forms/confirm-message-form.js +81 -0
- package/js/messages/common-message-forms/simple-message-form.js +67 -0
- package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
- package/js/messages/message-form.js +107 -0
- package/js/messages/messages.js +21 -0
- package/js/messages/sticky-modal-page.js +98 -0
- package/js/messages/sticky-modal-progress-message.js +27 -0
- package/js/messages/toast.js +164 -0
- package/js/navigation.js +654 -0
- package/js/pages/breadcrumbs.js +124 -0
- package/js/pages/common-pages/common-pages.js +6 -0
- package/js/pages/common-pages/modal-progress-page.js +83 -0
- package/js/pages/common-pages/structured-page.js +46 -0
- package/js/pages/page.js +1018 -0
- package/js/pages/pages-context.js +154 -0
- package/js/pages/pages.js +252 -0
- package/js/pwa.js +337 -0
- package/js/sections/section.js +612 -0
- package/js/sections/sections.js +300 -0
- package/js/tables/common-cells/common-cells.js +7 -0
- package/js/tables/common-cells/icon-label-table-cell.js +68 -0
- package/js/tables/common-cells/push-button-table-cell.js +133 -0
- package/js/tables/common-cells/simple-table-cell.js +37 -0
- package/js/tables/common-tables/common-tables.js +5 -0
- package/js/tables/common-tables/page-table.js +55 -0
- package/js/tables/table-cell.js +198 -0
- package/js/tables/table-row.js +126 -0
- package/js/tables/table.js +492 -0
- package/js/tables/tables.js +36 -0
- package/js/theme.js +304 -0
- package/js/utils/ajax.js +126 -0
- package/js/utils/array.js +194 -0
- package/js/utils/colors.js +445 -0
- package/js/utils/cookies.js +65 -0
- package/js/utils/crypto.js +439 -0
- package/js/utils/css.js +234 -0
- package/js/utils/date.js +300 -0
- package/js/utils/files.js +27 -0
- package/js/utils/gestures.js +165 -0
- package/js/utils/html.js +76 -0
- package/js/utils/misc.js +81 -0
- package/js/utils/object.js +324 -0
- package/js/utils/promise.js +49 -0
- package/js/utils/string.js +192 -0
- package/js/utils/url.js +187 -0
- package/js/utils/utils.js +3 -0
- package/js/utils/visibility-manager.js +211 -0
- package/js/views/common-views/common-views.js +7 -0
- package/js/views/common-views/icon-label-view.js +210 -0
- package/js/views/common-views/progress-view.js +89 -0
- package/js/views/common-views/structured-view.js +368 -0
- package/js/views/view.js +467 -0
- package/js/views/views.js +3 -0
- package/js/widgets/base-widget.js +23 -0
- package/js/widgets/common-widgets/check-button.js +109 -0
- package/js/widgets/common-widgets/choice-buttons.js +322 -0
- package/js/widgets/common-widgets/color-combo.js +104 -0
- package/js/widgets/common-widgets/combo.js +645 -0
- package/js/widgets/common-widgets/common-widgets.js +17 -0
- package/js/widgets/common-widgets/email-input.js +7 -0
- package/js/widgets/common-widgets/file-picker.js +133 -0
- package/js/widgets/common-widgets/labels.js +132 -0
- package/js/widgets/common-widgets/numeric-input.js +49 -0
- package/js/widgets/common-widgets/password-input.js +91 -0
- package/js/widgets/common-widgets/phone-input.js +7 -0
- package/js/widgets/common-widgets/progress-disk.js +174 -0
- package/js/widgets/common-widgets/push-button.js +155 -0
- package/js/widgets/common-widgets/slider.js +455 -0
- package/js/widgets/common-widgets/text-area.js +52 -0
- package/js/widgets/common-widgets/text-input.js +174 -0
- package/js/widgets/common-widgets/up-down.js +351 -0
- package/js/widgets/widgets.js +57 -0
- package/js/window.js +557 -0
- package/jsdoc.conf.json +20 -0
- package/less/base.less +123 -0
- package/less/forms/common-fields.less +101 -0
- package/less/forms/common-forms.less +5 -0
- package/less/forms/form-button.less +21 -0
- package/less/forms/form-field.less +266 -0
- package/less/forms/form.less +131 -0
- package/less/global-glass.less +64 -0
- package/less/icon-label-view.less +82 -0
- package/less/icons.less +144 -0
- package/less/lists.less +105 -0
- package/less/main-ui.less +328 -0
- package/less/messages.less +189 -0
- package/less/no-effects.less +24 -0
- package/less/pages/breadcrumbs.less +98 -0
- package/less/pages/common-pages.less +36 -0
- package/less/pages/page.less +70 -0
- package/less/progress-view.less +51 -0
- package/less/stock-icons.less +43 -0
- package/less/structured-view.less +245 -0
- package/less/tables.less +84 -0
- package/less/theme-dark.less +133 -0
- package/less/theme-light.less +132 -0
- package/less/theme.less +419 -0
- package/less/visibility-manager.less +11 -0
- package/less/widgets/check-button.less +96 -0
- package/less/widgets/choice-buttons.less +160 -0
- package/less/widgets/color-combo.less +33 -0
- package/less/widgets/combo.less +230 -0
- package/less/widgets/common-buttons.less +120 -0
- package/less/widgets/common.less +24 -0
- package/less/widgets/input.less +258 -0
- package/less/widgets/labels.less +81 -0
- package/less/widgets/progress-disk.less +70 -0
- package/less/widgets/slider.less +199 -0
- package/less/widgets/updown.less +115 -0
- package/less/widgets/various.less +36 -0
- package/package.json +47 -0
- package/pyproject.toml +45 -0
- package/qui/__init__.py +110 -0
- package/qui/constants.py +1 -0
- package/qui/exceptions.py +2 -0
- package/qui/j2template.py +71 -0
- package/qui/settings.py +60 -0
- package/qui/templates/manifest.json +25 -0
- package/qui/templates/qui.html +126 -0
- package/qui/templates/service-worker.js +188 -0
- package/qui/web/__init__.py +0 -0
- package/qui/web/tornado.py +220 -0
- package/scripts/postinstall.sh +10 -0
- package/webpack/webpack-adjust-css-urls-loader.js +36 -0
- package/webpack/webpack-common.js +384 -0
package/js/forms/form.js
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
|
|
2
|
+
import $ from '$qui/lib/jquery.module.js'
|
|
3
|
+
|
|
4
|
+
import {AssertionError} from '$qui/base/errors.js'
|
|
5
|
+
import {mix} from '$qui/base/mixwith.js'
|
|
6
|
+
import * as Theme from '$qui/theme.js'
|
|
7
|
+
import {asap} from '$qui/utils/misc.js'
|
|
8
|
+
import * as ObjectUtils from '$qui/utils/object.js'
|
|
9
|
+
import {ProgressViewMixin} from '$qui/views/common-views/common-views.js'
|
|
10
|
+
import {StructuredViewMixin} from '$qui/views/common-views/common-views.js'
|
|
11
|
+
import {STATE_NORMAL} from '$qui/views/view.js'
|
|
12
|
+
import ViewMixin from '$qui/views/view.js'
|
|
13
|
+
import * as Window from '$qui/window.js'
|
|
14
|
+
|
|
15
|
+
import {ErrorMapping} from './forms.js'
|
|
16
|
+
import {ValidationError} from './forms.js'
|
|
17
|
+
import {STATE_APPLIED} from './forms.js'
|
|
18
|
+
import FormField from './form-field.js'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A form.
|
|
23
|
+
* @alias qui.forms.Form
|
|
24
|
+
* @mixes qui.views.ViewMixin
|
|
25
|
+
* @mixes qui.views.commonviews.StructuredViewMixin
|
|
26
|
+
* @mixes qui.views.commonviews.ProgressViewMixin
|
|
27
|
+
*/
|
|
28
|
+
class Form extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin) {
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @constructs
|
|
32
|
+
* @param {Number|String} [width] a specific form width to be used instead of the default
|
|
33
|
+
* @param {Boolean} [noBackground] indicates that the form should have transparent background (defaults to
|
|
34
|
+
* `false`)
|
|
35
|
+
* @param {Boolean} [compact] indicates that the form should be compact to better fit in smaller containers;
|
|
36
|
+
* compact forms have field labels and values on separate lines, to reduce the overall form width (defaults to
|
|
37
|
+
* `false`)
|
|
38
|
+
* @param {Number} [valuesWidth] sets the width of the values column, as percent, relative to the form body; when
|
|
39
|
+
* set to `0` the width of each field's value will be computed automatically; this attribute is ignored for forms
|
|
40
|
+
* that have `compact` set to `true` (defaults to `60`)
|
|
41
|
+
* @param {Boolean} [continuousValidation] if set to `true`, each field will be validated upon change, instead of
|
|
42
|
+
* when the form data is applied (using {@link qui.forms.Form#applyData}). Defaults to `false`
|
|
43
|
+
* @param {Boolean} [closeOnApply] if set to `false`, the form will not automatically close when data is applied
|
|
44
|
+
* (using {@link qui.forms.Form#applyData}). Defaults to `true`
|
|
45
|
+
* @param {Boolean} [autoDisableDefaultButton] controls if the default button is automatically enabled and disabled
|
|
46
|
+
* based on currently changed fields, their validity and applied state. Defaults to `true`
|
|
47
|
+
* @param {qui.forms.FormField[]} [fields] fields to be added to the form
|
|
48
|
+
* @param {qui.forms.FormButton[]} [buttons] buttons to be added to the form
|
|
49
|
+
* @param {Object} [initialData] a dictionary with initial values for the fields
|
|
50
|
+
* @param {...*} args parent class parameters
|
|
51
|
+
*/
|
|
52
|
+
constructor({
|
|
53
|
+
width = null,
|
|
54
|
+
noBackground = false,
|
|
55
|
+
compact = false,
|
|
56
|
+
valuesWidth = 60,
|
|
57
|
+
continuousValidation = false,
|
|
58
|
+
closeOnApply = true,
|
|
59
|
+
autoDisableDefaultButton = true,
|
|
60
|
+
fields = [],
|
|
61
|
+
buttons = [],
|
|
62
|
+
initialData = null,
|
|
63
|
+
...args
|
|
64
|
+
} = {}) {
|
|
65
|
+
super(args)
|
|
66
|
+
|
|
67
|
+
this._width = width
|
|
68
|
+
this._noBackground = noBackground
|
|
69
|
+
this._compact = compact
|
|
70
|
+
this._valuesWidth = valuesWidth
|
|
71
|
+
this._continuousValidation = continuousValidation
|
|
72
|
+
this._closeOnApply = closeOnApply
|
|
73
|
+
this._autoDisableDefaultButton = autoDisableDefaultButton
|
|
74
|
+
this._fields = fields
|
|
75
|
+
this._buttons = buttons
|
|
76
|
+
this._initialData = initialData
|
|
77
|
+
|
|
78
|
+
/* Last known validity state */
|
|
79
|
+
this._isValid = null
|
|
80
|
+
this._validationCache = {}
|
|
81
|
+
this._updateValidationStateASAPHandle = null
|
|
82
|
+
|
|
83
|
+
this._fieldsByName = {}
|
|
84
|
+
|
|
85
|
+
this._errorDiv = null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/* Various attributes */
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Tell if the form uses the compact layout.
|
|
93
|
+
* @returns {Boolean}
|
|
94
|
+
*/
|
|
95
|
+
isCompact() {
|
|
96
|
+
return this._compact
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/* HTML */
|
|
101
|
+
|
|
102
|
+
makeHTML() {
|
|
103
|
+
return $('<form></form>', {class: 'qui-form'})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
initHTML(html) {
|
|
107
|
+
super.initHTML(html)
|
|
108
|
+
|
|
109
|
+
html.addClass('unexpanded')
|
|
110
|
+
|
|
111
|
+
if (this._width) {
|
|
112
|
+
html.css('width', this._width)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this._noBackground) {
|
|
116
|
+
html.addClass('no-background')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this._compact) {
|
|
120
|
+
html.addClass('compact')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* React on Escape and Enter keys */
|
|
124
|
+
html.on('keydown', function (e) {
|
|
125
|
+
|
|
126
|
+
if (e.which === 27) { /* Escape */
|
|
127
|
+
asap(function () {
|
|
128
|
+
this.cancelAction()
|
|
129
|
+
}.bind(this))
|
|
130
|
+
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
else if (e.which === 13) { /* Enter */
|
|
134
|
+
asap(function () {
|
|
135
|
+
this.defaultAction()
|
|
136
|
+
}.bind(this))
|
|
137
|
+
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
}.bind(this))
|
|
142
|
+
|
|
143
|
+
/* Override default form submit */
|
|
144
|
+
html.on('submit', function () {
|
|
145
|
+
|
|
146
|
+
asap(function () {
|
|
147
|
+
this.defaultAction()
|
|
148
|
+
}.bind(this))
|
|
149
|
+
|
|
150
|
+
return false
|
|
151
|
+
|
|
152
|
+
}.bind(this))
|
|
153
|
+
|
|
154
|
+
/* Fields */
|
|
155
|
+
let fields = this._fields
|
|
156
|
+
this._fields = []
|
|
157
|
+
|
|
158
|
+
fields.forEach(f => this.addField(-1, f))
|
|
159
|
+
|
|
160
|
+
/* Add buttons */
|
|
161
|
+
let buttons = this._buttons
|
|
162
|
+
this._buttons = []
|
|
163
|
+
|
|
164
|
+
buttons.forEach(b => this.addButton(-1, b))
|
|
165
|
+
|
|
166
|
+
/* Set initial data */
|
|
167
|
+
if (this._initialData) {
|
|
168
|
+
this.setData(this._initialData)
|
|
169
|
+
}
|
|
170
|
+
else if (this._continuousValidation) {
|
|
171
|
+
this.updateValidationState()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._updateBottom()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
makeBody() {
|
|
178
|
+
/* Body element */
|
|
179
|
+
let bodyDiv = $('<div></div>', {class: 'qui-form-body'})
|
|
180
|
+
|
|
181
|
+
/* Error */
|
|
182
|
+
this._errorDiv = $('<div></div>', {class: 'qui-form-error'})
|
|
183
|
+
let errorLabel = $('<div></div>', {class: 'qui-form-error-label'})
|
|
184
|
+
let errorIcon = $('<span></span>', {class: 'qui-form-error-icon'})
|
|
185
|
+
let errorText = $('<span></span>', {class: 'qui-form-error-text'})
|
|
186
|
+
errorLabel.append(errorIcon).append(errorText)
|
|
187
|
+
this._errorDiv.append(errorLabel)
|
|
188
|
+
|
|
189
|
+
bodyDiv.append(this._errorDiv)
|
|
190
|
+
|
|
191
|
+
return bodyDiv
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
makeBottom() {
|
|
195
|
+
return $('<div></div>', {class: 'qui-form-bottom'})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_updateBottom() {
|
|
199
|
+
/* Add/remove "buttonless" class */
|
|
200
|
+
this.getHTML().toggleClass('buttonless', this._buttons.length === 0)
|
|
201
|
+
|
|
202
|
+
/* Update grid template */
|
|
203
|
+
if (this.isCompact() || Window.isSmallScreen()) {
|
|
204
|
+
this.getBottom().css('grid-template-columns', `repeat(${this._buttons.length}, 1fr)`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
/* Buttons */
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Add a button to the form.
|
|
213
|
+
* @param {Number} index the index where the button should be added; `-1` will add the button at the end
|
|
214
|
+
* @param {qui.forms.FormButton} button the button
|
|
215
|
+
*/
|
|
216
|
+
addButton(index, button) {
|
|
217
|
+
button.getHTML().on('click', () => this.onButtonPress(button))
|
|
218
|
+
button.setForm(this)
|
|
219
|
+
|
|
220
|
+
if (index >= 0 && this._bottomDiv.children().length) {
|
|
221
|
+
this._buttons.splice(index, 0, button)
|
|
222
|
+
this._bottomDiv.children(`:eq(${index})`).before(button.getHTML())
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
this._buttons.push(button)
|
|
226
|
+
this._bottomDiv.append(button.getHTML())
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this._updateBottom()
|
|
230
|
+
this.updateButtonsState()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Remove a button from the form.
|
|
235
|
+
* @param {String} id the button id
|
|
236
|
+
*/
|
|
237
|
+
removeButton(id) {
|
|
238
|
+
if (!this._bottomDiv) { /* Not created yet */
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let index = this._buttons.findIndex(b => b.getId() === id)
|
|
243
|
+
if (index >= 0) {
|
|
244
|
+
this._buttons[index].getHTML().remove()
|
|
245
|
+
this._buttons.splice(index, 1)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._updateBottom()
|
|
249
|
+
this.updateButtonsState()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Return the form button with the given id.
|
|
254
|
+
* @param {String} id the button id
|
|
255
|
+
* @returns {?qui.forms.FormButton}
|
|
256
|
+
*/
|
|
257
|
+
getButton(id) {
|
|
258
|
+
/* Ensure HTML is created */
|
|
259
|
+
this.getHTML()
|
|
260
|
+
|
|
261
|
+
return this._buttons.find(b => b.getId() === id) || null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Return all form buttons.
|
|
266
|
+
* @returns {qui.forms.FormButton[]}
|
|
267
|
+
*/
|
|
268
|
+
getButtons() {
|
|
269
|
+
return this._buttons.slice()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Called when one of the form buttons is pressed.
|
|
274
|
+
* @param {qui.forms.FormButton} button
|
|
275
|
+
*/
|
|
276
|
+
onButtonPress(button) {
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Update internal form state related to buttons.
|
|
281
|
+
*/
|
|
282
|
+
updateButtonsState() {
|
|
283
|
+
if (this._autoDisableDefaultButton) {
|
|
284
|
+
let defaultButton = this._buttons.find(b => b.isDefault())
|
|
285
|
+
if (defaultButton) {
|
|
286
|
+
let enabled
|
|
287
|
+
if (this._continuousValidation) {
|
|
288
|
+
enabled = this._isValid !== false
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
enabled = (this.getChangedFieldNames().length > 0) ||
|
|
292
|
+
(this._fields.length === 0)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (enabled) {
|
|
296
|
+
defaultButton.enable()
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
defaultButton.disable()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
/* Fields */
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Add a field to the form.
|
|
310
|
+
* @param {Number} index the index where the field should be added; `-1` will add the field at the end
|
|
311
|
+
* @param {qui.forms.FormField} field the field
|
|
312
|
+
*/
|
|
313
|
+
addField(index, field) {
|
|
314
|
+
/* Ensure HTML is created */
|
|
315
|
+
this.getHTML()
|
|
316
|
+
|
|
317
|
+
let name = field.getName()
|
|
318
|
+
if (name in this._fieldsByName) {
|
|
319
|
+
throw new AssertionError(`Field with name "${name}" already present on form`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
field.setForm(this)
|
|
323
|
+
|
|
324
|
+
if (index >= 0 && index < this._fields.length) {
|
|
325
|
+
this._fields.splice(index, 0, field)
|
|
326
|
+
this._bodyDiv.children(`div.qui-form-field:eq(${index})`).before(field.getHTML())
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
this._fields.push(field)
|
|
330
|
+
this._bodyDiv.append(field.getHTML())
|
|
331
|
+
}
|
|
332
|
+
this._fieldsByName[name] = field
|
|
333
|
+
|
|
334
|
+
/* The form itself may no longer be valid */
|
|
335
|
+
this._clearValidationCache('')
|
|
336
|
+
|
|
337
|
+
this.updateButtonsState()
|
|
338
|
+
this.updateFieldsState()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Remove a field from the form.
|
|
343
|
+
* @param {String|qui.forms.FormField} nameOrField the name of the field or a field object
|
|
344
|
+
*/
|
|
345
|
+
removeField(nameOrField) {
|
|
346
|
+
if (!this._bodyDiv) { /* Not created yet */
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let name
|
|
351
|
+
if (nameOrField instanceof FormField) {
|
|
352
|
+
name = nameOrField.getName()
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
name = nameOrField
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let index = this._fields.findIndex(function (field) {
|
|
359
|
+
return field.getName() === name
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
if (index >= 0) {
|
|
363
|
+
this._fields.splice(index, 1)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this._bodyDiv.children(`div.qui-form-field[data-name=${name}]`).remove()
|
|
367
|
+
|
|
368
|
+
delete this._fieldsByName[name]
|
|
369
|
+
|
|
370
|
+
/* Invalidate form's validation cache */
|
|
371
|
+
this._clearValidationCache(name)
|
|
372
|
+
|
|
373
|
+
this.updateButtonsState()
|
|
374
|
+
this.updateFieldsState()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Return the field with a given name. If no such field is found, `null` is returned.
|
|
379
|
+
* @param {String} name the name of the field
|
|
380
|
+
* @returns {?qui.forms.FormField}
|
|
381
|
+
*/
|
|
382
|
+
getField(name) {
|
|
383
|
+
/* Ensure fields are properly added to form */
|
|
384
|
+
this.getHTML()
|
|
385
|
+
|
|
386
|
+
return this._fieldsByName[name] || null
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Return the index of the given field. If the field does not belong to this form, `-1` is returned.
|
|
391
|
+
* @param {qui.forms.FormField|String} field the field or a field name
|
|
392
|
+
* @returns {Number}
|
|
393
|
+
*/
|
|
394
|
+
getFieldIndex(field) {
|
|
395
|
+
/* Ensure fields are properly added to form */
|
|
396
|
+
this.getHTML()
|
|
397
|
+
|
|
398
|
+
if (typeof field === 'string') {
|
|
399
|
+
return this._fields.findIndex(f => f.getName() === field)
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
return this._fields.indexOf(field)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Return the list of all fields.
|
|
408
|
+
* @returns {qui.forms.FormField[]}
|
|
409
|
+
*/
|
|
410
|
+
getFields() {
|
|
411
|
+
return this._fields.slice()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Update internal form state related to fields.
|
|
416
|
+
*/
|
|
417
|
+
updateFieldsState() {
|
|
418
|
+
/* Update {first-last}-visible CSS classes */
|
|
419
|
+
let firstVisible = false
|
|
420
|
+
let lastVisibleIndex = -1
|
|
421
|
+
this._fields.forEach(function (field, i) {
|
|
422
|
+
let html = field.getHTML()
|
|
423
|
+
html.removeClass('first-visible last-visible')
|
|
424
|
+
|
|
425
|
+
if (!field.isHidden()) {
|
|
426
|
+
if (!firstVisible) {
|
|
427
|
+
firstVisible = true
|
|
428
|
+
html.addClass('first-visible')
|
|
429
|
+
}
|
|
430
|
+
lastVisibleIndex = i
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
if (lastVisibleIndex >= 0) {
|
|
435
|
+
this._fields[lastVisibleIndex].getHTML().addClass('last-visible')
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
/* Data & validation */
|
|
441
|
+
|
|
442
|
+
_validateData(fieldName) {
|
|
443
|
+
/* Ensure HTML is created */
|
|
444
|
+
this.getHTML()
|
|
445
|
+
|
|
446
|
+
let form = this
|
|
447
|
+
|
|
448
|
+
/* Gather raw form data for validation */
|
|
449
|
+
let data = ObjectUtils.mapValue(this._fieldsByName, function (field) {
|
|
450
|
+
return field.getValue()
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
let promisesDict = {}
|
|
454
|
+
|
|
455
|
+
/* Validate fields */
|
|
456
|
+
let fields = this._fields
|
|
457
|
+
if (fieldName) {
|
|
458
|
+
/* A specific field was given, restrict the validation to given field */
|
|
459
|
+
fields = [this._fieldsByName[fieldName]]
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fields.forEach(function (field) {
|
|
463
|
+
let name = field.getName()
|
|
464
|
+
let value = data[name]
|
|
465
|
+
let fieldPromise
|
|
466
|
+
|
|
467
|
+
/* Use cached validation result, if available */
|
|
468
|
+
let cached = form._validationCache[name]
|
|
469
|
+
if (cached != null && !(cached instanceof Promise)) {
|
|
470
|
+
if (cached === true) { /* Validation passed */
|
|
471
|
+
fieldPromise = Promise.resolve()
|
|
472
|
+
}
|
|
473
|
+
else { /* Assuming (cached instanceof qui.forms.ValidationError) */
|
|
474
|
+
fieldPromise = Promise.reject(cached)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
/* Field internal validation */
|
|
479
|
+
fieldPromise = field._validate(value, data).then(function () {
|
|
480
|
+
|
|
481
|
+
/* Field validation in form context */
|
|
482
|
+
return form.validateField(name, value, data)
|
|
483
|
+
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
/* Schedule after pending validation */
|
|
487
|
+
if (cached instanceof Promise) {
|
|
488
|
+
fieldPromise = cached.catch(() => {}).then(function () {
|
|
489
|
+
return this
|
|
490
|
+
}.bind(fieldPromise))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
fieldPromise = fieldPromise.then(function () {
|
|
494
|
+
|
|
495
|
+
form._validationCache[name] = true
|
|
496
|
+
return value
|
|
497
|
+
|
|
498
|
+
}).catch(function (error) {
|
|
499
|
+
|
|
500
|
+
if (field.isHidden()) {
|
|
501
|
+
/* Hidden fields are always considered valid */
|
|
502
|
+
form._validationCache[name] = true
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
form._validationCache[name] = error
|
|
506
|
+
throw error
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
/* Set pending validation */
|
|
512
|
+
form._validationCache[name] = fieldPromise
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
promisesDict[name] = fieldPromise
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
let errors = {}
|
|
519
|
+
ObjectUtils.forEach(promisesDict, function (fieldName, promise) {
|
|
520
|
+
promise.catch(function (error) {
|
|
521
|
+
errors[fieldName] = error
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
let fieldsPromise = Promise.all(Object.values(promisesDict)).then(function () {
|
|
526
|
+
|
|
527
|
+
if (fieldName) {
|
|
528
|
+
return data[fieldName]
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
return data
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
}).catch(function () {
|
|
535
|
+
|
|
536
|
+
if (fieldName) {
|
|
537
|
+
throw errors[fieldName]
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
throw new ErrorMapping(errors)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
if (!fieldName) {
|
|
546
|
+
/* Validate the form itself */
|
|
547
|
+
let formPromise
|
|
548
|
+
|
|
549
|
+
/* Use cached validation result, if available */
|
|
550
|
+
let cached = form._validationCache['']
|
|
551
|
+
if (cached != null && !(cached instanceof Promise)) {
|
|
552
|
+
if (cached === true) { /* Validation passed */
|
|
553
|
+
formPromise = Promise.resolve(data)
|
|
554
|
+
}
|
|
555
|
+
else { /* Assuming (cached instanceof qui.forms.ValidationError) */
|
|
556
|
+
formPromise = Promise.reject(cached)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return fieldsPromise.then(() => formPromise)
|
|
560
|
+
}
|
|
561
|
+
else { /* We don't have a cached result available right away */
|
|
562
|
+
|
|
563
|
+
/* Call form validation after the fields have been validated; this ensures that validate() is always
|
|
564
|
+
* called with valid field values */
|
|
565
|
+
|
|
566
|
+
formPromise = fieldsPromise.then(function () {
|
|
567
|
+
|
|
568
|
+
return form._validate(data)
|
|
569
|
+
|
|
570
|
+
}).then(function () {
|
|
571
|
+
|
|
572
|
+
form._validationCache[''] = true
|
|
573
|
+
return data
|
|
574
|
+
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
/* Schedule current validation after cached (pending) validation */
|
|
578
|
+
if (cached instanceof Promise) {
|
|
579
|
+
let p = formPromise
|
|
580
|
+
formPromise = cached.catch(() => {}).then(() => p)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* Set pending validation */
|
|
584
|
+
form._validationCache[''] = formPromise
|
|
585
|
+
|
|
586
|
+
return formPromise
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
return fieldsPromise
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Invalidate the validation cache for a specific field as well as for the form itself. If no field name is given,
|
|
596
|
+
* the entire validation cache is cleared.
|
|
597
|
+
* @private
|
|
598
|
+
* @param {String} [fieldName] the name of the field whose cache should be invalidated
|
|
599
|
+
*/
|
|
600
|
+
_clearValidationCache(fieldName) {
|
|
601
|
+
let keys
|
|
602
|
+
|
|
603
|
+
if (fieldName != null) {
|
|
604
|
+
keys = ['', fieldName]
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
keys = Object.keys(this._validationCache)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
keys.forEach(function (key) {
|
|
611
|
+
if (!(this._validationCache[key] instanceof Promise)) {
|
|
612
|
+
delete this._validationCache[key]
|
|
613
|
+
}
|
|
614
|
+
}, this)
|
|
615
|
+
|
|
616
|
+
this._isValid = null
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Wrap validate() into a try/catch and return a validation promise.
|
|
621
|
+
* @private
|
|
622
|
+
* @param {Object} data
|
|
623
|
+
* @returns {Promise}
|
|
624
|
+
*/
|
|
625
|
+
_validate(data) {
|
|
626
|
+
try {
|
|
627
|
+
return this.validate(data) || Promise.resolve()
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
if (e instanceof ValidationError) {
|
|
631
|
+
return Promise.reject(e)
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
throw e
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Tell if given form data is valid as a whole or not. This method is called only with valid field data. Override
|
|
641
|
+
* this method to implement custom form validation.
|
|
642
|
+
* @param {Object} data the form data to validate
|
|
643
|
+
* @returns {?Promise<*,qui.forms.ValidationError>} `null` or a resolved promise, if the data is valid; a promise
|
|
644
|
+
* rejected with a validation error otherwise;
|
|
645
|
+
* @throws qui.forms.ValidationError the validation error can also be thrown instead of being returned in a rejected
|
|
646
|
+
* promise
|
|
647
|
+
*/
|
|
648
|
+
validate(data) {
|
|
649
|
+
return null
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Override this method to implement custom field validation in the context of this form.
|
|
654
|
+
* @param {String} name the field name
|
|
655
|
+
* @param {*} value the field value to validate
|
|
656
|
+
* @param {Object} data the form unvalidated data
|
|
657
|
+
* @returns {?Promise<*,qui.forms.ValidationError>} `null` or a resolved promise, if the value is valid; a promise
|
|
658
|
+
* rejected with a validation error otherwise;
|
|
659
|
+
* @throws qui.forms.ValidationError the validation error can also be thrown instead of being returned in a rejected
|
|
660
|
+
* response
|
|
661
|
+
*/
|
|
662
|
+
validateField(name, value, data) {
|
|
663
|
+
return null
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Used for continuous validation. Runs a "background" validation to:
|
|
668
|
+
* * show possible field/form errors
|
|
669
|
+
* * update internal validity state
|
|
670
|
+
* * enable/disable default form button
|
|
671
|
+
* @param {Object<String,Error>} [extraErrors] a dictionary containing any extra field errors that should be
|
|
672
|
+
* considered in addition to those returned by the validation mechanism.
|
|
673
|
+
* @returns {Promise<Object>} a promise that is resolved with form data, if the form is valid (the promise is never
|
|
674
|
+
* rejected)
|
|
675
|
+
*/
|
|
676
|
+
updateValidationState(extraErrors = null) {
|
|
677
|
+
let hasExtraErrorsSentinel = new Error()
|
|
678
|
+
|
|
679
|
+
return this._validateData().then(function (data) {
|
|
680
|
+
|
|
681
|
+
this._clearErrors()
|
|
682
|
+
|
|
683
|
+
if (this._isValid === false || this._isValid == null) { /* Form just became valid */
|
|
684
|
+
this._isValid = true
|
|
685
|
+
this.onValid(data)
|
|
686
|
+
this.updateButtonsState()
|
|
687
|
+
this.updateFieldsState()
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (extraErrors && Object.keys(extraErrors).length) {
|
|
691
|
+
throw hasExtraErrorsSentinel /* extraErrors will be merged in soon, in catch() */
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return data
|
|
695
|
+
|
|
696
|
+
}.bind(this)).catch(function (e) {
|
|
697
|
+
|
|
698
|
+
let errors
|
|
699
|
+
if (e === hasExtraErrorsSentinel) {
|
|
700
|
+
errors = extraErrors
|
|
701
|
+
}
|
|
702
|
+
else if (e instanceof ErrorMapping) {
|
|
703
|
+
errors = e.errors
|
|
704
|
+
}
|
|
705
|
+
else { /* If an exception is thrown during validation, consider it a form error */
|
|
706
|
+
errors = {'': e}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
ObjectUtils.forEach(errors, function (name, error) {
|
|
710
|
+
|
|
711
|
+
/* Do not show error messages for unchanged fields */
|
|
712
|
+
if (name && !this._fieldsByName[name].isChanged()) {
|
|
713
|
+
errors[name] = ''
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
}, this)
|
|
717
|
+
|
|
718
|
+
/* Do not show form error unless at least one field has been changed */
|
|
719
|
+
if (this.getChangedFieldNames().length === 0) {
|
|
720
|
+
delete errors['']
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
this._clearErrors()
|
|
724
|
+
this._setErrors(errors)
|
|
725
|
+
|
|
726
|
+
if (this._isValid === true || this._isValid == null) { /* Form just became invalid */
|
|
727
|
+
this._isValid = false
|
|
728
|
+
this.onInvalid()
|
|
729
|
+
this.updateButtonsState()
|
|
730
|
+
this.updateFieldsState()
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
}.bind(this))
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Calls {@link qui.forms.Form#updateValidationState} asap, preventing multiple unnecessary successive calls.
|
|
738
|
+
*/
|
|
739
|
+
updateValidationStateASAP() {
|
|
740
|
+
if (this._updateValidationStateASAPHandle) {
|
|
741
|
+
clearTimeout(this._updateValidationStateASAPHandle)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
this._updateValidationStateASAPHandle = asap(function () {
|
|
745
|
+
|
|
746
|
+
this._updateValidationStateASAPHandle = null
|
|
747
|
+
this.updateValidationState()
|
|
748
|
+
|
|
749
|
+
}.bind(this))
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Return the current value of a field, in a promise, after making sure the entire form is valid.
|
|
754
|
+
* @returns {Promise<*>} a promise that is resolved with the field value, if the form is valid, and is rejected, if
|
|
755
|
+
* the form is invalid
|
|
756
|
+
*/
|
|
757
|
+
getFieldValue(fieldName) {
|
|
758
|
+
return this._validateData(fieldName).catch(function (error) {
|
|
759
|
+
return Promise.reject(error)
|
|
760
|
+
})
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Return the current value of a field without performing any validation.
|
|
765
|
+
* @returns {?*} the current field value or `null` if no such field is found
|
|
766
|
+
*/
|
|
767
|
+
getUnvalidatedFieldValue(fieldName) {
|
|
768
|
+
let field = this._fieldsByName[fieldName]
|
|
769
|
+
if (!field) {
|
|
770
|
+
return null
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return field.getValue()
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Return the current form data in a promise, after making sure it is valid.
|
|
778
|
+
* @returns {Promise} a promise that is resolved with a dictionary of field values, if the form is valid, and is
|
|
779
|
+
* rejected with a dictionary of errors associated to invalid fields, if the form is invalid
|
|
780
|
+
*/
|
|
781
|
+
getData() {
|
|
782
|
+
return this._validateData()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Return the current form data without validating fields or the form.
|
|
787
|
+
* @returns {Object} a dictionary with field values
|
|
788
|
+
*/
|
|
789
|
+
getUnvalidatedData() {
|
|
790
|
+
return ObjectUtils.mapValue(this._fieldsByName, function (field) {
|
|
791
|
+
return field.getValue()
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Update the form data.
|
|
797
|
+
* @param {Object} data the new form data
|
|
798
|
+
*/
|
|
799
|
+
setData(data) {
|
|
800
|
+
/* Ensure HTML is created */
|
|
801
|
+
this.getHTML()
|
|
802
|
+
|
|
803
|
+
this._clearValidationCache()
|
|
804
|
+
|
|
805
|
+
ObjectUtils.forEach(data, function (name, value) {
|
|
806
|
+
|
|
807
|
+
let field = this._fieldsByName[name]
|
|
808
|
+
if (!field) {
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
field.setValue(value)
|
|
813
|
+
|
|
814
|
+
}.bind(this))
|
|
815
|
+
|
|
816
|
+
if (this._continuousValidation) {
|
|
817
|
+
this.updateValidationState()
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Return the names of the fields whose values have been changed.
|
|
823
|
+
* @returns {String[]}
|
|
824
|
+
*/
|
|
825
|
+
getChangedFieldNames() {
|
|
826
|
+
return this._fields.filter(f => f.isChanged()).map(f => f.getName())
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Called whenever a field value changes.
|
|
831
|
+
* @param {Object} data the *unvalidated* form data
|
|
832
|
+
* @param {String} fieldName the name of the field that was changed
|
|
833
|
+
*/
|
|
834
|
+
onChange(data, fieldName) {
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Called whenever a field value changes and the form is entirely valid. This method is only called for forms with
|
|
839
|
+
* `continuousValidation` set to `true`.
|
|
840
|
+
* @param {Object} data the validated form data
|
|
841
|
+
* @param {String} fieldName the name of the field that was changed
|
|
842
|
+
*/
|
|
843
|
+
onChangeValid(data, fieldName) {
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Called when the form data becomes valid.
|
|
848
|
+
* @param {Object} data the form data
|
|
849
|
+
*/
|
|
850
|
+
onValid(data) {
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Called when the form data becomes invalid.
|
|
855
|
+
*/
|
|
856
|
+
onInvalid() {
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
/* Applying */
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* A function that applies the form data.
|
|
864
|
+
*
|
|
865
|
+
* If `null` or `undefined` is returned, the form data is considered applied right away. If a promise is returned,
|
|
866
|
+
* data is considered applied when the promise is resolved and {@link qui.forms.Form#setApplied} is called to mark
|
|
867
|
+
* the form as applied.
|
|
868
|
+
*
|
|
869
|
+
* A rejected promise will set a form error using {@link qui.views.ViewMixin#setError}. To set field errors, reject
|
|
870
|
+
* the returned promise with a {@link qui.forms.ErrorMapping}. A null error will simply cancel the application,
|
|
871
|
+
* without setting any error.
|
|
872
|
+
*
|
|
873
|
+
* Form will be put in progress using {@link qui.views.ViewMixin#setProgress} while promise is active.
|
|
874
|
+
*
|
|
875
|
+
* @param {Object} data form data to be applied
|
|
876
|
+
* @returns {?Promise<*,Error>}
|
|
877
|
+
* @throws Error errors while applying the data can also be thrown
|
|
878
|
+
*/
|
|
879
|
+
applyData(data) {
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Called when the user closes the form without applying the data.
|
|
884
|
+
*/
|
|
885
|
+
onCancel() {
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Implement this method to define how a field value is applied when it changes. This method can be used to
|
|
890
|
+
* implement continuous form saving, eliminating the need of a general form save button.
|
|
891
|
+
*
|
|
892
|
+
* For continuous form saving to work, the form attribute `continuousValidation` must be set to `true`.
|
|
893
|
+
*
|
|
894
|
+
* If a promise is returned, value is considered applied when the promise is resolved and
|
|
895
|
+
* {@link qui.forms.Form#setApplied} is called to mark the form as applied. A rejected promise will set a field
|
|
896
|
+
* error using {@link qui.views.ViewMixin#setError}.
|
|
897
|
+
*
|
|
898
|
+
* The field will be put in progress using {@link qui.views.ViewMixin#setProgress} while promise is active.
|
|
899
|
+
*
|
|
900
|
+
* Returning `null` or `undefined` indicates that values cannot be applied continuously for the given field (this is
|
|
901
|
+
* the default behaviour).
|
|
902
|
+
*
|
|
903
|
+
* @param {*} value the validated field value
|
|
904
|
+
* @param {String} fieldName the name of the field that was changed
|
|
905
|
+
* @returns {?Promise<*,Error>}
|
|
906
|
+
* @throws Error errors in applying the value can also be thrown
|
|
907
|
+
*/
|
|
908
|
+
applyField(value, fieldName) {
|
|
909
|
+
return null
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
_handleFieldChange(field) {
|
|
913
|
+
let name = field.getName()
|
|
914
|
+
|
|
915
|
+
/* If continuous validation is disabled, simply call onChange with corresponding arguments and return */
|
|
916
|
+
if (!this._continuousValidation) {
|
|
917
|
+
/* Gather raw (unvalidated) form data */
|
|
918
|
+
let data = ObjectUtils.mapValue(this._fieldsByName, function (field) {
|
|
919
|
+
return field.getValue()
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
this.onChange(data, name)
|
|
923
|
+
this.updateButtonsState()
|
|
924
|
+
this.updateFieldsState()
|
|
925
|
+
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/* Invalidate validation cache for this field, as well as the form's */
|
|
930
|
+
this.clearApplied()
|
|
931
|
+
this._clearValidationCache(name)
|
|
932
|
+
this._clearErrors(name)
|
|
933
|
+
this._validateData(name).then(function (value) {
|
|
934
|
+
|
|
935
|
+
let whenApplied = this.applyField(value, name)
|
|
936
|
+
if (whenApplied) {
|
|
937
|
+
field.setProgress()
|
|
938
|
+
whenApplied.then(function () {
|
|
939
|
+
field.setApplied()
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
return whenApplied
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
}.bind(this)).catch(function (error) {
|
|
946
|
+
|
|
947
|
+
field.setError(error)
|
|
948
|
+
|
|
949
|
+
return error /* Pass error further as a simple argument */
|
|
950
|
+
|
|
951
|
+
}).then(function (error) {
|
|
952
|
+
|
|
953
|
+
/* Gather raw (unvalidated) form data */
|
|
954
|
+
let data = ObjectUtils.mapValue(this._fieldsByName, function (field) {
|
|
955
|
+
return field.getValue()
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
this.onChange(data, name)
|
|
959
|
+
let extraErrors = error ? {[name]: error} : null
|
|
960
|
+
|
|
961
|
+
this.updateValidationState(extraErrors).then(function (data) {
|
|
962
|
+
|
|
963
|
+
if (this._isValid) {
|
|
964
|
+
this.onChangeValid(data, name)
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
}.bind(this))
|
|
968
|
+
|
|
969
|
+
}.bind(this))
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
/* Error state */
|
|
974
|
+
|
|
975
|
+
showError(message) {
|
|
976
|
+
if (message) {
|
|
977
|
+
this._errorDiv.find('span.qui-form-error-text').html(message)
|
|
978
|
+
this.getHTML().addClass('has-error')
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this._errorDiv[0].scrollIntoView({
|
|
982
|
+
behavior: 'smooth',
|
|
983
|
+
block: 'center',
|
|
984
|
+
inline: 'nearest'
|
|
985
|
+
})
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
hideError() {
|
|
989
|
+
this.getHTML().removeClass('has-error')
|
|
990
|
+
Theme.afterTransition(function () {
|
|
991
|
+
|
|
992
|
+
if (this.hasError()) {
|
|
993
|
+
return /* Error has been reshown */
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
this._errorDiv.find('span.qui-form-error-text').html('')
|
|
997
|
+
|
|
998
|
+
}.bind(this), this.getHTML())
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
/* Applied state */
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Put the form in the applied state. The view state is set to {@link qui.forms.STATE_APPLIED}.
|
|
1006
|
+
*/
|
|
1007
|
+
setApplied() {
|
|
1008
|
+
this.setState(STATE_APPLIED)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Put the form in the normal state, but only if the current state is {@link qui.forms.STATE_APPLIED}.
|
|
1013
|
+
*/
|
|
1014
|
+
clearApplied() {
|
|
1015
|
+
if (this.getState() === STATE_APPLIED) {
|
|
1016
|
+
this.setState(STATE_NORMAL)
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Tell if the form data has been applied (and thus there are no pending changes).
|
|
1022
|
+
* @returns {Boolean}
|
|
1023
|
+
*/
|
|
1024
|
+
isApplied() {
|
|
1025
|
+
/* A form is applied when either it is itself in applied state, or when all of its fields are applied
|
|
1026
|
+
* themselves */
|
|
1027
|
+
|
|
1028
|
+
if (this.getState() === STATE_APPLIED) {
|
|
1029
|
+
return true
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return this._fields.every(function (f) {
|
|
1033
|
+
return f.isApplied()
|
|
1034
|
+
})
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
/* Errors */
|
|
1039
|
+
|
|
1040
|
+
_clearErrors(fieldName) {
|
|
1041
|
+
if (!fieldName) {
|
|
1042
|
+
/* General form error */
|
|
1043
|
+
this.clearError()
|
|
1044
|
+
|
|
1045
|
+
/* Field errors */
|
|
1046
|
+
this.getFields().forEach(field => field.clearError())
|
|
1047
|
+
}
|
|
1048
|
+
else { /* Specific field error */
|
|
1049
|
+
let field = this.getField(fieldName)
|
|
1050
|
+
if (field) {
|
|
1051
|
+
field.clearError()
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
_setErrors(errors) {
|
|
1057
|
+
ObjectUtils.forEach(errors, function (name, msg) {
|
|
1058
|
+
if (name) {
|
|
1059
|
+
let field = this.getField(name)
|
|
1060
|
+
if (field) {
|
|
1061
|
+
field.setError(msg)
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
this.setError(msg)
|
|
1066
|
+
}
|
|
1067
|
+
}, this)
|
|
1068
|
+
|
|
1069
|
+
/* Always unminimize form upon errors */
|
|
1070
|
+
if (this.isMinimizable() && this.isMinimized()) {
|
|
1071
|
+
this.unminimize()
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
/* Actions */
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Execute the default form action. By default this is {@link qui.forms.Form#proceed}.
|
|
1080
|
+
*/
|
|
1081
|
+
defaultAction() {
|
|
1082
|
+
this.proceed()
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Execute the cancel form action. By default this is {@link qui.forms.Form#close}.
|
|
1087
|
+
*/
|
|
1088
|
+
cancelAction() {
|
|
1089
|
+
this.close(/* force = */ true)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Run the form by gathering, validating and applying form data.
|
|
1094
|
+
* @returns {Promise}
|
|
1095
|
+
*/
|
|
1096
|
+
proceed() {
|
|
1097
|
+
this.setProgress()
|
|
1098
|
+
|
|
1099
|
+
let dataValidated
|
|
1100
|
+
|
|
1101
|
+
if (!this._continuousValidation) {
|
|
1102
|
+
/* When continuous validation is disabled, we need to manually catch errors when form data is applied, and
|
|
1103
|
+
* display them onto the form */
|
|
1104
|
+
|
|
1105
|
+
this._clearValidationCache()
|
|
1106
|
+
this._clearErrors()
|
|
1107
|
+
|
|
1108
|
+
dataValidated = this._validateData().catch(function (e) {
|
|
1109
|
+
|
|
1110
|
+
let errorMapping = new ErrorMapping(e)
|
|
1111
|
+
this._clearErrors()
|
|
1112
|
+
this._setErrors(errorMapping.errors)
|
|
1113
|
+
|
|
1114
|
+
throw errorMapping
|
|
1115
|
+
|
|
1116
|
+
}.bind(this))
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
dataValidated = this._validateData()
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return dataValidated.then(function (data) {
|
|
1123
|
+
|
|
1124
|
+
let whenApplied
|
|
1125
|
+
try {
|
|
1126
|
+
whenApplied = this.applyData(data)
|
|
1127
|
+
}
|
|
1128
|
+
catch (e) {
|
|
1129
|
+
whenApplied = Promise.reject(e)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (!whenApplied) {
|
|
1133
|
+
whenApplied = Promise.resolve()
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
whenApplied.then(function () {
|
|
1137
|
+
|
|
1138
|
+
this.setApplied()
|
|
1139
|
+
this._fields.forEach(f => f.clearChanged())
|
|
1140
|
+
this.updateButtonsState()
|
|
1141
|
+
this.updateFieldsState()
|
|
1142
|
+
|
|
1143
|
+
if (this._closeOnApply) {
|
|
1144
|
+
if (!this.isClosed()) {
|
|
1145
|
+
this.close(/* force = */ true)
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
}.bind(this)).catch(function (e) {
|
|
1150
|
+
|
|
1151
|
+
this.clearProgress()
|
|
1152
|
+
|
|
1153
|
+
/* e may be null/undefined, in which case, the form apply has simply been cancelled */
|
|
1154
|
+
if (e) {
|
|
1155
|
+
let errorMapping = new ErrorMapping(e)
|
|
1156
|
+
this._clearErrors()
|
|
1157
|
+
this._setErrors(errorMapping.errors)
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
}.bind(this))
|
|
1161
|
+
|
|
1162
|
+
}.bind(this)).catch(function () {
|
|
1163
|
+
|
|
1164
|
+
this.clearProgress()
|
|
1165
|
+
|
|
1166
|
+
}.bind(this))
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Close the form.
|
|
1171
|
+
*/
|
|
1172
|
+
close() {
|
|
1173
|
+
if (!this.isApplied()) {
|
|
1174
|
+
this.onCancel()
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
export default Form
|