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,2457 @@
1
+ /**
2
+ * Part of w2ui 2.0 library
3
+ * - Dependencies: mQuery, w2utils, w2base, w2locale
4
+ *
5
+ * == TODO ==
6
+ * - add w2utils.lang wrap for all captions in all buttons.
7
+ * - check transition (also with layout)
8
+ * - deprecate w2utils.tooltip
9
+ *
10
+ * == 2.0 changes
11
+ * - CSP - fixed inline events (w2utils.tooltip still has it)
12
+ * - transition returns a promise
13
+ * - removed jQuery
14
+ * - refactores w2utils.message()
15
+ * - added w2utils.confirm()
16
+ * - added isPlainObject
17
+ * - added stripSpaces
18
+ * - implemented marker - can now take an element or just html
19
+ * - cssPrefix - deprecated
20
+ * - w2utils.debounce
21
+ * - w2utils.prepareParams
22
+ * - w2utils.getStrHeight
23
+ * - w2utils.alrert() - same as w2utils.message()
24
+ * - w2utils.prompt() - similar to w2prompt
25
+ * - w2utils.normMenu(..., options) got options parameter that can have itemMap
26
+ * - w2utils.getNested()
27
+ * - w2utils.wait() - async timer
28
+ */
29
+
30
+ import { w2base } from './w2base.js'
31
+ import { w2locale } from './w2locale.js'
32
+ import { query } from './query.js'
33
+
34
+ // variable that holds all w2ui objects
35
+ let w2ui = {}
36
+
37
+ class Utils {
38
+ constructor () {
39
+ this.version = '2.0.x'
40
+ this.tmp = {}
41
+ this.settings = this.extend({}, {
42
+ 'dataType' : 'JSON', // can be HTTP, JSON, RESTFULL (case sensitive)
43
+ 'dateStartYear' : 1950, // start year for date-picker
44
+ 'dateEndYear' : 2030, // end year for date picker
45
+ 'macButtonOrder' : false, // if true, Yes on the right side
46
+ 'warnNoPhrase' : false, // call console.warn if lang() encounters a missing phrase
47
+ }, w2locale, { phrases: null }), // if there are no phrases, then it is original language
48
+ this.i18nCompare = Intl.Collator().compare
49
+ this.hasLocalStorage = testLocalStorage()
50
+
51
+ // some internal variables
52
+ this.isMac = /Mac/i.test(navigator.platform)
53
+ this.isMobile = /(iphone|ipod|mobile|android)/i.test(navigator.userAgent)
54
+ this.isIOS = /(iphone|ipod|ipad)/i.test(navigator.platform)
55
+ this.isAndroid = /(android)/i.test(navigator.userAgent)
56
+ this.isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
57
+ this.isFirefox = /(Firefox)/i.test(navigator.userAgent)
58
+
59
+ // Formatters: Primarily used in grid
60
+ this.formatters = {
61
+ 'number'(record, extra) {
62
+ if (extra == undefined) extra = record
63
+ let { value, params } = extra
64
+ if (parseInt(params) > 20) params = 20
65
+ if (parseInt(params) < 0) params = 0
66
+ if (value == null || value === '') return ''
67
+ return w2utils.formatNumber(parseFloat(value), params, true)
68
+ },
69
+
70
+ 'float'(record, extra) {
71
+ return w2utils.formatters.number(record, extra)
72
+ },
73
+
74
+ 'int'(record, extra) {
75
+ return w2utils.formatters.number(record, extra)
76
+ },
77
+
78
+ 'money'(record, extra) {
79
+ if (extra == undefined) extra = record
80
+ let { value, params } = extra
81
+ if (value == null || value === '') return ''
82
+ let data = w2utils.formatNumber(Number(value), w2utils.settings.currencyPrecision, true)
83
+ return (w2utils.settings.currencyPrefix || '') + data + (w2utils.settings.currencySuffix || '')
84
+ },
85
+
86
+ 'currency'(record, extra) {
87
+ return w2utils.formatters.money(record, extra)
88
+ },
89
+
90
+ 'percent'(record, extra) {
91
+ if (extra == undefined) extra = record
92
+ let { value, params } = extra
93
+ if (value == null || value === '') return ''
94
+ return w2utils.formatNumber(value, params || 1) + '%'
95
+ },
96
+
97
+ 'size'(record, extra) {
98
+ if (extra == undefined) extra = record
99
+ let { value, params } = extra
100
+ if (value == null || value === '') return ''
101
+ return w2utils.formatSize(parseInt(value))
102
+ },
103
+
104
+ 'date'(record, extra) {
105
+ if (extra == undefined) extra = record
106
+ let { value, params } = extra
107
+ if (params === '') params = w2utils.settings.dateFormat
108
+ if (value == null || value === 0 || value === '') return ''
109
+ let dt = w2utils.isDateTime(value, params, true)
110
+ if (dt === false) dt = w2utils.isDate(value, params, true)
111
+ return '<span title="'+ dt +'">' + w2utils.formatDate(dt, params) + '</span>'
112
+ },
113
+
114
+ 'datetime'(record, extra) {
115
+ if (extra == undefined) extra = record
116
+ let { value, params } = extra
117
+ if (params === '') params = w2utils.settings.datetimeFormat
118
+ if (value == null || value === 0 || value === '') return ''
119
+ let dt = w2utils.isDateTime(value, params, true)
120
+ if (dt === false) dt = w2utils.isDate(value, params, true)
121
+ return '<span title="'+ dt +'">' + w2utils.formatDateTime(dt, params) + '</span>'
122
+ },
123
+
124
+ 'time'(record, extra) {
125
+ if (extra == undefined) extra = record
126
+ let { value, params } = extra
127
+ if (params === '') params = w2utils.settings.timeFormat
128
+ if (params === 'h12') params = 'hh:mi pm'
129
+ if (params === 'h24') params = 'h24:mi'
130
+ if (value == null || value === 0 || value === '') return ''
131
+ let dt = w2utils.isDateTime(value, params, true)
132
+ if (dt === false) dt = w2utils.isDate(value, params, true)
133
+ return '<span title="'+ dt +'">' + w2utils.formatTime(value, params) + '</span>'
134
+ },
135
+
136
+ 'timestamp'(record, extra) {
137
+ if (extra == undefined) extra = record
138
+ let { value, params } = extra
139
+ if (params === '') params = w2utils.settings.datetimeFormat
140
+ if (value == null || value === 0 || value === '') return ''
141
+ let dt = w2utils.isDateTime(value, params, true)
142
+ if (dt === false) dt = w2utils.isDate(value, params, true)
143
+ return dt.toString ? dt.toString() : ''
144
+ },
145
+
146
+ 'gmt'(record, extra) {
147
+ if (extra == undefined) extra = record
148
+ let { value, params } = extra
149
+ if (params === '') params = w2utils.settings.datetimeFormat
150
+ if (value == null || value === 0 || value === '') return ''
151
+ let dt = w2utils.isDateTime(value, params, true)
152
+ if (dt === false) dt = w2utils.isDate(value, params, true)
153
+ return dt.toUTCString ? dt.toUTCString() : ''
154
+ },
155
+
156
+ 'age'(record, extra) {
157
+ if (extra == undefined) extra = record
158
+ let { value, params } = extra
159
+ if (value == null || value === 0 || value === '') return ''
160
+ let dt = w2utils.isDateTime(value, null, true)
161
+ if (dt === false) dt = w2utils.isDate(value, null, true)
162
+ return '<span title="'+ dt +'">' + w2utils.age(value) + (params ? (' ' + params) : '') + '</span>'
163
+ },
164
+
165
+ 'interval'(record, extra) {
166
+ if (extra == undefined) extra = record
167
+ let { value, params } = extra
168
+ if (value == null || value === 0 || value === '') return ''
169
+ return w2utils.interval(value) + (params ? (' ' + params) : '')
170
+ },
171
+
172
+ 'toggle'(record, extra) {
173
+ if (extra == undefined) extra = record
174
+ let { value, params } = extra
175
+ return (value ? w2utils.lang('Yes') : '')
176
+ },
177
+
178
+ 'password'(record, extra) {
179
+ if (extra == undefined) extra = record
180
+ let { value, params } = extra
181
+ let ret = ''
182
+ if (!value) return ret
183
+ for (let i = 0; i < value.length; i++) {
184
+ ret += '*'
185
+ }
186
+ return ret
187
+ }
188
+ }
189
+ return
190
+
191
+ function testLocalStorage() {
192
+ // test if localStorage is available, see issue #1282
193
+ let str = 'w2ui_test'
194
+ try {
195
+ localStorage.setItem(str, str)
196
+ localStorage.removeItem(str)
197
+ return true
198
+ } catch (e) {
199
+ return false
200
+ }
201
+ }
202
+ }
203
+
204
+ isBin(val) {
205
+ let re = /^[0-1]+$/
206
+ return re.test(val)
207
+ }
208
+
209
+ isInt(val) {
210
+ let re = /^[-+]?[0-9]+$/
211
+ return re.test(val)
212
+ }
213
+
214
+ isFloat(val) {
215
+ if (typeof val === 'string') {
216
+ val = val.replace(new RegExp(this.settings.groupSymbol, 'g'), '')
217
+ .replace(this.settings.decimalSymbol, '.')
218
+ }
219
+ return (typeof val === 'number' || (typeof val === 'string' && val !== '')) && !isNaN(Number(val))
220
+ }
221
+
222
+ isMoney(val) {
223
+ if (typeof val === 'object' || val === '') return false
224
+ if (this.isFloat(val)) return true
225
+ let se = this.settings
226
+ let re = new RegExp('^'+ (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') +
227
+ '[-+]?'+ (se.currencyPrefix ? '\\' + se.currencyPrefix + '?' : '') +
228
+ '[0-9]*[\\'+ se.decimalSymbol +']?[0-9]+'+ (se.currencySuffix ? '\\' + se.currencySuffix + '?' : '') +'$', 'i')
229
+ if (typeof val === 'string') {
230
+ val = val.replace(new RegExp(se.groupSymbol, 'g'), '')
231
+ }
232
+ return re.test(val)
233
+ }
234
+
235
+ isHex(val) {
236
+ let re = /^(0x)?[0-9a-fA-F]+$/
237
+ return re.test(val)
238
+ }
239
+
240
+ isAlphaNumeric(val) {
241
+ let re = /^[a-zA-Z0-9_-]+$/
242
+ return re.test(val)
243
+ }
244
+
245
+ isEmail(val) {
246
+ let email = /^[a-zA-Z0-9._%\-+]+@[а-яА-Яa-zA-Z0-9.-]+\.[а-яА-Яa-zA-Z]+$/
247
+ return email.test(val)
248
+ }
249
+
250
+ isIpAddress(val) {
251
+ let re = new RegExp('^' +
252
+ '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}' +
253
+ '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' +
254
+ '$')
255
+ return re.test(val)
256
+ }
257
+
258
+ isDate(val, format, retDate) {
259
+ if (!val) return false
260
+
261
+ let dt = 'Invalid Date'
262
+ let month, day, year
263
+
264
+ if (format == null) format = this.settings.dateFormat
265
+
266
+ if (typeof val.getFullYear === 'function') { // date object
267
+ year = val.getFullYear()
268
+ month = val.getMonth() + 1
269
+ day = val.getDate()
270
+ } else if (parseInt(val) == val && parseInt(val) > 0) {
271
+ val = new Date(parseInt(val))
272
+ year = val.getFullYear()
273
+ month = val.getMonth() + 1
274
+ day = val.getDate()
275
+ } else {
276
+ val = String(val)
277
+ // convert month formats
278
+ if (new RegExp('mon', 'ig').test(format)) {
279
+ format = format.replace(/month/ig, 'm').replace(/mon/ig, 'm').replace(/dd/ig, 'd').replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase()
280
+ val = val.replace(/[, ]/ig, '/').replace(/\/\//g, '/').toLowerCase()
281
+ for (let m = 0, len = this.settings.fullmonths.length; m < len; m++) {
282
+ let t = this.settings.fullmonths[m]
283
+ val = val.replace(new RegExp(t, 'ig'), (parseInt(m) + 1)).replace(new RegExp(t.substr(0, 3), 'ig'), (parseInt(m) + 1))
284
+ }
285
+ }
286
+ // format date
287
+ let tmp = val.replace(/-/g, '/').replace(/\./g, '/').toLowerCase().split('/')
288
+ let tmp2 = format.replace(/-/g, '/').replace(/\./g, '/').toLowerCase()
289
+ if (tmp2 === 'mm/dd/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2] }
290
+ if (tmp2 === 'm/d/yyyy') { month = tmp[0]; day = tmp[1]; year = tmp[2] }
291
+ if (tmp2 === 'dd/mm/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2] }
292
+ if (tmp2 === 'd/m/yyyy') { month = tmp[1]; day = tmp[0]; year = tmp[2] }
293
+ if (tmp2 === 'yyyy/dd/mm') { month = tmp[2]; day = tmp[1]; year = tmp[0] }
294
+ if (tmp2 === 'yyyy/d/m') { month = tmp[2]; day = tmp[1]; year = tmp[0] }
295
+ if (tmp2 === 'yyyy/mm/dd') { month = tmp[1]; day = tmp[2]; year = tmp[0] }
296
+ if (tmp2 === 'yyyy/m/d') { month = tmp[1]; day = tmp[2]; year = tmp[0] }
297
+ if (tmp2 === 'mm/dd/yy') { month = tmp[0]; day = tmp[1]; year = tmp[2] }
298
+ if (tmp2 === 'm/d/yy') { month = tmp[0]; day = tmp[1]; year = parseInt(tmp[2]) + 1900 }
299
+ if (tmp2 === 'dd/mm/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900 }
300
+ if (tmp2 === 'd/m/yy') { month = tmp[1]; day = tmp[0]; year = parseInt(tmp[2]) + 1900 }
301
+ if (tmp2 === 'yy/dd/mm') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900 }
302
+ if (tmp2 === 'yy/d/m') { month = tmp[2]; day = tmp[1]; year = parseInt(tmp[0]) + 1900 }
303
+ if (tmp2 === 'yy/mm/dd') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900 }
304
+ if (tmp2 === 'yy/m/d') { month = tmp[1]; day = tmp[2]; year = parseInt(tmp[0]) + 1900 }
305
+ }
306
+ if (!this.isInt(year)) return false
307
+ if (!this.isInt(month)) return false
308
+ if (!this.isInt(day)) return false
309
+ year = +year
310
+ month = +month
311
+ day = +day
312
+ dt = new Date(year, month - 1, day)
313
+ dt.setFullYear(year)
314
+ // do checks
315
+ if (month == null) return false
316
+ if (String(dt) === 'Invalid Date') return false
317
+ if ((dt.getMonth() + 1 !== month) || (dt.getDate() !== day) || (dt.getFullYear() !== year)) return false
318
+ if (retDate === true) return dt; else return true
319
+ }
320
+
321
+ isTime(val, retTime) {
322
+ // Both formats 10:20pm and 22:20
323
+ if (val == null) return false
324
+ let max, am, pm
325
+ // -- process american format
326
+ val = String(val)
327
+ val = val.toUpperCase()
328
+ am = val.indexOf('AM') >= 0
329
+ pm = val.indexOf('PM') >= 0
330
+ let ampm = (pm || am)
331
+ if (ampm) max = 12; else max = 24
332
+ val = val.replace('AM', '').replace('PM', '').trim()
333
+ // ---
334
+ let tmp = val.split(':')
335
+ let h = parseInt(tmp[0] || 0), m = parseInt(tmp[1] || 0), s = parseInt(tmp[2] || 0)
336
+ // accept edge case: 3PM is a good timestamp, but 3 (without AM or PM) is NOT:
337
+ if ((!ampm || tmp.length !== 1) && tmp.length !== 2 && tmp.length !== 3) { return false }
338
+ if (tmp[0] === '' || h < 0 || h > max || !this.isInt(tmp[0]) || tmp[0].length > 2) { return false }
339
+ if (tmp.length > 1 && (tmp[1] === '' || m < 0 || m > 59 || !this.isInt(tmp[1]) || tmp[1].length !== 2)) { return false }
340
+ if (tmp.length > 2 && (tmp[2] === '' || s < 0 || s > 59 || !this.isInt(tmp[2]) || tmp[2].length !== 2)) { return false }
341
+ // check the edge cases: 12:01AM is ok, as is 12:01PM, but 24:01 is NOT ok while 24:00 is (midnight; equivalent to 00:00).
342
+ // meanwhile, there is 00:00 which is ok, but 0AM nor 0PM are okay, while 0:01AM and 0:00AM are.
343
+ if (!ampm && max === h && (m !== 0 || s !== 0)) { return false }
344
+ if (ampm && tmp.length === 1 && h === 0) { return false }
345
+
346
+ if (retTime === true) {
347
+ if (pm && h !== 12) h += 12 // 12:00pm - is noon
348
+ if (am && h === 12) h += 12 // 12:00am - is midnight
349
+ return {
350
+ hours: h,
351
+ minutes: m,
352
+ seconds: s
353
+ }
354
+ }
355
+ return true
356
+ }
357
+
358
+ isDateTime(val, format, retDate) {
359
+ if (typeof val.getFullYear === 'function') { // date object
360
+ if (retDate !== true) return true
361
+ return val
362
+ }
363
+ let intVal = parseInt(val)
364
+ if (intVal === val) {
365
+ if (intVal < 0) return false
366
+ else if (retDate !== true) return true
367
+ else return new Date(intVal)
368
+ }
369
+ let tmp = String(val).indexOf(' ')
370
+ if (tmp < 0) {
371
+ if (String(val).indexOf('T') < 0 || String(new Date(val)) == 'Invalid Date') return false
372
+ else if (retDate !== true) return true
373
+ else return new Date(val)
374
+ } else {
375
+ if (format == null) format = this.settings.datetimeFormat
376
+ let formats = format.split('|')
377
+ let values = [val.substr(0, tmp), val.substr(tmp).trim()]
378
+ formats[0] = formats[0].trim()
379
+ if (formats[1]) formats[1] = formats[1].trim()
380
+ // check
381
+ let tmp1 = this.isDate(values[0], formats[0], true)
382
+ let tmp2 = this.isTime(values[1], true)
383
+ if (tmp1 !== false && tmp2 !== false) {
384
+ if (retDate !== true) return true
385
+ tmp1.setHours(tmp2.hours)
386
+ tmp1.setMinutes(tmp2.minutes)
387
+ tmp1.setSeconds(tmp2.seconds)
388
+ return tmp1
389
+ } else {
390
+ return false
391
+ }
392
+ }
393
+ }
394
+
395
+ age(dateStr) {
396
+ let d1
397
+ if (dateStr === '' || dateStr == null) return ''
398
+ if (typeof dateStr.getFullYear === 'function') { // date object
399
+ d1 = dateStr
400
+ } else if (parseInt(dateStr) == dateStr && parseInt(dateStr) > 0) {
401
+ d1 = new Date(parseInt(dateStr))
402
+ } else {
403
+ d1 = new Date(dateStr)
404
+ }
405
+ if (String(d1) === 'Invalid Date') return ''
406
+
407
+ let d2 = new Date()
408
+ let sec = (d2.getTime() - d1.getTime()) / 1000
409
+ let amount = ''
410
+ let type = ''
411
+ if (sec < 0) {
412
+ amount = 0
413
+ type = 'sec'
414
+ } else if (sec < 60) {
415
+ amount = Math.floor(sec)
416
+ type = 'sec'
417
+ if (sec < 0) { amount = 0; type = 'sec' }
418
+ } else if (sec < 60*60) {
419
+ amount = Math.floor(sec/60)
420
+ type = 'min'
421
+ } else if (sec < 24*60*60) {
422
+ amount = Math.floor(sec/60/60)
423
+ type = 'hour'
424
+ } else if (sec < 30*24*60*60) {
425
+ amount = Math.floor(sec/24/60/60)
426
+ type = 'day'
427
+ } else if (sec < 365*24*60*60) {
428
+ amount = Math.floor(sec/30/24/60/60*10)/10
429
+ type = 'month'
430
+ } else if (sec < 365*4*24*60*60) {
431
+ amount = Math.floor(sec/365/24/60/60*10)/10
432
+ type = 'year'
433
+ } else if (sec >= 365*4*24*60*60) {
434
+ // factor in leap year shift (only older then 4 years)
435
+ amount = Math.floor(sec/365.25/24/60/60*10)/10
436
+ type = 'year'
437
+ }
438
+ return amount + ' ' + type + (amount > 1 ? 's' : '')
439
+ }
440
+
441
+ interval(value) {
442
+ let ret = ''
443
+ if (value < 100) {
444
+ ret = '< 0.01 sec'
445
+ } else if (value < 1000) {
446
+ ret = (Math.floor(value / 10) / 100) + ' sec'
447
+ } else if (value < 10000) {
448
+ ret = (Math.floor(value / 100) / 10) + ' sec'
449
+ } else if (value < 60000) {
450
+ ret = Math.floor(value / 1000) + ' secs'
451
+ } else if (value < 3600000) {
452
+ ret = Math.floor(value / 60000) + ' mins'
453
+ } else if (value < 86400000) {
454
+ ret = Math.floor(value / 3600000 * 10) / 10 + ' hours'
455
+ } else if (value < 2628000000) {
456
+ ret = Math.floor(value / 86400000 * 10) / 10 + ' days'
457
+ } else if (value < 3.1536e+10) {
458
+ ret = Math.floor(value / 2628000000 * 10) / 10 + ' months'
459
+ } else {
460
+ ret = Math.floor(value / 3.1536e+9) / 10 + ' years'
461
+ }
462
+ return ret
463
+ }
464
+
465
+ date(dateStr) {
466
+ if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
467
+ let d1 = new Date(dateStr)
468
+ if (this.isInt(dateStr)) d1 = new Date(Number(dateStr)) // for unix timestamps
469
+ if (String(d1) === 'Invalid Date') return ''
470
+
471
+ let months = this.settings.shortmonths
472
+ let d2 = new Date() // today
473
+ let d3 = new Date()
474
+ d3.setTime(d3.getTime() - 86400000) // yesterday
475
+
476
+ let dd1 = months[d1.getMonth()] + ' ' + d1.getDate() + ', ' + d1.getFullYear()
477
+ let dd2 = months[d2.getMonth()] + ' ' + d2.getDate() + ', ' + d2.getFullYear()
478
+ let dd3 = months[d3.getMonth()] + ' ' + d3.getDate() + ', ' + d3.getFullYear()
479
+
480
+ let time = (d1.getHours() - (d1.getHours() > 12 ? 12 :0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am')
481
+ let time2 = (d1.getHours() - (d1.getHours() > 12 ? 12 :0)) + ':' + (d1.getMinutes() < 10 ? '0' : '') + d1.getMinutes() + ':' + (d1.getSeconds() < 10 ? '0' : '') + d1.getSeconds() + ' ' + (d1.getHours() >= 12 ? 'pm' : 'am')
482
+ let dsp = dd1
483
+ if (dd1 === dd2) dsp = time
484
+ if (dd1 === dd3) dsp = this.lang('Yesterday')
485
+
486
+ return '<span title="'+ dd1 +' ' + time2 +'">'+ dsp +'</span>'
487
+ }
488
+
489
+ formatSize(sizeStr) {
490
+ if (!this.isFloat(sizeStr) || sizeStr === '') return ''
491
+ sizeStr = parseFloat(sizeStr)
492
+ if (sizeStr === 0) return 0
493
+ let sizes = ['Bt', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']
494
+ let i = parseInt( Math.floor( Math.log(sizeStr) / Math.log(1024) ) )
495
+ return (Math.floor(sizeStr / Math.pow(1024, i) * 10) / 10).toFixed(i === 0 ? 0 : 1) + ' ' + (sizes[i] || '??')
496
+ }
497
+
498
+ formatNumber(val, fraction, useGrouping) {
499
+ if (val == null || val === '' || typeof val === 'object') return ''
500
+ let options = {
501
+ minimumFractionDigits: parseInt(fraction),
502
+ maximumFractionDigits: parseInt(fraction),
503
+ useGrouping: !!useGrouping
504
+ }
505
+ if (fraction == null || fraction < 0) {
506
+ options.minimumFractionDigits = 0
507
+ options.maximumFractionDigits = 20
508
+ }
509
+ return parseFloat(val).toLocaleString(this.settings.locale, options)
510
+ }
511
+
512
+ formatDate(dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String
513
+ if (!format) format = this.settings.dateFormat
514
+ if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
515
+
516
+ let dt = new Date(dateStr)
517
+ if (this.isInt(dateStr)) dt = new Date(Number(dateStr)) // for unix timestamps
518
+ if (String(dt) === 'Invalid Date') return ''
519
+
520
+ let year = dt.getFullYear()
521
+ let month = dt.getMonth()
522
+ let date = dt.getDate()
523
+ return format.toLowerCase()
524
+ .replace('month', this.settings.fullmonths[month])
525
+ .replace('mon', this.settings.shortmonths[month])
526
+ .replace(/yyyy/g, ('000' + year).slice(-4))
527
+ .replace(/yyy/g, ('000' + year).slice(-4))
528
+ .replace(/yy/g, ('0' + year).slice(-2))
529
+ .replace(/(^|[^a-z$])y/g, '$1' + year) // only y's that are not preceded by a letter
530
+ .replace(/mm/g, ('0' + (month + 1)).slice(-2))
531
+ .replace(/dd/g, ('0' + date).slice(-2))
532
+ .replace(/th/g, (date == 1 ? 'st' : 'th'))
533
+ .replace(/th/g, (date == 2 ? 'nd' : 'th'))
534
+ .replace(/th/g, (date == 3 ? 'rd' : 'th'))
535
+ .replace(/(^|[^a-z$])m/g, '$1' + (month + 1)) // only y's that are not preceded by a letter
536
+ .replace(/(^|[^a-z$])d/g, '$1' + date) // only y's that are not preceded by a letter
537
+ }
538
+
539
+ formatTime(dateStr, format) { // IMPORTANT dateStr HAS TO BE valid JavaScript Date String
540
+ if (!format) format = this.settings.timeFormat
541
+ if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
542
+
543
+ let dt = new Date(dateStr)
544
+ if (this.isInt(dateStr)) dt = new Date(Number(dateStr)) // for unix timestamps
545
+ if (this.isTime(dateStr)) {
546
+ let tmp = this.isTime(dateStr, true)
547
+ dt = new Date()
548
+ dt.setHours(tmp.hours)
549
+ dt.setMinutes(tmp.minutes)
550
+ }
551
+ if (String(dt) === 'Invalid Date') return ''
552
+ if (format == 'h12') format = 'hh:mi pm'
553
+
554
+ let type = 'am'
555
+ let hour = dt.getHours()
556
+ let h24 = dt.getHours()
557
+ let min = dt.getMinutes()
558
+ let sec = dt.getSeconds()
559
+ if (min < 10) min = '0' + min
560
+ if (sec < 10) sec = '0' + sec
561
+ if (format.indexOf('am') !== -1 || format.indexOf('pm') !== -1) {
562
+ if (hour >= 12) type = 'pm'
563
+ if (hour > 12) hour = hour - 12
564
+ if (hour === 0) hour = 12
565
+ }
566
+ return format.toLowerCase()
567
+ .replace('am', type)
568
+ .replace('pm', type)
569
+ .replace('hhh', (hour < 10 ? '0' + hour : hour))
570
+ .replace('hh24', (h24 < 10 ? '0' + h24 : h24))
571
+ .replace('h24', h24)
572
+ .replace('hh', hour)
573
+ .replace('mm', min)
574
+ .replace('mi', min)
575
+ .replace('ss', sec)
576
+ .replace(/(^|[^a-z$])h/g, '$1' + hour) // only y's that are not preceded by a letter
577
+ .replace(/(^|[^a-z$])m/g, '$1' + min) // only y's that are not preceded by a letter
578
+ .replace(/(^|[^a-z$])s/g, '$1' + sec) // only y's that are not preceded by a letter
579
+ }
580
+
581
+ formatDateTime(dateStr, format) {
582
+ let fmt
583
+ if (dateStr === '' || dateStr == null || (typeof dateStr === 'object' && !dateStr.getMonth)) return ''
584
+ if (typeof format !== 'string') {
585
+ fmt = [this.settings.dateFormat, this.settings.timeFormat]
586
+ } else {
587
+ fmt = format.split('|')
588
+ fmt[0] = fmt[0].trim()
589
+ fmt[1] = (fmt.length > 1 ? fmt[1].trim() : this.settings.timeFormat)
590
+ }
591
+ // older formats support
592
+ if (fmt[1] === 'h12') fmt[1] = 'h:m pm'
593
+ if (fmt[1] === 'h24') fmt[1] = 'h24:m'
594
+ return this.formatDate(dateStr, fmt[0]) + ' ' + this.formatTime(dateStr, fmt[1])
595
+ }
596
+
597
+ stripSpaces(html) {
598
+ if (html == null) return html
599
+ switch (typeof html) {
600
+ case 'number':
601
+ break
602
+ case 'string':
603
+ html = String(html).replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\s\s+/g, ' ').trim()
604
+ break
605
+ case 'object':
606
+ // does not modify original object, but creates a copy
607
+ if (Array.isArray(html)) {
608
+ html = this.extend([], html)
609
+ html.forEach((key, ind) => {
610
+ html[ind] = this.stripSpaces(key)
611
+ })
612
+ } else {
613
+ html = this.extend({}, html)
614
+ Object.keys(html).forEach(key => {
615
+ html[key] = this.stripSpaces(html[key])
616
+ })
617
+ }
618
+ break
619
+ }
620
+ return html
621
+ }
622
+
623
+ stripTags(html) {
624
+ if (html == null) return html
625
+ switch (typeof html) {
626
+ case 'number':
627
+ break
628
+ case 'string':
629
+ html = String(html).replace(/<(?:[^>=]|='[^']*'|="[^"]*"|=[^'"][^\s>]*)*>/ig, '')
630
+ break
631
+ case 'object':
632
+ // does not modify original object, but creates a copy
633
+ if (Array.isArray(html)) {
634
+ html = this.extend([], html)
635
+ html.forEach((key, ind) => {
636
+ html[ind] = this.stripTags(key)
637
+ })
638
+ } else {
639
+ html = this.extend({}, html)
640
+ Object.keys(html).forEach(key => {
641
+ html[key] = this.stripTags(html[key])
642
+ })
643
+ }
644
+ break
645
+ }
646
+ return html
647
+ }
648
+
649
+ encodeTags(html) {
650
+ if (html == null) return html
651
+ switch (typeof html) {
652
+ case 'number':
653
+ break
654
+ case 'string':
655
+ html = String(html).replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;')
656
+ break
657
+ case 'object':
658
+ // does not modify original object, but creates a copy
659
+ if (Array.isArray(html)) {
660
+ html = this.extend([], html)
661
+ html.forEach((key, ind) => {
662
+ html[ind] = this.encodeTags(key)
663
+ })
664
+ } else {
665
+ html = this.extend({}, html)
666
+ Object.keys(html).forEach(key => {
667
+ html[key] = this.encodeTags(html[key])
668
+ })
669
+ }
670
+ break
671
+ }
672
+ return html
673
+ }
674
+
675
+ decodeTags(html) {
676
+ if (html == null) return html
677
+ switch (typeof html) {
678
+ case 'number':
679
+ break
680
+ case 'string':
681
+ html = String(html).replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&quot;/g, '"').replace(/&amp;/g, '&')
682
+ break
683
+ case 'object':
684
+ // does not modify original object, but creates a copy
685
+ if (Array.isArray(html)) {
686
+ html = this.extend([], html)
687
+ html.forEach((key, ind) => {
688
+ html[ind] = this.decodeTags(key)
689
+ })
690
+ } else {
691
+ html = this.extend({}, html)
692
+ Object.keys(html).forEach(key => {
693
+ html[key] = this.decodeTags(html[key])
694
+ })
695
+ }
696
+ break
697
+ }
698
+ return html
699
+ }
700
+
701
+ escapeId(id) {
702
+ // This logic is borrowed from jQuery
703
+ if (id === '' || id == null) return ''
704
+ let re = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g
705
+ return (id + '').replace(re, (ch, asCodePoint) => {
706
+ if (asCodePoint) {
707
+ if (ch === '\0') return '\uFFFD'
708
+ return ch.slice( 0, -1 ) + '\\' + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + ' '
709
+ }
710
+ return '\\' + ch
711
+ })
712
+ }
713
+
714
+ unescapeId(id) {
715
+ // This logic is borrowed from jQuery
716
+ if (id === '' || id == null) return ''
717
+ let re = /\\[\da-fA-F]{1,6}[\x20\t\r\n\f]?|\\([^\r\n\f])/g
718
+ return id.replace(re, (escape, nonHex) => {
719
+ let high = '0x' + escape.slice( 1 ) - 0x10000
720
+ return nonHex ? nonHex : high < 0
721
+ ? String.fromCharCode(high + 0x10000 )
722
+ : String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00)
723
+ })
724
+ }
725
+
726
+ base64encode(str) {
727
+ // Fast Native support in Chrome since 2010
728
+ let utf8Bytes = new TextEncoder().encode(str)
729
+ let binaryString = ''
730
+ for (let byte of utf8Bytes) {
731
+ binaryString += String.fromCharCode(byte)
732
+ }
733
+ return btoa(binaryString)
734
+ }
735
+
736
+ base64decode(encodedStr) {
737
+ // Fast Native support in Chrome since 2010
738
+ let binaryString = atob(encodedStr)
739
+ let utf8Bytes = new Uint8Array(binaryString.length)
740
+ for (let i = 0; i < binaryString.length; i++) {
741
+ utf8Bytes[i] = binaryString.charCodeAt(i)
742
+ }
743
+ return new TextDecoder().decode(utf8Bytes)
744
+ }
745
+
746
+ async sha256(str) {
747
+ const utf8 = new TextEncoder().encode(str)
748
+ return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => {
749
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
750
+ return hashArray.map((bytes) => bytes.toString(16).padStart(2, '0')).join('')
751
+ })
752
+ }
753
+
754
+ transition(div_old, div_new, type, callBack) {
755
+ return new Promise((resolve, reject) => {
756
+ let styles = getComputedStyle(div_old)
757
+ let width = parseInt(styles.width)
758
+ let height = parseInt(styles.height)
759
+ let time = 0.5
760
+
761
+ if (!div_old || !div_new) {
762
+ console.log('ERROR: Cannot do transition when one of the divs is null')
763
+ return
764
+ }
765
+
766
+ div_old.parentNode.style.cssText += 'perspective: 900px; overflow: hidden;'
767
+ div_old.style.cssText += '; position: absolute; z-index: 1019; backface-visibility: hidden'
768
+ div_new.style.cssText += '; position: absolute; z-index: 1020; backface-visibility: hidden'
769
+
770
+ switch (type) {
771
+ case 'slide-left':
772
+ // init divs
773
+ div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
774
+ div_new.style.cssText += 'overflow: hidden; transform: translate3d('+ width + 'px, 0, 0)'
775
+ query(div_new).show()
776
+ // -- need a timing function because otherwise not working
777
+ setTimeout(() => {
778
+ div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
779
+ div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d(-'+ width +'px, 0, 0)'
780
+ }, 1)
781
+ break
782
+
783
+ case 'slide-right':
784
+ // init divs
785
+ div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
786
+ div_new.style.cssText += 'overflow: hidden; transform: translate3d(-'+ width +'px, 0, 0)'
787
+ query(div_new).show()
788
+ // -- need a timing function because otherwise not working
789
+ setTimeout(() => {
790
+ div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0px, 0, 0)'
791
+ div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d('+ width +'px, 0, 0)'
792
+ }, 1)
793
+ break
794
+
795
+ case 'slide-down':
796
+ // init divs
797
+ div_old.style.cssText += 'overflow: hidden; z-index: 1; transform: translate3d(0, 0, 0)'
798
+ div_new.style.cssText += 'overflow: hidden; z-index: 0; transform: translate3d(0, 0, 0)'
799
+ query(div_new).show()
800
+ // -- need a timing function because otherwise not working
801
+ setTimeout(() => {
802
+ div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
803
+ div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, '+ height +'px, 0)'
804
+ }, 1)
805
+ break
806
+
807
+ case 'slide-up':
808
+ // init divs
809
+ div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
810
+ div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, '+ height +'px, 0)'
811
+ query(div_new).show()
812
+ // -- need a timing function because otherwise not working
813
+ setTimeout(() => {
814
+ div_new.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
815
+ div_old.style.cssText += 'transition: '+ time +'s; transform: translate3d(0, 0, 0)'
816
+ }, 1)
817
+ break
818
+
819
+ case 'flip-left':
820
+ // init divs
821
+ div_old.style.cssText += 'overflow: hidden; transform: rotateY(0deg)'
822
+ div_new.style.cssText += 'overflow: hidden; transform: rotateY(-180deg)'
823
+ query(div_new).show()
824
+ // -- need a timing function because otherwise not working
825
+ setTimeout(() => {
826
+ div_new.style.cssText += 'transition: '+ time +'s; transform: rotateY(0deg)'
827
+ div_old.style.cssText += 'transition: '+ time +'s; transform: rotateY(180deg)'
828
+ }, 1)
829
+ break
830
+
831
+ case 'flip-right':
832
+ // init divs
833
+ div_old.style.cssText += 'overflow: hidden; transform: rotateY(0deg)'
834
+ div_new.style.cssText += 'overflow: hidden; transform: rotateY(180deg)'
835
+ query(div_new).show()
836
+ // -- need a timing function because otherwise not working
837
+ setTimeout(() => {
838
+ div_new.style.cssText += 'transition: '+ time +'s; transform: rotateY(0deg)'
839
+ div_old.style.cssText += 'transition: '+ time +'s; transform: rotateY(-180deg)'
840
+ }, 1)
841
+ break
842
+
843
+ case 'flip-down':
844
+ // init divs
845
+ div_old.style.cssText += 'overflow: hidden; transform: rotateX(0deg)'
846
+ div_new.style.cssText += 'overflow: hidden; transform: rotateX(180deg)'
847
+ query(div_new).show()
848
+ // -- need a timing function because otherwise not working
849
+ setTimeout(() => {
850
+ div_new.style.cssText += 'transition: '+ time +'s; transform: rotateX(0deg)'
851
+ div_old.style.cssText += 'transition: '+ time +'s; transform: rotateX(-180deg)'
852
+ }, 1)
853
+ break
854
+
855
+ case 'flip-up':
856
+ // init divs
857
+ div_old.style.cssText += 'overflow: hidden; transform: rotateX(0deg)'
858
+ div_new.style.cssText += 'overflow: hidden; transform: rotateX(-180deg)'
859
+ query(div_new).show()
860
+ // -- need a timing function because otherwise not working
861
+ setTimeout(() => {
862
+ div_new.style.cssText += 'transition: '+ time +'s; transform: rotateX(0deg)'
863
+ div_old.style.cssText += 'transition: '+ time +'s; transform: rotateX(180deg)'
864
+ }, 1)
865
+ break
866
+
867
+ case 'pop-in':
868
+ // init divs
869
+ div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
870
+ div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); transform: scale(.8); opacity: 0;'
871
+ query(div_new).show()
872
+ // -- need a timing function because otherwise not working
873
+ setTimeout(() => {
874
+ div_new.style.cssText += 'transition: '+ time +'s; transform: scale(1); opacity: 1;'
875
+ div_old.style.cssText += 'transition: '+ time +'s;'
876
+ }, 1)
877
+ break
878
+
879
+ case 'pop-out':
880
+ // init divs
881
+ div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); transform: scale(1); opacity: 1;'
882
+ div_new.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0); opacity: 0;'
883
+ query(div_new).show()
884
+ // -- need a timing function because otherwise not working
885
+ setTimeout(() => {
886
+ div_new.style.cssText += 'transition: '+ time +'s; opacity: 1;'
887
+ div_old.style.cssText += 'transition: '+ time +'s; transform: scale(1.7); opacity: 0;'
888
+ }, 1)
889
+ break
890
+
891
+ default:
892
+ // init divs
893
+ div_old.style.cssText += 'overflow: hidden; transform: translate3d(0, 0, 0)'
894
+ div_new.style.cssText += 'overflow: hidden; translate3d(0, 0, 0); opacity: 0;'
895
+ query(div_new).show()
896
+ // -- need a timing function because otherwise not working
897
+ setTimeout(() => {
898
+ div_new.style.cssText += 'transition: '+ time +'s; opacity: 1;'
899
+ div_old.style.cssText += 'transition: '+ time +'s'
900
+ }, 1)
901
+ break
902
+ }
903
+
904
+ setTimeout(() => {
905
+ if (type === 'slide-down') {
906
+ query(div_old).css('z-index', '1019')
907
+ query(div_new).css('z-index', '1020')
908
+ }
909
+ if (div_new) {
910
+ query(div_new)
911
+ .css({ 'opacity': '1' })
912
+ .css({ 'transition': '', 'transform' : '' })
913
+ }
914
+ if (div_old) {
915
+ query(div_old)
916
+ .css({ 'opacity': '1' })
917
+ .css({ 'transition': '', 'transform' : '' })
918
+ }
919
+ if (typeof callBack === 'function') callBack()
920
+ resolve()
921
+ }, time * 1000)
922
+ })
923
+ }
924
+
925
+ lock(box, options = {}) {
926
+ if (box == null) return
927
+ if (typeof options == 'string') {
928
+ options = { msg: options }
929
+ }
930
+ if (arguments[2]) {
931
+ options.spinner = arguments[2]
932
+ }
933
+ options = this.extend({
934
+ spinner: false
935
+ }, options)
936
+ // for backward compatibility
937
+ if (box?.[0] instanceof Node) {
938
+ box = Array.isArray(box) ? box : box.get()
939
+ }
940
+ if (!options.msg && options.msg !== 0) options.msg = ''
941
+ this.unlock(box)
942
+ let el = query(box).get(0)
943
+ let pWidth = el.scrollWidth
944
+ let pHeight = el.scrollHeight
945
+ // if it is body and only has absolute elements, its height will be 0, need to lock entire window
946
+ let style = `height: ${pHeight}px; width: ${pWidth}px`
947
+ if (el.tagName == 'BODY') {
948
+ style = 'position: fixed; right: 0; bottom: 0;'
949
+ }
950
+ query(box).prepend(
951
+ `<div class="w2ui-lock" style="${style}"></div>` +
952
+ '<div class="w2ui-lock-msg"></div>'
953
+ )
954
+ let $lock = query(box).find('.w2ui-lock')
955
+ let $mess = query(box).find('.w2ui-lock-msg')
956
+ if (!options.msg) {
957
+ $mess.css({
958
+ 'background-color': 'transparent',
959
+ 'background-image': 'none',
960
+ 'border': '0px',
961
+ 'box-shadow': 'none'
962
+ })
963
+ }
964
+ if (options.spinner === true) {
965
+ options.msg = `<div class="w2ui-spinner" ${(!options.msg ? 'style="width: 35px; height: 35px"' : '')}></div>`
966
+ + options.msg
967
+ }
968
+ if (options.msg) {
969
+ $mess.html(options.msg).css('display', 'block')
970
+ } else {
971
+ $mess.remove()
972
+ }
973
+ if (options.opacity != null) {
974
+ $lock.css('opacity', options.opacity)
975
+ }
976
+ $lock.css({ display: 'block' })
977
+ if (options.bgColor) {
978
+ $lock.css({ 'background-color': options.bgColor })
979
+ }
980
+ let styles = getComputedStyle($lock.get(0))
981
+ let opacity = styles.opacity ?? 0.15
982
+ $lock
983
+ .on('mousedown', function() {
984
+ if (typeof options.onClick == 'function') {
985
+ options.onClick()
986
+ } else {
987
+ $lock.css({
988
+ 'transition': '.2s',
989
+ 'opacity': opacity * 1.5
990
+ })
991
+ }
992
+ })
993
+ .on('mouseup', function() {
994
+ if (typeof options.onClick !== 'function') {
995
+ $lock.css({
996
+ 'transition': '.2s',
997
+ 'opacity': opacity
998
+ })
999
+ }
1000
+ })
1001
+ .on('mousewheel', function(event) {
1002
+ if (event) {
1003
+ event.stopPropagation()
1004
+ event.preventDefault()
1005
+ }
1006
+ })
1007
+ }
1008
+
1009
+ unlock(box, speed) {
1010
+ if (box == null) return
1011
+ clearTimeout(box._prevUnlock)
1012
+ // for backward compatibility
1013
+ if (box?.[0] instanceof Node) {
1014
+ box = Array.isArray(box) ? box : box.get()
1015
+ }
1016
+ if (this.isInt(speed) && speed > 0) {
1017
+ query(box).find('.w2ui-lock').css({
1018
+ transition: (speed/1000) + 's',
1019
+ opacity: 0,
1020
+ })
1021
+ let _box = query(box).get(0)
1022
+ clearTimeout(_box._prevUnlock)
1023
+ _box._prevUnlock = setTimeout(() => {
1024
+ query(box).find('.w2ui-lock').remove()
1025
+ }, speed)
1026
+ query(box).find('.w2ui-lock-msg').remove()
1027
+ } else {
1028
+ query(box).find('.w2ui-lock').remove()
1029
+ query(box).find('.w2ui-lock-msg').remove()
1030
+ }
1031
+ }
1032
+
1033
+ /**
1034
+ * Opens a context message, similar in parameters as w2popup.open()
1035
+ *
1036
+ * Sample Calls
1037
+ * w2utils.message({ box: '#div', text: 'message' }).ok(() => {})
1038
+ * w2utils.message({ box: '#div', text: 'message', width: 300 }).ok(() => {})
1039
+ * w2utils.message({ box: '#div', text: 'message', actions: ['Save'] }).Save(() => {})
1040
+ *
1041
+ * Used in w2grid, w2form, w2layout (should be in w2popup too)
1042
+ * should be called with .call(...) method
1043
+ *
1044
+ * @param where = {
1045
+ * box, // where to open
1046
+ * after, // title if any, adds title heights
1047
+ * param // additional parameters, used in layouts for panel
1048
+ * }
1049
+ * @param options {
1050
+ * width, // (int), width in px, if negative, then it is maxWidth - width
1051
+ * height, // (int), height in px, if negative, then it is maxHeight - height
1052
+ * text, // centered text
1053
+ * body, // body of the message
1054
+ * buttons, // buttons of the message
1055
+ * html, // if body & buttons are not defined, then html is the entire message
1056
+ * focus, // int or id with a selector, default is 0
1057
+ * hideOn, // ['esc', 'click'], default is ['esc']
1058
+ * actions, // array of actions (only if buttons is not defined)
1059
+ * onOpen, // event when opened
1060
+ * onClose, // event when closed
1061
+ * onAction, // event on action
1062
+ * }
1063
+ */
1064
+ message(where, options) {
1065
+ let closeTimer, openTimer, edata
1066
+ let removeLast = () => {
1067
+ let msgs = query(where?.box).find('.w2ui-message')
1068
+ if (msgs.length == 0) return // no messages already
1069
+ options = msgs.get(0)._msg_options || {}
1070
+ if (typeof options?.close == 'function') {
1071
+ options.close()
1072
+ }
1073
+ }
1074
+ let closeComplete = (options) => {
1075
+ let focus = options.box._msg_prevFocus
1076
+ if (query(where.box).find('.w2ui-message').length <= 1) {
1077
+ if (where.owner) {
1078
+ where.owner.unlock(where.param, 150)
1079
+ } else {
1080
+ this.unlock(where.box, 150)
1081
+ }
1082
+ } else {
1083
+ query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex-1}`).css('z-index', 1500)
1084
+ }
1085
+ if (focus) {
1086
+ let msg = query(focus).closest('.w2ui-message')
1087
+ if (msg.length > 0) {
1088
+ let opt = msg.get(0)._msg_options
1089
+ opt.setFocus(focus)
1090
+ } else {
1091
+ focus.focus()
1092
+ }
1093
+ } else {
1094
+ if (typeof where.owner?.focus == 'function') where.owner.focus()
1095
+ }
1096
+ query(options.box).remove()
1097
+ if (options.msgIndex === 0) {
1098
+ head.css('z-index', options.tmp.zIndex)
1099
+ query(where.box).css('overflow', options.tmp.overflow)
1100
+ }
1101
+ // event after
1102
+ if (options.trigger) {
1103
+ edata.finish()
1104
+ }
1105
+ }
1106
+
1107
+ if (typeof options == 'string' || typeof options == 'number') {
1108
+ options = {
1109
+ width : (String(options).length < 300 ? 350 : 550),
1110
+ height: (String(options).length < 300 ? 170: 250),
1111
+ text : String(options),
1112
+ }
1113
+ }
1114
+ if (arguments.length == 1 || options == null) {
1115
+ options = where
1116
+ }
1117
+ options ??= {}
1118
+ if (options == null || (options.text === '' || options.text == null) && (options.body === '' || options.body == null)) {
1119
+ removeLast()
1120
+ return
1121
+ }
1122
+ if (options.text != null) options.body = `<div class="w2ui-centered w2ui-msg-text">${options.text}</div>`
1123
+ if (options.width == null) options.width = 350
1124
+ if (options.height == null) options.height = 170
1125
+ if (options.hideOn == null) options.hideOn = ['esc']
1126
+ options.cancelAction ??= 'Ok'
1127
+ // mix in events
1128
+ if (options.on == null) {
1129
+ let opts = options
1130
+ options = new w2base()
1131
+ w2utils.extend(options, opts) // needs to be w2utils
1132
+ }
1133
+ options.on('open', (event) => {
1134
+ w2utils.bindEvents(query(options.box).find('.w2ui-eaction'), options) // options is w2base object
1135
+ query(event.detail.box).find('button, input, textarea, [name=hidden-first]')
1136
+ .off('.message')
1137
+ .on('keydown.message', function(evt) {
1138
+ if (evt.keyCode == 27 && options.hideOn.includes('esc')) {
1139
+ if (options.cancelAction) {
1140
+ options.action(options.cancelAction)
1141
+ } else {
1142
+ options.close()
1143
+ }
1144
+ }
1145
+ })
1146
+ // timeout is needed because messages opens over 0.3 seconds
1147
+ setTimeout(() => options.setFocus(options.focus), 300)
1148
+ })
1149
+ options.off('.prom')
1150
+ let prom = {
1151
+ self: options,
1152
+ action(callBack) {
1153
+ options.on('action.prom', callBack)
1154
+ return prom
1155
+ },
1156
+ close(callBack) {
1157
+ options.on('close.prom', callBack)
1158
+ return prom
1159
+ },
1160
+ open(callBack) {
1161
+ options.on('open.prom', callBack)
1162
+ return prom
1163
+ },
1164
+ then(callBack) {
1165
+ options.on('open:after.prom', callBack)
1166
+ return prom
1167
+ }
1168
+ }
1169
+ if (options.actions == null && options.buttons == null && options.html == null) {
1170
+ options.actions = { Ok(event) { event.detail.self.close() }}
1171
+ }
1172
+ options.off('.buttons')
1173
+ if (options.actions != null) {
1174
+ options.buttons = ''
1175
+ Object.keys(options.actions).forEach((action) => {
1176
+ let handler = options.actions[action]
1177
+ let btnAction = action
1178
+ if (typeof handler == 'function') {
1179
+ options.buttons += `<button class="w2ui-btn w2ui-eaction" data-click='["action","${action}","event"]' name="${action}">${action}</button>`
1180
+ }
1181
+ if (typeof handler == 'object') {
1182
+ options.buttons += `<button class="w2ui-btn w2ui-eaction ${handler.class || ''}" name="${action}" data-click='["action","${action}","event"]'
1183
+ style="${handler.style ?? ''}" ${handler.attrs ?? ''}>${handler.text || action}</button>`
1184
+ btnAction = Array.isArray(options.actions) ? handler.text : action
1185
+ }
1186
+ if (typeof handler == 'string') {
1187
+ options.buttons += `<button class="w2ui-btn w2ui-eaction" name="${handler}" data-click='["action","${handler}","event"]'>${handler}</button>`
1188
+ btnAction = handler
1189
+ }
1190
+ if (typeof btnAction == 'string') {
1191
+ btnAction = btnAction[0].toLowerCase() + btnAction.substr(1).replace(/\s+/g, '')
1192
+ }
1193
+ prom[btnAction] = function (callBack) {
1194
+ options.on('action.buttons', (event) => {
1195
+ let target = event.detail.action[0].toLowerCase() + event.detail.action.substr(1).replace(/\s+/g, '')
1196
+ if (target == btnAction) callBack(event)
1197
+ })
1198
+ return prom
1199
+ }
1200
+ })
1201
+ }
1202
+ // trim if any
1203
+ Array('html', 'body', 'buttons').forEach(param => {
1204
+ options[param] = String(options[param] ?? '').trim()
1205
+ })
1206
+ if (options.body !== '' || options.buttons !== '') {
1207
+ options.html = `
1208
+ <div class="w2ui-message-body">${options.body || ''}</div>
1209
+ <div class="w2ui-message-buttons">${options.buttons || ''}</div>
1210
+ `
1211
+ }
1212
+ let styles = getComputedStyle(query(where.box).get(0))
1213
+ let pWidth = parseFloat(styles.width)
1214
+ let pHeight = parseFloat(styles.height)
1215
+ let titleHeight = 0
1216
+ if (query(where.after).length > 0) {
1217
+ styles = getComputedStyle(query(where.after).get(0))
1218
+ titleHeight = parseInt(styles.display != 'none' ? parseInt(styles.height) : 0)
1219
+ }
1220
+ if (options.width > pWidth) options.width = pWidth - 10
1221
+ if (options.height > pHeight - titleHeight) options.height = pHeight - 10 - titleHeight
1222
+ options.originalWidth = options.width
1223
+ options.originalHeight = options.height
1224
+ if (parseInt(options.width) < 0) options.width = pWidth + options.width
1225
+ if (parseInt(options.width) < 10) options.width = 10
1226
+ if (parseInt(options.height) < 0) options.height = pHeight + options.height - titleHeight
1227
+ if (parseInt(options.height) < 10) options.height = 10
1228
+ // negative value means margin
1229
+ if (options.originalHeight < 0) options.height = pHeight + options.originalHeight - titleHeight
1230
+ if (options.originalWidth < 0) options.width = pWidth + options.originalWidth * 2 // x 2 because there is left and right margin
1231
+ let head = query(where.box).find(where.after) // needed for z-index manipulations
1232
+ if (!options.tmp) {
1233
+ options.tmp = {
1234
+ zIndex: head.css('z-index'),
1235
+ overflow: styles.overflow
1236
+ }
1237
+ }
1238
+ // remove message
1239
+ if (options.html === '' && options.body === '' && options.buttons === '') {
1240
+ removeLast()
1241
+ } else {
1242
+ options.msgIndex = query(where.box).find('.w2ui-message').length
1243
+ if (options.msgIndex === 0 && typeof this.lock == 'function') {
1244
+ query(where.box).css('overflow', 'hidden')
1245
+ if (where.owner) { // where.praram is used in the panel
1246
+ where.owner.lock(where.param)
1247
+ } else {
1248
+ this.lock(where.box)
1249
+ }
1250
+ }
1251
+ // send back previous messages
1252
+ query(where.box).find('.w2ui-message').css('z-index', 1390)
1253
+ head.css('z-index', 1501)
1254
+ // add message
1255
+ let content = `
1256
+ <div id="w2ui-message-${where.owner?.name}-${options.msgIndex}" class="w2ui-message" data-mousedown="stop"
1257
+ style="z-index: 1500; left: ${((pWidth - options.width) / 2)}px; top: ${titleHeight}px;
1258
+ width: ${options.width}px; height: ${options.height}px; transform: translateY(-${options.height}px)"
1259
+ ${options.hideOn.includes('click')
1260
+ ? where.param
1261
+ ? `data-click='["message", "${where.param}"]`
1262
+ : 'data-click="message"'
1263
+ : ''}>
1264
+ <span name="hidden-first" tabindex="0" style="position: absolute; top: 0; outline: none"></span>
1265
+ ${options.html}
1266
+ <span name="hidden-last" tabindex="0" style="position: absolute; top: 0; outline: none"></span>
1267
+ </div>`
1268
+ if (query(where.after).length > 0) {
1269
+ query(where.box).find(where.after).after(content)
1270
+ } else {
1271
+ query(where.box).prepend(content)
1272
+ }
1273
+ options.box = query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`)[0]
1274
+ w2utils.bindEvents(options.box, this)
1275
+ query(options.box)
1276
+ .addClass('animating')
1277
+ // remember options and prev focus
1278
+ options.box._msg_options = options
1279
+ options.box._msg_prevFocus = document.activeElement
1280
+ // timeout is needs so that callBacks are setup
1281
+ setTimeout(() => {
1282
+ // before event
1283
+ edata = options.trigger('open', { target: this.name, box: options.box, self: options })
1284
+ if (edata.isCancelled === true) {
1285
+ query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`).remove()
1286
+ if (options.msgIndex === 0) {
1287
+ head.css('z-index', options.tmp.zIndex)
1288
+ query(where.box).css('overflow', options.tmp.overflow)
1289
+ }
1290
+ return
1291
+ }
1292
+ // slide down
1293
+ query(options.box).css({
1294
+ transition: '0.3s',
1295
+ transform: 'translateY(0px)'
1296
+ })
1297
+ }, 0)
1298
+ // timeout is needed so that animation can finish
1299
+ openTimer = setTimeout(() => {
1300
+ // has to be on top of lock
1301
+ query(where.box)
1302
+ .find(`#w2ui-message-${where.owner?.name}-${options.msgIndex}`)
1303
+ .removeClass('animating')
1304
+ .css({ 'transition': '0s' })
1305
+ // event after
1306
+ edata.finish()
1307
+ }, 300)
1308
+ }
1309
+ // action handler
1310
+ options.action = (action, event) => {
1311
+ let click = options.actions?.[action]
1312
+ if (click instanceof Object && click.onClick) click = click.onClick
1313
+ // event before
1314
+ let edata = options.trigger('action', { target: this.name, action, self: options,
1315
+ originalEvent: event, value: options.input ? options.input.value : null })
1316
+ if (edata.isCancelled === true) return
1317
+ // default actions
1318
+ if (typeof click === 'function') click(edata)
1319
+ // event after
1320
+ edata.finish()
1321
+ }
1322
+ options.close = () => {
1323
+ edata = options.trigger('close', { target: 'self', box: options.box, self: options })
1324
+ if (edata.isCancelled === true) return
1325
+ clearTimeout(openTimer)
1326
+ if (query(options.box).hasClass('animating')) {
1327
+ clearTimeout(closeTimer)
1328
+ closeComplete(options)
1329
+ return
1330
+ }
1331
+ // default behavior
1332
+ query(options.box)
1333
+ .addClass('w2ui-closing animating')
1334
+ .css({
1335
+ 'transition': '0.15s',
1336
+ 'transform': 'translateY(-' + options.height + 'px)'
1337
+ })
1338
+ if (options.msgIndex !== 0) {
1339
+ // previous message
1340
+ query(where.box).find(`#w2ui-message-${where.owner?.name}-${options.msgIndex-1}`).css('z-index', 1499)
1341
+ }
1342
+ closeTimer = setTimeout(() => { closeComplete(options) }, 150)
1343
+ }
1344
+ options.setFocus = (focus) => {
1345
+ // in message or popup
1346
+ let cnt = query(where.box).find('.w2ui-message').length - 1
1347
+ let box = query(where.box).find(`#w2ui-message-${where.owner?.name}-${cnt}`)
1348
+ let sel = 'input, button, select, textarea, [contentEditable], .w2ui-input'
1349
+ if (focus != null) {
1350
+ let el = isNaN(focus)
1351
+ ? box.find(sel).filter(focus).get(0)
1352
+ : box.find(sel).get(focus)
1353
+ el?.focus()
1354
+ } else {
1355
+ box.find('[name=hidden-first]').get(0)?.focus()
1356
+ }
1357
+
1358
+ // clear focus if there are other messages
1359
+ query(where.box)
1360
+ .find('.w2ui-message')
1361
+ .find(sel + ',[name=hidden-first],[name=hidden-last]')
1362
+ .off('.keep-focus')
1363
+
1364
+ // keep focus/blur inside popup
1365
+ query(box)
1366
+ .find(sel + ',[name=hidden-first],[name=hidden-last]')
1367
+ .on('blur.keep-focus', function (event) {
1368
+ setTimeout(() => {
1369
+ let focus = document.activeElement
1370
+ let inside = query(box).find(sel).filter(focus).length > 0
1371
+ let name = query(focus).attr('name')
1372
+ if (!inside && focus && focus !== document.body) {
1373
+ query(box).find(sel).get(0)?.focus()
1374
+ }
1375
+ if (name == 'hidden-last') {
1376
+ query(box).find(sel).get(0)?.focus()
1377
+ }
1378
+ if (name == 'hidden-first') {
1379
+ query(box).find(sel).get(-1)?.focus()
1380
+ }
1381
+ }, 1)
1382
+ })
1383
+ }
1384
+ return prom
1385
+ }
1386
+
1387
+ alert(where, options) {
1388
+ return this.message(...arguments)
1389
+ }
1390
+
1391
+ /**
1392
+ * Shows a prompt as a context message. It will use same where: { box: ... } as w2utils.message() function
1393
+ * but it will have options similar to w2prompt dialog
1394
+ *
1395
+ * Example:
1396
+ * - w2utils.conrirm({
1397
+ * box: '#custom',
1398
+ * text: 'Some message'
1399
+ * })
1400
+ * .yes(event => console.log(event))
1401
+ */
1402
+ confirm(where, options) {
1403
+ if (['string', 'number'].includes(typeof options)) {
1404
+ options = { text: options }
1405
+ }
1406
+ if (arguments.length == 1) {
1407
+ options = where
1408
+ }
1409
+ w2utils.normButtons(options, { yes: 'Yes', no: 'No' })
1410
+ options.cancelAction ??= 'No'
1411
+ let prom = w2utils.message(where, options)
1412
+ if (prom) {
1413
+ prom.action(event => {
1414
+ event.detail.self.close()
1415
+ })
1416
+ }
1417
+ return prom
1418
+ }
1419
+
1420
+ /**
1421
+ * Shows a prompt as a context message. It will use same where: { box: ... } as w2utils.message() function
1422
+ * but it will have options similar to w2prompt dialog
1423
+ *
1424
+ * Example:
1425
+ * - w2utils.prompt({
1426
+ * box: '#custom',
1427
+ * label: 'Enter Name',
1428
+ * textarea: false,
1429
+ * attrs: 'style="border: 1px solid red"'
1430
+ * })
1431
+ * .ok(event => console.log(event))
1432
+ */
1433
+ prompt(where, options) {
1434
+ let prom
1435
+ if (['string', 'number'].includes(typeof options)) {
1436
+ options = { label: options }
1437
+ }
1438
+ if (arguments.length == 1) {
1439
+ options = where
1440
+ }
1441
+ options.cancelAction ??= 'Cancel'
1442
+ if (options.label) {
1443
+ options.focus = 0 // the input should be in focus, which is first in the popup
1444
+ options.body = (options.textarea
1445
+ ? `<div class="w2ui-prompt textarea">
1446
+ <div>${options.label}</div>
1447
+ <textarea id="w2prompt" class="w2ui-input" ${options.attrs ?? ''}
1448
+ data-keydown="keydown|event" data-keyup="change|event"></textarea>
1449
+ </div>`
1450
+ : `<div class="w2ui-prompt w2ui-centered">
1451
+ <label>${options.label}&nbsp;</label>
1452
+ <input id="w2prompt" class="w2ui-input" ${options.attrs ?? ''}
1453
+ data-keydown="keydown|event" data-keyup="change|event">
1454
+ </div>`
1455
+ )
1456
+ }
1457
+ w2utils.normButtons(options, { ok: w2utils.lang('Ok'), cancel: w2utils.lang('Cancel') })
1458
+ prom = w2utils.message(where, options)
1459
+ if (prom) {
1460
+ prom.change = function(callBack) {
1461
+ prom.self.on('change.prom', callBack)
1462
+ return prom
1463
+ }
1464
+ prom
1465
+ .action(event => {
1466
+ event.detail.self.close()
1467
+ })
1468
+ .then(event => {
1469
+ event.detail.self.input = query(event.detail.box).find('#w2prompt').get(0)
1470
+ query(event.detail.box)
1471
+ .find('#w2prompt')
1472
+ .on('keydown', event => {
1473
+ if (event.keyCode == 13 && event.shiftKey === false) {
1474
+ event.preventDefault()
1475
+ }
1476
+ })
1477
+ .on('keyup', event => {
1478
+ let edata = prom.self.trigger('change', { value: event.target.value, input: event.target, originalEvent: event })
1479
+ if (event.keyCode == 13 && event.shiftKey === false) {
1480
+ prom.self.action('Ok', event)
1481
+ }
1482
+ edata.finish()
1483
+ })
1484
+ })
1485
+ }
1486
+ return prom
1487
+ }
1488
+
1489
+ /**
1490
+ * Normalizes yes, no buttons for confirmation dialog
1491
+ *
1492
+ * @param {*} options
1493
+ * @returns options
1494
+ */
1495
+ normButtons(options, btn) {
1496
+ options.actions = options.actions ?? {}
1497
+ let btns = Object.keys(btn)
1498
+ btns.forEach(name => {
1499
+ let action = options['btn_' + name]
1500
+ if (action) {
1501
+ btn[name] = {
1502
+ text: w2utils.lang(action.text ?? btn[name] ?? ''),
1503
+ class: action.class ?? '',
1504
+ style: action.style ?? '',
1505
+ attrs: action.attrs ?? ''
1506
+ }
1507
+ delete options['btn_' + name]
1508
+ }
1509
+ Array('text', 'class', 'style', 'attrs').forEach(suffix => {
1510
+ if (options[name + '_' + suffix]) {
1511
+ if (typeof btn[name] == 'string') {
1512
+ btn[name] = { text: btn[name] }
1513
+ }
1514
+ btn[name][suffix] = options[name + '_' + suffix]
1515
+ delete options[name + '_' + suffix]
1516
+ }
1517
+ })
1518
+ })
1519
+ if (btns.includes('yes') && btns.includes('no')) {
1520
+ if (w2utils.settings.macButtonOrder) {
1521
+ w2utils.extend(options.actions, { no: btn.no, yes: btn.yes })
1522
+ } else {
1523
+ w2utils.extend(options.actions, { yes: btn.yes, no: btn.no })
1524
+ }
1525
+ }
1526
+ if (btns.includes('ok') && btns.includes('cancel')) {
1527
+ if (w2utils.settings.macButtonOrder) {
1528
+ w2utils.extend(options.actions, { cancel: btn.cancel, ok: btn.ok })
1529
+ } else {
1530
+ w2utils.extend(options.actions, { ok: btn.ok, cancel: btn.cancel })
1531
+ }
1532
+ }
1533
+ return options
1534
+ }
1535
+
1536
+ /**
1537
+ * Shows small notification message at the bottom of the page, or containter that you specify
1538
+ * in options.where (could be element or a selector)
1539
+ *
1540
+ * w2utils.notify('Document saved')
1541
+ * w2utils.notify('Mesage sent ${udon}', { actions: { undo: function () {...} }})
1542
+ *
1543
+ * @param {String/Object} options can be {
1544
+ * text: string, // message, can be html
1545
+ * where: el/selector, // element or selector where to show, default is document.body
1546
+ * timeout: int, // timeout when to hide, if 0 - indefinite
1547
+ * error: boolean, // add error clases
1548
+ * class: string, // additional class strings
1549
+ * actions: object // object with action functions, it should correspot to templated text: '... ${action} ...'
1550
+ * }
1551
+ * @returns promise
1552
+ */
1553
+ notify(text, options) {
1554
+ return new Promise(resolve => {
1555
+ if (typeof text == 'object') {
1556
+ options = text
1557
+ text = options.text
1558
+ }
1559
+ options = options || {}
1560
+ options.where = options.where ?? document.body
1561
+ options.timeout = options.timeout ?? 15_000 // 15 secodns or will be hidden on route change
1562
+ if (typeof this.tmp.notify_resolve == 'function') {
1563
+ this.tmp.notify_resolve()
1564
+ query(this.tmp.notify_where).find('#w2ui-notify').remove()
1565
+ }
1566
+ this.tmp.notify_resolve = resolve
1567
+ this.tmp.notify_where = options.where
1568
+ clearTimeout(this.tmp.notify_timer)
1569
+ if (text) {
1570
+ if (typeof options.actions == 'object') {
1571
+ let actions = {}
1572
+ Object.keys(options.actions).forEach(action => {
1573
+ actions[action] = `<a class="w2ui-notify-link" value="${action}">${action}</a>`
1574
+ })
1575
+ text = this.execTemplate(text, actions)
1576
+ }
1577
+ let html = `
1578
+ <div id="w2ui-notify" style="${options.where == document.body ? 'position: fixed' : ''}">
1579
+ <div class="${options.class ?? ''} ${options.error ? 'w2ui-notify-error' : ''} ${options.success ? 'w2ui-notify-success' : ''}">
1580
+ ${text}
1581
+ <span class="w2ui-notify-close w2ui-icon-cross"></span>
1582
+ </div>
1583
+ </div>`
1584
+ query(options.where).append(html)
1585
+ query(options.where).find('#w2ui-notify').find('.w2ui-notify-close')
1586
+ .on('click', event => {
1587
+ query(options.where).find('#w2ui-notify').remove()
1588
+ resolve()
1589
+ })
1590
+ if (options.actions) {
1591
+ query(options.where).find('#w2ui-notify .w2ui-notify-link')
1592
+ .on('click', event => {
1593
+ let value = query(event.target).attr('value')
1594
+ options.actions[value]()
1595
+ query(options.where).find('#w2ui-notify').remove()
1596
+ resolve()
1597
+ })
1598
+ }
1599
+ if (options.timeout > 0) {
1600
+ this.tmp.notify_timer = setTimeout(() => {
1601
+ query(options.where).find('#w2ui-notify').remove()
1602
+ resolve()
1603
+ }, options.timeout)
1604
+ }
1605
+ }
1606
+ })
1607
+ }
1608
+
1609
+ getSize(el, type) {
1610
+ el = query(el) // for backward compatibility
1611
+ let ret = 0
1612
+ if (el.length > 0) {
1613
+ el = el[0]
1614
+ let styles = getComputedStyle(el)
1615
+ switch (type) {
1616
+ case 'width' :
1617
+ ret = parseFloat(styles.width)
1618
+ if (styles.width === 'auto') ret = 0
1619
+ break
1620
+ case 'height' :
1621
+ ret = parseFloat(styles.height)
1622
+ if (styles.height === 'auto') ret = 0
1623
+ break
1624
+ default:
1625
+ ret = parseFloat(styles[type] ?? 0) || 0
1626
+ break
1627
+ }
1628
+ }
1629
+ return ret
1630
+ }
1631
+
1632
+ getStrWidth(str, styles, raw) {
1633
+ let div = query('body > #_tmp_width')
1634
+ if (div.length === 0) {
1635
+ query('body').append('<div id="_tmp_width" style="position: absolute; top: -9000px;"></div>')
1636
+ div = query('body > #_tmp_width')
1637
+ }
1638
+ div.html(raw ? str : this.encodeTags(str ?? '')).attr('style', `position: absolute; top: -9000px; ${styles || ''}`)
1639
+ return div[0].clientWidth
1640
+ }
1641
+
1642
+ getStrHeight(str, styles, raw) {
1643
+ let div = query('body > #_tmp_width')
1644
+ if (div.length === 0) {
1645
+ query('body').append('<div id="_tmp_width" style="position: absolute; top: -9000px;"></div>')
1646
+ div = query('body > #_tmp_width')
1647
+ }
1648
+ div.html(raw ? str : this.encodeTags(str ?? '')).attr('style', `position: absolute; top: -9000px; ${styles || ''}`)
1649
+ return div[0].clientHeight
1650
+ }
1651
+
1652
+ execTemplate(str, replace_obj) {
1653
+ if (typeof str !== 'string' || !replace_obj || typeof replace_obj !== 'object') {
1654
+ return str
1655
+ }
1656
+ return str.replace(/\${([^}]+)?}/g, function($1, $2) { return replace_obj[$2]||$2 })
1657
+ }
1658
+
1659
+ marker(el, items, options = { onlyFirst: false, wholeWord: false }) {
1660
+ options.tag ??= 'span'
1661
+ options.class ??= 'w2ui-marker'
1662
+ options.raplace = (matched) => `<${options.tag} class="${options.class}">${matched}</${options.tag}>`
1663
+ if (!Array.isArray(items)) {
1664
+ if (items != null && items !== '') {
1665
+ items = [items]
1666
+ } else {
1667
+ items = []
1668
+ }
1669
+ }
1670
+ if (typeof el == 'string') {
1671
+ _clearMerkers(el)
1672
+ items.forEach(item => {
1673
+ el = _replace(el, item, options.raplace)
1674
+ })
1675
+ } else {
1676
+ query(el).each(el => {
1677
+ _clearMerkers(el)
1678
+ items.forEach(item => {
1679
+ el.innerHTML = _replace(el.innerHTML, item, options.raplace)
1680
+ })
1681
+ })
1682
+ }
1683
+ return el
1684
+
1685
+ function _replace(html, term, replaceWith) {
1686
+ let ww = options.wholeWord
1687
+ if (typeof term !== 'string') term = String(term)
1688
+ // escape regex special chars
1689
+ term = term
1690
+ .replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
1691
+ .replace(/&/g, '&amp;')
1692
+ .replace(/</g, '&gt;')
1693
+ .replace(/>/g, '&lt;')
1694
+ // only outside tags
1695
+ // and only outside of quotes
1696
+ // let regex = new RegExp((ww ? '\\b' : '') + term + (ww ? '\\b' : '') + '(?=([a-z-0-9]+="[^"]*")*?[^"]+$)' + '(?!([^<]+)?>)', 'i' + (!options.onlyFirst ? 'g' : ''))
1697
+ // -- the one above would not match inside html tags
1698
+ let regex = new RegExp((ww ? '\\b' : '') + term + (ww ? '\\b' : '') + '(?![^<]*>)', 'i' + (!options.onlyFirst ? 'g' : ''));
1699
+ return html = html.replace(regex, replaceWith)
1700
+ }
1701
+
1702
+ function _clearMerkers(el) {
1703
+ let markerRE = new RegExp(`<${options.tag}[^>]*class=["']${options.class.replace(/-/g, '\\-')}["'][^>]*>([\\s\\S]*?)<\\/${options.tag}>`, 'ig')
1704
+ if (typeof el == 'string') {
1705
+ while (el.indexOf(`<${options.tag} class="${options.class}"`) !== -1) {
1706
+ el = el.replace(markerRE, '$1') // unmark
1707
+ }
1708
+ } else {
1709
+ while (el.innerHTML.indexOf(`<${options.tag} class="${options.class}"`) !== -1) {
1710
+ el.innerHTML = el.innerHTML.replace(markerRE, '$1') // unmark
1711
+ }
1712
+ }
1713
+ }
1714
+ }
1715
+
1716
+ lang(phrase, params) {
1717
+ if (!phrase || this.settings.phrases == null // if no phrases at all
1718
+ || typeof phrase !== 'string' || '<=>='.includes(phrase)) {
1719
+ return this.execTemplate(phrase, params)
1720
+ }
1721
+ let translation = this.settings.phrases[phrase]
1722
+ if (translation == null) {
1723
+ translation = phrase
1724
+ if (this.settings.warnNoPhrase) {
1725
+ if (!this.settings.missing) {
1726
+ this.settings.missing = {}
1727
+ }
1728
+ this.settings.missing[phrase] = '---' // collect phrases for translation, warn once
1729
+ this.settings.phrases[phrase] = '---'
1730
+ console.log(`Missing translation for "%c${phrase}%c", see %c w2utils.settings.phrases %c with value "---"`,
1731
+ 'color: orange', '',
1732
+ 'color: #999', '')
1733
+ }
1734
+ } else if (translation === '---' && !this.settings.warnNoPhrase) {
1735
+ translation = phrase
1736
+ }
1737
+ if (translation === '---') {
1738
+ translation = `<span ${this.tooltip(phrase)}>---</span>`
1739
+ }
1740
+ return this.execTemplate(translation, params)
1741
+ }
1742
+
1743
+ locale(locale, keepPhrases, noMerge) {
1744
+ return new Promise((resolve, reject) => {
1745
+ // if locale is an array we call this function recursively and merge the results
1746
+ if (Array.isArray(locale)) {
1747
+ this.settings.phrases = {}
1748
+ let proms = []
1749
+ let files = {}
1750
+ locale.forEach((file, ind) => {
1751
+ if (file.length === 5) {
1752
+ file = 'locale/'+ file.toLowerCase() +'.json'
1753
+ locale[ind] = file
1754
+ }
1755
+ proms.push(this.locale(file, true, false))
1756
+ })
1757
+ Promise.allSettled(proms)
1758
+ .then(res => {
1759
+ // order of files is important to merge
1760
+ res.forEach(r => { if (r.value) files[r.value.file] = r.value.data })
1761
+ locale.forEach(file => {
1762
+ this.settings = this.extend({}, this.settings, files[file])
1763
+ })
1764
+ resolve()
1765
+ })
1766
+ return
1767
+ }
1768
+ if (!locale) locale = 'en-us'
1769
+
1770
+ // if locale is an object, then merge it with w2utils.settings
1771
+ if (locale instanceof Object) {
1772
+ this.settings = this.extend({}, this.settings, w2locale, locale)
1773
+ return
1774
+ }
1775
+
1776
+ if (locale.length === 5) {
1777
+ locale = 'locale/'+ locale.toLowerCase() +'.json'
1778
+ }
1779
+
1780
+ // load from the file
1781
+ fetch(locale, { method: 'GET' })
1782
+ .then(res => res.json())
1783
+ .then(data => {
1784
+ if (noMerge !== true) {
1785
+ if (keepPhrases) {
1786
+ // keep phrases, useful for recursive calls
1787
+ this.settings = this.extend({}, this.settings, data)
1788
+ } else {
1789
+ // clear phrases from language before merging
1790
+ this.settings = this.extend({}, this.settings, w2locale, { phrases: {} }, data)
1791
+ }
1792
+ }
1793
+ resolve({ file: locale, data })
1794
+ })
1795
+ .catch((err) => {
1796
+ console.log('ERROR: Cannot load locale '+ locale)
1797
+ reject(err)
1798
+ })
1799
+ })
1800
+ }
1801
+
1802
+ scrollBarSize() {
1803
+ if (this.tmp.scrollBarSize) return this.tmp.scrollBarSize
1804
+ let html = `
1805
+ <div id="_scrollbar_width" style="position: absolute; top: -300px; width: 100px; height: 100px; overflow-y: scroll;">
1806
+ <div style="height: 120px">1</div>
1807
+ </div>
1808
+ `
1809
+ query('body').append(html)
1810
+ this.tmp.scrollBarSize = 100 - query('#_scrollbar_width > div')[0].clientWidth
1811
+ query('#_scrollbar_width').remove()
1812
+ return this.tmp.scrollBarSize
1813
+ }
1814
+
1815
+ checkName(name) {
1816
+ if (name == null) {
1817
+ console.log('ERROR: Property "name" is required but not supplied.')
1818
+ return false
1819
+ }
1820
+ if (w2ui[name] != null) {
1821
+ console.log(`ERROR: Object named "${name}" is already registered as w2ui.${name}.`)
1822
+ return false
1823
+ }
1824
+ if (!this.isAlphaNumeric(name)) {
1825
+ console.log('ERROR: Property "name" has to be alpha-numeric (a-z, 0-9, dash and underscore).')
1826
+ return false
1827
+ }
1828
+ return true
1829
+ }
1830
+
1831
+ checkUniqueId(id, items, desc, obj) {
1832
+ if (!Array.isArray(items)) items = [items]
1833
+ let isUnique = true
1834
+ items.forEach(item => {
1835
+ if (item.id === id) {
1836
+ console.log(`ERROR: The item id="${id}" is not unique within the ${desc} "${obj}".`, items)
1837
+ isUnique = false
1838
+ }
1839
+ })
1840
+ return isUnique
1841
+ }
1842
+
1843
+ /**
1844
+ * Takes an object and encodes it into params string to be passed as a url
1845
+ * { a: 1, b: 'str'} => "a=1&b=str"
1846
+ * { a: 1, b: { c: 2 }} => "a=1&b[c]=2"
1847
+ * { a: 1, b: {c: { k: 'dfdf' } } } => "a=1&b[c][k]=dfdf"
1848
+ */
1849
+ encodeParams(obj, prefix = '') {
1850
+ let str = ''
1851
+ Object.keys(obj).forEach(key => {
1852
+ if (str != '') str += '&'
1853
+ if (typeof obj[key] == 'object') {
1854
+ str += this.encodeParams(obj[key], prefix + key + (prefix ? ']' : '') + '[')
1855
+ } else {
1856
+ str += `${prefix}${key}${prefix ? ']' : ''}=${obj[key]}`
1857
+ }
1858
+ })
1859
+ return str
1860
+ }
1861
+
1862
+ parseRoute(route) {
1863
+ let keys = []
1864
+ let path = route
1865
+ .replace(/\/\(/g, '(?:/')
1866
+ .replace(/\+/g, '__plus__')
1867
+ .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash, format, key, capture, optional) => {
1868
+ keys.push({ name: key, optional: !! optional })
1869
+ slash = slash || ''
1870
+ return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + (optional || '')
1871
+ })
1872
+ .replace(/([\/.])/g, '\\$1')
1873
+ .replace(/__plus__/g, '(.+)')
1874
+ .replace(/\*/g, '(.*)')
1875
+ return {
1876
+ path : new RegExp('^' + path + '$', 'i'),
1877
+ keys : keys
1878
+ }
1879
+ }
1880
+
1881
+ getCursorPosition(input) {
1882
+ if (input == null) return null
1883
+ let caretOffset = 0
1884
+ let doc = input.ownerDocument || input.document
1885
+ let win = doc.defaultView || doc.parentWindow
1886
+ let sel
1887
+ if (['INPUT', 'TEXTAREA'].includes(input.tagName)) {
1888
+ caretOffset = input.selectionStart
1889
+ } else {
1890
+ if (win.getSelection) {
1891
+ sel = win.getSelection()
1892
+ if (sel.rangeCount > 0) {
1893
+ let range = sel.getRangeAt(0)
1894
+ let preCaretRange = range.cloneRange()
1895
+ preCaretRange.selectNodeContents(input)
1896
+ preCaretRange.setEnd(range.endContainer, range.endOffset)
1897
+ caretOffset = preCaretRange.toString().length
1898
+ }
1899
+ } else if ( (sel = doc.selection) && sel.type !== 'Control') {
1900
+ let textRange = sel.createRange()
1901
+ let preCaretTextRange = doc.body.createTextRange()
1902
+ preCaretTextRange.moveToElementText(input)
1903
+ preCaretTextRange.setEndPoint('EndToEnd', textRange)
1904
+ caretOffset = preCaretTextRange.text.length
1905
+ }
1906
+ }
1907
+ return caretOffset
1908
+ }
1909
+
1910
+ setCursorPosition(input, pos, posEnd) {
1911
+ if (input == null) return
1912
+ let range = document.createRange()
1913
+ let el, sel = window.getSelection()
1914
+ if (['INPUT', 'TEXTAREA'].includes(input.tagName)) {
1915
+ input.setSelectionRange(pos, posEnd ?? pos)
1916
+ } else {
1917
+ for (let i = 0; i < input.childNodes.length; i++) {
1918
+ let tmp = query(input.childNodes[i]).text()
1919
+ if (input.childNodes[i].tagName) {
1920
+ tmp = query(input.childNodes[i]).html()
1921
+ tmp = tmp.replace(/&lt;/g, '<')
1922
+ .replace(/&gt;/g, '>')
1923
+ .replace(/&amp;/g, '&')
1924
+ .replace(/&quot;/g, '"')
1925
+ .replace(/&nbsp;/g, ' ')
1926
+ }
1927
+ if (pos <= tmp.length) {
1928
+ el = input.childNodes[i]
1929
+ if (el.childNodes && el.childNodes.length > 0) el = el.childNodes[0]
1930
+ if (el.childNodes && el.childNodes.length > 0) el = el.childNodes[0]
1931
+ break
1932
+ } else {
1933
+ pos -= tmp.length
1934
+ }
1935
+ }
1936
+ if (el == null) return
1937
+ if (pos > el.length) pos = el.length
1938
+ range.setStart(el, pos)
1939
+ if (posEnd) {
1940
+ range.setEnd(el, posEnd)
1941
+ } else {
1942
+ range.collapse(true)
1943
+ }
1944
+ sel.removeAllRanges()
1945
+ sel.addRange(range)
1946
+ }
1947
+ }
1948
+
1949
+ parseColor(str) {
1950
+ if (typeof str !== 'string') return null; else str = str.trim().toUpperCase()
1951
+ if (str[0] === '#') str = str.substr(1)
1952
+ let color = {}
1953
+ if (str.length === 3) {
1954
+ color = {
1955
+ r: parseInt(str[0] + str[0], 16),
1956
+ g: parseInt(str[1] + str[1], 16),
1957
+ b: parseInt(str[2] + str[2], 16),
1958
+ a: 1
1959
+ }
1960
+ } else if (str.length === 6) {
1961
+ color = {
1962
+ r: parseInt(str.substr(0, 2), 16),
1963
+ g: parseInt(str.substr(2, 2), 16),
1964
+ b: parseInt(str.substr(4, 2), 16),
1965
+ a: 1
1966
+ }
1967
+ } else if (str.length === 8) {
1968
+ color = {
1969
+ r: parseInt(str.substr(0, 2), 16),
1970
+ g: parseInt(str.substr(2, 2), 16),
1971
+ b: parseInt(str.substr(4, 2), 16),
1972
+ a: Math.round(parseInt(str.substr(6, 2), 16) / 255 * 100) / 100 // alpha channel 0-1
1973
+ }
1974
+ } else if (str.length > 4 && str.substr(0, 4) === 'RGB(') {
1975
+ let tmp = str.replace('RGB', '').replace(/\(/g, '').replace(/\)/g, '').split(',')
1976
+ color = {
1977
+ r: parseInt(tmp[0], 10),
1978
+ g: parseInt(tmp[1], 10),
1979
+ b: parseInt(tmp[2], 10),
1980
+ a: 1
1981
+ }
1982
+ } else if (str.length > 5 && str.substr(0, 5) === 'RGBA(') {
1983
+ let tmp = str.replace('RGBA', '').replace(/\(/g, '').replace(/\)/g, '').split(',')
1984
+ color = {
1985
+ r: parseInt(tmp[0], 10),
1986
+ g: parseInt(tmp[1], 10),
1987
+ b: parseInt(tmp[2], 10),
1988
+ a: parseFloat(tmp[3])
1989
+ }
1990
+ } else {
1991
+ // word color
1992
+ return null
1993
+ }
1994
+ return color
1995
+ }
1996
+
1997
+ colorContrast(color1, color2) {
1998
+ let lum1 = calcLumens(color1)
1999
+ let lum2 = calcLumens(color2)
2000
+ let ratio = (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05)
2001
+ return ratio.toFixed(2)
2002
+
2003
+ function calcLumens(color) {
2004
+ let { r, g, b } = w2utils.parseColor(color) ?? { r: 0, g: 0, b: 0 }
2005
+ let gamma = 2.2
2006
+ let normR = r / 255
2007
+ let normG = g / 255
2008
+ let normB = b / 255
2009
+ let sR = (normR <= 0.03928) ? normR / 12.92 : Math.pow((normR + 0.055) / 1.055, gamma)
2010
+ let sG = (normG <= 0.03928) ? normG / 12.92 : Math.pow((normG + 0.055) / 1.055, gamma)
2011
+ let sB = (normB <= 0.03928) ? normB / 12.92 : Math.pow((normB + 0.055) / 1.055, gamma)
2012
+ return 0.2126 * sR + 0.7152 * sG + 0.0722 * sB
2013
+ }
2014
+ }
2015
+
2016
+ // h=0..360, s=0..100, v=0..100
2017
+ hsv2rgb(h, s, v, a) {
2018
+ let r, g, b, i, f, p, q, t
2019
+ if (arguments.length === 1) {
2020
+ s = h.s; v = h.v; a = h.a; h = h.h
2021
+ }
2022
+ h = h / 360
2023
+ s = s / 100
2024
+ v = v / 100
2025
+ i = Math.floor(h * 6)
2026
+ f = h * 6 - i
2027
+ p = v * (1 - s)
2028
+ q = v * (1 - f * s)
2029
+ t = v * (1 - (1 - f) * s)
2030
+ switch (i % 6) {
2031
+ case 0: r = v, g = t, b = p; break
2032
+ case 1: r = q, g = v, b = p; break
2033
+ case 2: r = p, g = v, b = t; break
2034
+ case 3: r = p, g = q, b = v; break
2035
+ case 4: r = t, g = p, b = v; break
2036
+ case 5: r = v, g = p, b = q; break
2037
+ }
2038
+ return {
2039
+ r: Math.round(r * 255),
2040
+ g: Math.round(g * 255),
2041
+ b: Math.round(b * 255),
2042
+ a: (a != null ? a : 1)
2043
+ }
2044
+ }
2045
+
2046
+ // r=0..255, g=0..255, b=0..255
2047
+ rgb2hsv(r, g, b, a) {
2048
+ if (arguments.length === 1) {
2049
+ g = r.g; b = r.b; a = r.a; r = r.r
2050
+ }
2051
+ let max = Math.max(r, g, b), min = Math.min(r, g, b),
2052
+ d = max - min,
2053
+ h,
2054
+ s = (max === 0 ? 0 : d / max),
2055
+ v = max / 255
2056
+ switch (max) {
2057
+ case min: h = 0; break
2058
+ case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break
2059
+ case g: h = (b - r) + d * 2; h /= 6 * d; break
2060
+ case b: h = (r - g) + d * 4; h /= 6 * d; break
2061
+ }
2062
+ return {
2063
+ h: Math.round(h * 360),
2064
+ s: Math.round(s * 100),
2065
+ v: Math.round(v * 100),
2066
+ a: (a != null ? a : 1)
2067
+ }
2068
+ }
2069
+
2070
+ tooltip(html, options) {
2071
+ let actions
2072
+ let showOn = 'mouseenter'
2073
+ let hideOn = 'mouseleave'
2074
+ if (typeof html == 'object') {
2075
+ options = html
2076
+ }
2077
+ options = options || {}
2078
+ if (typeof html == 'string') {
2079
+ options.html = html
2080
+ }
2081
+ if (options.showOn) {
2082
+ showOn = options.showOn
2083
+ delete options.showOn
2084
+ }
2085
+ if (options.hideOn) {
2086
+ hideOn = options.hideOn
2087
+ delete options.hideOn
2088
+ }
2089
+ if (!options.name) options.name = 'no-name'
2090
+ // base64 is needed to avoid '"<> and other special chars conflicts
2091
+ actions = ` on${showOn}="w2tooltip.show(this, `
2092
+ + `JSON.parse(w2utils.base64decode('${this.base64encode(JSON.stringify(options))}')))" `
2093
+ + `on${hideOn}="w2tooltip.hide('${options.name}')"`
2094
+ return actions
2095
+ }
2096
+
2097
+ // determins if it is plain Object, not DOM element, nor a function, event, etc.
2098
+ isPlainObject(value) {
2099
+ if (value == null) { // null or undefined
2100
+ return false
2101
+ }
2102
+ if (Object.prototype.toString.call(value) !== '[object Object]') {
2103
+ return false
2104
+ }
2105
+ if (value.constructor === undefined) {
2106
+ return true
2107
+ }
2108
+ let proto = Object.getPrototypeOf(value)
2109
+ return proto === null || proto === Object.prototype
2110
+ }
2111
+
2112
+ /**
2113
+ * Deep copy of an object or an array. Function, events and HTML elements will not be cloned,
2114
+ * you can choose to include them or not, by default they are included.
2115
+ * You can also exclude certain elements from final object if used with options: { exclude }
2116
+ */
2117
+ clone(obj, options) {
2118
+ let ret
2119
+ options = Object.assign({ functions: true, elements: true, events: true, exclude: [] }, options ?? {})
2120
+ if (Array.isArray(obj)) {
2121
+ ret = Array.from(obj)
2122
+ ret.forEach((value, ind) => {
2123
+ ret[ind] = this.clone(value, { ...options, parent: (options.parent ?? '') + '[]' })
2124
+ })
2125
+ } else if (this.isPlainObject(obj)) {
2126
+ ret = {}
2127
+ Object.assign(ret, obj)
2128
+ // delete excluded keys
2129
+ if (Array.isArray(options.exclude)) {
2130
+ options.exclude.forEach(key => { delete ret[key] })
2131
+ }
2132
+ Object.keys(ret).forEach(key => {
2133
+ if (typeof options.exclude == 'function' && options.exclude(key, { obj, parent: options.parent ?? '' })) {
2134
+ ret[key] = undefined
2135
+ } else {
2136
+ ret[key] = this.clone(ret[key], { ...options, parent: (options.parent ?? '') + (options.parent != null ? '.' : '') + key })
2137
+ }
2138
+ if (ret[key] === undefined) delete ret[key] // do not include undefined elements
2139
+ })
2140
+ } else {
2141
+ if ((obj instanceof Function && !options.functions)
2142
+ || (obj instanceof Node && !options.elements)
2143
+ || (obj instanceof Event && !options.events)
2144
+ ) {
2145
+ // do not include these objects, otherwise include them uncloned
2146
+ } else {
2147
+ // primitive variable or function, event, dom element, etc, - all these are not cloned
2148
+ ret = obj
2149
+ }
2150
+ }
2151
+ return ret
2152
+ }
2153
+
2154
+ /**
2155
+ * Deep extend an object, if an array, it overwrrites it, cloning objects in the process
2156
+ * target, source1, source2, ...
2157
+ */
2158
+ extend(target, source) {
2159
+ if (Array.isArray(target)) {
2160
+ if (Array.isArray(source)) {
2161
+ target.splice(0, target.length) // empty array but keep the reference
2162
+ source.forEach(s => { target.push(this.clone(s)) })
2163
+ } else {
2164
+ throw new Error('Arrays can be extended with arrays only')
2165
+ }
2166
+ } else if (target instanceof Node || target instanceof Event) {
2167
+ throw new Error('HTML elmenents and events cannot be extended')
2168
+ } else if (target && typeof target == 'object' && source != null) {
2169
+ if (typeof source != 'object') {
2170
+ throw new Error('Object can be extended with other objects only.')
2171
+ }
2172
+ Object.keys(source).forEach(key => {
2173
+ if (target[key] != null && typeof target[key] == 'object'
2174
+ && source[key] != null && typeof source[key] == 'object') {
2175
+ let src = this.clone(source[key])
2176
+ // do not extend HTML elements and events, but overwrite them
2177
+ if (target[key] instanceof Node || target[key] instanceof Event) {
2178
+ target[key] = src
2179
+ } else {
2180
+ // if an array needs to be extended with an object, then convert it to empty object
2181
+ if (Array.isArray(target[key]) && this.isPlainObject(src)) {
2182
+ target[key] = {}
2183
+ }
2184
+ this.extend(target[key], src)
2185
+ }
2186
+ } else {
2187
+ target[key] = this.clone(source[key])
2188
+ }
2189
+ })
2190
+ } else if (source != null) {
2191
+ throw new Error('Object is not extendable, only {} or [] can be extended.')
2192
+ }
2193
+ // other arguments
2194
+ if (arguments.length > 2) {
2195
+ for (let i = 2; i < arguments.length; i++) {
2196
+ this.extend(target, arguments[i])
2197
+ }
2198
+ }
2199
+ return target
2200
+ }
2201
+
2202
+ /*
2203
+ * @author Lauri Rooden (https://github.com/litejs/natural-compare-lite)
2204
+ * @license MIT License
2205
+ */
2206
+ naturalCompare(a, b) {
2207
+ let i, codeA
2208
+ , codeB = 1
2209
+ , posA = 0
2210
+ , posB = 0
2211
+ , alphabet = String.alphabet
2212
+
2213
+ function getCode(str, pos, code) {
2214
+ if (code) {
2215
+ for (i = pos; code = getCode(str, i), code < 76 && code > 65;) ++i
2216
+ return +str.slice(pos - 1, i)
2217
+ }
2218
+ code = alphabet && alphabet.indexOf(str.charAt(pos))
2219
+ return code > -1 ? code + 76 : ((code = str.charCodeAt(pos) || 0), code < 45 || code > 127) ? code
2220
+ : code < 46 ? 65 // -
2221
+ : code < 48 ? code - 1
2222
+ : code < 58 ? code + 18 // 0-9
2223
+ : code < 65 ? code - 11
2224
+ : code < 91 ? code + 11 // A-Z
2225
+ : code < 97 ? code - 37
2226
+ : code < 123 ? code + 5 // a-z
2227
+ : code - 63
2228
+ }
2229
+
2230
+
2231
+ if ((a+='') != (b+='')) for (;codeB;) {
2232
+ codeA = getCode(a, posA++)
2233
+ codeB = getCode(b, posB++)
2234
+
2235
+ if (codeA < 76 && codeB < 76 && codeA > 66 && codeB > 66) {
2236
+ codeA = getCode(a, posA, posA)
2237
+ codeB = getCode(b, posB, posA = i)
2238
+ posB = i
2239
+ }
2240
+
2241
+ if (codeA != codeB) return (codeA < codeB) ? -1 : 1
2242
+ }
2243
+ return 0
2244
+ }
2245
+
2246
+ /**
2247
+ * Takes a menu (used in drop downs, context menu, field: list/combo/enum) and normalizes it to the common structure, which
2248
+ * is { id: ..., text: ... }. In options you can pass { itemMap: { id: 'id_field', text: 'text_field' }} that will be used
2249
+ * to find out id and text fields.
2250
+ */
2251
+ normMenu(menu, options = {}) {
2252
+ if (Array.isArray(menu)) {
2253
+ menu.forEach((it, m) => {
2254
+ if (typeof it === 'string' || typeof it === 'number') {
2255
+ menu[m] = { id: it, text: String(it) }
2256
+ } else if (it != null) {
2257
+ if (options.itemMap != null) {
2258
+ let val = w2utils.getNested(it, options.itemMap.id)
2259
+ if (options.itemMap.id != null && val != null) {
2260
+ it.id = val
2261
+ }
2262
+ val = w2utils.getNested(it, options.itemMap.text)
2263
+ if (options.itemMap.text != null && val) {
2264
+ it.text = val
2265
+ }
2266
+ }
2267
+ if (it.caption != null && it.text == null) it.text = it.caption
2268
+ if (it.text != null && it.id == null) it.id = it.text
2269
+ if (it.text == null && it.id != null) it.text = it.id
2270
+ } else {
2271
+ menu[m] = { id: null, text: 'null' }
2272
+ }
2273
+ })
2274
+ return menu
2275
+ } else if (typeof menu === 'function') {
2276
+ let newMenu = menu.call(this, menu, options)
2277
+ return w2utils.normMenu.call(this, newMenu, options)
2278
+ } else if (typeof menu === 'object') {
2279
+ return Object.keys(menu).map(key => { return { id: key, text: menu[key] } })
2280
+ }
2281
+ }
2282
+
2283
+ /**
2284
+ * Takes Url object and fetchOptions and changes it in place applying selected user dataType. Since
2285
+ * dataType is in w2utils. This method is used in grid, form and tooltip to prepare fetch parameters
2286
+ */
2287
+ prepareParams(url, fetchOptions, options = {}) {
2288
+ let dataType = options?.dataType ?? w2utils.settings.dataType
2289
+ let postParams = fetchOptions.body
2290
+ fetchOptions.method = String(fetchOptions.method).toUpperCase()
2291
+ switch (dataType) {
2292
+ /**
2293
+ * Will submit GET, POST, PUT, DELETE
2294
+ * - if GET - it will be in URL
2295
+ * - if POST, PUT, DELETE it will be JSON encoded
2296
+ */
2297
+ case 'RESTFULL':
2298
+ case 'RESTFULJSON': {
2299
+ if (['POST', 'PUT', 'DELETE'].includes(fetchOptions.method)) {
2300
+ fetchOptions.headers['Content-Type'] = 'application/json'
2301
+ }
2302
+ if (fetchOptions.method == 'GET') {
2303
+ if (dataType == 'RESTFULLJSON') {
2304
+ postParams = { request: postParams }
2305
+ }
2306
+ body2params()
2307
+ }
2308
+ break
2309
+ }
2310
+ /**
2311
+ * Will submit either GET or POST and
2312
+ * - if POST it will be JSON encoded
2313
+ * - if GET it will be in URL
2314
+ * - if HTTPJSON and GET then it will be JSON encoded
2315
+ */
2316
+ case 'HTTP':
2317
+ case 'HTTPJSON':
2318
+ case 'JSON': {
2319
+ if (fetchOptions.method == 'GET') {
2320
+ if (dataType == 'JSON' || dataType === 'HTTPJSON') {
2321
+ postParams = { request: postParams }
2322
+ }
2323
+ body2params()
2324
+ } else {
2325
+ fetchOptions.headers['Content-Type'] = 'application/json'
2326
+ fetchOptions.method = 'POST'
2327
+ }
2328
+ break
2329
+ }
2330
+ default: {
2331
+ if (typeof dataType == 'fuction') {
2332
+ // do nothing, it is custom function that will handle everything
2333
+ fetchOptions = dataType(url, fetchOptions, options)
2334
+ } else {
2335
+ console.log(`ERROR: Unsupported dataType "${dataType}". Supported types are JSON (default), HTTP, RESTFULL. For backward compatibility HTTPJSON is same as JSON. RESTULFLJSON will encode GET request as JSON.`)
2336
+ }
2337
+ }
2338
+ }
2339
+ if (fetchOptions.body != null) {
2340
+ fetchOptions.body = typeof fetchOptions.body == 'string' ? fetchOptions.body : JSON.stringify(fetchOptions.body)
2341
+ }
2342
+ return fetchOptions
2343
+
2344
+ function body2params() {
2345
+ Object.keys(postParams).forEach(key => {
2346
+ let param = postParams[key]
2347
+ if (typeof param == 'object') param = JSON.stringify(param)
2348
+ url.searchParams.append(key, param)
2349
+ })
2350
+ delete fetchOptions.body
2351
+ }
2352
+ }
2353
+
2354
+ bindEvents(selector, subject) {
2355
+ // format is
2356
+ // <div ... data-<event>='["<method>","param1","param2",...]'> -- should be valid JSON (no undefined)
2357
+ // <div ... data-<event>="<method>|param1|param2">
2358
+ // -- can have "event", "this", "stop", "stopPrevent", "alert" - as predefined objects
2359
+ if (selector.length == 0) return
2360
+ // for backward compatibility
2361
+ if (selector?.[0] instanceof Node) {
2362
+ selector = Array.isArray(selector) ? selector : selector.get()
2363
+ }
2364
+ query(selector).each((el) => {
2365
+ let actions = query(el).data()
2366
+ Object.keys(actions).forEach(name => {
2367
+ let events = ['click', 'dblclick', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousedown', 'mousemove', 'mouseup',
2368
+ 'contextmenu', 'focus', 'focusin', 'focusout', 'blur', 'input', 'change', 'keydown', 'keyup', 'keypress']
2369
+ if (events.indexOf(String(name).toLowerCase()) == -1) {
2370
+ return
2371
+ }
2372
+ let params = actions[name]
2373
+ if (typeof params == 'string') {
2374
+ params = params.split('|').map(key => {
2375
+ if (key === 'true') key = true
2376
+ if (key === 'false') key = false
2377
+ if (key === 'undefined') key = undefined
2378
+ if (key === 'null') key = null
2379
+ if (parseFloat(key) == key) key = parseFloat(key)
2380
+ let quotes = ['\'', '"', '`']
2381
+ if (typeof key == 'string' && quotes.includes(key[0]) && quotes.includes(key[key.length-1])) {
2382
+ key = key.substring(1, key.length-1)
2383
+ }
2384
+ return key
2385
+ })
2386
+ }
2387
+ let method = params[0]
2388
+ params = params.slice(1) // should be new array
2389
+ query(el)
2390
+ .off(name + '.w2utils-bind')
2391
+ .on(name + '.w2utils-bind', function(event) {
2392
+ switch (method) {
2393
+ case 'alert':
2394
+ alert(params[0]) // for testing purposes
2395
+ break
2396
+ case 'stop':
2397
+ event.stopPropagation()
2398
+ break
2399
+ case 'prevent':
2400
+ event.preventDefault()
2401
+ break
2402
+ case 'stopPrevent':
2403
+ event.stopPropagation()
2404
+ event.preventDefault()
2405
+ return false
2406
+ break
2407
+ default:
2408
+ if (subject[method] == null) {
2409
+ throw new Error(`Cannot dispatch event as the method "${method}" does not exist.`)
2410
+ }
2411
+ subject[method].apply(subject, params.map((key, ind) => {
2412
+ switch (String(key).toLowerCase()) {
2413
+ case 'event':
2414
+ return event
2415
+ case 'this':
2416
+ return this
2417
+ default:
2418
+ return key
2419
+ }
2420
+ }))
2421
+ }
2422
+ })
2423
+ })
2424
+ })
2425
+ }
2426
+
2427
+ debounce(func, wait = 250) {
2428
+ let timeout
2429
+ return (...args) => {
2430
+ clearTimeout(timeout)
2431
+ timeout = setTimeout(() => { func(...args) }, wait)
2432
+ }
2433
+ }
2434
+
2435
+ async wait(time = 0) {
2436
+ return new Promise(resolve => {
2437
+ setTimeout(() => resolve(), time)
2438
+ })
2439
+ }
2440
+
2441
+ getNested(obj, prop) {
2442
+ let val
2443
+ try { // need this to make sure no error in props
2444
+ val = obj
2445
+ let tmp = String(prop).split('.')
2446
+ for (let i = 0; i < tmp.length; i++) {
2447
+ val = val[tmp[i]]
2448
+ }
2449
+ } catch (event) {
2450
+ val = undefined
2451
+ }
2452
+ return val
2453
+
2454
+ }
2455
+ }
2456
+ var w2utils = new Utils() // eslint-disable-line -- needs to be functional/module scope variable
2457
+ export { w2ui, w2utils, query }