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,1854 @@
1
+ /**
2
+ * Part of w2ui 2.0 library
3
+ * - Dependencies: mQuery, w2utils, w2base, w2tooltip, w2color, w2menu, w2date
4
+ *
5
+ * == TODO ==
6
+ * - upload (regular files)
7
+ * - BUG with prefix/postfix and arrows (test in different contexts)
8
+ * - multiple date selection
9
+ * - month selection, year selections
10
+ * - MultiSelect - Allow Copy/Paste for single and multi values
11
+ * - add routeData to list/enum
12
+ * - ENUM, LIST: should have same as grid (limit, offset, search, sort)
13
+ * - ENUM, LIST: should support wild chars
14
+ * - add selection of predefined times (used for appointments)
15
+ * - options.items - can be an array
16
+ * - options.msgNoItems - can be a function
17
+ * - REMOTE fields
18
+ *
19
+ * == 2.0 changes
20
+ * - removed jQuery dependency
21
+ * - enum options.autoAdd
22
+ * - [numeric, date] - options.autoCorrect to enforce range and validity
23
+ * - remote source response items => records or just an array
24
+ * - deprecated "success" field for remote source response
25
+ * - CSP - fixed inline events
26
+ * - remove clear, use reset instead
27
+ * - options.msgSearch
28
+ * - options.msgNoItems
29
+ */
30
+
31
+ import query from './query.js'
32
+ import { w2base } from './w2base.js'
33
+ import { w2utils } from './w2utils.js'
34
+ import { w2tooltip, w2color, w2menu, w2date } from './w2tooltip.js'
35
+
36
+ class w2field extends w2base {
37
+ constructor(type, options) {
38
+ super()
39
+ // sanitization
40
+ if (typeof type == 'string' && options == null) {
41
+ options = { type: type }
42
+ }
43
+ if (typeof type == 'object' && options == null) {
44
+ options = w2utils.clone(type)
45
+ }
46
+ if (typeof type == 'string' && typeof options == 'object') {
47
+ options.type = type
48
+ }
49
+ options.type = String(options.type).toLowerCase()
50
+ this.el = options.el ?? null
51
+ this.selected = null
52
+ this.helpers = {} // object or helper elements
53
+ this.type = options.type ?? 'text'
54
+ this.options = w2utils.clone(options)
55
+ this.onClick = options.onClick ?? null
56
+ this.onAdd = options.onAdd ?? null
57
+ this.onNew = options.onNew ?? null
58
+ this.onRemove = options.onRemove ?? null
59
+ this.onMouseEnter= options.onMouseEnter ?? null
60
+ this.onMouseLeave= options.onMouseLeave ?? null
61
+ this.onScroll = options.onScroll ?? null
62
+ this.tmp = {} // temp object
63
+ // clean up some options
64
+ delete this.options.type
65
+ delete this.options.onClick
66
+ delete this.options.onMouseEnter
67
+ delete this.options.onMouseLeave
68
+ delete this.options.onScroll
69
+
70
+ if (this.el) {
71
+ this.render(this.el)
72
+ }
73
+ }
74
+
75
+ render(el) {
76
+ if (!(el instanceof HTMLElement)) {
77
+ console.log('ERROR: Cannot init w2field on empty subject')
78
+ return
79
+ }
80
+ el._w2field?.reset?.() // will remove all previous events
81
+ el._w2field = this
82
+ this.el = el
83
+ this.init()
84
+ }
85
+
86
+ init() {
87
+ let options = this.options
88
+ let defaults
89
+
90
+ // only for INPUT or TEXTAREA
91
+ if (!['INPUT', 'TEXTAREA'].includes(this.el.tagName.toUpperCase())) {
92
+ console.log('ERROR: w2field could only be applied to INPUT or TEXTAREA.', this.el)
93
+ return
94
+ }
95
+
96
+ switch (this.type) {
97
+ case 'text':
98
+ case 'int':
99
+ case 'float':
100
+ case 'money':
101
+ case 'currency':
102
+ case 'percent':
103
+ case 'alphanumeric':
104
+ case 'bin':
105
+ case 'hex': {
106
+ defaults = {
107
+ min: null,
108
+ max: null,
109
+ step: 1,
110
+ autoFormat: true,
111
+ autoCorrect: true,
112
+ currency: {
113
+ prefix: w2utils.settings.currencyPrefix,
114
+ suffix: w2utils.settings.currencySuffix,
115
+ precision: w2utils.settings.currencyPrecision
116
+ },
117
+ decimalSymbol: w2utils.settings.decimalSymbol,
118
+ groupSymbol: w2utils.settings.groupSymbol,
119
+ arrows: false,
120
+ keyboard: true,
121
+ precision: null,
122
+ prefix: '',
123
+ suffix: ''
124
+ }
125
+ this.options = w2utils.extend({}, defaults, options)
126
+ options = this.options // since object is re-created, need to re-assign
127
+ options.numberRE = new RegExp('['+ options.groupSymbol + ']', 'g')
128
+ options.moneyRE = new RegExp('['+ options.currency.prefix + options.currency.suffix + options.groupSymbol +']', 'g')
129
+ options.percentRE = new RegExp('['+ options.groupSymbol + '%]', 'g')
130
+ // no keyboard support needed
131
+ if (['text', 'alphanumeric', 'hex', 'bin'].includes(this.type)) {
132
+ options.arrows = false
133
+ options.keyboard = false
134
+ }
135
+ break
136
+ }
137
+ case 'color': {
138
+ let size = parseInt(getComputedStyle(this.el)['font-size']) || 12
139
+ defaults = {
140
+ prefix : '#',
141
+ suffix : `<div style="width: ${size}px; height: ${size}px; margin-top: -2px;
142
+ position: relative; top: 50%; transform: translateY(-50%);">&#160;</div>`,
143
+ arrows : false,
144
+ advanced : null, // open advanced by default
145
+ transparent : true
146
+ }
147
+ this.options = w2utils.extend({}, defaults, options)
148
+ options = this.options // since object is re-created, need to re-assign
149
+ break
150
+ }
151
+ case 'date': {
152
+ defaults = {
153
+ format : w2utils.settings.dateFormat, // date format
154
+ keyboard : true, // if true, allows to select date with format
155
+ autoCorrect : true, // correc date or shows the error
156
+ start : null, // first date allowed to select
157
+ end : null, // last date allowed to select
158
+ blockDates : [], // array of blocked dates
159
+ blockWeekdays : [], // blocked weekdays 0 - sunday, 1 - monday, etc
160
+ colored : {}, // ex: { '3/13/2022': 'bg-color|text-color' }
161
+ btnNow : true // if true, displays Now button
162
+ }
163
+ this.options = w2utils.extend({ type: 'date' }, defaults, options)
164
+ options = this.options // since object is re-created, need to re-assign
165
+ if (query(this.el).attr('placeholder') == null) {
166
+ query(this.el).attr('placeholder', options.format)
167
+ }
168
+ break
169
+ }
170
+ case 'time': {
171
+ defaults = {
172
+ format : w2utils.settings.timeFormat,
173
+ keyboard : true,
174
+ autoCorrect : true,
175
+ start : null,
176
+ end : null,
177
+ btnNow : true,
178
+ noMinutes : false
179
+ }
180
+ this.options = w2utils.extend({ type: 'time' }, defaults, options)
181
+ options = this.options // since object is re-created, need to re-assign
182
+ if (query(this.el).attr('placeholder') == null) {
183
+ query(this.el).attr('placeholder', options.format)
184
+ }
185
+ break
186
+ }
187
+ case 'datetime': {
188
+ defaults = {
189
+ format : w2utils.settings.dateFormat + '|' + w2utils.settings.timeFormat,
190
+ keyboard : true,
191
+ autoCorrect : true,
192
+ start : null,
193
+ end : null,
194
+ startTime : null,
195
+ endTime : null,
196
+ blockDates : [], // array of blocked dates
197
+ blockWeekdays : [], // blocked weekdays 0 - sunday, 1 - monday, etc
198
+ colored : {}, // ex: { '3/13/2022': 'bg-color|text-color' }
199
+ btnNow : true,
200
+ noMinutes : false
201
+ }
202
+ this.options = w2utils.extend({ type: 'datetime' }, defaults, options)
203
+ options = this.options // since object is re-created, need to re-assign
204
+ if (query(this.el).attr('placeholder') == null) {
205
+ query(this.el).attr('placeholder', options.placeholder || options.format)
206
+ }
207
+ break
208
+ }
209
+ case 'list':
210
+ case 'combo': {
211
+ defaults = {
212
+ items : [], // array of items, can be a function
213
+ selected : {}, // selected item
214
+ itemMap : null, // can be { id: 'id', text: 'text' } to specify field mapping for an item
215
+ match : 'begins', // ['contains', 'is', 'begins', 'ends']
216
+ filter : true, // weather to filter at all
217
+ compare : null, // compare function for filtering
218
+ prefix : '', // prefix for input
219
+ suffix : '', // sufix for input
220
+ icon : null, // icon class for selected item
221
+ iconStyle : '', // icon style for selected item
222
+ // -- remote items --
223
+ url : null, // remove data source for items
224
+ method : null, // default comes from w2utils.settings.dataType
225
+ postData : {}, // additional data to submit to URL
226
+ recId : null, // map retrieved data from url to id, can be string or function
227
+ recText : null, // map retrieved data from url to text, can be string or function
228
+ debounce : 250, // number of ms to wait before sending server call on search
229
+ minLength : 1, // min number of chars when trigger search
230
+ cacheMax : 250,
231
+ // -- drop items --
232
+ renderDrop : null, // render function for drop down item
233
+ maxDropHeight : 350, // max height for drop down menu
234
+ maxDropWidth : null, // if null then auto set
235
+ minDropWidth : null, // if null then auto set
236
+ // -- misc --
237
+ markSearch : false, // if true, highlights search phrase
238
+ align : 'both', // align with the input ['left', 'right', 'both', 'none']
239
+ altRows : true, // alternate row color for drop itesm
240
+ openOnFocus : false, // if true, shows drop items on focus
241
+ hideSelected : false, // hide selected item from drop down
242
+ msgNoItems : 'No matches',
243
+ msgSearch : 'Type to search...',
244
+ // -- events --
245
+ onSearch : null, // when search needs to be performed
246
+ onRequest : null, // when request is submitted
247
+ onLoad : null, // when data is received
248
+ onError : null, // when data fails to load due to server error
249
+ }
250
+ if (typeof options.items == 'function') {
251
+ options._items_fun = options.items
252
+ }
253
+ // need to be first
254
+ options.items = w2utils.normMenu.call(this, options.items, options)
255
+ if (this.type === 'list') {
256
+ // defaults.search = (options.items && options.items.length >= 10 ? true : false);
257
+ query(this.el).addClass('w2ui-select')
258
+ // if simple value - look it up
259
+ if (!w2utils.isPlainObject(options.selected) && Array.isArray(options.items)) {
260
+ options.items.forEach(item => {
261
+ if (item && item.id === options.selected) {
262
+ options.selected = w2utils.clone(item)
263
+ }
264
+ })
265
+ }
266
+ }
267
+ options = w2utils.extend({}, defaults, options)
268
+ // validate match
269
+ let valid = ['is', 'begins', 'contains', 'ends']
270
+ if (!valid.includes(options.match)) {
271
+ console.log(`ERROR: invalid value "${options.match}" for option.match. It should be one of following: ${valid.join(', ')}.`)
272
+ }
273
+ this.options = options
274
+ if (!w2utils.isPlainObject(options.selected)) options.selected = {}
275
+ this.selected = options.selected
276
+ query(this.el)
277
+ .attr('autocapitalize', 'off')
278
+ .attr('autocomplete', 'off')
279
+ .attr('autocorrect', 'off')
280
+ .attr('spellcheck', 'false')
281
+ if (options.selected.text != null) {
282
+ query(this.el).val(options.selected.text)
283
+ }
284
+ break
285
+ }
286
+ case 'enum': {
287
+ defaults = {
288
+ items : [], // id, text, tooltip, icon
289
+ selected : [],
290
+ itemMap : null, // can be { id: 'id', text: 'text' } to specify field mapping for an item
291
+ max : 0, // max number of selected items, 0 - unlimited
292
+ match : 'begins', // ['contains', 'is', 'begins', 'ends']
293
+ filter : true, // if true, will apply filtering
294
+ compare : null, // compare function for filtering
295
+ // -- remote items --
296
+ url : null, // remove source for items
297
+ method : null, // default httpMethod
298
+ postData : {},
299
+ recId : null, // map retrieved data from url to id, can be string or function
300
+ recText : null, // map retrieved data from url to text, can be string or function
301
+ debounce : 250, // number of ms to wait before sending server call on search
302
+ minLength : 1, // min number of chars when trigger search
303
+ cacheMax : 250,
304
+ // -- item and drop items --
305
+ maxItemWidth : 250, // max width for a single item
306
+ maxDropHeight : 350, // max height for drop down menu
307
+ maxDropWidth : null, // if null then auto set
308
+ renderItem : null, // render selected item
309
+ renderDrop : null, // render function for drop down item
310
+ // -- misc --
311
+ style : '', // style for container div
312
+ openOnFocus : false, // if true, opens drop down on focus
313
+ markSearch : false, // if true, highlights search phrase
314
+ align : 'both',// align with the input ['left', 'right', 'both', 'none']
315
+ altRows : true, // if ture, will use alternate row colors
316
+ hideSelected : true, // hide selected items from drop down
317
+ msgNoItems : 'No matches',
318
+ msgSearch : 'Type to search...',
319
+ // -- events --
320
+ onAdd : null, // when item is selected from drop down
321
+ onNew : null, // when new item should be added
322
+ onRemove : null, // when item is removed
323
+ onSearch : null, // when search is triggered
324
+ onClick : null, // when item is clicked
325
+ onRequest : null, // when data is requested
326
+ onLoad : null, // when data is received
327
+ onError : null, // when data fails to load due to server error
328
+ onScroll : null, // when div with selected items is scrolled
329
+ onMouseEnter : null, // when mouse enters item
330
+ onMouseLeave : null, // when mouse leaves item
331
+ }
332
+ options = w2utils.extend({}, defaults, options, { suffix: '' })
333
+ if (typeof options.items == 'function') {
334
+ options._items_fun = options.items
335
+ }
336
+ // validate match
337
+ let valid = ['is', 'begins', 'contains', 'ends']
338
+ if (!valid.includes(options.match)) {
339
+ console.log(`ERROR: invalid value "${options.match}" for option.match. It should be one of following: ${valid.join(', ')}.`)
340
+ }
341
+ options.items = w2utils.normMenu.call(this, options.items, options)
342
+ options.selected = w2utils.normMenu.call(this, options.selected, options)
343
+ this.options = options
344
+ if (!Array.isArray(options.selected)) options.selected = []
345
+ this.selected = options.selected
346
+ break
347
+ }
348
+ case 'file': {
349
+ defaults = {
350
+ selected : [], // array of selected files
351
+ max : 0, // max number of selected files, 0 - unlim
352
+ maxSize : 0, // max size of all files, 0 - unlimited
353
+ maxFileSize : 0, // max size of a single file, 0 -unlimited
354
+ renderItem : null, // render function fo the selected item
355
+ // -- misc --
356
+ maxItemWidth : 250, // max width for a single item
357
+ maxDropHeight : 350, // max height for drop down menu
358
+ maxDropWidth : null, // if null then auto set
359
+ readContent : true, // if true, it will readAsDataURL content of the file
360
+ showErrors : true, // if not true, will show errors
361
+ align : 'both', // align with the input ['left', 'right', 'both', 'none']
362
+ altRows : true, // alternate row color for drop itesm
363
+ style : '', // style for container div
364
+ // -- events --
365
+ onClick : null, // when item is clicked
366
+ onAdd : null, // when item is added
367
+ onRemove : null, // when item is removed
368
+ onMouseEnter : null, // when item is mouse over
369
+ onMouseLeave : null // when item is mouse out
370
+ }
371
+ options = w2utils.extend({}, defaults, options)
372
+ this.options = options
373
+ if (!Array.isArray(options.selected)) options.selected = []
374
+ this.selected = options.selected
375
+ if (query(this.el).attr('placeholder') == null) {
376
+ query(this.el).attr('placeholder', w2utils.lang('Attach files by dragging and dropping or Click to Select'))
377
+ }
378
+ break
379
+ }
380
+ default: {
381
+ console.log(`ERROR: field type "${this.type}" is not supported.`)
382
+ break
383
+ }
384
+ }
385
+ // attach events
386
+ query(this.el)
387
+ .css('box-sizing', 'border-box')
388
+ .addClass('w2field w2ui-input')
389
+ .off('.w2field')
390
+ .on('change.w2field', (event) => { this.change(event) })
391
+ .on('click.w2field', (event) => { this.click(event) })
392
+ .on('focus.w2field', (event) => { this.focus(event) })
393
+ .on('blur.w2field', (event) => { if (this.type !== 'list') this.blur(event) })
394
+ .on('keydown.w2field', (event) => { this.keyDown(event) })
395
+ .on('keyup.w2field', (event) => { this.keyUp(event) })
396
+ // suffix and prefix need to be after styles
397
+ this.addPrefix() // only will add if needed
398
+ this.addSuffix() // only will add if needed
399
+ this.addSearch()
400
+ this.addMultiSearch()
401
+ // this.refresh() // do not call refresh, on change will trigger refresh (for list at list)
402
+ // format initial value
403
+ this.change(new Event('change'))
404
+ }
405
+
406
+ get() {
407
+ let ret
408
+ if (['list', 'enum', 'file'].indexOf(this.type) !== -1) {
409
+ ret = this.selected
410
+ } else {
411
+ ret = query(this.el).val()
412
+ }
413
+ return ret
414
+ }
415
+
416
+ set(val, append) {
417
+ if (['list', 'enum', 'file'].indexOf(this.type) !== -1) {
418
+ let overlay = w2menu.get(this.el.id + '_menu')
419
+ overlay?.hide()
420
+ if (this.type !== 'list' && append) {
421
+ if (!Array.isArray(this.selected)) this.selected = []
422
+ this.selected.push(val)
423
+ // update selected array in overlay
424
+ if (overlay) overlay.options.selected = this.selected
425
+ query(this.el).trigger('input').trigger('change')
426
+ } else {
427
+ if (val == null) val = []
428
+ let it = (this.type === 'enum' && !Array.isArray(val) ? [val] : val)
429
+ this.selected = it
430
+ query(this.el).trigger('input').trigger('change')
431
+ }
432
+ this.refresh()
433
+ } else {
434
+ query(this.el).val(val)
435
+ }
436
+ }
437
+
438
+ setIndex(ind, append) {
439
+ if (['list', 'enum'].indexOf(this.type) !== -1) {
440
+ let overlay = w2menu.get(this.el.id + '_menu')
441
+ overlay?.hide()
442
+ let items = this.options.items
443
+ if (items && items[ind]) {
444
+ if (this.type == 'list') {
445
+ this.selected = items[ind]
446
+ }
447
+ if (this.type == 'enum') {
448
+ if (!append) this.selected = []
449
+ this.selected.push(items[ind])
450
+ }
451
+ if (overlay) overlay.options.selected = this.selected
452
+ query(this.el).trigger('input').trigger('change')
453
+ this.refresh()
454
+ return true
455
+ }
456
+ }
457
+ return false
458
+ }
459
+
460
+ refresh() {
461
+ let options = this.options
462
+ let time = Date.now()
463
+ let styles = getComputedStyle(this.el)
464
+ // update color
465
+ if (this.type == 'color') {
466
+ let color = this.el.value
467
+ if (color.substr(0, 1) != '#' && color.substr(0, 3) != 'rgb') {
468
+ color = '#' + color
469
+ }
470
+ query(this.helpers.suffix).find(':scope > div').css('background-color', color)
471
+ }
472
+ // enum
473
+ if (this.type == 'list') {
474
+ // next line will not work in a form with span: -1
475
+ // query(this.el).parent().css('white-space', 'nowrap') // needs this for arrow always to appear on the right side
476
+ // hide focus and show text
477
+ if (this.helpers.prefix) this.helpers.prefix.hide()
478
+ if (!this.helpers.search) return
479
+ // if empty show no icon
480
+ if (this.selected == null && options.icon) {
481
+ options.prefix = `
482
+ <span class="w2ui-icon ${options.icon} "style="cursor: pointer; font-size: 14px;
483
+ display: inline-block; margin-top: -1px; color: #7F98AD; ${options.iconStyle}">
484
+ </span>`
485
+ this.addPrefix()
486
+ } else {
487
+ options.prefix = ''
488
+ this.addPrefix()
489
+ }
490
+ // focus helper
491
+ let focus = query(this.helpers.search_focus)
492
+ let icon = query(focus[0].previousElementSibling)
493
+ focus.css({ outline: 'none' })
494
+ if (focus.val() === '') {
495
+ focus.css('opacity', 0)
496
+ icon.css('opacity', 0)
497
+ if (this.selected?.id != null) { // id could be "", then it is valid
498
+ let text = this.selected.text
499
+ let ind = this.findItemIndex(options.items, this.selected.id)
500
+ if (text != null) {
501
+ query(this.el)
502
+ .val(w2utils.lang(text))
503
+ .data({
504
+ selected: text,
505
+ selectedIndex: ind[0]
506
+ })
507
+ }
508
+ } else {
509
+ this.el.value = ''
510
+ query(this.el).removeData('selected selectedIndex')
511
+ }
512
+ } else {
513
+ focus.css('opacity', 1)
514
+ icon.css('opacity', 1)
515
+ query(this.el).val('')
516
+ setTimeout(() => {
517
+ if (this.helpers.prefix) this.helpers.prefix.hide()
518
+ if (options.icon) {
519
+ focus.css('margin-left', '17px')
520
+ query(this.helpers.search).find('.w2ui-icon-search')
521
+ .addClass('show-search')
522
+ } else {
523
+ focus.css('margin-left', '0px')
524
+ query(this.helpers.search).find('.w2ui-icon-search')
525
+ .removeClass('show-search')
526
+ }
527
+ }, 1)
528
+ }
529
+ // if readonly or disabled
530
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) {
531
+ setTimeout(() => {
532
+ if (this.helpers.prefix) query(this.helpers.prefix).css('opacity', '0.6')
533
+ if (this.helpers.suffix) query(this.helpers.suffix).css('opacity', '0.6')
534
+ }, 1)
535
+ } else {
536
+ setTimeout(() => {
537
+ if (this.helpers.prefix) query(this.helpers.prefix).css('opacity', '1')
538
+ if (this.helpers.suffix) query(this.helpers.suffix).css('opacity', '1')
539
+ }, 1)
540
+ }
541
+ }
542
+ // multi select control
543
+ let div = this.helpers.multi
544
+ if (['enum', 'file'].includes(this.type) && div) {
545
+ let html = ''
546
+ if (Array.isArray(this.selected)) {
547
+ this.selected.forEach((it, ind) => {
548
+ if (it == null) return
549
+ html += `
550
+ <div class="li-item" index="${ind}" style="max-width: ${parseInt(options.maxItemWidth)}px; ${it.style ? it.style : ''}">
551
+ ${
552
+ typeof options.renderItem === 'function'
553
+ ? options.renderItem(it, ind, `<div class="w2ui-list-remove" index="${ind}">&#160;&#160;</div>`)
554
+ : `
555
+ ${it.icon ? `<span class="w2ui-icon ${it.icon}"></span>` : ''}
556
+ <div class="w2ui-list-remove" index="${ind}">&#160;&#160;</div>
557
+ ${(this.type === 'enum' ? it.text : it.name) ?? it.id ?? it }
558
+ ${it.size ? `<span class="file-size"> - ${w2utils.formatSize(it.size)}</span>` : ''}
559
+ `
560
+ }
561
+ </div>`
562
+ })
563
+ }
564
+ let ul = div.find('.w2ui-multi-items')
565
+ if (options.style) {
566
+ div.attr('style', div.attr('style') + ';' + options.style)
567
+ }
568
+ query(this.el).css('z-index', '-1')
569
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) {
570
+ setTimeout(() => {
571
+ div[0].scrollTop = 0 // scroll to the top
572
+ div.addClass('w2ui-readonly')
573
+ .find('.li-item').css('opacity', '0.9')
574
+ .parent().find('.li-search').hide()
575
+ .find('input').prop('readOnly', true)
576
+ .closest('.w2ui-multi-items')
577
+ .find('.w2ui-list-remove').hide()
578
+ }, 1)
579
+ } else {
580
+ setTimeout(() => {
581
+ div.removeClass('w2ui-readonly')
582
+ .find('.li-item').css('opacity', '1')
583
+ .parent().find('.li-search').show()
584
+ .find('input').prop('readOnly', false)
585
+ .closest('.w2ui-multi-items')
586
+ .find('.w2ui-list-remove').show()
587
+ }, 1)
588
+ }
589
+
590
+ // clean
591
+ if (this.selected?.length > 0) {
592
+ query(this.el).attr('placeholder', '')
593
+ }
594
+ div.find('.w2ui-enum-placeholder').remove()
595
+ ul.find('.li-item').remove()
596
+
597
+ // add new list
598
+ if (html !== '') {
599
+ ul.prepend(html)
600
+ } else if (query(this.el).attr('placeholder') != null && div.find('input').val() === '') {
601
+ let style = w2utils.stripSpaces(`
602
+ padding-top: ${styles['padding-top']};
603
+ padding-left: ${styles['padding-left']};
604
+ box-sizing: ${styles['box-sizing']};
605
+ line-height: ${styles['line-height']};
606
+ font-size: ${styles['font-size']};
607
+ font-family: ${styles['font-family']};
608
+ `)
609
+ div.prepend(`<div class="w2ui-enum-placeholder" style="${style}">${query(this.el).attr('placeholder')}</div>`)
610
+ }
611
+ // ITEMS events
612
+ div.off('.w2item')
613
+ .on('scroll.w2item', (event) => {
614
+ let edata = this.trigger('scroll', { target: this.el, originalEvent: event })
615
+ if (edata.isCancelled === true) return
616
+ // hide tooltip if any
617
+ w2tooltip.hide(this.el.id + '_preview')
618
+ // event after
619
+ edata.finish()
620
+ })
621
+ .find('.li-item')
622
+ .on('click.w2item', (event) => {
623
+ let target = query(event.target).closest('.li-item')
624
+ let index = target.attr('index')
625
+ let item = this.selected[index]
626
+ if (query(target).hasClass('li-search')) return
627
+ event.stopPropagation()
628
+ let edata
629
+ // default behavior
630
+ if (query(event.target).hasClass('w2ui-list-remove')) {
631
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
632
+ // trigger event
633
+ edata = this.trigger('remove', { target: this.el, originalEvent: event, item })
634
+ if (edata.isCancelled === true) return
635
+ // remove file from input element
636
+ let transfer = new DataTransfer()
637
+ let input = query(event.target).closest('.w2ui-list').find('input.file-input').get(0)
638
+ if (input) {
639
+ Array.from(input.files)
640
+ .filter(f => f.name != item.name)
641
+ .forEach(f => transfer.items.add(f))
642
+ input.files = transfer.files
643
+ }
644
+ // remove placeholder in the field
645
+ this.selected.splice(index, 1)
646
+ query(this.el).trigger('input').trigger('change')
647
+ query(event.target).remove()
648
+ } else {
649
+ // trigger event
650
+ edata = this.trigger('click', { target: this.el, originalEvent: event.originalEvent, item })
651
+ if (edata.isCancelled === true) return
652
+ // if file - show image preview
653
+ let preview = item.tooltip
654
+ if (this.type === 'file') {
655
+ if ((/image/i).test(item.type)) { // image
656
+ preview = `
657
+ <div class="w2ui-file-preview">
658
+ <img src="${(item.content ? 'data:'+ item.type +';base64,'+ item.content : '')}"
659
+ style="max-width: 300px">
660
+ </div>`
661
+ }
662
+ preview += `
663
+ <div class="w2ui-file-info">
664
+ <div class="file-caption">${w2utils.lang('Name')}:</div>
665
+ <div class="file-value">${item.name}</div>
666
+ <div class="file-caption">${w2utils.lang('Size')}:</div>
667
+ <div class="file-value">${w2utils.formatSize(item.size)}</div>
668
+ <div class="file-caption">${w2utils.lang('Type')}:</div>
669
+ <div class="file-value file-type">${item.type}</div>
670
+ <div class="file-caption">${w2utils.lang('Modified')}:</div>
671
+ <div class="file-value">${w2utils.date(item.modified)}</div>
672
+ </div>`
673
+ }
674
+ if (preview) {
675
+ let name = this.el.id + '_preview'
676
+ w2tooltip.show({
677
+ name,
678
+ anchor: target.get(0),
679
+ html: preview,
680
+ hideOn: ['doc-click'],
681
+ class: ''
682
+ })
683
+ .show((event) => {
684
+ let $img = query(`#w2overlay-${name} img`)
685
+ $img.on('load', function (event) {
686
+ let w = this.clientWidth
687
+ let h = this.clientHeight
688
+ if (w < 300 & h < 300) return
689
+ if (w >= h && w > 300) query(this).css('width', '300px')
690
+ if (w < h && h > 300) query(this).css('height', '300px')
691
+ })
692
+ .on('error', function (event) {
693
+ this.style.display = 'none'
694
+ })
695
+
696
+ })
697
+ }
698
+ }
699
+ edata.finish()
700
+ })
701
+ .on('mouseenter.w2item', (event) => {
702
+ let target = query(event.target).closest('.li-item')
703
+ if (query(target).hasClass('li-search')) return
704
+ let item = this.selected[query(event.target).attr('index')]
705
+ // trigger event
706
+ let edata = this.trigger('mouseEnter', { target: this.el, originalEvent: event, item })
707
+ if (edata.isCancelled === true) return
708
+ // event after
709
+ edata.finish()
710
+ })
711
+ .on('mouseleave.w2item', (event) => {
712
+ let target = query(event.target).closest('.li-item')
713
+ if (query(target).hasClass('li-search')) return
714
+ let item = this.selected[query(event.target).attr('index')]
715
+ // trigger event
716
+ let edata = this.trigger('mouseLeave', { target: this.el, originalEvent: event, item })
717
+ if (edata.isCancelled === true) return
718
+ // event after
719
+ edata.finish()
720
+ })
721
+
722
+ // update size for enum, hide for file
723
+ if (this.type === 'enum') {
724
+ let search = this.helpers.multi.find('input')
725
+ search.css({ width: '15px' })
726
+ } else {
727
+ this.helpers.multi.find('.li-search').hide()
728
+ }
729
+ this.resize()
730
+ }
731
+ return Date.now() - time
732
+ }
733
+
734
+ // resizing width of list, enum, file controls
735
+ resize() {
736
+ let width = this.el.clientWidth
737
+ // let height = this.el.clientHeight
738
+ // if (this.tmp.current_width == width && height > 0) return
739
+ let styles = getComputedStyle(this.el)
740
+
741
+ let focus = this.helpers.search
742
+ let multi = this.helpers.multi
743
+ let suffix = this.helpers.suffix
744
+ let prefix = this.helpers.prefix
745
+
746
+ // resize helpers
747
+ if (focus) {
748
+ query(focus).css('width', width)
749
+ }
750
+ if (multi) {
751
+ query(multi).css('width', width - parseInt(styles['margin-left'], 10) - parseInt(styles['margin-right'], 10))
752
+ }
753
+ if (suffix) {
754
+ this.addSuffix()
755
+ }
756
+ if (prefix) {
757
+ this.addPrefix()
758
+ }
759
+ // enum or file
760
+ let div = this.helpers.multi
761
+ if (['enum', 'file'].includes(this.type) && div) {
762
+ // adjust height
763
+ query(this.el).css('height', '')
764
+ let cntHeight = query(div).find(':scope div.w2ui-multi-items').get(0).clientHeight + 5
765
+ if (cntHeight < 20) cntHeight = 20
766
+ // max height
767
+ if (cntHeight > this.tmp['max-height']) {
768
+ cntHeight = this.tmp['max-height']
769
+ }
770
+ // min height
771
+ if (cntHeight < this.tmp['min-height']) {
772
+ cntHeight = this.tmp['min-height']
773
+ }
774
+ let inpHeight = w2utils.getSize(this.el, 'height') - 2
775
+ if (inpHeight > cntHeight) cntHeight = inpHeight
776
+ query(div).css({
777
+ 'height': cntHeight + 'px',
778
+ overflow: (cntHeight == this.tmp['max-height'] ? 'auto' : 'hidden')
779
+ })
780
+ query(div).css('height', cntHeight + 'px')
781
+ query(this.el).css({ 'height': cntHeight + 'px' })
782
+ }
783
+ // remember width
784
+ this.tmp.current_width = width
785
+ }
786
+
787
+ reset() {
788
+ // restore paddings
789
+ if (this.tmp != null) {
790
+ query(this.el).css('height', '')
791
+ Array('padding-left', 'padding-right', 'background-color', 'border-color').forEach(prop => {
792
+ if (this.tmp && this.tmp['old-'+ prop] != null) {
793
+ query(this.el).css(prop, this.tmp['old-' + prop])
794
+ delete this.tmp['old-' + prop]
795
+ }
796
+ })
797
+ // remove resize watcher
798
+ clearInterval(this.tmp.sizeTimer)
799
+ }
800
+ // remove events and (data)
801
+ query(this.el)
802
+ .val(this.clean(query(this.el).val()))
803
+ .removeClass('w2field w2ui-input')
804
+ .removeData('selected selectedIndex')
805
+ .off('.w2field') // remove only events added by w2field
806
+ // remove helpers
807
+ Object.keys(this.helpers).forEach(key => {
808
+ query(this.helpers[key]).remove()
809
+ })
810
+ this.helpers = {}
811
+ delete this.el._w2field
812
+ }
813
+
814
+ clean(val) {
815
+ // issue #499
816
+ if (typeof val === 'number'){
817
+ return val
818
+ }
819
+ let options = this.options
820
+ val = String(val).trim()
821
+ // clean
822
+ if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) {
823
+ if (typeof val === 'string') {
824
+ if (options.autoFormat) {
825
+ if (['money', 'currency'].includes(this.type)) {
826
+ val = String(val).replace(options.moneyRE, '')
827
+ }
828
+ if (this.type === 'percent') {
829
+ val = String(val).replace(options.percentRE, '')
830
+ }
831
+ if (['int', 'float'].includes(this.type)) {
832
+ val = String(val).replace(options.numberRE, '')
833
+ }
834
+ }
835
+ // escape group symbol for regex as it could be a ., which is wild card in regex
836
+ let esc_gsroupSymbol = options.groupSymbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
837
+ let esc_decimalSymbol = options.decimalSymbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
838
+ val = val.replace(/\s+/g, '')
839
+ .replace(new RegExp(esc_gsroupSymbol, 'g'), '')
840
+ .replace(new RegExp(esc_decimalSymbol, 'g'), '.')
841
+ }
842
+ if (val !== '' && w2utils.isFloat(val)) val = Number(val); else val = ''
843
+ }
844
+ return val
845
+ }
846
+
847
+ format(val) {
848
+ let options = this.options
849
+ // auto format numbers or money
850
+ if (options.autoFormat && val !== '') {
851
+ switch (this.type) {
852
+ case 'money':
853
+ case 'currency':
854
+ val = w2utils.formatNumber(val, options.currency.precision, true)
855
+ if (val !== '') val = options.currency.prefix + val + options.currency.suffix
856
+ break
857
+ case 'percent':
858
+ val = w2utils.formatNumber(val, options.precision, true)
859
+ if (val !== '') val += '%'
860
+ break
861
+ case 'float':
862
+ val = w2utils.formatNumber(val, options.precision, true)
863
+ break
864
+ case 'int':
865
+ val = w2utils.formatNumber(val, 0, true)
866
+ break
867
+ }
868
+ // if default group symbol does not match - replase it
869
+ let group = parseInt(1000).toLocaleString(w2utils.settings.locale, { useGrouping: true }).slice(1, 2)
870
+ if (group !== this.options.groupSymbol) {
871
+ val = val.replaceAll(group, this.options.groupSymbol)
872
+ }
873
+ }
874
+ return val
875
+ }
876
+
877
+ change(event) {
878
+ // numeric
879
+ if (['int', 'float', 'money', 'currency', 'percent'].indexOf(this.type) !== -1) {
880
+ // check max/min
881
+ let val = query(this.el).val()
882
+ let new_val = this.format(this.clean(query(this.el).val()))
883
+ // if was modified
884
+ if (val !== '' && val != new_val) {
885
+ query(this.el).val(new_val)
886
+ // cancel event
887
+ event.stopPropagation()
888
+ event.preventDefault()
889
+ return false
890
+ }
891
+ }
892
+ // color
893
+ if (this.type === 'color') {
894
+ let color = query(this.el).val()
895
+ if (color.substr(0, 3).toLowerCase() !== 'rgb') {
896
+ color = '#' + color
897
+ let len = query(this.el).val().length
898
+ if (len !== 8 && len !== 6 && len !== 3) color = ''
899
+ }
900
+ let next = query(this.el).get(0).nextElementSibling
901
+ query(next).find('div').css('background-color', color)
902
+ if (query(this.el).hasClass('has-focus')) {
903
+ this.updateOverlay()
904
+ }
905
+ }
906
+ // list, enum
907
+ if (['list', 'enum', 'file'].indexOf(this.type) !== -1) {
908
+ this.refresh()
909
+ }
910
+ // date, time
911
+ if (['date', 'time', 'datetime'].indexOf(this.type) !== -1) {
912
+ // convert linux timestamps
913
+ let tmp = parseInt(this.el.value)
914
+ if (w2utils.isInt(this.el.value) && tmp > 3000) {
915
+ if (this.type === 'time') tmp = w2utils.formatTime(new Date(tmp), this.options.format)
916
+ if (this.type === 'date') tmp = w2utils.formatDate(new Date(tmp), this.options.format)
917
+ if (this.type === 'datetime') tmp = w2utils.formatDateTime(new Date(tmp), this.options.format)
918
+ query(this.el).val(tmp).trigger('input').trigger('change')
919
+ }
920
+ }
921
+ }
922
+
923
+ click(event) {
924
+ // lists
925
+ if (['list', 'combo', 'enum'].includes(this.type)) {
926
+ if (!query(this.el).hasClass('has-focus')) {
927
+ this.focus(event)
928
+ }
929
+ if (this.type == 'list' || this.type == 'combo') {
930
+ // if overlay is already open (and not just opened on focus event) then hide it
931
+ if (!this.tmp.openedOnFocus) {
932
+ let name = this.el.id + '_menu'
933
+ let overlay = w2menu.get(name)
934
+ if (overlay?.displayed) {
935
+ w2menu.hide(name)
936
+ } else {
937
+ this.updateOverlay()
938
+ }
939
+ }
940
+ delete this.tmp.openedOnFocus
941
+ if (this.type == 'list') {
942
+ // since list has separate search input, in order to keep the overlay open, need to stop
943
+ event.stopPropagation()
944
+ }
945
+ }
946
+ if (this.type == 'enum') {
947
+ this.updateOverlay()
948
+ }
949
+ }
950
+ // other fields with drops
951
+ if (['date', 'time', 'datetime', 'color'].includes(this.type)) {
952
+ this.updateOverlay()
953
+ }
954
+ }
955
+
956
+ focus(event) {
957
+ if (this.type == 'list' && document.activeElement == this.el) {
958
+ this.helpers.search_focus.focus()
959
+ // update overlay if needed
960
+ if (event.showMenu !== false && (this.options.openOnFocus !== false && query(this.el).hasClass('has-focus'))
961
+ && !this.tmp.overlay?.overlay?.displayed) {
962
+ setTimeout(() => {
963
+ this.tmp.openedOnFocus = true
964
+ this.updateOverlay()
965
+ }, 0) // execute at the end of event loop
966
+ }
967
+ return
968
+ }
969
+ // color, date, time
970
+ if (['color', 'date', 'time', 'datetime'].indexOf(this.type) !== -1) {
971
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
972
+ this.updateOverlay()
973
+ }
974
+ // menu
975
+ if (['list', 'combo', 'enum'].indexOf(this.type) !== -1) {
976
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) {
977
+ // still add focus
978
+ query(this.el).addClass('has-focus')
979
+ return
980
+ }
981
+ // regenerate items
982
+ if (typeof this.options._items_fun == 'function') {
983
+ this.options.items = w2utils.normMenu.call(this, this.options._items_fun, this.options)
984
+ }
985
+ if (this.helpers.search) {
986
+ let search = this.helpers.search_focus
987
+ search.value = ''
988
+ search.select()
989
+ }
990
+ if (this.type == 'enum') {
991
+ // file control in particular need to receive focus after file select
992
+ let search = query(this.el.previousElementSibling).find('.li-search input').get(0)
993
+ if (document.activeElement !== search) {
994
+ search.focus()
995
+ }
996
+ }
997
+ this.resize()
998
+ // update overlay if needed
999
+ if (event.showMenu !== false && (this.options.openOnFocus !== false && query(this.el).hasClass('has-focus'))
1000
+ && !this.tmp.overlay?.overlay?.displayed) {
1001
+ setTimeout(() => {
1002
+ this.tmp.openedOnFocus = true
1003
+ this.updateOverlay()
1004
+ }, 0) // execute at the end of event loop
1005
+ }
1006
+ }
1007
+ if (this.type == 'file') {
1008
+ let prev = query(this.el).get(0).previousElementSibling
1009
+ query(prev).addClass('has-focus')
1010
+ }
1011
+ query(this.el).addClass('has-focus')
1012
+ }
1013
+
1014
+ blur(event) {
1015
+ let val = query(this.el).val().trim()
1016
+ query(this.el).removeClass('has-focus')
1017
+
1018
+ if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) {
1019
+ if (val !== '') {
1020
+ let newVal = val
1021
+ let error = ''
1022
+ if (!this.isStrValid(val)) { // validity is also checked in blur
1023
+ newVal = ''
1024
+ } else {
1025
+ let rVal = this.clean(val)
1026
+ if (this.options.min != null && rVal < this.options.min) {
1027
+ newVal = this.options.min
1028
+ error = `Should be >= ${this.options.min}`
1029
+ }
1030
+ if (this.options.max != null && rVal > this.options.max) {
1031
+ newVal = this.options.max
1032
+ error = `Should be <= ${this.options.max}`
1033
+ }
1034
+ }
1035
+ if (this.options.autoCorrect) {
1036
+ query(this.el).val(newVal).trigger('input').trigger('change')
1037
+ if (error) {
1038
+ w2tooltip.show({
1039
+ name: this.el.id + '_error',
1040
+ anchor: this.el,
1041
+ html: error
1042
+ })
1043
+ setTimeout(() => { w2tooltip.hide(this.el.id + '_error') }, 3000)
1044
+ }
1045
+ }
1046
+ }
1047
+ }
1048
+ // date or time
1049
+ if (['date', 'time', 'datetime'].includes(this.type) && this.options.autoCorrect) {
1050
+ if (val !== '') {
1051
+ let check = this.type == 'date' ? w2utils.isDate :
1052
+ (this.type == 'time' ? w2utils.isTime : w2utils.isDateTime)
1053
+ if (!w2date.inRange(this.el.value, this.options)
1054
+ || !check.bind(w2utils)(this.el.value, this.options.format)) {
1055
+ // if not in range or wrong value - clear it
1056
+ query(this.el).val('').trigger('input').trigger('change')
1057
+ }
1058
+ }
1059
+ }
1060
+ // clear search input
1061
+ if (this.type === 'enum') {
1062
+ query(this.helpers.multi).find('input').val('').css('width', '15px')
1063
+ // don't hide menu on blur, it should be hidden on tab key up instead, or it will not alow select with click
1064
+ // w2menu.hide(this.el.id + '_menu')
1065
+ }
1066
+ if (this.type == 'file') {
1067
+ let prev = this.el.previousElementSibling
1068
+ query(prev).removeClass('has-focus')
1069
+ }
1070
+ if (this.type === 'list') {
1071
+ this.el.value = this.selected?.text ?? ''
1072
+ // don't hide menu on blur, it should be hidden on tab key up instead, or it will not alow select with click
1073
+ // w2menu.hide(this.el.id + '_menu')
1074
+ }
1075
+ }
1076
+
1077
+ keyDown(event, extra) {
1078
+ let options = this.options
1079
+ let key = event.keyCode || (extra && extra.keyCode)
1080
+ let cancel = false
1081
+ let val, inc, daymil, dt, newValue, newDT
1082
+ // ignore wrong pressed key
1083
+ if (['int', 'float', 'money', 'currency', 'percent', 'hex', 'bin', 'color', 'alphanumeric'].includes(this.type)) {
1084
+ if (!event.metaKey && !event.ctrlKey && !event.altKey) {
1085
+ if (!this.isStrValid(event.key ?? '1', true) && // valid & is not arrows, dot, comma, etc keys
1086
+ ![9, 8, 13, 27, 37, 38, 39, 40, 46].includes(event.keyCode)) {
1087
+ event.preventDefault()
1088
+ if (event.stopPropagation) event.stopPropagation(); else event.cancelBubble = true
1089
+ return false
1090
+ }
1091
+ }
1092
+ }
1093
+ // numeric
1094
+ if (['int', 'float', 'money', 'currency', 'percent'].includes(this.type)) {
1095
+ if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1096
+ val = parseFloat(query(this.el).val().replace(options.moneyRE, '')) || 0
1097
+ inc = options.step
1098
+ if (event.ctrlKey || event.metaKey) inc = options.step * 10
1099
+ switch (key) {
1100
+ case 38: // up
1101
+ if (event.shiftKey) break // no action if shift key is pressed
1102
+ newValue = (val + inc <= options.max || options.max == null ? Number((val + inc).toFixed(12)) : options.max)
1103
+ query(this.el).val(newValue).trigger('input').trigger('change')
1104
+ cancel = true
1105
+ break
1106
+ case 40: // down
1107
+ if (event.shiftKey) break // no action if shift key is pressed
1108
+ newValue = (val - inc >= options.min || options.min == null ? Number((val - inc).toFixed(12)) : options.min)
1109
+ query(this.el).val(newValue).trigger('input').trigger('change')
1110
+ cancel = true
1111
+ break
1112
+ }
1113
+ if (cancel) {
1114
+ event.preventDefault()
1115
+ this.moveCaret2end()
1116
+ }
1117
+ }
1118
+ // date/datetime
1119
+ if (['date', 'datetime'].includes(this.type)) {
1120
+ if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1121
+ let is = (this.type == 'date' ? w2utils.isDate : w2utils.isDateTime).bind(w2utils)
1122
+ let format = (this.type == 'date' ? w2utils.formatDate : w2utils.formatDateTime).bind(w2utils)
1123
+
1124
+ daymil = 24*60*60*1000
1125
+ inc = 1
1126
+ if (event.ctrlKey || event.metaKey) inc = 10 // by month
1127
+ dt = is(query(this.el).val(), options.format, true)
1128
+ if (!dt) { dt = new Date(); daymil = 0 }
1129
+ switch (key) {
1130
+ case 38: // up
1131
+ if (event.shiftKey) break // no action if shift key is pressed
1132
+ if (inc == 10) {
1133
+ dt.setMonth(dt.getMonth() + 1)
1134
+ } else {
1135
+ dt.setTime(dt.getTime() + daymil)
1136
+ }
1137
+ newDT = format(dt.getTime(), options.format)
1138
+ query(this.el).val(newDT).trigger('input').trigger('change')
1139
+ cancel = true
1140
+ break
1141
+ case 40: // down
1142
+ if (event.shiftKey) break // no action if shift key is pressed
1143
+ if (inc == 10) {
1144
+ dt.setMonth(dt.getMonth() - 1)
1145
+ } else {
1146
+ dt.setTime(dt.getTime() - daymil)
1147
+ }
1148
+ newDT = format(dt.getTime(), options.format)
1149
+ query(this.el).val(newDT).trigger('input').trigger('change')
1150
+ cancel = true
1151
+ break
1152
+ }
1153
+ if (cancel) {
1154
+ event.preventDefault()
1155
+ this.moveCaret2end()
1156
+ this.updateOverlay()
1157
+ }
1158
+ }
1159
+ // time
1160
+ if (this.type === 'time') {
1161
+ if (!options.keyboard || query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1162
+ inc = (event.ctrlKey || event.metaKey ? 60 : 1)
1163
+ val = query(this.el).val()
1164
+ let time = w2date.str2min(val) || w2date.str2min((new Date()).getHours() + ':' + ((new Date()).getMinutes() - 1))
1165
+ switch (key) {
1166
+ case 38: // up
1167
+ if (event.shiftKey) break // no action if shift key is pressed
1168
+ time += inc
1169
+ cancel = true
1170
+ break
1171
+ case 40: // down
1172
+ if (event.shiftKey) break // no action if shift key is pressed
1173
+ time -= inc
1174
+ cancel = true
1175
+ break
1176
+ }
1177
+ if (cancel) {
1178
+ event.preventDefault()
1179
+ query(this.el).val(w2date.min2str(time)).trigger('input').trigger('change')
1180
+ this.moveCaret2end()
1181
+ }
1182
+ }
1183
+ // list/enum
1184
+ if (['list', 'enum'].includes(this.type)) {
1185
+ switch (key) {
1186
+ case 8: // delete
1187
+ case 46: // backspace
1188
+ if (this.type == 'list') {
1189
+ let search = query(this.helpers.search_focus)
1190
+ if (search.val() == '') {
1191
+ let edata = this.trigger('remove', { target: this.el, originalEvent: event, item: this.selected })
1192
+ if (edata.isCancelled === true) return
1193
+ this.selected = null
1194
+ w2menu.hide(this.el.id + '_menu')
1195
+ query(this.el).val('').trigger('input').trigger('change')
1196
+ edata.finish()
1197
+ }
1198
+ } else {
1199
+ let search = query(this.helpers.multi).find('input')
1200
+ if (search.val() == '') {
1201
+ let edata = this.trigger('remove', { target: this.el, originalEvent: event, item: this.selected[this.selected.length - 1] })
1202
+ if (edata.isCancelled === true) return
1203
+
1204
+ w2menu.hide(this.el.id + '_menu')
1205
+ this.selected.pop()
1206
+ // update selected array in overlay
1207
+ let overlay = w2menu.get(this.el.id + '_menu')
1208
+ if (overlay) overlay.options.selected = this.selected
1209
+ this.refresh()
1210
+ edata.finish()
1211
+ }
1212
+ }
1213
+ break
1214
+ case 9: // tab key
1215
+ case 16: // shift key (when shift+tab)
1216
+ w2menu.hide(this.el.id + '_menu')
1217
+ break
1218
+ case 27: // escape
1219
+ w2menu.hide(this.el.id + '_menu')
1220
+ this.refresh()
1221
+ break
1222
+ default: {
1223
+ // intentionally blank
1224
+ }
1225
+ }
1226
+ }
1227
+ }
1228
+
1229
+ keyUp(event) {
1230
+ if (this.type == 'list') {
1231
+ let search = query(this.helpers.search_focus)
1232
+ if (search.val() !== '') {
1233
+ query(this.el).attr('placeholder', '')
1234
+ } else {
1235
+ query(this.el).attr('placeholder', this.tmp.pholder)
1236
+ }
1237
+ if (event.keyCode == 13) {
1238
+ setTimeout(() => {
1239
+ search.val('')
1240
+ w2menu.hide(this.el.id + '_menu')
1241
+ this.refresh()
1242
+ }, 1)
1243
+ }
1244
+ // if arrows are clicked, it will show overlay
1245
+ if ([38, 40].includes(event.keyCode) && !this.tmp.overlay?.overlay?.displayed) {
1246
+ this.updateOverlay()
1247
+ }
1248
+ this.refresh()
1249
+ }
1250
+ if (this.type == 'combo') {
1251
+ if (![9, 16, 27].includes(event.keyCode) && this.options.openOnFocus !== true) {
1252
+ // do not show when receives focus on tab or shift + tab or on esc
1253
+ this.updateOverlay()
1254
+ }
1255
+ // if arrows are clicked, it will show overlay
1256
+ if ([38, 40].includes(event.keyCode) && !this.tmp.overlay?.overlay?.displayed) {
1257
+ this.updateOverlay()
1258
+ }
1259
+ }
1260
+ if (this.type == 'enum') {
1261
+ let search = this.helpers.multi.find('input')
1262
+ let styles = getComputedStyle(search.get(0))
1263
+ let width = w2utils.getStrWidth(search.val(),
1264
+ `font-family: ${styles['font-family']}; font-size: ${styles['font-size']};`)
1265
+ search.css({ width: (width + 15) + 'px' })
1266
+ this.resize()
1267
+ // if delete, backspace, tab, shift, escape - hide menu
1268
+ if ([8, 46, 9, 16, 27].includes(event.keyCode)) {
1269
+ if (this.tmp.overlay?.overlay?.displayed) {
1270
+ w2menu.hide(this.el.id + '_menu')
1271
+ }
1272
+ } else {
1273
+ this.updateOverlay()
1274
+ }
1275
+ }
1276
+ }
1277
+
1278
+ findItemIndex(items, id, parents) {
1279
+ let inds = []
1280
+ if (!parents) parents = []
1281
+ if (['list', 'combo', 'enum'].includes(this.type) && this.options.url) {
1282
+ // remove source, so get it from overlay
1283
+ let overlay = w2menu.get(this.el.id + '_menu')
1284
+ if (overlay) {
1285
+ items = overlay.options.items
1286
+ this.options.items = items
1287
+ }
1288
+ }
1289
+ items.forEach((item, ind) => {
1290
+ if (item.id === id) {
1291
+ inds = parents.concat([ind])
1292
+ this.options.index = [ind]
1293
+ }
1294
+ if (inds.length == 0 && item.items && item.items.length > 0) {
1295
+ parents.push(ind)
1296
+ inds = this.findItemIndex(item.items, id, parents)
1297
+ parents.pop()
1298
+ }
1299
+ })
1300
+ return inds
1301
+ }
1302
+
1303
+ updateOverlay(indexOnly) {
1304
+ let options = this.options
1305
+ let params
1306
+ // color
1307
+ if (this.type === 'color') {
1308
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1309
+ w2color.show(w2utils.extend({
1310
+ name: this.el.id + '_color',
1311
+ anchor: this.el,
1312
+ transparent: options.transparent,
1313
+ advanced: options.advanced,
1314
+ color: this.el.value,
1315
+ liveUpdate: true
1316
+ }, this.options))
1317
+ .select(event => {
1318
+ let color = event.detail.color
1319
+ query(this.el).val(color).trigger('input').trigger('change')
1320
+ })
1321
+ .liveUpdate(event => {
1322
+ let color = event.detail.color
1323
+ query(this.helpers.suffix).find(':scope > div').css('background-color', '#' + color)
1324
+ })
1325
+ }
1326
+ // list
1327
+ if (['list', 'combo', 'enum'].includes(this.type)) {
1328
+ let el = this.el
1329
+ let input = this.el
1330
+ if (this.type === 'enum') {
1331
+ el = this.helpers.multi.get(0)
1332
+ input = query(el).find('input').get(0)
1333
+ }
1334
+ if (this.type === 'list') {
1335
+ let sel = this.selected
1336
+ if (w2utils.isPlainObject(sel) && Object.keys(sel).length > 0) {
1337
+ let ind = this.findItemIndex(options.items, sel.id)
1338
+ if (ind.length > 0) {
1339
+ options.index = ind
1340
+ }
1341
+ }
1342
+ input = this.helpers.search_focus
1343
+ }
1344
+ if (query(this.el).hasClass('has-focus') && !this.el.readOnly && !this.el.disabled) {
1345
+ params = w2utils.extend({}, options, {
1346
+ name: this.el.id + '_menu',
1347
+ anchor: input,
1348
+ selected: this.selected,
1349
+ search: false,
1350
+ render: options.renderDrop,
1351
+ anchorClass: '',
1352
+ offsetY: 5,
1353
+ maxHeight: options.maxDropHeight, // TODO: check
1354
+ maxWidth: options.maxDropWidth, // TODO: check
1355
+ minWidth: options.minDropWidth // TODO: check
1356
+ })
1357
+ this.tmp.overlay = w2menu.show(params)
1358
+ .select(event => {
1359
+ if (['list', 'combo'].includes(this.type)) {
1360
+ this.selected = event.detail.item
1361
+ query(input).val('')
1362
+ query(this.el).val(this.selected.text).trigger('input').trigger('change')
1363
+ this.focus({ showMenu: false })
1364
+ } else {
1365
+ let selected = this.selected
1366
+ let newItem = event.detail?.item
1367
+ if (newItem) {
1368
+ // trigger event
1369
+ let edata = this.trigger('add', { target: this.el, item: newItem, originalEvent: event })
1370
+ if (edata.isCancelled === true) return
1371
+ // default behavior
1372
+ if (selected.length >= options.max && options.max > 0) selected.pop()
1373
+ delete newItem.hidden
1374
+ selected.push(newItem)
1375
+ query(this.el).trigger('input').trigger('change')
1376
+ query(this.helpers.multi).find('input').val('')
1377
+ // updaet selected array in overlays
1378
+ let overlay = w2menu.get(this.el.id + '_menu')
1379
+ if (overlay) overlay.options.selected = this.selected
1380
+ // event after
1381
+ edata.finish()
1382
+ }
1383
+ }
1384
+ })
1385
+ }
1386
+ }
1387
+ // date
1388
+ if (['date', 'time', 'datetime'].includes(this.type)) {
1389
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1390
+ w2date.show(w2utils.extend({
1391
+ name: this.el.id + '_date',
1392
+ anchor: this.el,
1393
+ value: this.el.value,
1394
+ }, this.options))
1395
+ .select(event => {
1396
+ let date = event.detail.date
1397
+ if (date != null) {
1398
+ query(this.el).val(date).trigger('input').trigger('change')
1399
+ }
1400
+ })
1401
+ }
1402
+ }
1403
+
1404
+ /*
1405
+ * INTERNAL FUNCTIONS
1406
+ */
1407
+
1408
+ isStrValid(ch, loose) {
1409
+ let isValid = true
1410
+ switch (this.type) {
1411
+ case 'int':
1412
+ if (loose && ['-', this.options.groupSymbol].includes(ch)) {
1413
+ isValid = true
1414
+ } else {
1415
+ isValid = w2utils.isInt(ch.replace(this.options.numberRE, ''))
1416
+ }
1417
+ break
1418
+ case 'percent':
1419
+ ch = ch.replace(/%/g, '')
1420
+ case 'float':
1421
+ if (loose && ['-', '', this.options.decimalSymbol, this.options.groupSymbol].includes(ch)) {
1422
+ isValid = true
1423
+ } else {
1424
+ isValid = w2utils.isFloat(ch.replace(this.options.numberRE, ''))
1425
+ }
1426
+ break
1427
+ case 'money':
1428
+ case 'currency':
1429
+ if (loose && ['-', this.options.decimalSymbol, this.options.groupSymbol, this.options.currency.prefix,
1430
+ this.options.currency.suffix].includes(ch)) {
1431
+ isValid = true
1432
+ } else {
1433
+ isValid = w2utils.isFloat(ch.replace(this.options.moneyRE, ''))
1434
+ }
1435
+ break
1436
+ case 'bin':
1437
+ isValid = w2utils.isBin(ch)
1438
+ break
1439
+ case 'color':
1440
+ case 'hex':
1441
+ isValid = w2utils.isHex(ch)
1442
+ break
1443
+ case 'alphanumeric':
1444
+ isValid = w2utils.isAlphaNumeric(ch)
1445
+ break
1446
+ }
1447
+ return isValid
1448
+ }
1449
+
1450
+ addPrefix() {
1451
+ if (!this.options.prefix) {
1452
+ return
1453
+ }
1454
+ let helper
1455
+ let styles = getComputedStyle(this.el)
1456
+ if (this.tmp['old-padding-left'] == null) {
1457
+ this.tmp['old-padding-left'] = styles['padding-left']
1458
+ }
1459
+ // remove if already displayed
1460
+ if (this.helpers.prefix) query(this.helpers.prefix).remove()
1461
+ query(this.el).before(`<div class="w2ui-field-helper">${this.options.prefix}</div>`)
1462
+ helper = query(this.el).get(0).previousElementSibling
1463
+ query(helper)
1464
+ .css({
1465
+ 'color' : styles.color,
1466
+ 'font-family' : styles['font-family'],
1467
+ 'font-size' : styles['font-size'],
1468
+ 'height' : this.el.clientHeight + 'px',
1469
+ 'padding-top' : parseInt(styles['padding-top'], 10) + 1 + 'px',
1470
+ 'padding-bottom' : parseInt(styles['padding-bottom'], 10) - 1 + 'px',
1471
+ 'padding-left' : this.tmp['old-padding-left'],
1472
+ 'padding-right' : 0,
1473
+ 'margin-top' : (parseInt(styles['margin-top'], 10)) + 'px',
1474
+ 'margin-bottom' : (parseInt(styles['margin-bottom'], 10)) + 'px',
1475
+ 'margin-left' : styles['margin-left'],
1476
+ 'margin-right' : 0,
1477
+ 'z-index' : 1,
1478
+ 'display' : 'inline-flex',
1479
+ 'align-items' : 'center'
1480
+ })
1481
+ // only if visible
1482
+ query(this.el).css('padding-left', helper.clientWidth + 'px !important')
1483
+ // remember helper
1484
+ this.helpers.prefix = helper
1485
+ }
1486
+
1487
+ addSuffix() {
1488
+ if (!this.options.suffix && !this.options.arrows) {
1489
+ return
1490
+ }
1491
+ let helper
1492
+ let self = this
1493
+ let styles = getComputedStyle(this.el)
1494
+ if (this.tmp['old-padding-right'] == null) {
1495
+ this.tmp['old-padding-right'] = styles['padding-right']
1496
+ }
1497
+ let pr = parseInt(styles['padding-right'] || 0)
1498
+ if (this.options.arrows) {
1499
+ // remove if already displayed
1500
+ if (this.helpers.arrows) query(this.helpers.arrows).remove()
1501
+ // add fresh
1502
+ query(this.el).after(
1503
+ '<div class="w2ui-field-helper" style="border: 1px solid transparent">&#160;'+
1504
+ ' <div class="w2ui-field-up" type="up">'+
1505
+ ' <div class="arrow-up" type="up"></div>'+
1506
+ ' </div>'+
1507
+ ' <div class="w2ui-field-down" type="down">'+
1508
+ ' <div class="arrow-down" type="down"></div>'+
1509
+ ' </div>'+
1510
+ '</div>')
1511
+ helper = query(this.el).get(0).nextElementSibling
1512
+ query(helper).css({
1513
+ 'color' : styles.color,
1514
+ 'font-family' : styles['font-family'],
1515
+ 'font-size' : styles['font-size'],
1516
+ 'height' : this.el.clientHeight + 'px',
1517
+ 'padding' : 0,
1518
+ 'margin-top' : (parseInt(styles['margin-top'], 10) + 1) + 'px',
1519
+ 'margin-bottom' : 0,
1520
+ 'border-left' : '1px solid silver',
1521
+ 'width' : '16px',
1522
+ 'transform' : 'translateX(-100%)'
1523
+ })
1524
+ .on('mousedown', function(event) {
1525
+ if (query(event.target).hasClass('arrow-up')) {
1526
+ self.keyDown(event, { keyCode: 38 })
1527
+ }
1528
+ if (query(event.target).hasClass('arrow-down')) {
1529
+ self.keyDown(event, { keyCode: 40 })
1530
+ }
1531
+ })
1532
+ pr += helper.clientWidth // width of the control
1533
+ query(this.el).css('padding-right', pr + 'px !important')
1534
+ this.helpers.arrows = helper
1535
+ }
1536
+ if (this.options.suffix !== '') {
1537
+ // remove if already displayed
1538
+ if (this.helpers.suffix) query(this.helpers.suffix).remove()
1539
+ // add fresh
1540
+ query(this.el).after(`<div class="w2ui-field-helper">${this.options.suffix}</div>`)
1541
+ helper = query(this.el).get(0).nextElementSibling
1542
+ query(helper)
1543
+ .css({
1544
+ 'color' : styles.color,
1545
+ 'font-family' : styles['font-family'],
1546
+ 'font-size' : styles['font-size'],
1547
+ 'height' : this.el.clientHeight + 'px',
1548
+ 'padding-top' : styles['padding-top'],
1549
+ 'padding-bottom' : styles['padding-bottom'],
1550
+ 'padding-left' : 0,
1551
+ 'padding-right' : styles['padding-right'],
1552
+ 'margin-top' : (parseInt(styles['margin-top'], 10) + 2) + 'px',
1553
+ 'margin-bottom' : (parseInt(styles['margin-bottom'], 10) + 1) + 'px',
1554
+ 'transform' : 'translateX(-100%)'
1555
+ })
1556
+
1557
+ query(this.el).css('padding-right', helper.clientWidth + 'px !important')
1558
+ this.helpers.suffix = helper
1559
+ }
1560
+ }
1561
+
1562
+ // Only used for list
1563
+ addSearch() {
1564
+ if (this.type !== 'list') return
1565
+ // clean up & init
1566
+ if (this.helpers.search) query(this.helpers.search).remove()
1567
+ // remember original tabindex
1568
+ let tabIndex = parseInt(query(this.el).attr('tabIndex'))
1569
+ if (!isNaN(tabIndex) && tabIndex !== -1) this.tmp['old-tabIndex'] = tabIndex
1570
+ if (this.tmp['old-tabIndex']) tabIndex = this.tmp['old-tabIndex']
1571
+ if (tabIndex == null || isNaN(tabIndex)) tabIndex = 0
1572
+ // if there is id, add to search with "_search"
1573
+ let searchId = ''
1574
+ if (query(this.el).attr('id') != null) {
1575
+ searchId = 'id="' + query(this.el).attr('id') + '_search"'
1576
+ }
1577
+ // build helper
1578
+ let html = `
1579
+ <div class="w2ui-field-helper">
1580
+ <span class="w2ui-icon w2ui-icon-search"></span>
1581
+ <input ${searchId} type="text" tabIndex="${tabIndex}" ${query(this.el).prop('readOnly') ? 'readonly' : ''}
1582
+ autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"/>
1583
+ </div>`
1584
+ query(this.el).attr('tabindex', -1).before(html)
1585
+ let helper = query(this.el).get(0).previousElementSibling
1586
+ this.helpers.search = helper
1587
+ this.helpers.search_focus = query(helper).find('input').get(0)
1588
+ let styles = getComputedStyle(this.el)
1589
+ query(helper).css({
1590
+ width : this.el.clientWidth + 'px',
1591
+ 'margin-top' : styles['margin-top'],
1592
+ 'margin-left' : styles['margin-left'],
1593
+ 'margin-bottom' : styles['margin-bottom'],
1594
+ 'margin-right' : styles['margin-right']
1595
+ })
1596
+ .find('input')
1597
+ .css({
1598
+ cursor : 'default',
1599
+ width : '100%',
1600
+ opacity : 1,
1601
+ padding : styles.padding,
1602
+ margin : styles.margin,
1603
+ border : '1px solid transparent',
1604
+ 'background-color' : 'transparent'
1605
+ })
1606
+ // INPUT events
1607
+ query(helper).find('input')
1608
+ .off('.w2ui-helper')
1609
+ .on('focus.w2ui-helper', event => {
1610
+ query(event.target).val('')
1611
+ this.tmp.pholder = query(this.el).attr('placeholder') ?? ''
1612
+ this.focus(event)
1613
+ event.stopPropagation()
1614
+ })
1615
+ .on('blur.w2ui-helper', event => {
1616
+ query(event.target).val('')
1617
+ if (this.tmp.pholder != null) query(this.el).attr('placeholder', this.tmp.pholder)
1618
+ this.blur(event)
1619
+ event.stopPropagation()
1620
+ })
1621
+ .on('keydown.w2ui-helper', event => { this.keyDown(event) })
1622
+ .on('keyup.w2ui-helper', event => { this.keyUp(event) })
1623
+ // MAIN div
1624
+ query(helper)
1625
+ .off('.w2ui-helper')
1626
+ .on('click.w2ui-helper', event => {
1627
+ query(helper).find('input').get(0).focus()
1628
+ })
1629
+ }
1630
+
1631
+ // Used in enum/file
1632
+ addMultiSearch() {
1633
+ if (!['enum', 'file'].includes(this.type)) {
1634
+ return
1635
+ }
1636
+ // clean up & init
1637
+ query(this.helpers.multi).remove()
1638
+ // build helper
1639
+ let html = ''
1640
+ let styles = getComputedStyle(this.el)
1641
+ let margin = w2utils.stripSpaces(`
1642
+ margin-top: 0px;
1643
+ margin-bottom: 0px;
1644
+ margin-left: ${styles['margin-left']};
1645
+ margin-right: ${styles['margin-right']};
1646
+ width: ${(w2utils.getSize(this.el, 'width') - parseInt(styles['margin-left'], 10)
1647
+ - parseInt(styles['margin-right'], 10))}px;
1648
+ `)
1649
+ if (this.tmp['min-height'] == null) {
1650
+ let min = this.tmp['min-height'] = parseInt((styles['min-height'] != 'none' ? styles['min-height'] : 0) || 0)
1651
+ let current = parseInt(styles.height)
1652
+ this.tmp['min-height'] = Math.max(min, current)
1653
+ }
1654
+ if (this.tmp['max-height'] == null && styles['max-height'] != 'none') {
1655
+ this.tmp['max-height'] = parseInt(styles['max-height'])
1656
+ }
1657
+
1658
+ // if there is id, add to search with "_search"
1659
+ let searchId = ''
1660
+ if (query(this.el).attr('id') != null) {
1661
+ searchId = `id="${query(this.el).attr('id')}_search"`
1662
+ }
1663
+ // remember original tabindex
1664
+ let tabIndex = parseInt(query(this.el).attr('tabIndex'))
1665
+ if (!isNaN(tabIndex) && tabIndex !== -1) this.tmp['old-tabIndex'] = tabIndex
1666
+ if (this.tmp['old-tabIndex']) tabIndex = this.tmp['old-tabIndex']
1667
+ if (tabIndex == null || isNaN(tabIndex)) tabIndex = 0
1668
+
1669
+ if (this.type === 'enum') {
1670
+ html = `
1671
+ <div class="w2ui-field-helper w2ui-list" style="${margin}">
1672
+ <div class="w2ui-multi-items">
1673
+ <div class="li-search">
1674
+ <input ${searchId} type="text" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
1675
+ tabindex="${tabIndex}"
1676
+ ${query(this.el).prop('readOnly') ? 'readonly': '' }
1677
+ ${query(this.el).prop('disabled') ? 'disabled': '' }>
1678
+ </div>
1679
+ </div>
1680
+ </div>`
1681
+ }
1682
+ if (this.type === 'file') {
1683
+ html = `
1684
+ <div class="w2ui-field-helper w2ui-list" style="${margin}">
1685
+ <div class="w2ui-multi-file">
1686
+ <input name="attachment" class="file-input" type="file" tabindex="-1"'
1687
+ style="width: 100%; height: 100%; opacity: 0" title=""
1688
+ ${this.options.max !== 1 ? 'multiple' : ''}
1689
+ ${query(this.el).prop('readOnly') || query(this.el).prop('disabled') ? 'disabled': ''}
1690
+ ${query(this.el).attr('accept') ? ' accept="'+ query(this.el).attr('accept') +'"': ''}>
1691
+ </div>
1692
+ <div class="w2ui-multi-items">
1693
+ <div class="li-search" style="display: none">
1694
+ <input ${searchId} type="text" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false"
1695
+ tabindex="${tabIndex}"
1696
+ ${query(this.el).prop('readOnly') ? 'readonly': '' }
1697
+ ${query(this.el).prop('disabled') ? 'disabled': '' }>
1698
+ </div>
1699
+ </div>
1700
+ </div>`
1701
+ }
1702
+ // old bg and border
1703
+ this.tmp['old-background-color'] = styles['background-color']
1704
+ this.tmp['old-border-color'] = styles['border-color']
1705
+
1706
+ query(this.el)
1707
+ .before(html)
1708
+ .css({
1709
+ 'border-color': 'transparent',
1710
+ 'background-color': 'transparent'
1711
+ })
1712
+
1713
+ let div = query(this.el.previousElementSibling)
1714
+ this.helpers.multi = div
1715
+ query(this.el).attr('tabindex', -1)
1716
+ // click anywhere on the field
1717
+ div.on('mousedown', event => { query(event.target).addClass('has-focus') }) // this is needed so that visually focus is there
1718
+ .on('mouseup', event => { query(event.target).removeClass('has-focus') })
1719
+ .on('click', event => { this.focus(event); this.updateOverlay() })
1720
+
1721
+ // search field (small and growing one)
1722
+ div.find('input:not(.file-input)')
1723
+ .on('click', event => { this.click(event) })
1724
+ .on('focus', event => { this.focus(event) })
1725
+ .on('blur', event => { this.blur(event) })
1726
+ .on('keydown', event => { this.keyDown(event) })
1727
+ .on('keyup', event => { this.keyUp(event) })
1728
+
1729
+ // file input
1730
+ if (this.type === 'file') {
1731
+ div.find('input.file-input')
1732
+ .off('.drag')
1733
+ .on('click.drag', (event) => {
1734
+ event.stopPropagation()
1735
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1736
+ this.focus(event)
1737
+ })
1738
+ .on('dragenter.drag', (event) => {
1739
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1740
+ div.addClass('w2ui-file-dragover')
1741
+ })
1742
+ .on('dragleave.drag', (event) => {
1743
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1744
+ div.removeClass('w2ui-file-dragover')
1745
+ })
1746
+ .on('drop.drag', (event) => {
1747
+ if (query(this.el).prop('readOnly') || query(this.el).prop('disabled')) return
1748
+ div.removeClass('w2ui-file-dragover')
1749
+ let files = Array.from(event.dataTransfer.files)
1750
+ files.forEach(file => { this.addFile(file) })
1751
+ this.focus(event)
1752
+ // cancel to stop browser behaviour
1753
+ event.preventDefault()
1754
+ event.stopPropagation()
1755
+ })
1756
+ .on('dragover.drag', (event) => {
1757
+ // cancel to stop browser behaviour
1758
+ event.preventDefault()
1759
+ event.stopPropagation()
1760
+ })
1761
+ .on('change.drag', (event) => {
1762
+ if (typeof event.target.files !== 'undefined') {
1763
+ Array.from(event.target.files).forEach(file => { this.addFile(file) })
1764
+ }
1765
+ this.focus(event)
1766
+ })
1767
+ }
1768
+ this.refresh()
1769
+ }
1770
+
1771
+ addFile(file) {
1772
+ let options = this.options
1773
+ let selected = this.selected
1774
+ let newItem = {
1775
+ name : file.name,
1776
+ type : file.type,
1777
+ modified : file.lastModifiedDate,
1778
+ size : file.size,
1779
+ content : null,
1780
+ file : file
1781
+ }
1782
+ let size = 0
1783
+ let cnt = 0
1784
+ let errors = []
1785
+ if (Array.isArray(selected)) {
1786
+ selected.forEach(item => {
1787
+ if (item.name == file.name && item.size == file.size) {
1788
+ errors.push(w2utils.lang('The file "${name}" (${size}) is already added.', {
1789
+ name: file.name, size: w2utils.formatSize(file.size) }))
1790
+ }
1791
+ size += item.size
1792
+ cnt++
1793
+ })
1794
+ }
1795
+ if (options.maxFileSize !== 0 && newItem.size > options.maxFileSize) {
1796
+ errors.push(w2utils.lang('Maximum file size is ${size}', { size: w2utils.formatSize(options.maxFileSize) }))
1797
+ }
1798
+ if (options.maxSize !== 0 && size + newItem.size > options.maxSize) {
1799
+ errors.push(w2utils.lang('Maximum total size is ${size}', { size: w2utils.formatSize(options.maxSize) }))
1800
+ }
1801
+ if (options.max !== 0 && cnt >= options.max) {
1802
+ errors.push(w2utils.lang('Maximum number of files is ${count}', { count: options.max }))
1803
+ }
1804
+
1805
+ // trigger event
1806
+ let edata = this.trigger('add', { target: this.el, file: newItem, total: cnt, totalSize: size, errors })
1807
+ if (edata.isCancelled === true) return
1808
+ // if errors
1809
+ if (errors.length > 0) {
1810
+ if (options.showErrors) {
1811
+ w2tooltip.show({
1812
+ anchor: this.el,
1813
+ html: 'Errors: ' + errors.join('<br>'),
1814
+ hideOn: ['input', 'doc-click']
1815
+ })
1816
+ }
1817
+ console.log('ERRORS (while adding files): ', errors)
1818
+ return
1819
+ }
1820
+ // check params
1821
+ selected.push(newItem)
1822
+ // read file as base64
1823
+ if (typeof FileReader !== 'undefined' && options.readContent === true) {
1824
+ let reader = new FileReader()
1825
+ let self = this
1826
+ // need a closure
1827
+ reader.onload = (function onload() {
1828
+ return function closure(event) {
1829
+ let fl = event.target.result
1830
+ let ind = fl.indexOf(',')
1831
+ newItem.content = fl.substr(ind + 1)
1832
+ self.refresh()
1833
+ query(self.el).trigger('input').trigger('change')
1834
+ // event after
1835
+ edata.finish()
1836
+ }
1837
+ })()
1838
+ reader.readAsDataURL(file)
1839
+ } else {
1840
+ this.refresh()
1841
+ query(this.el).trigger('input').trigger('change')
1842
+ edata.finish()
1843
+ }
1844
+ }
1845
+
1846
+ // move cursror to end
1847
+ moveCaret2end() {
1848
+ setTimeout(() => {
1849
+ this.el.setSelectionRange(this.el.value.length, this.el.value.length)
1850
+ }, 0)
1851
+ }
1852
+ }
1853
+
1854
+ export { w2field }