datafolder 1.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 (182) hide show
  1. package/README.md +169 -0
  2. package/index.js +3 -0
  3. package/package.json +27 -0
  4. package/sample.js +37 -0
  5. package/scripts/airbnb.js +73 -0
  6. package/scripts/airbnb2.js +74 -0
  7. package/scripts/airbnb2_queries.js +231 -0
  8. package/scripts/airbnb_queries.js +264 -0
  9. package/scripts/downloadFile.js +42 -0
  10. package/scripts/express.js +46 -0
  11. package/scripts/fastify.js +79 -0
  12. package/scripts/flights.js +75 -0
  13. package/scripts/flights2.js +81 -0
  14. package/scripts/flights2_queries.js +453 -0
  15. package/scripts/flights_queries.js +403 -0
  16. package/src/QyAcl.js +252 -0
  17. package/src/QyAclSaver.js +14 -0
  18. package/src/QyAclSaver_Worker.js +29 -0
  19. package/src/QyAclWriter.js +19 -0
  20. package/src/QyBatch.js +501 -0
  21. package/src/QyBinWriter.js +140 -0
  22. package/src/QyCD.js +61 -0
  23. package/src/QyCache.js +212 -0
  24. package/src/QyCompressor.js +60 -0
  25. package/src/QyCompressor_Worker.js +13 -0
  26. package/src/QyDB.js +122 -0
  27. package/src/QyDefaultOptions.js +59 -0
  28. package/src/QyDir.js +192 -0
  29. package/src/QyFetch.js +38 -0
  30. package/src/QyFile.js +124 -0
  31. package/src/QyFileContentUtils.js +175 -0
  32. package/src/QyFileLogger.js +58 -0
  33. package/src/QyIndex.js +237 -0
  34. package/src/QyIndexPropTree.js +92 -0
  35. package/src/QyIntersectNode.js +48 -0
  36. package/src/QyItem.js +105 -0
  37. package/src/QyJsonSaver.js +20 -0
  38. package/src/QyKVData.js +289 -0
  39. package/src/QyKVDataCleaner.js +17 -0
  40. package/src/QyKVDataCleaner_Worker.js +94 -0
  41. package/src/QyKVDataLoader.js +65 -0
  42. package/src/QyKVDataSaver.js +17 -0
  43. package/src/QyKVDataSaver_Worker.js +81 -0
  44. package/src/QyLogFileSaver.js +14 -0
  45. package/src/QyLogFileSaver_Worker.js +26 -0
  46. package/src/QyLogger.js +76 -0
  47. package/src/QyMessageWorker.js +41 -0
  48. package/src/QyMessager.js +84 -0
  49. package/src/QyMessager_Run.js +13 -0
  50. package/src/QyMod.js +15 -0
  51. package/src/QyOpRunner.js +73 -0
  52. package/src/QyQueue.js +60 -0
  53. package/src/QySaver.js +83 -0
  54. package/src/QySnapshotCompactor.js +17 -0
  55. package/src/QySnapshotCompactor_Worker.js +151 -0
  56. package/src/QySnapshotSaver.js +17 -0
  57. package/src/QySnapshotSaver_Worker.js +164 -0
  58. package/src/QySnapshots.js +354 -0
  59. package/src/QyStructure.js +50 -0
  60. package/src/QyTrigger.js +133 -0
  61. package/src/QyUnionNode.js +105 -0
  62. package/src/QyUtils.js +250 -0
  63. package/src/QyWorker.js +33 -0
  64. package/src/hackernews.js +93 -0
  65. package/src/rpc/evalCode.js +9 -0
  66. package/src/rpc/getFileContent.js +16 -0
  67. package/src/rpc/getSubItems.js +32 -0
  68. package/src/rpc/queryFiles.js +33 -0
  69. package/src/rpc/reloadRpc.js +1 -0
  70. package/src/simulate.js +114 -0
  71. package/src/simulate2.js +110 -0
  72. package/src/soc_pokec.js +43 -0
  73. package/static/bootstrap-5.3.8-dist/css/bootstrap.min.css +6 -0
  74. package/static/bootstrap-5.3.8-dist/js/bootstrap.bundle.js +6312 -0
  75. package/static/datafolderui/QyStyledElement.js +30 -0
  76. package/static/datafolderui/QyUIFolderTree.js +203 -0
  77. package/static/datafolderui/QyUIUtils.js +42 -0
  78. package/static/datafolderui/display-tree.js +108 -0
  79. package/static/datafolderui/explore-tree.js +122 -0
  80. package/static/datafolderui/folder-tree.js +5 -0
  81. package/static/datafolderui/hscrpt.js +25 -0
  82. package/static/datafolderui/index.css +6 -0
  83. package/static/datafolderui/index.html +20 -0
  84. package/static/datafolderui/json-view.js +74 -0
  85. package/static/datafolderui/kv-input.js +66 -0
  86. package/static/datafolderui/list-input.js +52 -0
  87. package/static/datafolderui/main-page.js +98 -0
  88. package/static/datafolderui/num-input.js +27 -0
  89. package/static/datafolderui/query-editor.js +229 -0
  90. package/static/datafolderui/querySelectorShadowRoot.js +24 -0
  91. package/static/datafolderui/resizable-input.js +67 -0
  92. package/static/favicon.ico +0 -0
  93. package/static/icons-1.13.1/font/bootstrap-icons.min.css +5 -0
  94. package/static/icons-1.13.1/font/fonts/bootstrap-icons.woff +0 -0
  95. package/static/icons-1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
  96. package/static/json-view/dist/jsonview.js +433 -0
  97. package/static/tree.js/tree.css +106 -0
  98. package/static/tree.js/tree.js +374 -0
  99. package/static/w2ui/dist/w2ui.min.css +2 -0
  100. package/static/w2ui/src/less/icons/icons.json +1 -0
  101. package/static/w2ui/src/less/icons/preview.html +33 -0
  102. package/static/w2ui/src/less/icons/readme.md +15 -0
  103. package/static/w2ui/src/less/icons/svg/box.svg +3 -0
  104. package/static/w2ui/src/less/icons/svg/check.svg +3 -0
  105. package/static/w2ui/src/less/icons/svg/colors.svg +3 -0
  106. package/static/w2ui/src/less/icons/svg/columns.svg +5 -0
  107. package/static/w2ui/src/less/icons/svg/cross.svg +4 -0
  108. package/static/w2ui/src/less/icons/svg/drop.svg +6 -0
  109. package/static/w2ui/src/less/icons/svg/empty.svg +3 -0
  110. package/static/w2ui/src/less/icons/svg/eye-dropper.svg +4 -0
  111. package/static/w2ui/src/less/icons/svg/info.svg +8 -0
  112. package/static/w2ui/src/less/icons/svg/paste.svg +4 -0
  113. package/static/w2ui/src/less/icons/svg/pencil.svg +5 -0
  114. package/static/w2ui/src/less/icons/svg/plus.svg +3 -0
  115. package/static/w2ui/src/less/icons/svg/reload.svg +5 -0
  116. package/static/w2ui/src/less/icons/svg/search.svg +3 -0
  117. package/static/w2ui/src/less/icons/svg/settings.svg +8 -0
  118. package/static/w2ui/src/less/icons/w2ui-font.css +36 -0
  119. package/static/w2ui/src/less/icons/w2ui-font.woff +0 -0
  120. package/static/w2ui/src/less/src/buttons.less +184 -0
  121. package/static/w2ui/src/less/src/common.less +541 -0
  122. package/static/w2ui/src/less/src/fields.less +302 -0
  123. package/static/w2ui/src/less/src/form.less +319 -0
  124. package/static/w2ui/src/less/src/grid.less +1057 -0
  125. package/static/w2ui/src/less/src/icons.less +37 -0
  126. package/static/w2ui/src/less/src/layout.less +86 -0
  127. package/static/w2ui/src/less/src/mixins.less +87 -0
  128. package/static/w2ui/src/less/src/popup.less +277 -0
  129. package/static/w2ui/src/less/src/sidebar.less +343 -0
  130. package/static/w2ui/src/less/src/tabs.less +145 -0
  131. package/static/w2ui/src/less/src/toolbar.less +219 -0
  132. package/static/w2ui/src/less/src/tooltip.less +948 -0
  133. package/static/w2ui/src/less/w2ui.less +196 -0
  134. package/static/w2ui/src/locale/az-az.json +114 -0
  135. package/static/w2ui/src/locale/ba-ba.json +114 -0
  136. package/static/w2ui/src/locale/bg-bg.json +114 -0
  137. package/static/w2ui/src/locale/ca-es.json +114 -0
  138. package/static/w2ui/src/locale/da-dk.json +114 -0
  139. package/static/w2ui/src/locale/de-de.json +114 -0
  140. package/static/w2ui/src/locale/en-gb.json +17 -0
  141. package/static/w2ui/src/locale/en-us.json +17 -0
  142. package/static/w2ui/src/locale/es-ar.json +115 -0
  143. package/static/w2ui/src/locale/es-cr.json +119 -0
  144. package/static/w2ui/src/locale/es-es.json +114 -0
  145. package/static/w2ui/src/locale/es-mx.json +114 -0
  146. package/static/w2ui/src/locale/fr-fr.json +114 -0
  147. package/static/w2ui/src/locale/gl-es.json +114 -0
  148. package/static/w2ui/src/locale/hr-hr.json +114 -0
  149. package/static/w2ui/src/locale/hu-hu.json +114 -0
  150. package/static/w2ui/src/locale/id-id.json +114 -0
  151. package/static/w2ui/src/locale/it-it.json +114 -0
  152. package/static/w2ui/src/locale/ja-jp.json +114 -0
  153. package/static/w2ui/src/locale/kk-kz.json +116 -0
  154. package/static/w2ui/src/locale/ko-kr.json +114 -0
  155. package/static/w2ui/src/locale/lt-lt.json +114 -0
  156. package/static/w2ui/src/locale/nl-nl.json +114 -0
  157. package/static/w2ui/src/locale/no-no.json +114 -0
  158. package/static/w2ui/src/locale/pl-pl.json +114 -0
  159. package/static/w2ui/src/locale/pt-br.json +114 -0
  160. package/static/w2ui/src/locale/ru-ru.json +116 -0
  161. package/static/w2ui/src/locale/sk-sk.json +114 -0
  162. package/static/w2ui/src/locale/sl-si.json +116 -0
  163. package/static/w2ui/src/locale/sr-rs.json +114 -0
  164. package/static/w2ui/src/locale/tr-tr.json +114 -0
  165. package/static/w2ui/src/locale/uk-ua.json +114 -0
  166. package/static/w2ui/src/locale/zh-cn.json +114 -0
  167. package/static/w2ui/src/locale/zh-tw.json +114 -0
  168. package/static/w2ui/src/query.js +583 -0
  169. package/static/w2ui/src/readme.md +11 -0
  170. package/static/w2ui/src/w2base.js +311 -0
  171. package/static/w2ui/src/w2compat.js +207 -0
  172. package/static/w2ui/src/w2field.js +1854 -0
  173. package/static/w2ui/src/w2form.js +2411 -0
  174. package/static/w2ui/src/w2grid.js +8916 -0
  175. package/static/w2ui/src/w2layout.js +1194 -0
  176. package/static/w2ui/src/w2locale.js +129 -0
  177. package/static/w2ui/src/w2popup.js +909 -0
  178. package/static/w2ui/src/w2sidebar.js +1769 -0
  179. package/static/w2ui/src/w2tabs.js +669 -0
  180. package/static/w2ui/src/w2toolbar.js +1180 -0
  181. package/static/w2ui/src/w2tooltip.js +3134 -0
  182. package/static/w2ui/src/w2utils.js +2457 -0
@@ -0,0 +1,2411 @@
1
+ /**
2
+ * Part of w2ui 2.0 library
3
+ * - Dependencies: mQuery, w2utils, w2base, w2tabs, w2toolbar, w2tooltip, w2field
4
+ *
5
+ * == TODO ==
6
+ * - include delta on save
7
+ * - tabs below some fields (could already be implemented)
8
+ * - form with toolbar & tabs
9
+ * - promise for load, save, etc.
10
+ *
11
+ * == 2.0 changes
12
+ * - CSP - fixed inline events
13
+ * - removed jQuery dependency
14
+ * - better groups support tabs now
15
+ * - form.confirm - refactored
16
+ * - form.message - refactored
17
+ * - observeResize for the box
18
+ * - removed msgNotJSON, msgAJAXerror
19
+ * - applyFocus -> setFocus
20
+ * - getFieldValue(fieldName) = returns { curent, previous, original }
21
+ * - setFieldVallue(fieldName, value)
22
+ * - getValue(..., original) -- return original if any
23
+ * - added .hideErrors()
24
+ * - reuqest, save, submit - return promises
25
+ * - this.recid = null if no record needs to be pulled
26
+ * - remove form.multiplart
27
+ * - this.method - for saving only
28
+ * - added field.html.class
29
+ * - setValue(..., noRefresh)
30
+ * - rememberOriginal()
31
+ * - saveCleanRecord
32
+ * - added options.itemMap = { id: 'id', text: 'text' } - to map id, text fields if needed
33
+ * - hideGroup/showGroup - new methods
34
+ * - getAction/actionHide/actionShow/actionDisable/actionEnable - new methods
35
+ */
36
+
37
+ import { w2base } from './w2base.js'
38
+ import { w2ui, w2utils } from './w2utils.js'
39
+ import { query } from './query.js'
40
+ import { w2tabs } from './w2tabs.js'
41
+ import { w2toolbar } from './w2toolbar.js'
42
+ import { w2tooltip } from './w2tooltip.js'
43
+ import { w2field } from './w2field.js'
44
+
45
+ class w2form extends w2base {
46
+ constructor(options) {
47
+ super(options.name)
48
+ this.name = null
49
+ this.header = ''
50
+ this.box = null // HTML element that hold this element
51
+ this.url = ''
52
+ this.method = null // if defined, it will be http method when saving
53
+ this.routeData = {} // data for dynamic routes
54
+ this.formURL = '' // url where to get form HTML
55
+ this.formHTML = '' // form HTML (might be loaded from the url)
56
+ this.page = 0 // current page
57
+ this.pageStyle = ''
58
+ this.recid = null // if not null, then load record
59
+ this.fields = []
60
+ this.actions = {}
61
+ this.record = {}
62
+ this.original = null
63
+ this.dataType = null // only used when not null, otherwise from w2utils.settings.dataType
64
+ this.saveCleanRecord = true // if true, it will submit clean record when saving
65
+ this.postData = {}
66
+ this.httpHeaders = {}
67
+ this.toolbar = {} // if not empty, then it is toolbar
68
+ this.tabs = {} // if not empty, then it is tabs object
69
+ this.style = ''
70
+ this.focus = 0 // focus first or other element
71
+ this.autosize = true // autosize, if false the container must have a height set
72
+ this.nestedFields = true // use field name containing dots as separator to look into object
73
+ this.tabindexBase = 0 // this will be added to the auto numbering
74
+ this.isGenerated = false
75
+ this.last = {
76
+ fetchCtrl: null, // last fetch AbortController
77
+ fetchOptions: null, // last fetch options
78
+ errors: []
79
+ }
80
+ this.onRequest = null
81
+ this.onLoad = null
82
+ this.onValidate = null
83
+ this.onSubmit = null
84
+ this.onProgress = null
85
+ this.onSave = null
86
+ this.onChange = null
87
+ this.onInput = null
88
+ this.onRender = null
89
+ this.onRefresh = null
90
+ this.onResize = null
91
+ this.onDestroy = null
92
+ this.onAction = null
93
+ this.onToolbar = null
94
+ this.onError = null
95
+ this.msgRefresh = 'Loading...'
96
+ this.msgSaving = 'Saving...'
97
+ this.msgServerError = 'Server error'
98
+ this.ALL_TYPES = [ 'text', 'textarea', 'email', 'pass', 'password', 'int', 'float', 'money', 'currency',
99
+ 'percent', 'hex', 'alphanumeric', 'color', 'date', 'time', 'datetime', 'toggle', 'checkbox', 'radio',
100
+ 'check', 'checks', 'list', 'combo', 'enum', 'file', 'select', 'switch', 'map', 'array', 'div', 'custom', 'html',
101
+ 'empty', 'columns']
102
+ this.LIST_TYPES = ['select', 'radio', 'check', 'checks', 'list', 'combo', 'enum', 'switch']
103
+ this.W2FIELD_TYPES = ['int', 'float', 'money', 'currency', 'percent', 'hex', 'alphanumeric', 'color',
104
+ 'date', 'time', 'datetime', 'list', 'combo', 'enum', 'file']
105
+ // mix in options
106
+ w2utils.extend(this, options)
107
+
108
+ // remember items
109
+ let record = options.record
110
+ let original = options.original
111
+ let fields = options.fields
112
+ let toolbar = options.toolbar
113
+ let tabs = options.tabs
114
+ // extend items
115
+ Object.assign(this, { record: {}, original: null, fields: [], tabs: {}, toolbar: {}, handlers: [] })
116
+ // preprocess fields
117
+ if (fields) {
118
+ let sub =_processFields(fields)
119
+ this.fields = sub.fields
120
+ if (!tabs && sub.tabs.length > 0) {
121
+ tabs = sub.tabs
122
+ }
123
+ }
124
+ // prepare tabs
125
+ if (Array.isArray(tabs)) {
126
+ w2utils.extend(this.tabs, { tabs: [] })
127
+ for (let t = 0; t < tabs.length; t++) {
128
+ let tmp = tabs[t]
129
+ if (typeof tmp === 'object') {
130
+ this.tabs.tabs.push(tmp)
131
+ if (tmp.active === true) {
132
+ this.tabs.active = tmp.id
133
+ }
134
+ } else {
135
+ this.tabs.tabs.push({ id: tmp, text: tmp })
136
+ }
137
+ }
138
+ } else {
139
+ w2utils.extend(this.tabs, tabs)
140
+ }
141
+ w2utils.extend(this.toolbar, toolbar)
142
+ for (let p in record) { // it is an object
143
+ if (w2utils.isPlainObject(record[p])) {
144
+ this.record[p] = w2utils.clone(record[p])
145
+ } else {
146
+ this.record[p] = record[p]
147
+ }
148
+ }
149
+ for (let p in original) { // it is an object
150
+ if (w2utils.isPlainObject(original[p])) {
151
+ this.original[p] = w2utils.clone(original[p])
152
+ } else {
153
+ this.original[p] = original[p]
154
+ }
155
+ }
156
+ // generate html if necessary
157
+ if (this.formURL !== '') {
158
+ fetch(this.formURL)
159
+ .then(resp => resp.text())
160
+ .then(text => {
161
+ this.formHTML = text
162
+ this.isGenerated = true
163
+ if (this.box) this.render(this.box)
164
+ })
165
+ } else if (!this.formURL && !this.formHTML) {
166
+ this.formHTML = this.generateHTML()
167
+ this.isGenerated = true
168
+ } else if (this.formHTML) {
169
+ this.isGenerated = true
170
+ }
171
+
172
+ // render if box specified
173
+ if (typeof this.box == 'string') this.box = query(this.box).get(0)
174
+ if (this.box) this.render(this.box)
175
+
176
+ function _processFields(fields) {
177
+ let newFields = []
178
+ let tabs = []
179
+ // if it is an object
180
+ if (w2utils.isPlainObject(fields)) {
181
+ let tmp = fields
182
+ fields = []
183
+ Object.keys(tmp).forEach((key) => {
184
+ let fld = tmp[key]
185
+ if (fld.type == 'group') {
186
+ fld.text = key
187
+ if (w2utils.isPlainObject(fld.fields)) {
188
+ let tmp2 = fld.fields
189
+ fld.fields = []
190
+ Object.keys(tmp2).forEach((key2) => {
191
+ let fld2 = tmp2[key2]
192
+ fld2.field = key2
193
+ fld.fields.push(_process(fld2))
194
+
195
+ })
196
+ }
197
+ fields.push(fld)
198
+ } else if (fld.type == 'tab') {
199
+ // add tab
200
+ let tab = { id: key, text: key }
201
+ if (fld.style) {
202
+ tab.style = fld.style
203
+ }
204
+ tabs.push(tab)
205
+ // add page to fields
206
+ let sub = _processFields(fld.fields).fields
207
+ sub.forEach(fld2 => {
208
+ fld2.html = fld2.html || {}
209
+ fld2.html.page = tabs.length -1
210
+ _process2(fld, fld2)
211
+ })
212
+ fields.push(...sub)
213
+ } else {
214
+ fld.field = key
215
+ fields.push(_process(fld))
216
+ }
217
+ })
218
+
219
+ function _process(fld) {
220
+ let ignore = ['html']
221
+ if (fld.html == null) fld.html = {}
222
+ Object.keys(fld).forEach((key => {
223
+ if (ignore.indexOf(key) != -1) return
224
+ if (['label', 'attr', 'style', 'text', 'span', 'page', 'column', 'anchor',
225
+ 'group', 'groupStyle', 'groupTitleStyle', 'groupCollapsible'].indexOf(key) != -1) {
226
+ fld.html[key] = fld[key]
227
+ delete fld[key]
228
+ }
229
+ }))
230
+ return fld
231
+ }
232
+
233
+ function _process2(fld, fld2) {
234
+ let ignore = ['style', 'html']
235
+ Object.keys(fld).forEach((key => {
236
+ if (ignore.indexOf(key) != -1) return
237
+ if (['span', 'column', 'attr', 'text', 'label'].indexOf(key) != -1) {
238
+ if (fld[key] && !fld2.html[key]) {
239
+ fld2.html[key] = fld[key]
240
+ }
241
+ }
242
+ }))
243
+ }
244
+ }
245
+ // process groups
246
+ fields.forEach(field => {
247
+ if (field.type == 'group') {
248
+ // group properties
249
+ let group = {
250
+ group: field.text || '',
251
+ groupStyle: field.style || '',
252
+ groupTitleStyle: field.titleStyle || '',
253
+ groupCollapsible: field.collapsible === true ? true : false,
254
+ }
255
+ // loop through fields
256
+ if (Array.isArray(field.fields)) {
257
+ field.fields.forEach(gfield => {
258
+ let fld = w2utils.clone(gfield)
259
+ if (fld.html == null) fld.html = {}
260
+ w2utils.extend(fld.html, group)
261
+ Array('span', 'column', 'attr', 'label', 'page').forEach(key => {
262
+ if (fld.html[key] == null && field[key] != null) {
263
+ fld.html[key] = field[key]
264
+ }
265
+ })
266
+ if (fld.field == null && fld.name != null) {
267
+ console.log('NOTICE: form field.name property is deprecated, please use field.field. Field ->', field)
268
+ fld.field = fld.name
269
+ }
270
+ newFields.push(fld)
271
+ })
272
+ }
273
+ } else {
274
+ let fld = w2utils.clone(field)
275
+ if (fld.field == null && fld.name != null) {
276
+ console.log('NOTICE: form field.name property is deprecated, please use field.field. Field ->', field)
277
+ fld.field = fld.name
278
+ }
279
+ newFields.push(fld)
280
+ }
281
+ })
282
+ return { fields: newFields, tabs }
283
+ }
284
+ }
285
+
286
+ get(field, returnIndex) {
287
+ if (arguments.length === 0) {
288
+ let all = []
289
+ for (let f1 = 0; f1 < this.fields.length; f1++) {
290
+ if (this.fields[f1].field != null) all.push(this.fields[f1].field)
291
+ }
292
+ return all
293
+ } else {
294
+ for (let f2 = 0; f2 < this.fields.length; f2++) {
295
+ if (this.fields[f2].field == field) {
296
+ if (returnIndex === true) return f2; else return this.fields[f2]
297
+ }
298
+ }
299
+ return null
300
+ }
301
+ }
302
+
303
+ set(field, obj) {
304
+ for (let f = 0; f < this.fields.length; f++) {
305
+ if (this.fields[f].field == field) {
306
+ w2utils.extend(this.fields[f] , obj)
307
+ delete this.fields[f].w2field // otherwise options are not updates
308
+ this.refresh(field)
309
+ return true
310
+ }
311
+ }
312
+ return false
313
+ }
314
+
315
+ getValue(field, original) {
316
+ if (this.nestedFields) {
317
+ let val = undefined
318
+ try { // need this to make sure no error in fields
319
+ let rec = original === true ? this.original : this.record
320
+ val = String(field).split('.').reduce((rec, i) => { return rec[i] }, rec)
321
+ } catch (event) {
322
+ }
323
+ return val
324
+ } else {
325
+ return this.record[field]
326
+ }
327
+ }
328
+
329
+ setValue(field, value, noRefresh) {
330
+ // will not refresh the form!
331
+ if (value === '' || value == null
332
+ || (Array.isArray(value) && value.length === 0)
333
+ || (w2utils.isPlainObject(value) && Object.keys(value).length == 0)) {
334
+ value = null
335
+ }
336
+ if (this.nestedFields) {
337
+ try { // need this to make sure no error in fields
338
+ let rec = this.record
339
+ String(field).split('.').map((fld, i, arr) => {
340
+ if (arr.length - 1 !== i) {
341
+ if (rec[fld]) rec = rec[fld]; else { rec[fld] = {}; rec = rec[fld] }
342
+ } else {
343
+ rec[fld] = value
344
+ }
345
+ })
346
+ if (!noRefresh) this.setFieldValue(field, value)
347
+ return true
348
+ } catch (event) {
349
+ return false
350
+ }
351
+ } else {
352
+ this.record[field] = value
353
+ if (!noRefresh) this.setFieldValue(field, value)
354
+ return true
355
+ }
356
+ }
357
+
358
+ rememberOriginal() {
359
+ // remember original
360
+ if (this.original == null) {
361
+ if (Object.keys(this.record).length > 0) {
362
+ this.original = w2utils.clone(this.record)
363
+ } else {
364
+ this.original = {}
365
+ }
366
+ }
367
+ }
368
+
369
+ getFieldValue(name) {
370
+ let field = this.get(name)
371
+ if (field == null) return
372
+ let el = field.el
373
+ let previous = this.getValue(name)
374
+ let original = this.getValue(name, true)
375
+ // orginary input control
376
+ let current = el.value
377
+ // should not be set to '', incosistent logic
378
+ // if (previous == null) previous = ''
379
+
380
+ // clean extra chars
381
+ if (['int', 'float', 'percent', 'money', 'currency'].includes(field.type)) {
382
+ current = field.w2field.clean(current)
383
+ }
384
+ // radio list
385
+ if (['radio'].includes(field.type)) {
386
+ let selected = query(el).closest('.w2ui-field-group').find('input:checked').get(0)
387
+ if (selected) {
388
+ let item = field.options.items[query(selected).data('index')]
389
+ current = item.id
390
+ } else {
391
+ current = null
392
+ }
393
+ }
394
+ // single checkbox
395
+ if (['toggle', 'checkbox'].includes(field.type)) {
396
+ current = el.checked
397
+ }
398
+ // check list
399
+ if (['check', 'checks'].indexOf(field.type) !== -1) {
400
+ current = []
401
+ let selected = query(el).closest('.w2ui-field-group').find('input:checked')
402
+ if (selected.length > 0) {
403
+ selected.each(el => {
404
+ let item = field.options.items[query(el).data('index')]
405
+ current.push(item.id)
406
+ })
407
+ }
408
+ if (!Array.isArray(previous)) previous = []
409
+ }
410
+ // lists
411
+ let selected = field.w2field?.selected // drop downs and other w2field objects
412
+ if (['list', 'enum', 'file'].includes(field.type) && selected) {
413
+ let nv = selected
414
+ let cv = previous
415
+ if (Array.isArray(nv)) {
416
+ current = []
417
+ for (let i = 0; i < nv.length; i++) current[i] = w2utils.clone(nv[i]) // clone array
418
+ }
419
+ if (Array.isArray(cv)) {
420
+ previous = []
421
+ for (let i = 0; i < cv.length; i++) previous[i] = w2utils.clone(cv[i]) // clone array
422
+ }
423
+ if (w2utils.isPlainObject(nv)) {
424
+ current = w2utils.clone(nv) // clone object
425
+ }
426
+ if (w2utils.isPlainObject(cv)) {
427
+ previous = w2utils.clone(cv) // clone object
428
+ }
429
+ }
430
+ // map, array
431
+ if (['map', 'array'].includes(field.type)) {
432
+ current = (field.type == 'map' ? {} : [])
433
+ field.$el.parent().find('.w2ui-map-field').each((div, ind) => {
434
+ let key = query(div).find('.w2ui-map.key').val()
435
+ let value = query(div).find('.w2ui-map.value').val()
436
+ if (typeof field.html?.render == 'function') {
437
+ current[ind] ??= {}
438
+ query(div).find('input').each(inp => {
439
+ let name = inp.dataset.name ?? inp.name
440
+ if (name != null && name != '') {
441
+ current[ind][name] = ['checkbox', 'radio'].includes(inp.type) ? inp.checked : inp.value
442
+ }
443
+ })
444
+ } else if (field.type == 'map') {
445
+ current[key] = value
446
+ } else {
447
+ current.push(value)
448
+ }
449
+ })
450
+ }
451
+ return { current, previous, original } // current - in input, previous - in form.record, original - before form change
452
+ }
453
+
454
+ findItem(item, items) {
455
+ return items.find(it => (it.id === item || it.id === item?.id))
456
+ }
457
+
458
+ setFieldValue(name, value) {
459
+ let field = this.get(name)
460
+ if (field == null) return
461
+ let el = field.el
462
+ switch (field.type) {
463
+ case 'toggle':
464
+ case 'checkbox': {
465
+ el.checked = value ? true : false
466
+ break
467
+ }
468
+ case 'radio': {
469
+ value = value?.id ?? value
470
+ let inputs = query(el).closest('.w2ui-field-group').find('input')
471
+ let items = field.options.items
472
+ items.forEach((it, ind) => {
473
+ let input = inputs.filter(`[data-index="${ind}"]`)
474
+ if (it.id === value) { // need exact match so to match empty string and 0
475
+ input.prop('checked', true)
476
+ } else {
477
+ input.prop('checked', false)
478
+ }
479
+ // show or hide the whole line
480
+ if (it.hidden === true) {
481
+ input.closest('.w2ui-field-item').hide()
482
+ } else {
483
+ input.closest('.w2ui-field-item').show()
484
+ }
485
+ })
486
+ break
487
+ }
488
+ case 'check':
489
+ case 'checks': {
490
+ if (!Array.isArray(value)) {
491
+ if (value != null) {
492
+ value = [value]
493
+ } else {
494
+ value = []
495
+ }
496
+ }
497
+ value = value.map(val => val?.id ?? val) // convert if array of objects
498
+ let inputs = query(el).closest('div.w2ui-field-group').find('input')
499
+ let items = field.options.items
500
+ items.forEach((it, ind) => {
501
+ let input = inputs.filter(`[data-index="${ind}"]`)
502
+ input.prop('checked', value.includes(it.id) ? true : false)
503
+ // show or hide the whole line
504
+ if (it.hidden === true) {
505
+ input.closest('.w2ui-field-item').hide()
506
+ } else {
507
+ input.closest('.w2ui-field-item').show()
508
+ }
509
+ })
510
+ break
511
+ }
512
+ case 'list':
513
+ case 'combo':
514
+ let item = value
515
+ let map = field.options?.itemMap
516
+ /**
517
+ * if it is a "simple" value, then find item in options.items
518
+ */
519
+ if (item?.id == null && Array.isArray(field.options?.items)) {
520
+ field.options.items.forEach(it => {
521
+ let val = w2utils.getNested(it, map?.id ?? 'id')
522
+ if (val === value) item = it
523
+ })
524
+ }
525
+ /**
526
+ * If item.id is there, but item.text is not there, then look up item.text in options.items
527
+ */
528
+ if (item?.id != null && item?.text == null && Array.isArray(field.options?.items)) {
529
+ field.options.items.forEach(it => {
530
+ let id = w2utils.getNested(it, map?.id ?? 'id')
531
+ if (id === item.id) {
532
+ item.text = w2utils.getNested(it, map.text ?? 'text')
533
+ }
534
+ })
535
+ }
536
+ // if item is found in field.options, update it in the this.records
537
+ if (item != value) {
538
+ this.setValue(field.name, item, true)
539
+ }
540
+ if (field.type == 'list') {
541
+ field.w2field.selected = item
542
+ field.w2field.refresh()
543
+ } else {
544
+ field.el.value = item?.text ?? value
545
+ }
546
+ break
547
+ case 'switch': {
548
+ el.value = value
549
+ field.toolbar.uncheck(...field.toolbar.get())
550
+ field.toolbar.check(value)
551
+ break
552
+ }
553
+ case 'enum':
554
+ case 'file': {
555
+ if (!Array.isArray(value)) {
556
+ value = value != null ? [value] : []
557
+ }
558
+ let items = [...value]
559
+ // find item in options.items, if any
560
+ let updated = false
561
+ items.forEach((item, ind) => {
562
+ if (item?.id == null && Array.isArray(field.options.items)) {
563
+ field.options.items.forEach(it => {
564
+ if (it.id == item) {
565
+ items[ind] = it
566
+ updated = true
567
+ }
568
+ })
569
+ }
570
+ })
571
+ if (updated) {
572
+ this.setValue(field.name, items, true)
573
+ }
574
+ field.w2field.selected = items
575
+ field.w2field.refresh()
576
+ break
577
+ }
578
+ case 'map':
579
+ case 'array': {
580
+ // init map
581
+ if (field.type == 'map' && (value == null || !w2utils.isPlainObject(value))) {
582
+ this.setValue(field.field, {}, true)
583
+ value = this.getValue(field.field)
584
+ }
585
+ if (field.type == 'array' && (value == null || !Array.isArray(value))) {
586
+ this.setValue(field.field, [], true)
587
+ value = this.getValue(field.field)
588
+ }
589
+ let container = query(field.el).parent().find('.w2ui-map-container')
590
+ field.el.mapRefresh(value, container)
591
+ break
592
+ }
593
+ case 'div':
594
+ case 'custom': {
595
+ query(el).html(value)
596
+ break
597
+ }
598
+ case 'color': {
599
+ el.value = value ?? ''
600
+ field.w2field.refresh()
601
+ break
602
+ }
603
+ case 'html':
604
+ case 'empty':
605
+ break
606
+ default:
607
+ // all other fields, text, int, float
608
+ if (value != null && el._w2field?.format) {
609
+ let obj = el._w2field
610
+ value = obj.format(obj.clean(value))
611
+ }
612
+ el.value = value ?? ''
613
+ break
614
+ }
615
+ // now go through all fields and see if there is a dependent field
616
+ this.fields.forEach(fld => {
617
+ if (fld?.options?.parentList != null) {
618
+ let updated
619
+ let values = this.getValue(fld.options.parentList)
620
+ if (Array.isArray(values)) {
621
+ values = values.map(vv => vv.id)
622
+ } else {
623
+ values = values?.id != null ? [values.id] : []
624
+ }
625
+ fld.options?.items?.forEach?.(item => {
626
+ let parent = w2utils.getNested(item, fld.options.parentField ?? 'parentId')
627
+ if (parent == null) {
628
+ return
629
+ }
630
+ let possible = w2utils.clone(Array.isArray(parent) ? parent : [parent])
631
+ possible.unshift('')
632
+ let includes = values.some(item => possible.includes(item))
633
+ if (includes && item.hidden === true) {
634
+ item.hidden = false
635
+ updated = true
636
+ } else if (!includes && item.hidden !== true) {
637
+ item.hidden = true
638
+ updated = true
639
+ }
640
+ })
641
+ if (updated) {
642
+ let value = this.getValue(fld.field)
643
+ if (value?.id != null) value = value.id
644
+ // if item is not visible, then clear its field
645
+ if (fld.type == 'enum') {
646
+ let valid = fld.options.items.filter(it => !it.hidden).map(it => it.id)
647
+ let values = this.getValue(fld.field)
648
+ if (!Array.isArray(values)) values = [values]
649
+ // make sure they are objects
650
+ values = values.map(it => {
651
+ if (typeof it == 'string' || typeof it == 'number') {
652
+ it = fld.options.items.find(ii => ii.id == it)
653
+ }
654
+ return it
655
+ })
656
+ let new_values = values.filter(it => valid.includes(it.id))
657
+ this.setValue(fld.field, new_values, true)
658
+ } else {
659
+ fld.options.items.forEach(it => {
660
+ if (it.id == value && it.hidden) {
661
+ this.setValue(fld.field, null, true)
662
+ }
663
+ })
664
+ }
665
+ // set new items
666
+ this.set(fld.field, { items: fld.options.items })
667
+ }
668
+ }
669
+ })
670
+ }
671
+
672
+ show() {
673
+ let effected = []
674
+ for (let a = 0; a < arguments.length; a++) {
675
+ let fld = this.get(arguments[a])
676
+ if (fld && fld.hidden) {
677
+ fld.hidden = false
678
+ effected.push(fld.field)
679
+ }
680
+ }
681
+ if (effected.length > 0) this.refresh.apply(this, effected)
682
+ this.updateEmptyGroups()
683
+ return effected
684
+ }
685
+
686
+ hide() {
687
+ let effected = []
688
+ for (let a = 0; a < arguments.length; a++) {
689
+ let fld = this.get(arguments[a])
690
+ if (fld && !fld.hidden) {
691
+ fld.hidden = true
692
+ effected.push(fld.field)
693
+ }
694
+ }
695
+ if (effected.length > 0) this.refresh.apply(this, effected)
696
+ this.updateEmptyGroups()
697
+ return effected
698
+ }
699
+
700
+ enable() {
701
+ let effected = []
702
+ for (let a = 0; a < arguments.length; a++) {
703
+ let fld = this.get(arguments[a])
704
+ if (fld && fld.disabled) {
705
+ fld.disabled = false
706
+ effected.push(fld.field)
707
+ }
708
+ }
709
+ if (effected.length > 0) this.refresh.apply(this, effected)
710
+ return effected
711
+ }
712
+
713
+ disable() {
714
+ let effected = []
715
+ for (let a = 0; a < arguments.length; a++) {
716
+ let fld = this.get(arguments[a])
717
+ if (fld && !fld.disabled) {
718
+ fld.disabled = true
719
+ effected.push(fld.field)
720
+ }
721
+ }
722
+ if (effected.length > 0) this.refresh.apply(this, effected)
723
+ return effected
724
+ }
725
+
726
+ updateEmptyGroups() {
727
+ // hide empty groups
728
+ query(this.box).find('.w2ui-group').each((group) =>{
729
+ if (isHidden(query(group).find('.w2ui-field'))) {
730
+ query(group).hide()
731
+ } else {
732
+ query(group).show()
733
+ }
734
+ })
735
+ function isHidden($els) {
736
+ let flag = true
737
+ $els.each((el) => {
738
+ if (el.style.display != 'none') flag = false
739
+ })
740
+ return flag
741
+ }
742
+ }
743
+
744
+ hideGroup(groupName) {
745
+ let fields = []
746
+ let current = ''
747
+ this.fields.forEach(fld => {
748
+ if (fld.html.group != null && fld.html.group !== '') {
749
+ current = String(fld.html.group).toLowerCase()
750
+ }
751
+ if (groupName.toLowerCase() == current) {
752
+ fields.push(fld.field)
753
+ }
754
+ })
755
+ this.hide(...fields)
756
+ this.resize()
757
+ }
758
+
759
+ showGroup(groupName) {
760
+ let fields = []
761
+ let current = ''
762
+ this.fields.forEach(fld => {
763
+ if (fld.html.group != null && fld.html.group !== '') {
764
+ current = String(fld.html.group).toLowerCase()
765
+ }
766
+ if (groupName.toLowerCase() == current) {
767
+ fields.push(fld.field)
768
+ }
769
+ })
770
+ this.show(...fields)
771
+ this.resize()
772
+ }
773
+
774
+ /**
775
+ * When user clicks on group title, it will toggle the group (collapse or expand it).
776
+ */
777
+ toggleGroup(groupName, show) {
778
+ let el = query(this.box).find('.w2ui-group-title[data-group="' + w2utils.base64encode(groupName) + '"]')
779
+ if (el.length === 0) return
780
+ let el_next = query(el.prop('nextElementSibling'))
781
+ if (typeof show === 'undefined') {
782
+ show = (el_next.css('display') == 'none')
783
+ }
784
+ if (show) {
785
+ el_next.show()
786
+ el.find('span').addClass('w2ui-icon-collapse').removeClass('w2ui-icon-expand')
787
+ } else {
788
+ el_next.hide()
789
+ el.find('span').addClass('w2ui-icon-expand').removeClass('w2ui-icon-collapse')
790
+ }
791
+ }
792
+
793
+ change() {
794
+ Array.from(arguments).forEach((field) => {
795
+ let tmp = this.get(field)
796
+ if (tmp.$el) tmp.$el.change()
797
+ })
798
+ }
799
+
800
+ reload(callBack) {
801
+ let url = (typeof this.url !== 'object' ? this.url : this.url.get)
802
+ if (url && this.recid != null) {
803
+ // this.clear();
804
+ return this.request(callBack) // returns promise
805
+ } else {
806
+ // this.refresh(); // no need to refresh
807
+ if (typeof callBack === 'function') callBack()
808
+ return new Promise(resolve => { resolve() }) // resolved promise
809
+ }
810
+ }
811
+
812
+ clear() {
813
+ if (arguments.length != 0) {
814
+ Array.from(arguments).forEach((field) => {
815
+ let rec = this.record
816
+ String(field).split('.').map((fld, i, arr) => {
817
+ if (arr.length - 1 !== i) rec = rec[fld]; else delete rec[fld]
818
+ })
819
+ this.refresh(field)
820
+ })
821
+ } else {
822
+ this.recid = null
823
+ this.record = {}
824
+ this.original = null
825
+ this.refresh()
826
+ this.hideErrors()
827
+ }
828
+ }
829
+
830
+ error(msg) {
831
+ // let the management of the error outside of the form
832
+ let edata = this.trigger('error', {
833
+ target: this.name,
834
+ message: msg,
835
+ fetchCtrl: this.last.fetchCtrl,
836
+ fetchOptions: this.last.fetchOptions
837
+ })
838
+ if (edata.isCancelled === true) return
839
+ // need a time out because message might be already up)
840
+ setTimeout(() => { this.message(msg) }, 1)
841
+ // event after
842
+ edata.finish()
843
+ }
844
+
845
+ message(options) {
846
+ return w2utils.message({
847
+ owner: this,
848
+ box : this.box,
849
+ after: '.w2ui-form-header'
850
+ }, options)
851
+ }
852
+
853
+ confirm(options) {
854
+ return w2utils.confirm({
855
+ owner: this,
856
+ box : this.box,
857
+ after: '.w2ui-form-header'
858
+ }, options)
859
+ }
860
+
861
+ validate(showErrors) {
862
+ if (showErrors == null) showErrors = true
863
+ // validate before saving
864
+ let errors = []
865
+ for (let f = 0; f < this.fields.length; f++) {
866
+ let field = this.fields[f]
867
+ if (field.type == 'columns' || field.field == null) {
868
+ continue
869
+ }
870
+ if (this.getValue(field.field) == null) this.setValue(field.field, '')
871
+ if (['int', 'float', 'currency', 'money'].indexOf(field.type) != -1) {
872
+ let val = this.getValue(field.field)
873
+ let min = field.options.min
874
+ let max = field.options.max
875
+ if (min != null && val != null && val < min) {
876
+ errors.push({ field: field, error: w2utils.lang('Should be more than ${min}', { min }) })
877
+ }
878
+ if (max != null && val != null && val > max) {
879
+ errors.push({ field: field, error: w2utils.lang('Should be less than ${max}', { max }) })
880
+ }
881
+ }
882
+ switch (field.type) {
883
+ case 'alphanumeric':
884
+ if (this.getValue(field.field) && !w2utils.isAlphaNumeric(this.getValue(field.field))) {
885
+ errors.push({ field: field, error: w2utils.lang('Not alpha-numeric') })
886
+ }
887
+ break
888
+ case 'int':
889
+ if (this.getValue(field.field) && !w2utils.isInt(this.getValue(field.field))) {
890
+ errors.push({ field: field, error: w2utils.lang('Not an integer') })
891
+ }
892
+ break
893
+ case 'percent':
894
+ case 'float':
895
+ if (this.getValue(field.field) && !w2utils.isFloat(this.getValue(field.field))) {
896
+ errors.push({ field: field, error: w2utils.lang('Not a float') })
897
+ }
898
+ break
899
+ case 'currency':
900
+ case 'money':
901
+ if (this.getValue(field.field) && !w2utils.isMoney(this.getValue(field.field))) {
902
+ errors.push({ field: field, error: w2utils.lang('Not in money format') })
903
+ }
904
+ break
905
+ case 'color':
906
+ case 'hex':
907
+ if (this.getValue(field.field) && !w2utils.isHex(this.getValue(field.field))) {
908
+ errors.push({ field: field, error: w2utils.lang('Not a hex number') })
909
+ }
910
+ break
911
+ case 'email':
912
+ if (this.getValue(field.field) && !w2utils.isEmail(this.getValue(field.field))) {
913
+ errors.push({ field: field, error: w2utils.lang('Not a valid email') })
914
+ }
915
+ break
916
+ case 'checkbox':
917
+ // convert true/false
918
+ if (this.getValue(field.field) == true) {
919
+ this.setValue(field.field, true)
920
+ } else {
921
+ this.setValue(field.field, false)
922
+ }
923
+ break
924
+ case 'date':
925
+ // format date before submit
926
+ if (!field.options.format) field.options.format = w2utils.settings.dateFormat
927
+ if (this.getValue(field.field) && !w2utils.isDate(this.getValue(field.field), field.options.format)) {
928
+ errors.push({ field: field, error: w2utils.lang('Not a valid date') + ': ' + field.options.format })
929
+ }
930
+ break
931
+ case 'list':
932
+ case 'combo':
933
+ case 'switch':
934
+ break
935
+ case 'enum':
936
+ break
937
+ }
938
+ // === check required - if field is '0' it should be considered not empty
939
+ let val = this.getValue(field.field)
940
+ if (field.hidden !== true && field.required
941
+ && !['div', 'custom', 'html', 'empty'].includes(field.type)
942
+ && (val == null || val === '' || (Array.isArray(val) && val.length === 0)
943
+ || (w2utils.isPlainObject(val) && Object.keys(val).length == 0))) {
944
+ errors.push({ field: field, error: w2utils.lang('Required field') })
945
+ }
946
+ if (field.hidden !== true && field.options?.minLength > 0
947
+ && !['enum', 'list', 'combo'].includes(field.type) // since minLength is used there for other purpose
948
+ && (val == null || val.length < field.options.minLength)) {
949
+ errors.push({ field: field, error: w2utils.lang('Field should be at least ${count} characters.',
950
+ { count: field.options.minLength })})
951
+ }
952
+ }
953
+ // event before
954
+ let edata = this.trigger('validate', { target: this.name, errors: errors })
955
+ if (edata.isCancelled === true) return
956
+ // show error
957
+ this.last.errors = errors
958
+ if (showErrors) this.showErrors()
959
+ // event after
960
+ edata.finish()
961
+ return errors
962
+ }
963
+
964
+ showErrors() {
965
+ // TODO: check edge cases
966
+ // -- invisible pages
967
+ // -- form refresh
968
+ let errors = this.last.errors
969
+ if (errors.length <= 0) return
970
+ // show errors
971
+ this.goto(errors[0].field.page)
972
+ query(errors[0].field.$el).parents('.w2ui-field')[0].scrollIntoView({ block: 'nearest', inline: 'nearest' })
973
+ // show errors
974
+ // show only for visible controls
975
+ errors.forEach(error => {
976
+ let opt = w2utils.extend({
977
+ anchorClass: 'w2ui-error',
978
+ class: 'w2ui-light',
979
+ position: 'right|left',
980
+ hideOn: ['input']
981
+ }, error.options)
982
+ if (error.field == null) return
983
+ let anchor = error.field.el
984
+ if (error.field.type === 'radio') { // for radio and checkboxes
985
+ anchor = query(error.field.el).closest('div').get(0)
986
+ } else if (['enum', 'file'].includes(error.field.type)) {
987
+ // TODO: check
988
+ // anchor = (error.field.el).data('w2field').helpers.multi
989
+ // $(fld).addClass('w2ui-error')
990
+ }
991
+ w2tooltip.show(w2utils.extend({
992
+ anchor,
993
+ name: `${this.name}-${error.field.field}-error`,
994
+ html: error.error
995
+ }, opt))
996
+ })
997
+ // on scroll update errors so they will appear in correct places
998
+ this.last.errorsShown = true
999
+ query(errors[0].field.$el).parents('.w2ui-page')
1000
+ .off('.hideErrors')
1001
+ .on('scroll.hideErrors', (evt) => {
1002
+ if (this.last.errorsShown) {
1003
+ this.showErrors()
1004
+ }
1005
+ })
1006
+ }
1007
+
1008
+ hideErrors() {
1009
+ this.last.errorsShown = false
1010
+ this.fields.forEach(field => {
1011
+ w2tooltip.hide(`${this.name}-${field.field}-error`)
1012
+ })
1013
+ }
1014
+
1015
+ getChanges() {
1016
+ // TODO: not working on nested structures
1017
+ let diff = {}
1018
+ if (this.original != null && typeof this.original == 'object' && Object.keys(this.record).length !== 0) {
1019
+ diff = doDiff(this.record, this.original, {})
1020
+ }
1021
+ return diff
1022
+
1023
+ function doDiff(record, original, result) {
1024
+ if (Array.isArray(record) && Array.isArray(original)) {
1025
+ while (record.length < original.length) {
1026
+ record.push(null)
1027
+ }
1028
+ }
1029
+ for (let i in record) {
1030
+ if (record[i] != null && typeof record[i] === 'object') {
1031
+ result[i] = doDiff(record[i], original[i] || {}, {})
1032
+ if (!result[i] || (Object.keys(result[i]).length == 0 && Object.keys(original[i].length == 0))) delete result[i]
1033
+ } else if (record[i] != original[i] || (record[i] == null && original[i] != null)) { // also catch field clear
1034
+ result[i] = record[i]
1035
+ }
1036
+ }
1037
+ return Object.keys(result).length != 0 ? result : null
1038
+ }
1039
+ }
1040
+
1041
+ getCleanRecord(strict) {
1042
+ let data = w2utils.clone(this.record)
1043
+ this.fields.forEach((fld) => {
1044
+ if (fld.type == 'columns' || fld.field == null) {
1045
+ return
1046
+ }
1047
+ if (['list', 'combo', 'enum'].indexOf(fld.type) != -1) {
1048
+ let tmp = { nestedFields: true, record: data }
1049
+ let val = this.getValue.call(tmp, fld.field)
1050
+ if (w2utils.isPlainObject(val) && val.id != null) { // should be true if val.id === ''
1051
+ this.setValue.call(tmp, fld.field, val.id)
1052
+ }
1053
+ if (Array.isArray(val)) {
1054
+ val.forEach((item, ind) => {
1055
+ if (w2utils.isPlainObject(item) && item.id) {
1056
+ val[ind] = item.id
1057
+ }
1058
+ })
1059
+ }
1060
+ }
1061
+ if (fld.type == 'map') {
1062
+ let tmp = { nestedFields: true, record: data }
1063
+ let val = this.getValue.call(tmp, fld.field)
1064
+ if (val._order) delete val._order
1065
+ }
1066
+ if (fld.type == 'file') {
1067
+ let tmp = { nestedFields: true, record: data }
1068
+ let val = this.getValue.call(tmp, fld.field) ?? []
1069
+ val.forEach(v => {
1070
+ delete v.file
1071
+ delete v.modified
1072
+ })
1073
+ this.setValue.call(tmp, fld.field, val)
1074
+ }
1075
+ })
1076
+ // return only records present in fields
1077
+ if (strict === true) {
1078
+ Object.keys(data).forEach((key) => {
1079
+ if (!this.get(key)) delete data[key]
1080
+ })
1081
+ }
1082
+ return data
1083
+ }
1084
+
1085
+ request(postData, callBack) { // if (1) param then it is call back if (2) then postData and callBack
1086
+ let self = this
1087
+ let resolve, reject
1088
+ let responseProm = new Promise((res, rej) => { resolve = res; reject = rej })
1089
+ // check for multiple params
1090
+ if (typeof postData === 'function') {
1091
+ callBack = postData
1092
+ postData = null
1093
+ }
1094
+ if (postData == null) postData = {}
1095
+ if (!this.url || (typeof this.url === 'object' && !this.url.get)) return
1096
+ // build parameters list
1097
+ let params = {}
1098
+ // add list params
1099
+ params.action = 'get'
1100
+ params.recid = this.recid
1101
+ params.name = this.name
1102
+ // append other params
1103
+ w2utils.extend(params, this.postData)
1104
+ w2utils.extend(params, postData)
1105
+ // event before
1106
+ let edata = this.trigger('request', { target: this.name, url: this.url, httpMethod: 'GET',
1107
+ postData: params, httpHeaders: this.httpHeaders })
1108
+ if (edata.isCancelled === true) return
1109
+ // default action
1110
+ this.record = {}
1111
+ this.original = null
1112
+ // call server to get data
1113
+ this.lock(w2utils.lang(this.msgRefresh))
1114
+ let url = edata.detail.url
1115
+ if (typeof url === 'object' && url.get) url = url.get
1116
+ if (this.last.fetchCtrl) try { this.last.fetchCtrl.abort() } catch (e) {}
1117
+ // process url with routeData
1118
+ if (Object.keys(this.routeData).length != 0) {
1119
+ let info = w2utils.parseRoute(url)
1120
+ if (info.keys.length > 0) {
1121
+ for (let k = 0; k < info.keys.length; k++) {
1122
+ if (this.routeData[info.keys[k].name] == null) continue
1123
+ url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), this.routeData[info.keys[k].name])
1124
+ }
1125
+ }
1126
+ }
1127
+ url = new URL(url, location)
1128
+ let fetchOptions = w2utils.prepareParams(url, {
1129
+ method: edata.detail.httpMethod,
1130
+ headers: edata.detail.httpHeaders,
1131
+ body: edata.detail.postData
1132
+ }, { dataType: this.dataType, caller: this, action: 'request' })
1133
+ this.last.fetchCtrl = new AbortController()
1134
+ fetchOptions.signal = this.last.fetchCtrl.signal
1135
+ this.last.fetchOptions = fetchOptions
1136
+ fetch(url, fetchOptions)
1137
+ .catch(processError)
1138
+ .then((resp) => {
1139
+ if (resp?.status != 200) {
1140
+ // if resp is undefined, it means request was aborted
1141
+ if (resp) processError(resp)
1142
+ return
1143
+ }
1144
+ resp.json()
1145
+ .catch(processError)
1146
+ .then(data => {
1147
+ // event before
1148
+ let edata = self.trigger('load', {
1149
+ target: self.name,
1150
+ fetchCtrl: this.last.fetchCtrl,
1151
+ fetchOptions: this.last.fetchOptions,
1152
+ data
1153
+ })
1154
+ if (edata.isCancelled === true) return
1155
+ // for backward compatibility
1156
+ if (data.error == null && data.status === 'error') {
1157
+ data.error = true
1158
+ }
1159
+ // if data.record is not present, then assume that entire response is the record
1160
+ if (!data.record) {
1161
+ Object.assign(data, { record: w2utils.clone(data) })
1162
+ }
1163
+ // server response error, not due to network issues
1164
+ if (data.error === true) {
1165
+ self.error(w2utils.lang(data.message ?? this.msgServerError))
1166
+ } else {
1167
+ self.record = w2utils.clone(data.record)
1168
+ }
1169
+ // event after
1170
+ self.unlock()
1171
+ edata.finish()
1172
+ self.refresh()
1173
+ self.setFocus()
1174
+ // call back
1175
+ if (typeof callBack === 'function') callBack(data)
1176
+ resolve(data)
1177
+ })
1178
+ })
1179
+ // event after
1180
+ edata.finish()
1181
+ return responseProm
1182
+
1183
+ function processError(response) {
1184
+ if (response.name === 'AbortError') {
1185
+ // request was aborted by the form
1186
+ return
1187
+ }
1188
+ self.unlock()
1189
+ // trigger event
1190
+ let edata2 = self.trigger('error', { response, fetchCtrl: self.last.fetchCtrl, fetchOptions: self.last.fetchOptions })
1191
+ if (edata2.isCancelled === true) return
1192
+ // default behavior
1193
+ if (response.status && response.status != 200) {
1194
+ self.error(response.status + ': ' + response.statusText)
1195
+ } else {
1196
+ console.log('ERROR: Server request failed.', response, '. ',
1197
+ 'Expected Response:', { error: false, record: { field1: 1, field2: 'item' }},
1198
+ 'OR:', { error: true, message: 'Error description' })
1199
+ self.error(String(response))
1200
+ }
1201
+ // event after
1202
+ edata2.finish()
1203
+ reject(response)
1204
+ }
1205
+ }
1206
+
1207
+ submit(postData, callBack) {
1208
+ return this.save(postData, callBack)
1209
+ }
1210
+
1211
+ save(postData, callBack) {
1212
+ let self = this
1213
+ let resolve, reject
1214
+ let saveProm = new Promise((res, rej) => { resolve = res; reject = rej })
1215
+ // check for multiple params
1216
+ if (typeof postData === 'function') {
1217
+ callBack = postData
1218
+ postData = null
1219
+ }
1220
+ // validation
1221
+ let errors = self.validate(true)
1222
+ if (errors.length !== 0) return
1223
+ // submit save
1224
+ if (postData == null) postData = {}
1225
+ if (!self.url || (typeof self.url === 'object' && !self.url.save)) {
1226
+ console.log('ERROR: Form cannot be saved because no url is defined.')
1227
+ return
1228
+ }
1229
+ self.lock(w2utils.lang(self.msgSaving) + ' <span id="'+ self.name +'_progress"></span>')
1230
+ // build parameters list
1231
+ let params = {}
1232
+ // add list params
1233
+ params.action = 'save'
1234
+ params.recid = self.recid
1235
+ params.name = self.name
1236
+ // append other params
1237
+ w2utils.extend(params, self.postData)
1238
+ w2utils.extend(params, postData)
1239
+ params.record = w2utils.clone(self.saveCleanRecord ? self.getCleanRecord() : self.record)
1240
+ // event before
1241
+ let edata = self.trigger('submit', { target: self.name, url: self.url, httpMethod: this.method ?? 'POST',
1242
+ postData: params, httpHeaders: self.httpHeaders })
1243
+ if (edata.isCancelled === true) return
1244
+ // default action
1245
+ let url = edata.detail.url
1246
+ if (typeof url === 'object' && url.save) url = url.save
1247
+ if (self.last.fetchCtrl) self.last.fetchCtrl.abort()
1248
+ // process url with routeData
1249
+ if (Object.keys(self.routeData).length > 0) {
1250
+ let info = w2utils.parseRoute(url)
1251
+ if (info.keys.length > 0) {
1252
+ for (let k = 0; k < info.keys.length; k++) {
1253
+ if (self.routeData[info.keys[k].name] == null) continue
1254
+ url = url.replace((new RegExp(':'+ info.keys[k].name, 'g')), self.routeData[info.keys[k].name])
1255
+ }
1256
+ }
1257
+ }
1258
+ url = new URL(url, location)
1259
+ let fetchOptions = w2utils.prepareParams(url, {
1260
+ method: edata.detail.httpMethod,
1261
+ headers: edata.detail.httpHeaders,
1262
+ body: edata.detail.postData
1263
+ }, { dataType: this.dataType, caller: this, action: 'save' })
1264
+ this.last.fetchCtrl = new AbortController()
1265
+ fetchOptions.signal = this.last.fetchCtrl.signal
1266
+ this.last.fetchOptions = fetchOptions
1267
+ fetch(url, fetchOptions)
1268
+ .catch(processError)
1269
+ .then(resp => {
1270
+ self.unlock()
1271
+ if (resp?.status != 200) {
1272
+ processError(resp ?? {})
1273
+ return
1274
+ }
1275
+ // parse server response
1276
+ resp.json()
1277
+ .catch(processError)
1278
+ .then(data => {
1279
+ // event before
1280
+ let edata = self.trigger('save', {
1281
+ target: self.name,
1282
+ fetchCtrl: this.last.fetchCtrl,
1283
+ fetchOptions: this.last.fetchOptions,
1284
+ data
1285
+ })
1286
+ if (edata.isCancelled === true) return
1287
+ // server error, not due to network issues
1288
+ if (data.error === true) {
1289
+ self.error(w2utils.lang(data.message ?? this.msgServerError))
1290
+ } else {
1291
+ self.original = null
1292
+ }
1293
+ // event after
1294
+ edata.finish()
1295
+ self.refresh()
1296
+ // call back
1297
+ if (typeof callBack === 'function') callBack(data)
1298
+ resolve(data)
1299
+ })
1300
+ })
1301
+ // event after
1302
+ edata.finish()
1303
+ return saveProm
1304
+
1305
+ function processError(response) {
1306
+ if (response?.name === 'AbortError') {
1307
+ // request was aborted by the form
1308
+ return
1309
+ }
1310
+ self.unlock()
1311
+ // trigger event
1312
+ let edata2 = self.trigger('error', { response, fetchCtrl: self.last.fetchCtrl, fetchOptions: self.last.fetchOptions })
1313
+ if (edata2.isCancelled === true) return
1314
+ // default behavior
1315
+ if (response.status && response.status != 200) {
1316
+ response.json().then((data) => {
1317
+ self.error(response.status + ': ' + data.message ?? response.statusText)
1318
+ }).catch(() => {
1319
+ self.error(response.status + ': ' + response.statusText)
1320
+ })
1321
+ } else {
1322
+ console.log('ERROR: Server request failed.', response, '. ',
1323
+ 'Expected Response:', { error: false, record: { field1: 1, field2: 'item' }},
1324
+ 'OR:', { error: true, message: 'Error description' })
1325
+ self.error(String(response))
1326
+ }
1327
+ // event after
1328
+ edata2.finish()
1329
+ reject()
1330
+ }
1331
+ }
1332
+
1333
+ lock(msg, showSpinner) {
1334
+ let args = Array.from(arguments)
1335
+ args.unshift(this.box)
1336
+ w2utils.lock(...args)
1337
+ }
1338
+
1339
+ unlock(speed) {
1340
+ let box = this.box
1341
+ w2utils.unlock(box, speed)
1342
+ }
1343
+
1344
+ lockPage(page, msg, spinner) {
1345
+ let $page = query(this.box).find('.page-' + page)
1346
+ if ($page.length){
1347
+ // page found
1348
+ w2utils.lock($page, msg, spinner)
1349
+ return true
1350
+ }
1351
+ // page with this id not found!
1352
+ return false
1353
+ }
1354
+
1355
+ unlockPage(page, speed) {
1356
+ let $page = query(this.box).find('.page-' + page)
1357
+ if ($page.length) {
1358
+ // page found
1359
+ w2utils.unlock($page, speed)
1360
+ return true
1361
+ }
1362
+ // page with this id not found!
1363
+ return false
1364
+ }
1365
+
1366
+ goto(page) {
1367
+ if (this.page === page) return // already on this page
1368
+ if (page != null) this.page = page
1369
+ // if it was auto size, resize it
1370
+ if (query(this.box).data('autoSize') === true) {
1371
+ query(this.box).get(0).clientHeight = 0
1372
+ }
1373
+ this.refresh()
1374
+ }
1375
+
1376
+ generateHTML() {
1377
+ let pages = [] // array for each page
1378
+ let group = ''
1379
+ let page
1380
+ let column
1381
+ let tabindex
1382
+ let tabindex_str
1383
+ for (let f = 0; f < this.fields.length; f++) {
1384
+ let html = ''
1385
+ tabindex = this.tabindexBase + f + 1
1386
+ tabindex_str = ' tabindex="'+ tabindex +'"'
1387
+ let field = this.fields[f]
1388
+ if (field.html == null) field.html = {}
1389
+ if (typeof field.html == 'string') {
1390
+ field.html = {
1391
+ html: field.html,
1392
+ span: 0,
1393
+ attr: 'tabindex'
1394
+ }
1395
+ tabindex_str = ''
1396
+ }
1397
+ if (field.options == null) field.options = {}
1398
+ if (field.html.caption != null && field.html.label == null) {
1399
+ console.log('NOTICE: form field.html.caption property is deprecated, please use field.html.label. Field ->', field)
1400
+ field.html.label = field.html.caption
1401
+ }
1402
+ if (field.html.label == null) field.html.label = field.field
1403
+ if (field.html.anchor != null && field.html.span == null) {
1404
+ field.html.span = ''
1405
+ }
1406
+ field.html = w2utils.extend({ label: '', span: 6, attr: '', text: '', style: '', page: 0, column: 0 }, field.html)
1407
+ if (page == null) page = field.html.page
1408
+ if (column == null) column = field.html.column
1409
+ // input control
1410
+ let input = `<input id="${field.field}" name="${field.field}" class="w2ui-input ${field.html.class ?? ''}" type="text" ${field.html.attr + tabindex_str}>`
1411
+ switch (field.type) {
1412
+ case 'pass':
1413
+ case 'password':
1414
+ input = input.replace('type="text"', 'type="password"')
1415
+ break
1416
+ case 'checkbox': {
1417
+ input = `
1418
+ <label class="w2ui-box-label">
1419
+ <input id="${field.field}" name="${field.field}" class="w2ui-input ${field.html.class ?? ''}" type="checkbox" ${field.html.attr + tabindex_str}>
1420
+ <span>${field.html.label}</span>
1421
+ </label>`
1422
+ break
1423
+ }
1424
+ case 'check':
1425
+ case 'checks': {
1426
+ if (field.options.items == null && field.html.items != null) field.options.items = field.html.items
1427
+ let items = field.options.items
1428
+ input = `<div class="w2ui-field-group" ${field.html.attr}>`
1429
+ // normalized options
1430
+ if (!Array.isArray(items)) items = []
1431
+ if (items.length > 0) {
1432
+ items = w2utils.normMenu.call(this, items, field.options)
1433
+ }
1434
+ // generate
1435
+ for (let i = 0; i < items.length; i++) {
1436
+ input += `
1437
+ <div class="w2ui-field-item">
1438
+ <label class="w2ui-box-label">
1439
+ <input id="${field.field + i}" name="${field.field}" class="w2ui-input ${field.html.class ?? ''}" type="checkbox"
1440
+ ${tabindex_str} data-value="${items[i].id}" data-index="${i}">
1441
+ <span>&#160;${items[i].text}</span>
1442
+ </label>
1443
+ </div>`
1444
+ }
1445
+ input += '</div>'
1446
+ break
1447
+ }
1448
+ case 'radio': {
1449
+ input = `<div class="w2ui-field-group"${field.html.attr}>`
1450
+ // normalized options
1451
+ if (field.options.items == null && field.html.items != null) field.options.items = field.html.items
1452
+ let items = field.options.items
1453
+ if (!Array.isArray(items)) items = []
1454
+ if (items.length > 0) {
1455
+ items = w2utils.normMenu.call(this, items, field.options)
1456
+ }
1457
+ // generate
1458
+ for (let i = 0; i < items.length; i++) {
1459
+ input += `
1460
+ <div class="w2ui-field-item">
1461
+ <label class="w2ui-box-label">
1462
+ <input id="${field.field + i}" name="${field.field}" class="w2ui-input ${field.html.class ?? ''}" type="radio"
1463
+ ${(i === 0 ? tabindex_str : '')}
1464
+ data-value="${items[i].id}" data-index="${i}">
1465
+ <span>&#160;${items[i].text}</span>
1466
+ </label>
1467
+ </div>`
1468
+ }
1469
+ input += '</div>'
1470
+ break
1471
+ }
1472
+ case 'select': {
1473
+ input = `<select id="${field.field}" name="${field.field}" class="w2ui-input ${field.html.class ?? ''}" ${field.html.attr + tabindex_str}>`
1474
+ // normalized options
1475
+ if (field.options.items == null && field.html.items != null) field.options.items = field.html.items
1476
+ let items = field.options.items
1477
+ if (!Array.isArray(items)) items = []
1478
+ if (items.length > 0) {
1479
+ items = w2utils.normMenu.call(this, items, field.options)
1480
+ }
1481
+ // generate
1482
+ for (let i = 0; i < items.length; i++) {
1483
+ input += `<option value="${items[i].id}">${items[i].text}</option>`
1484
+ }
1485
+ input += '</select>'
1486
+ break
1487
+ }
1488
+ case 'switch': {
1489
+ input = `
1490
+ <div>
1491
+ <div id="${field.field}-tb" class="w2ui-form-switch ${field.html.class ?? ''}" ${field.html.attr}></div>
1492
+ <input id="${field.field}" name="${field.field}" ${tabindex_str} class="w2ui-input"
1493
+ style="position: absolute; right: 0px; margin-top: -30px; width: 1px; padding: 0; opacity: 0">
1494
+ <span style="position: absolute; margin-top: -2px;">${field.html.text ?? ''}</span>
1495
+ </div>
1496
+ `
1497
+ break
1498
+ }
1499
+ case 'textarea':
1500
+ input = `<textarea id="${field.field}" name="${field.field}" class="w2ui-input ${field.html.class ?? ''}" ${field.html.attr + tabindex_str}></textarea>`
1501
+ break
1502
+ case 'toggle':
1503
+ input = `<input id="${field.field}" name="${field.field}" class="w2ui-input w2ui-toggle ${field.html.class ?? ''}"
1504
+ type="checkbox" ${field.html.attr + tabindex_str}>
1505
+ <div><div></div></div>`
1506
+ break
1507
+ case 'map':
1508
+ case 'array':
1509
+ field.html.key = field.html.key || {}
1510
+ field.html.value = field.html.value || {}
1511
+ field.html.tabindex = tabindex
1512
+ field.html.tabindex_str = tabindex_str
1513
+ input = '<span style="float: right">' + (field.html.text || '') + '</span>' +
1514
+ '<input id="'+ field.field +'" name="'+ field.field +'" type="hidden" '+ field.html.attr + tabindex_str + '>'+
1515
+ '<div class="w2ui-map-container"></div>'
1516
+ break
1517
+ case 'div':
1518
+ case 'custom':
1519
+ input = `<div id="${field.field}" name="${field.field}" ${field.html.attr + tabindex_str} class="w2ui-input ${field.html.class ?? ''}">`+
1520
+ (field && field.html && field.html.html ? field.html.html : '') +
1521
+ '</div>'
1522
+ break
1523
+ case 'html':
1524
+ case 'empty':
1525
+ input = `<div id="${field.field}" name="${field.field}" ${field.html.attr + tabindex_str} class="w2ui-input ${field.html.class ?? ''}">`+
1526
+ (field && field.html ? (field.html.html || '') + (field.html.text || '') : '') +
1527
+ '</div>'
1528
+ break
1529
+ }
1530
+ if (group !== '') {
1531
+ if (page != field.html.page || column != field.html.column || (field.html.group && (group != field.html.group))) {
1532
+ pages[page][column] += '\n </div>\n </div>'
1533
+ group = ''
1534
+ }
1535
+ }
1536
+ if (field.html.group && (group != field.html.group)) {
1537
+ let collapsible = ''
1538
+ if (field.html.groupCollapsible) {
1539
+ collapsible = '<span class="w2ui-icon-collapse" style="width: 15px; display: inline-block; position: relative; top: -2px;"></span>'
1540
+ }
1541
+ html += '\n <div class="w2ui-group">'
1542
+ + '\n <div class="w2ui-group-title w2ui-eaction" style="'+ (field.html.groupTitleStyle || '') + '; '
1543
+ + (collapsible != '' ? 'cursor: pointer; user-select: none' : '') + '"'
1544
+ + (collapsible != '' ? 'data-group="' + w2utils.base64encode(field.html.group) + '"' : '')
1545
+ + (collapsible != ''
1546
+ ? 'data-click="toggleGroup|' + field.html.group + '"'
1547
+ : '')
1548
+ + '>'
1549
+ + collapsible + w2utils.lang(field.html.group) + '</div>\n'
1550
+ + ' <div class="w2ui-group-fields" style="'+ (field.html.groupStyle || '') +'">'
1551
+ group = field.html.group
1552
+ }
1553
+ if (field.type == 'columns') {
1554
+ html += `<div class="w2ui-field-columns" style="${field.style ?? ''}">`
1555
+ field.columns.forEach(col => {
1556
+ html += `<div style="${col.style}"> ${col.content} </div>`
1557
+ })
1558
+ html += '</div>'
1559
+ } else if (field.html.col_anchor != null) {
1560
+ let span = (field.html.span != null ? 'w2ui-span'+ field.html.span : '')
1561
+ if (field.html.span == -1) span = 'w2ui-span-none'
1562
+ let label = `
1563
+ <label ${span == 'none' ? ' style="display: none"' : ''}>
1564
+ ${w2utils.lang(field.type != 'checkbox' ? field.html.label : field.html.text)}
1565
+ </label>`
1566
+ if (!field.html.label) label = ''
1567
+ let text = (field.type != 'array' && field.type != 'map' ? w2utils.lang(field.type != 'checkbox' ? field.html.text : '') : '')
1568
+ pages[field.html.page].anchors ??= {}
1569
+ pages[field.html.page].anchors[field.html.col_anchor] =`
1570
+ <div class="w2ui-field ${span}" style="${(field.hidden ? 'display: none;' : '') + field.html.style}">
1571
+ ${label}
1572
+ ${['empty', 'switch', 'radio', 'check', 'checks'].includes(field.type)
1573
+ ? input
1574
+ : `<div>${input + text}</div>`
1575
+ }
1576
+ </div>`
1577
+ } else if (field.html.anchor != null) {
1578
+ let span = (field.html.span != null ? 'w2ui-span'+ field.html.span : 'w2ui-span0')
1579
+ let label = w2utils.lang(field.type != 'checkbox' ? field.html.label : field.html.text, true)
1580
+ let text = w2utils.lang(field.type != 'checkbox' ? field.html.text : '')
1581
+ if (field.html.span == -1) {
1582
+ label = `<span style="position: absolute"> <span class="w2ui-anchor-span-none w2ui-inline-label"> ${label} </span> </span>`
1583
+ } else {
1584
+ label = `<span class="w2ui-inline-label"> ${label} </span>`
1585
+ }
1586
+ pages[field.html.page].anchors ??= {}
1587
+ pages[field.html.page].anchors[field.html.anchor] = `
1588
+ <div class="w2ui-field w2ui-field-inline ${span}" style="${(field.hidden ? 'display: none;' : '') + field.html.style}">
1589
+ ${((field.type === 'empty' || field.type == 'switch')
1590
+ ? input
1591
+ : ` <div>
1592
+ ${label} ${input} ${text}
1593
+ </div>`
1594
+ )}
1595
+ </div>`
1596
+ } else {
1597
+ let span = (field.html.span != null ? 'w2ui-span'+ field.html.span : '')
1598
+ if (field.html.span == -1) span = 'w2ui-span-none'
1599
+ let label = `
1600
+ <label ${span == 'none' ? ' style="display: none"' : ''}>
1601
+ ${w2utils.lang(field.type != 'checkbox' ? field.html.label : field.html.text)}
1602
+ </label>`
1603
+ if (!field.html.label) label = ''
1604
+ let text = (field.type != 'array' && field.type != 'map' ? w2utils.lang(field.type != 'checkbox' ? field.html.text : '') : '')
1605
+ html += `
1606
+ <div class="w2ui-field ${span}" style="${(field.hidden ? 'display: none;' : '') + field.html.style}">
1607
+ ${label}
1608
+ ${['empty', 'switch', 'radio', 'check', 'checks'].includes(field.type)
1609
+ ? input
1610
+ : `<div>${input + text}</div>`
1611
+ }
1612
+ </div>`
1613
+ }
1614
+ if (pages[field.html.page] == null) pages[field.html.page] = {}
1615
+ if (pages[field.html.page][field.html.column] == null) pages[field.html.page][field.html.column] = ''
1616
+ pages[field.html.page][field.html.column] += html
1617
+ page = field.html.page
1618
+ column = field.html.column
1619
+ }
1620
+ if (group !== '') pages[page][column] += '\n </div>\n </div>'
1621
+ if (this.tabs.tabs) {
1622
+ for (let i = 0; i < this.tabs.tabs.length; i++) if (pages[i] == null) pages[i] = []
1623
+ }
1624
+ // buttons if any
1625
+ let buttons = ''
1626
+ if (Object.keys(this.actions).length > 0) {
1627
+ buttons += '\n<div class="w2ui-buttons">'
1628
+ tabindex = this.tabindexBase + this.fields.length + 1
1629
+
1630
+ for (let a in this.actions) { // it is an object
1631
+ let act = this.actions[a]
1632
+ let info = { text: '', style: '', 'class': '' }
1633
+ if (w2utils.isPlainObject(act)) {
1634
+ if (act.text == null && act.caption != null) {
1635
+ console.log('NOTICE: form action.caption property is deprecated, please use action.text. Action ->', act)
1636
+ act.text = act.caption
1637
+ }
1638
+ if (act.text) info.text = act.text
1639
+ if (act.style) info.style = act.style
1640
+ if (act.class) info.class = act.class
1641
+ } else {
1642
+ info.text = a
1643
+ if (['save', 'update', 'create'].indexOf(a.toLowerCase()) !== -1) info.class = 'w2ui-btn-blue'; else info.class = ''
1644
+ }
1645
+ buttons += '\n <button name="'+ a +'" class="w2ui-btn '+ info.class +'" style="'+ info.style +'" tabindex="'+ tabindex +'">'+
1646
+ w2utils.lang(info.text) +'</button>'
1647
+ tabindex++
1648
+ }
1649
+ buttons += '\n</div>'
1650
+ }
1651
+ let html = ''
1652
+ for (let p = 0; p < pages.length; p++){
1653
+ html += '<div class="w2ui-page page-'+ p +'" style="' + (p !== 0 ? 'display: none;' : '') + this.pageStyle + '">'
1654
+ if (!pages[p]) {
1655
+ console.log(`ERROR: Page ${p} does not exist`)
1656
+ return false
1657
+ }
1658
+ if (pages[p].before) {
1659
+ html += pages[p].before
1660
+ }
1661
+ html += '<div class="w2ui-column-container">'
1662
+ Object.keys(pages[p]).sort().forEach((c, ind) => {
1663
+ if (c == parseInt(c)) {
1664
+ html += '<div class="w2ui-column col-'+ c +'">' + (pages[p][c] || '') + '\n</div>'
1665
+ }
1666
+ })
1667
+ html += '\n</div>'
1668
+ if (pages[p].after) {
1669
+ html += pages[p].after
1670
+ }
1671
+ html += '\n</div>'
1672
+ // process page anchors
1673
+ if (pages[p].anchors) {
1674
+ Object.keys(pages[p].anchors).forEach((key, ind) => {
1675
+ html = html.replace(key, pages[p].anchors[key])
1676
+ })
1677
+ }
1678
+ }
1679
+ html += buttons
1680
+ return html
1681
+ }
1682
+
1683
+ action(action, event) {
1684
+ let act = this.actions[action]
1685
+ let click = act
1686
+ if (w2utils.isPlainObject(act) && act.onClick) click = act.onClick
1687
+ // event before
1688
+ let edata = this.trigger('action', { target: action, action: act, originalEvent: event })
1689
+ if (edata.isCancelled === true) return
1690
+ // default actions
1691
+ if (typeof click === 'function') click.call(this, event)
1692
+ // event after
1693
+ edata.finish()
1694
+ }
1695
+
1696
+ getAction(action) {
1697
+ let ret = query(this.box).find('.w2ui-buttons button[name="' + action + '"]')
1698
+ if (ret.length === 0) {
1699
+ console.log('ERROR: Action "' + action + '" not found. Valid actions are: ' + Object.keys(this.actions).join(', '))
1700
+ }
1701
+ return ret
1702
+ }
1703
+
1704
+ actionHide(action) {
1705
+ this.getAction(action).hide()
1706
+ }
1707
+
1708
+ actionShow(action) {
1709
+ this.getAction(action).show()
1710
+ }
1711
+
1712
+ actionDisable(action) {
1713
+ this.getAction(action).prop('disabled', true)
1714
+ }
1715
+
1716
+ actionEnable(action) {
1717
+ this.getAction(action).prop('disabled', false)
1718
+ }
1719
+
1720
+ resize() {
1721
+ let self = this
1722
+ // event before
1723
+ let edata = this.trigger('resize', { target: this.name })
1724
+ if (edata.isCancelled === true) return
1725
+ // default behaviour
1726
+ if (this.box != null) {
1727
+ let header = query(this.box).find(':scope > div .w2ui-form-header')
1728
+ let toolbar = query(this.box).find(':scope > div .w2ui-form-toolbar')
1729
+ let tabs = query(this.box).find(':scope > div .w2ui-form-tabs')
1730
+ let page = query(this.box).find(':scope > div .w2ui-page')
1731
+ let dpage = query(this.box).find(':scope > div .w2ui-page.page-'+ this.page + ' > div')
1732
+ let buttons = query(this.box).find(':scope > div .w2ui-buttons')
1733
+ // if no height, calculate it
1734
+ let { headerHeight, tbHeight, tabsHeight } = resizeElements()
1735
+ if (this.autosize) { // we don't need autosize every time
1736
+ let cHeight = query(this.box).get(0).clientHeight
1737
+ if (cHeight === 0 || query(this.box).data('autosize') == 'yes') {
1738
+ query(this.box).css({
1739
+ height: headerHeight + tbHeight + tabsHeight + 15 // 15 is extra height
1740
+ + (page.length > 0 ? w2utils.getSize(dpage, 'height') : 0)
1741
+ + (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0)
1742
+ + 'px'
1743
+ })
1744
+ query(this.box).data('autosize', 'yes')
1745
+ }
1746
+ resizeElements()
1747
+ }
1748
+ // resize tabs and toolbar if any
1749
+ this.tabs?.resize?.()
1750
+ this.toolbar?.resize?.()
1751
+ // resize switch fields
1752
+ this.fields.forEach(field => {
1753
+ if (field.type == 'switch') {
1754
+ field.toolbar?.resize?.()
1755
+ }
1756
+ })
1757
+
1758
+ function resizeElements() {
1759
+ let headerHeight = (self.header !== '' ? w2utils.getSize(header, 'height') : 0)
1760
+ let tbHeight = (Array.isArray(self.toolbar?.items) && self.toolbar?.items?.length > 0)
1761
+ ? w2utils.getSize(toolbar, 'height')
1762
+ : 0
1763
+ let tabsHeight = (Array.isArray(self.tabs?.tabs) && self.tabs?.tabs?.length > 0)
1764
+ ? w2utils.getSize(tabs, 'height')
1765
+ : 0
1766
+ // resize elements
1767
+ toolbar.css({ top: headerHeight + 'px' })
1768
+ tabs.css({ top: headerHeight + tbHeight + 'px' })
1769
+ page.css({
1770
+ top: headerHeight + tbHeight + tabsHeight + 'px',
1771
+ bottom: (buttons.length > 0 ? w2utils.getSize(buttons, 'height') : 0) + 'px'
1772
+ })
1773
+ // return some params
1774
+ return { headerHeight, tbHeight, tabsHeight }
1775
+ }
1776
+ }
1777
+ // event after
1778
+ edata.finish()
1779
+ }
1780
+
1781
+ refresh() {
1782
+ let time = Date.now()
1783
+ let self = this
1784
+ if (!this.box) return
1785
+ if (!this.isGenerated || !query(this.box).html()) return
1786
+ // event before
1787
+ let edata = this.trigger('refresh', { target: this.name, page: this.page, field: arguments[0], fields: arguments })
1788
+ if (edata.isCancelled === true) return
1789
+ let fields = Array.from(this.fields.keys())
1790
+ if (arguments.length > 0) {
1791
+ fields = Array.from(arguments)
1792
+ .map((fld, ind) => {
1793
+ if (typeof fld != 'string') console.log('ERROR: Arguments in refresh functions should be field names')
1794
+ return this.get(fld, true) // get index of field
1795
+ })
1796
+ .filter((fld, ind) => {
1797
+ if (fld != null) return true; else return false
1798
+ })
1799
+ } else {
1800
+ // update field.page with page it belongs too
1801
+ query(this.box).find('input, textarea, select').each(el => {
1802
+ let name = (query(el).attr('name') != null ? query(el).attr('name') : query(el).attr('id'))
1803
+ let field = this.get(name)
1804
+ if (field) {
1805
+ // find page
1806
+ let div = query(el).closest('.w2ui-page')
1807
+ if (div.length > 0) {
1808
+ for (let i = 0; i < 100; i++) {
1809
+ if (div.hasClass('page-'+i)) { field.page = i; break }
1810
+ }
1811
+ }
1812
+ }
1813
+ })
1814
+ // default action
1815
+ query(this.box).find('.w2ui-page').hide()
1816
+ query(this.box).find('.w2ui-page.page-' + this.page).show()
1817
+ query(this.box).find('.w2ui-form-header').html(w2utils.lang(this.header))
1818
+ // refresh tabs if needed
1819
+ if (typeof this.tabs === 'object' && Array.isArray(this.tabs.tabs) && this.tabs.tabs.length > 0) {
1820
+ query(this.box).find('#form_'+ this.name +'_tabs').show()
1821
+ this.tabs.active = this.tabs.tabs[this.page].id
1822
+ this.tabs.refresh()
1823
+ } else {
1824
+ query(this.box).find('#form_'+ this.name +'_tabs').hide()
1825
+ }
1826
+ // refresh tabs if needed
1827
+ if (typeof this.toolbar === 'object' && Array.isArray(this.toolbar.items) && this.toolbar.items.length > 0) {
1828
+ query(this.box).find('#form_'+ this.name +'_toolbar').show()
1829
+ this.toolbar.refresh()
1830
+ } else {
1831
+ query(this.box).find('#form_'+ this.name +'_toolbar').hide()
1832
+ }
1833
+ }
1834
+ // refresh values of fields
1835
+ for (let f = 0; f < fields.length; f++) {
1836
+ let field = this.fields[fields[f]]
1837
+ if (field.name == null && field.field != null) field.name = field.field
1838
+ if (field.field == null && field.name != null) field.field = field.name
1839
+ field.$el = query(this.box).find(`[name='${String(field.name).replace(/\\/g, '\\\\')}']`)
1840
+ field.el = field.$el.get(0)
1841
+ if (field.el) field.el.id = field.name
1842
+ if (field.w2field) {
1843
+ field.w2field.reset()
1844
+ }
1845
+ field.$el
1846
+ .off('.w2form')
1847
+ .on('change.w2form', function(event) {
1848
+ let value = self.getFieldValue(field.field)
1849
+ // clear error class
1850
+ if (['enum', 'file'].includes(field.type)) {
1851
+ let helper = field.w2field?.helpers?.multi
1852
+ query(helper).removeClass('w2ui-error')
1853
+ }
1854
+ if (this._previous != null) {
1855
+ value.previous = this._previous
1856
+ delete this._previous
1857
+ }
1858
+ // event before
1859
+ let edata2 = self.trigger('change', { target: this.name, field: this.name, value, originalEvent: event })
1860
+ if (edata2.isCancelled === true) return
1861
+ // default behavior
1862
+ self.setValue(this.name, value.current)
1863
+ // event after
1864
+ edata2.finish()
1865
+ })
1866
+ .on('input.w2form', function(event) {
1867
+ self.rememberOriginal()
1868
+ let value = self.getFieldValue(field.field)
1869
+ // save previous for change event
1870
+ if (this._previous == null) {
1871
+ this._previous = value.previous
1872
+ }
1873
+ // event before
1874
+ let edata2 = self.trigger('input', { target: self.name, field, value, originalEvent: event })
1875
+ if (edata2.isCancelled === true) return
1876
+ // default action
1877
+ self.setValue(this.name, value.current, true)
1878
+ // event after
1879
+ edata2.finish()
1880
+ })
1881
+ // required
1882
+ if (field.required) {
1883
+ field.$el.closest('.w2ui-field').addClass('w2ui-required')
1884
+ } else {
1885
+ field.$el.closest('.w2ui-field').removeClass('w2ui-required')
1886
+ }
1887
+ // disabled
1888
+ if (field.disabled != null) {
1889
+ if (field.disabled) {
1890
+ if (field.$el.data('tabIndex') == null) {
1891
+ field.$el.data('tabIndex', field.$el.prop('tabIndex'))
1892
+ }
1893
+ field.$el
1894
+ .prop('disabled', true)
1895
+ .prop('tabIndex', -1)
1896
+ .closest('.w2ui-field')
1897
+ .addClass('w2ui-disabled')
1898
+ } else {
1899
+ field.$el
1900
+ .prop('disabled', false)
1901
+ .prop('tabIndex', field.$el.data('tabIndex') ?? field.$el.prop('tabIndex') ?? 0)
1902
+ .closest('.w2ui-field')
1903
+ .removeClass('w2ui-disabled')
1904
+ }
1905
+ }
1906
+ // hidden
1907
+ let tmp = field.el
1908
+ if (!tmp) tmp = query(this.box).find('#' + field.field)
1909
+ if (field.hidden) {
1910
+ query(tmp).closest('.w2ui-field').hide()
1911
+ } else {
1912
+ query(tmp).closest('.w2ui-field').show()
1913
+ }
1914
+ }
1915
+ // attach actions on buttons
1916
+ query(this.box).find('button, input[type=button]').each(el => {
1917
+ query(el).off('click').on('click', function(event) {
1918
+ let action = this.value
1919
+ if (this.id) action = this.id
1920
+ if (this.name) action = this.name
1921
+ self.action(action, event)
1922
+ })
1923
+ })
1924
+ // init controls with record
1925
+ for (let f = 0; f < fields.length; f++) {
1926
+ let field = this.fields[fields[f]]
1927
+ if (!field.el) continue
1928
+ if (!field.$el.hasClass('w2ui-input')) field.$el.addClass('w2ui-input')
1929
+ field.type = String(field.type).toLowerCase()
1930
+ if (!field.options) field.options = {}
1931
+ // list type
1932
+ if (this.LIST_TYPES.includes(field.type)) {
1933
+ let items = field.options.items
1934
+ if (items == null) field.options.items = []
1935
+ if (field.type == 'switch') {
1936
+ // should not have .text if it is not explicitly set, or toolbar will have text
1937
+ items.forEach((item, ind) => {
1938
+ return items[ind] = typeof item != 'object'
1939
+ ? { id: item, text: item }
1940
+ : item
1941
+ })
1942
+ } else {
1943
+ field.options.items = w2utils.normMenu.call(this, items ?? [], field.options)
1944
+ }
1945
+ }
1946
+ // switch
1947
+ if (field.type == 'switch') {
1948
+ if (field.toolbar) {
1949
+ w2ui[this.name + '_' + field.name + '_tb'].destroy()
1950
+ }
1951
+ field.options?.items?.forEach?.(it => it.text == null ? it.text = '' : '')
1952
+ let items = w2utils.normMenu.call(this, field.options.items, field.options)
1953
+ items.forEach(item => item.type ??= 'radio')
1954
+ field.toolbar = new w2toolbar({
1955
+ box: field.$el.prev().get(0),
1956
+ name: this.name + '_' + field.name + '_tb',
1957
+ items,
1958
+ onClick(event) {
1959
+ self.rememberOriginal()
1960
+ let value = self.getFieldValue(field.name)
1961
+ value.current = event.detail.item.id
1962
+ let edata = self.trigger('change', { target: field.name, field: field.name, value, originalEvent: event })
1963
+ if (edata.isCancelled === true) {
1964
+ return
1965
+ }
1966
+ self.record[field.name] = value.current
1967
+ self.setFieldValue(field.name, value.current)
1968
+ edata.finish()
1969
+ }
1970
+ })
1971
+ field.$el.prev().addClass('w2ui-form-switch') // need to add this class, as toolbar render will remove all w2ui-* classes
1972
+ field.toolbar.resize()
1973
+ field.$el
1974
+ .off('.form-input')
1975
+ .on('focus.form-input', event => {
1976
+ let ind = field.toolbar.get(field.$el.val(), true)
1977
+ query(event.target).prop('_index', ind)
1978
+ query(field.toolbar.box).addClass('w2ui-tb-focus')
1979
+ })
1980
+ .on('blur.form-input', event => {
1981
+ query(event.target).removeProp('_index')
1982
+ query(`#${field.name}-tb .w2ui-tb-button`).removeClass('over')
1983
+ query(field.toolbar.box).removeClass('w2ui-tb-focus')
1984
+ })
1985
+ .on('keydown.form-input', event => {
1986
+ let ind = query(event.target).prop('_index')
1987
+ switch (event.key) {
1988
+ case 'ArrowLeft': {
1989
+ if (ind > 0) ind--
1990
+ query(`#${field.name}-tb .w2ui-tb-button`)
1991
+ .removeClass('over')
1992
+ .eq(ind)
1993
+ .addClass('over')
1994
+ query(event.target).prop('_index', ind)
1995
+ break
1996
+ }
1997
+ case 'ArrowRight': {
1998
+ if (ind < field.toolbar.items.length -1) ind++
1999
+ query(`#${field.name}-tb .w2ui-tb-button`)
2000
+ .removeClass('over')
2001
+ .eq(ind)
2002
+ .addClass('over')
2003
+ query(event.target).prop('_index', ind)
2004
+ break
2005
+ }
2006
+ }
2007
+ if (event.keyCode == 32 || event.keyCode == 13) {
2008
+ // space or enter - apply selected
2009
+ self.rememberOriginal()
2010
+ let value = self.getFieldValue(field.name)
2011
+ value.current = field.toolbar.items[ind].id
2012
+ let edata = self.trigger('change', { target: field.name, field: field.name, value, originalEvent: event })
2013
+ if (edata.isCancelled === true) {
2014
+ return
2015
+ }
2016
+ self.record[field.name] = value.current
2017
+ self.setFieldValue(field.name, value.current)
2018
+ edata.finish()
2019
+ query(`#${field.name}-tb .w2ui-tb-button`).removeClass('over')
2020
+ }
2021
+ // do not allow any input, besides a tab
2022
+ if (!event.metaKey && !event.ctrlKey && event.keyCode != 9) {
2023
+ event.preventDefault()
2024
+ }
2025
+ })
2026
+ }
2027
+
2028
+ // HTML select
2029
+ if (field.type == 'select') {
2030
+ // generate options
2031
+ let items = field.options.items
2032
+ let options = ''
2033
+ items.forEach(item => {
2034
+ options += `<option value="${item.id}">${item.text}</option>`
2035
+ })
2036
+ field.$el.html(options)
2037
+ }
2038
+ // w2fields
2039
+ if (this.W2FIELD_TYPES.includes(field.type)) {
2040
+ field.w2field = field.w2field
2041
+ ?? new w2field(w2utils.extend({}, field.options, { type: field.type }))
2042
+ field.w2field.render(field.el)
2043
+ }
2044
+ // map and arrays
2045
+ if (['map', 'array'].includes(field.type)) {
2046
+ // need closure
2047
+ (function (obj, field) {
2048
+ let keepFocus
2049
+ field.el.mapAdd = function(field, div, cnt, empty) {
2050
+ let attr = (field.disabled ? ' readOnly ' : '') + (field.html.tabindex_str || '')
2051
+ let html = `<input type="text" ${(field.html.value.attr ?? '') + attr} class="w2ui-input ${field.html.class ?? ''} w2ui-map value">`
2052
+ + `${field.html.value.text || ''}`
2053
+
2054
+ if (typeof field.html.render == 'function') {
2055
+ html = field.html.render.call(self, { empty: empty === true, ind: cnt, field, div })
2056
+ // make sure all inputs have names as it is important for array objects
2057
+ if (!field.el._errorDisplayed) {
2058
+ query.html(html).filter('input').each(inp => {
2059
+ let name = inp.dataset.name ?? inp.name
2060
+ if (name == null || name == '') {
2061
+ console.log(`ERROR: All inputs of the field %c"${field.name}"%c must have name attribute defined. No name for %c${inp.outerHTML}`,
2062
+ 'color: blue', '', 'color: red')
2063
+ }
2064
+ })
2065
+ field.el._errorDisplayed = true
2066
+ }
2067
+ } else if (field.type == 'map') {
2068
+ // has key input in front
2069
+ html = `<input type="text" ${(field.html.key.attr ?? '') + attr} class="w2ui-input ${field.html.class ?? ''} w2ui-map key">
2070
+ ${field.html.key.text || ''}
2071
+ ` + html
2072
+ }
2073
+ div.append(`<div class="w2ui-map-field" style="margin-bottom: 5px" data-index="${cnt}">${html}</div>`)
2074
+ if (typeof field.html.render == 'function') {
2075
+ let box = div.find(`[data-index="${cnt}"]`)
2076
+ box.find('input').each(el => {
2077
+ // set only if it is not defined in the HTML
2078
+ if (query(el).attr('tabindex') == null) {
2079
+ query(el).attr('tabindex', field.html.tabindex)
2080
+ }
2081
+ })
2082
+ if (typeof field.html.onRefresh == 'function') {
2083
+ field.html.onRefresh.call(self, { index: cnt, empty, box: box.get(0) })
2084
+ }
2085
+ }
2086
+ }
2087
+ field.el.mapRefresh = function(map, div) {
2088
+ // generate options
2089
+ let keys, $k, $v
2090
+ if (field.type == 'map') {
2091
+ if (!w2utils.isPlainObject(map)) map = {}
2092
+ if (map._order == null) map._order = Object.keys(map)
2093
+ keys = map._order
2094
+ }
2095
+ if (field.type == 'array') {
2096
+ if (!Array.isArray(map)) map = []
2097
+ keys = map.map((item, ind) => { return ind })
2098
+ }
2099
+ // delete fields (including empty one)
2100
+ div.find('.w2ui-map-field').remove()
2101
+ for (let ind = 0; ind < keys.length; ind++) {
2102
+ let key = keys[ind]
2103
+ let fld = div.find(`div[data-index='${ind}']`)
2104
+ // add if does not exists
2105
+ if (fld.length == 0) {
2106
+ field.el.mapAdd(field, div, ind)
2107
+ fld = div.find(`div[data-index='${ind}']`)
2108
+ }
2109
+ fld.attr('data-key', key)
2110
+ if (typeof field.html?.render == 'function') {
2111
+ let val = map[key]
2112
+ fld.find('input').each(inp => {
2113
+ let name = inp.dataset.name ?? inp.name // <input data-name="higher priority" name="then">
2114
+ if (inp.type == 'checkbox') {
2115
+ inp.checked = val[name] ?? false
2116
+ } else if (inp.type == 'radio') {
2117
+ inp.checked = val[name] ?? false
2118
+ } else {
2119
+ inp.value = val[name] ?? ''
2120
+ }
2121
+ })
2122
+ } else {
2123
+ $k = fld.find('.w2ui-map.key')
2124
+ $v = fld.find('.w2ui-map.value')
2125
+ let val = map[key]
2126
+ if (field.type == 'array') {
2127
+ let tmp = map.filter((it) => { return it?.key == key ? true : false})
2128
+ if (tmp.length > 0) val = tmp[0].value
2129
+ }
2130
+ $k.val(key)
2131
+ $v.val(val)
2132
+ if (field.disabled === true || field.disabled === false) {
2133
+ $k.prop('readOnly', field.disabled ? true : false)
2134
+ $v.prop('readOnly', field.disabled ? true : false)
2135
+ }
2136
+ }
2137
+ // call refresh
2138
+ if (typeof field.html.onRefresh == 'function') {
2139
+ field.html.onRefresh.call(self, { index: ind, box: div.find(`[data-index="${ind}"]`).get(0) })
2140
+ }
2141
+ }
2142
+ if (typeof field.html.render == 'function') {
2143
+ $v = div.find('.w2ui-map-field:last-child input:first-child')
2144
+ }
2145
+ let cnt = keys.length
2146
+ let curr = div.find(`div[data-index='${cnt}']`)
2147
+ // if not disabled - add next if needed
2148
+ if (curr.length === 0 && (!$k || $k.val() != '' || $v.val() != '')
2149
+ && !($k && ($k.prop('readOnly') === true || $k.prop('disabled') === true))
2150
+ ) {
2151
+ field.el.mapAdd(field, div, cnt, true)
2152
+ }
2153
+ if (field.disabled === true || field.disabled === false) {
2154
+ curr.find('.key').prop('readOnly', field.disabled ? true : false)
2155
+ curr.find('.value').prop('readOnly', field.disabled ? true : false)
2156
+ }
2157
+ // attach events
2158
+ let container = query(field.el).get(0)?.nextSibling // should be div
2159
+ query(container)
2160
+ .off('.mapChange')
2161
+ .on('mouseup.mapChange', 'input', function (event) {
2162
+ /***
2163
+ * This hack is needed for the cases when this field is refreshed and focus in bettween of mousedown and mouse up.
2164
+ * In such a case, the field will not get focused, but should be as there was mouse click.
2165
+ */
2166
+ if (document.activeElement != event.target) {
2167
+ event.target.focus()
2168
+ }
2169
+ })
2170
+ .on('keyup.mapChange', 'input', function(event) {
2171
+ let $div = query(event.target).closest('.w2ui-map-field')
2172
+ let next = $div.get(0).nextElementSibling
2173
+ let prev = $div.get(0).previousElementSibling
2174
+ if (event.keyCode == 13) {
2175
+ let el = keepFocus ?? next
2176
+ if (el instanceof HTMLElement) {
2177
+ let inp = query(el).find('input')
2178
+ if (inp.length > 0) {
2179
+ inp.get(0).focus()
2180
+ }
2181
+ }
2182
+ keepFocus = undefined
2183
+ }
2184
+ let className = query(event.target).hasClass('key') ? 'key' : 'value'
2185
+ if (event.keyCode == 38 && prev) { // up key
2186
+ query(prev).find(`input.${className}, input[name="${event.target.name}"]`).get(0).select()
2187
+ event.preventDefault()
2188
+ }
2189
+ if (event.keyCode == 40 && next) { // down key
2190
+ event.target.blur() // blur is neeeded because because it will trigger change which will re-render fields
2191
+ let next = $div.get(0).nextElementSibling // need to query it again because it was re-rendered
2192
+ query(next).find(`input.${className}, input[name="${event.target.name}"]`).get(0).select()
2193
+ event.preventDefault()
2194
+ }
2195
+ })
2196
+ .on('keydown.mapChange', 'input', function(event) {
2197
+ if (event.keyCode == 9) { // tab
2198
+ /**
2199
+ * In some cases, when elements are added dynamically after element was focused, hitting tab would not
2200
+ * consider newly created elements are focusable, therefore we check here if focus goes to body on tab key
2201
+ * then move it next input
2202
+ */
2203
+ setTimeout(() => {
2204
+ if (document.activeElement?.tagName == 'BODY') {
2205
+ query(event.target.parentNode).next().find('input').get(0)?.focus()
2206
+ }
2207
+ }, 10)
2208
+ }
2209
+ if (event.keyCode == 38 || event.keyCode == 40) {
2210
+ event.preventDefault()
2211
+ }
2212
+ })
2213
+ .on('input.mapChange', 'input', function(event) {
2214
+ let fld = query(event.target).closest('div')
2215
+ let cnt = fld.data('index')
2216
+ let next = fld.get(0).nextElementSibling
2217
+ // if last one, add new empty
2218
+ let isEmpty = true
2219
+ query(fld).find('input').each(el => {
2220
+ if (!['checkbox', 'button'].includes(el.type) && el.value != '') isEmpty = false
2221
+ })
2222
+ let isNextEmpty = true
2223
+ query(next).find('input').each(el => {
2224
+ if (!['checkbox', 'button'].includes(el.type) && el.value != '') isNextEmpty = false
2225
+ })
2226
+ if (!isEmpty && !next) {
2227
+ field.el.mapAdd(field, div, parseInt(cnt) + 1, true)
2228
+ } else if (isEmpty && next && isNextEmpty) {
2229
+ query(next).remove()
2230
+ }
2231
+ })
2232
+ .on('change.mapChange', 'input', function(event) {
2233
+ self.rememberOriginal()
2234
+ // event before
2235
+ let { current, previous, original } = self.getFieldValue(field.field)
2236
+ let $cnt = query(event.target).closest('.w2ui-map-container')
2237
+ // delete empty
2238
+ if (typeof field.html?.render == 'function') {
2239
+ current = current.filter(kk => {
2240
+ let val = [...(new Set(Object.values(kk).filter(vv => typeof vv != 'boolean')))]
2241
+ return !(val.length == 0 || (val.length == 1 && val[0] === ''))
2242
+ })
2243
+ } else if (field.type == 'map') {
2244
+ current._order = []
2245
+ $cnt.find('.w2ui-map.key').each(el => { current._order.push(el.value) })
2246
+ current._order = current._order.filter(k => k !== '')
2247
+ delete current['']
2248
+ } else if (field.type == 'array') {
2249
+ current = current.filter(k => k !== '')
2250
+ }
2251
+ let edata = self.trigger('change', { target: field.field, field: field.field, originalEvent: event,
2252
+ value: { current, previous, original }
2253
+ })
2254
+ if (edata.isCancelled === true) {
2255
+ return
2256
+ }
2257
+ if (query(event.target).parent().find('input').val() == '') {
2258
+ keepFocus = event.target
2259
+ }
2260
+ self.setValue(field.field, current)
2261
+ field.el.mapRefresh(current, div)
2262
+ // event after
2263
+ edata.finish()
2264
+ })
2265
+ }
2266
+ })(this, field)
2267
+ }
2268
+ // set value to HTML input field
2269
+ this.setFieldValue(field.field, this.getValue(field.name))
2270
+ }
2271
+ // event after
2272
+ edata.finish()
2273
+ this.resize()
2274
+ return Date.now() - time
2275
+ }
2276
+
2277
+ render(box) {
2278
+ let time = Date.now()
2279
+ let self = this
2280
+ if (typeof box == 'string') box = query(box).get(0)
2281
+ // event before
2282
+ let edata = this.trigger('render', { target: this.name, box: box ?? this.box })
2283
+ if (edata.isCancelled === true) return
2284
+ // default action
2285
+ if (box != null) {
2286
+ this.unmount() // clean previous control
2287
+ this.box = box
2288
+ }
2289
+ if (!this.isGenerated && !this.formHTML) return
2290
+ if (!this.box) return
2291
+ // render form
2292
+ let html = '<div class="w2ui-form-box">' +
2293
+ (this.header !== '' ? '<div class="w2ui-form-header">' + w2utils.lang(this.header) + '</div>' : '') +
2294
+ ' <div id="form_'+ this.name +'_toolbar" class="w2ui-form-toolbar" style="display: none"></div>' +
2295
+ ' <div id="form_'+ this.name +'_tabs" class="w2ui-form-tabs" style="display: none"></div>' +
2296
+ this.formHTML +
2297
+ '</div>'
2298
+ query(this.box).attr('name', this.name)
2299
+ .addClass('w2ui-reset w2ui-form')
2300
+ .html(html)
2301
+ if (query(this.box).length > 0) query(this.box)[0].style.cssText += this.style
2302
+ w2utils.bindEvents(query(this.box).find('.w2ui-eaction'), this)
2303
+
2304
+ // init toolbar regardless it is defined or not
2305
+ if (typeof this.toolbar.render !== 'function') {
2306
+ this.toolbar = new w2toolbar(w2utils.extend({}, this.toolbar, { name: this.name +'_toolbar', owner: this }))
2307
+ this.toolbar.on('click', function(event) {
2308
+ let edata = self.trigger('toolbar', { target: event.target, originalEvent: event })
2309
+ if (edata.isCancelled === true) return
2310
+ // no default action
2311
+ edata.finish()
2312
+ })
2313
+ }
2314
+ if (typeof this.toolbar === 'object' && typeof this.toolbar.render === 'function') {
2315
+ this.toolbar.render(query(this.box).find('#form_'+ this.name +'_toolbar')[0])
2316
+ }
2317
+ // init tabs regardless it is defined or not
2318
+ if (typeof this.tabs.render !== 'function') {
2319
+ this.tabs = new w2tabs(w2utils.extend({}, this.tabs, { name: this.name +'_tabs', owner: this, active: this.tabs.active }))
2320
+ this.tabs.on('click', function(event) {
2321
+ self.goto(this.get(event.target, true))
2322
+ })
2323
+ }
2324
+ if (typeof this.tabs === 'object' && typeof this.tabs.render === 'function') {
2325
+ this.tabs.render(query(this.box).find('#form_'+ this.name +'_tabs')[0])
2326
+ if (this.tabs.active) this.tabs.click(this.tabs.active)
2327
+ }
2328
+ // event after
2329
+ edata.finish()
2330
+ // after render actions
2331
+ this.resize()
2332
+ let url = (typeof this.url !== 'object' ? this.url : this.url.get)
2333
+ if (url && this.recid != null) {
2334
+ this.request().catch(error => this.refresh()) // even if there was error, still need refresh
2335
+ } else {
2336
+ this.refresh()
2337
+ }
2338
+ // observe div resize
2339
+ this.last.observeResize = new ResizeObserver(() => { this.resize() })
2340
+ this.last.observeResize.observe(this.box)
2341
+ // focus on load
2342
+ if (this.focus != -1) {
2343
+ let setCount = 0
2344
+ let setFocus = () => {
2345
+ if (query(self.box).find('input, select, textarea').length > 0) {
2346
+ self.setFocus()
2347
+ } else {
2348
+ setCount++
2349
+ if (setCount < 20) setTimeout(setFocus, 50) // 1 sec max
2350
+ }
2351
+ }
2352
+ setFocus()
2353
+ }
2354
+ return Date.now() - time
2355
+ }
2356
+
2357
+ unmount() {
2358
+ super.unmount()
2359
+ this.tabs?.unmount?.()
2360
+ this.toolbar?.unmount?.()
2361
+ this.last.observeResize?.disconnect()
2362
+ }
2363
+
2364
+ destroy() {
2365
+ // event before
2366
+ let edata = this.trigger('destroy', { target: this.name })
2367
+ if (edata.isCancelled === true) return
2368
+ // clean up
2369
+ this.tabs?.destroy?.()
2370
+ this.toolbar?.destroy?.()
2371
+ if (query(this.box).find('#form_'+ this.name +'_tabs').length > 0) {
2372
+ this.unmount()
2373
+ }
2374
+ this.last.observeResize?.disconnect()
2375
+ delete w2ui[this.name]
2376
+ // event after
2377
+ edata.finish()
2378
+ }
2379
+
2380
+ setFocus(focus) {
2381
+ if (typeof focus === 'undefined'){
2382
+ // no argument - use form's focus property
2383
+ focus = this.focus
2384
+ }
2385
+ let $input
2386
+ // focus field by index
2387
+ if (w2utils.isInt(focus)){
2388
+ if (focus < 0) {
2389
+ return
2390
+ }
2391
+ let inputs = query(this.box)
2392
+ .find('div:not(.w2ui-field-helper) > input, select, textarea, div > label:nth-child(1) > [type=radio]')
2393
+ .filter(':not(.file-input)')
2394
+ // find visible (offsetParent == null for any element is not visible)
2395
+ while (inputs[focus].offsetParent == null && inputs.length > focus) {
2396
+ focus++
2397
+ }
2398
+ if (inputs[focus]) {
2399
+ $input = query(inputs[focus])
2400
+ }
2401
+ } else if (typeof focus === 'string') {
2402
+ // focus field by name
2403
+ $input = query(this.box).find(`[name='${focus}']`)
2404
+ }
2405
+ if ($input.length > 0){
2406
+ $input.get(0).focus()
2407
+ }
2408
+ return $input
2409
+ }
2410
+ }
2411
+ export { w2form }