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,3134 @@
1
+ /**
2
+ * Part of w2ui 2.0 library
3
+ * - Dependencies: mQuery, w2utils, w2base
4
+ *
5
+ * TODO:
6
+ * - need help pages
7
+ *
8
+ * 2.0 Changes
9
+ * - multiple tooltips to the same anchor
10
+ * - options.contextMenu
11
+ * - options.prefilter - if true, it will show prefiltered items for w2menu, otherwise all
12
+ * - menu.item.help, menu.item.hotkey, menu.item.extra
13
+ * - options.selected -> for w2menu
14
+ * - options.tooltip => {}
15
+ * - w2menu event onTooltip
16
+ * - added onMouseEnter and onMouseLeave for w2menu
17
+ */
18
+
19
+ import { w2base } from './w2base.js'
20
+ import { w2utils } from './w2utils.js'
21
+ import { query } from './query.js'
22
+
23
+ class Tooltip {
24
+ // no need to extend w2base, as each individual tooltip extends it
25
+ static active = {} // all defined tooltips
26
+ constructor() {
27
+ this.defaults = {
28
+ name : null, // name for the overlay, otherwise input id is used
29
+ html : '', // text or html
30
+ style : '', // additional style for the overlay
31
+ class : '', // add class for w2ui-tooltip-body
32
+ position : 'top|bottom', // can be left, right, top, bottom
33
+ draggable : false, // if true, then tooltip can be move with mouse
34
+ align : '', // can be: both, both:XX left, right, both, top, bottom
35
+ anchor : null, // element it is attached to, if anchor is body, then it is context menu
36
+ contextMenu : false, // if true, then it is context menu
37
+ anchorClass : '', // add class for anchor when tooltip is shown
38
+ anchorStyle : '', // add style for anchor when tooltip is shown
39
+ autoShow : false, // if autoShow true, then tooltip will show on mouseEnter and hide on mouseLeave
40
+ autoShowOn : null, // when options.autoShow = true, mouse event to show on
41
+ autoHideOn : null, // when options.autoShow = true, mouse event to hide on
42
+ arrowSize : 8, // size of the carret
43
+ screenMargin : 2, // min margin from screen to tooltip
44
+ autoResize : true, // auto resize based on content size and available size
45
+ margin : 1, // distance from the anchor
46
+ offsetX : 0, // delta for left coordinate
47
+ offsetY : 0, // delta for top coordinate
48
+ maxWidth : null, // max width
49
+ maxHeight : null, // max height
50
+ watchScroll : null, // attach to onScroll event // TODO:
51
+ watchResize : null, // attach to onResize event // TODO:
52
+ hideOn : null, // events when to hide tooltip, ['click', 'change', 'key', 'focus', 'blur'],
53
+ onThen : null, // called when displayed
54
+ onShow : null, // callBack when shown
55
+ onHide : null, // callBack when hidden
56
+ onUpdate : null, // callback when tooltip gets updated
57
+ onMove : null // callback when tooltip is moved
58
+ }
59
+ }
60
+
61
+ static observeRemove = new MutationObserver((mutations) => {
62
+ let cnt = 0
63
+ Object.keys(Tooltip.active).forEach(name => {
64
+ let overlay = Tooltip.active[name]
65
+ if (overlay.displayed) {
66
+ if (!overlay.anchor || !overlay.anchor.isConnected) {
67
+ overlay.hide()
68
+ } else {
69
+ cnt++
70
+ }
71
+ }
72
+ })
73
+ // remove observer, as there is no active tooltips
74
+ if (cnt === 0) {
75
+ Tooltip.observeRemove.disconnect()
76
+ }
77
+ })
78
+
79
+ trigger(event, data) {
80
+ if (arguments.length == 2) {
81
+ let type = event
82
+ event = data
83
+ data.type = type
84
+ }
85
+ if (event.overlay) {
86
+ return event.overlay.trigger(event)
87
+ } else {
88
+ console.log('ERROR: cannot find overlay where to trigger events')
89
+ }
90
+ }
91
+
92
+ get(name) {
93
+ if (arguments.length == 0) {
94
+ return Object.keys(Tooltip.active)
95
+ } else if (name === true) {
96
+ return Tooltip.active
97
+ } else {
98
+ return Tooltip.active[name.replace(/[\s\.#]/g, '_')]
99
+ }
100
+ }
101
+
102
+ attach(anchor, text) {
103
+ let options, overlay
104
+ let self = this
105
+ if (arguments.length == 0) {
106
+ return
107
+ } else if (arguments.length == 1 && anchor instanceof Object) {
108
+ options = anchor
109
+ anchor = options.anchor
110
+ } else if (arguments.length === 2 && typeof text === 'string') {
111
+ options = { anchor, html: text }
112
+ text = options.html
113
+ } else if (arguments.length === 2 && text != null && typeof text === 'object') {
114
+ options = text
115
+ text = options.html
116
+ }
117
+ options = w2utils.extend({}, this.defaults, options || {})
118
+ if (!text && options.text) text = options.text
119
+ if (!text && options.html) text = options.html
120
+ // anchor is func var
121
+ delete options.anchor
122
+
123
+ // define tooltip
124
+ let name = (options.name ? options.name : anchor?.id)
125
+ if (anchor == document || anchor == null) {
126
+ anchor = document.body
127
+ }
128
+ if (options.contextMenu) {
129
+ anchor = document.body
130
+ name = name ?? 'context-menu'
131
+ }
132
+ if (!name) {
133
+ name = 'noname-' + Object.keys(Tooltip.active).length
134
+ console.log('NOTICE: name property is not defined for tooltip, could lead to too many instances')
135
+ }
136
+ // clean name as it is used as id and css selector
137
+ name = name.replace(/[\s\.#]/g, '_')
138
+ if (Tooltip.active[name]) {
139
+ overlay = Tooltip.active[name]
140
+ overlay.prevOptions = overlay.options
141
+ overlay.options = options // do not merge or extend, otherwiser menu items get merged too
142
+ // overlay.options = w2utils.extend({}, overlay.options, options)
143
+ overlay.anchor = anchor // as HTML elements are not copied
144
+ if (overlay.prevOptions.html != overlay.options.html || overlay.prevOptions.class != overlay.options.class
145
+ || overlay.prevOptions.style != overlay.options.style) {
146
+ overlay.needsUpdate = true
147
+ }
148
+ options = overlay.options // it was recreated
149
+ // clear all previous overlay events
150
+ Object.keys(overlay).forEach(key => {
151
+ let val = overlay[key]
152
+ if (key.startsWith('on') && typeof val == 'function') {
153
+ delete overlay[key]
154
+ }
155
+ })
156
+ } else {
157
+ overlay = new w2base()
158
+ Object.assign(overlay, {
159
+ id: 'w2overlay-' + name,
160
+ name, options, anchor, self,
161
+ displayed: false,
162
+ tmp: {
163
+ observeTooltipResize: new ResizeObserver(() => {
164
+ this.resize(overlay.name)
165
+ }),
166
+ observeAnchorResize: new ResizeObserver(() => {
167
+ this.resize(overlay.name)
168
+ }),
169
+ observeAnchorMove: new MutationObserver((mutations) => {
170
+ let target = mutations[0].target // all targets are the same
171
+ let currRect = target.getBoundingClientRect()
172
+ let lastRect = target._lastBoundingRect
173
+ if (!target._lastBoundingRect) {
174
+ target._lastBoundingRect = currRect
175
+ } else if (currRect.left !== lastRect.left || currRect.top !== lastRect.top) {
176
+ this.resize(overlay.name)
177
+ target._lastBoundingRect = currRect
178
+ }
179
+ })
180
+ },
181
+ hide() {
182
+ self.hide(name)
183
+ }
184
+ })
185
+ Tooltip.active[name] = overlay
186
+ }
187
+ // move events on to overlay layer
188
+ Object.keys(overlay.options).forEach(key => {
189
+ let val = overlay.options[key]
190
+ if (key.startsWith('on') && typeof val == 'function') {
191
+ overlay[key] = val
192
+ delete overlay.options[key]
193
+ }
194
+ })
195
+ // add event for auto show/hide
196
+ if (options.autoShow === true) {
197
+ options.autoShowOn = options.autoShowOn ?? 'mouseenter'
198
+ options.autoHideOn = options.autoHideOn ?? 'mouseleave'
199
+ options.autoShow = false
200
+ options._keep = true
201
+ }
202
+ if (options.autoShowOn) {
203
+ let scope = 'autoShow-' + overlay.name
204
+ query(anchor)
205
+ .off(`.${scope}`)
206
+ .on(`${options.autoShowOn}.${scope}`, event => {
207
+ self.show(overlay.name)
208
+ event.stopPropagation()
209
+ })
210
+ delete options.autoShowOn
211
+ options._keep = true
212
+ }
213
+ if (options.autoHideOn) {
214
+ let scope = 'autoHide-' + overlay.name
215
+ query(anchor)
216
+ .off(`.${scope}`)
217
+ .on(`${options.autoHideOn}.${scope}`, event => {
218
+ self.hide(overlay.name)
219
+ event.stopPropagation()
220
+ })
221
+ delete options.autoHideOn
222
+ options._keep = true
223
+ }
224
+ overlay.off('.attach')
225
+ let ret = {
226
+ overlay,
227
+ then: (callback) => {
228
+ overlay.on('show:after.attach', event => { callback(event) })
229
+ return ret
230
+ },
231
+ show: (callback) => {
232
+ overlay.on('show.attach', event => { callback(event) })
233
+ return ret
234
+ },
235
+ hide: (callback) => {
236
+ overlay.on('hide.attach', event => { callback(event) })
237
+ return ret
238
+ },
239
+ update: (callback) => {
240
+ overlay.on('update.attach', event => { callback(event) })
241
+ return ret
242
+ },
243
+ move: (callback) => {
244
+ overlay.on('move.attach', event => { callback(event) })
245
+ return ret
246
+ }
247
+ }
248
+ return ret
249
+ }
250
+
251
+ update(name, html) {
252
+ let overlay = Tooltip.active[name]
253
+ if (overlay) {
254
+ overlay.needsUpdate = true
255
+ overlay.options.html = html
256
+ this.show(name)
257
+ } else {
258
+ console.log(`Tooltip "${name}" is not displayed. Cannot update it.`)
259
+ }
260
+ }
261
+
262
+ show(name) {
263
+ if (name instanceof HTMLElement || name instanceof Object) {
264
+ let options = name
265
+ if (name instanceof HTMLElement) {
266
+ options = arguments[1] || {}
267
+ options.anchor = name
268
+ }
269
+ let ret = this.attach(options)
270
+ ret.overlay.tmp.hidden = false
271
+
272
+ query(ret.overlay.anchor)
273
+ .off('.autoShow-' + ret.overlay.name)
274
+ .off('.autoHide-' + ret.overlay.name)
275
+
276
+ /**
277
+ * Need a timer, so that events in the 'return ret` would be preperty set as it is using chaning mechanism
278
+ * to set listeners: w2tooltip.show({}).then(...).show(...). Since it could be hidden before timer kick in
279
+ * to show it, need the check in the timeout.
280
+ */
281
+ setTimeout(() => {
282
+ if (!ret.overlay.tmp.hidden) {
283
+ this.show(ret.overlay.name)
284
+ if (this.initControls) {
285
+ this.initControls(ret.overlay)
286
+ }
287
+ }
288
+ }, 1)
289
+ return ret
290
+ }
291
+ let edata
292
+ let self = this
293
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
294
+ if (!overlay) return
295
+ let options = overlay.options
296
+ if (!overlay || (overlay.displayed && !overlay.needsUpdate)) {
297
+ this.resize(overlay?.name)
298
+ return
299
+ }
300
+ let position = options.position.split('|')
301
+ let isVertical = ['top', 'bottom'].includes(position[0])
302
+ // enforce nowrap only when align=both and vertical
303
+ let overlayStyles = (options.align == 'both' && isVertical ? '' : 'white-space: nowrap;')
304
+ if (options.maxWidth && w2utils.getStrWidth(options.html, '') > options.maxWidth) {
305
+ overlayStyles = 'width: '+ options.maxWidth + 'px; white-space: inherit; overflow: auto;'
306
+ }
307
+ overlayStyles += ' max-height: '+ (options.maxHeight ? options.maxHeight : window.innerHeight - 4) + 'px;'
308
+ // if empty content - then hide it
309
+ if (options.html === '' || options.html == null) {
310
+ self.hide(name)
311
+ return
312
+ } else if (overlay.box) {
313
+ // if already present, update it
314
+ edata = this.trigger('update', { target: name, overlay })
315
+ if (edata.isCancelled === true) {
316
+ // restore previous options
317
+ if (overlay.prevOptions) {
318
+ overlay.options = overlay.prevOptions
319
+ delete overlay.prevOptions
320
+ }
321
+ return
322
+ }
323
+ query(overlay.box)
324
+ .find('.w2ui-overlay-body')
325
+ .attr('style', (options.style || '') + '; ' + overlayStyles)
326
+ .removeClass() // removes all classes
327
+ .addClass('w2ui-overlay-body ' + options.class + (options.draggable ? ' w2ui-draggable' : ''))
328
+ .html(options.html)
329
+ this.resize(overlay.name)
330
+ } else {
331
+ // event before
332
+ edata = this.trigger('show', { target: name, overlay })
333
+ if (edata.isCancelled === true) return
334
+ // normal processing
335
+ query('body').append(
336
+ // pointer-events will be re-enabled leter
337
+ `<div id="${overlay.id}" name="${name}" style="display: none; pointer-events: none" class="w2ui-overlay"
338
+ data-click="stop" data-focusin="stop">
339
+ <style></style>
340
+ <div class="w2ui-overlay-body w2ui-eaction ${options.class} ${options.draggable ? 'w2ui-draggable' : ''}"
341
+ style="${options.style || ''}; ${overlayStyles}" ${options.draggable ? 'data-mousedown="startDrag|event"' : ''}>
342
+ ${options.html}
343
+ </div>
344
+ </div>`)
345
+ overlay.box = query('#' + w2utils.escapeId(overlay.id))[0]
346
+ overlay.displayed = true
347
+ let names = query(overlay.anchor).data('tooltipName') ?? []
348
+ names.push(name)
349
+ query(overlay.anchor).data('tooltipName', names) // make available to element overlay attached to
350
+ w2utils.bindEvents(overlay.box, {})
351
+ // remember anchor's original styles
352
+ overlay.tmp.originalCSS = ''
353
+ if (query(overlay.anchor).length > 0) {
354
+ overlay.tmp.originalCSS = query(overlay.anchor)[0].style.cssText
355
+ }
356
+ this.resize(overlay.name)
357
+ }
358
+ if (options.anchorStyle) {
359
+ overlay.anchor.style.cssText += ';' + options.anchorStyle
360
+ }
361
+ if (options.anchorClass) {
362
+ // do not add w2ui-focus to body
363
+ if (!(options.anchorClass == 'w2ui-focus' && overlay.anchor == document.body)) {
364
+ query(overlay.anchor).addClass(options.anchorClass)
365
+ }
366
+ }
367
+ // add on hide events
368
+ if (typeof options.hideOn == 'string') options.hideOn = [options.hideOn]
369
+ if (!Array.isArray(options.hideOn)) options.hideOn = []
370
+ // initial scroll
371
+ Object.assign(overlay.tmp, {
372
+ scrollLeft: document.body.scrollLeft,
373
+ scrollTop: document.body.scrollTop
374
+ })
375
+ addHideEvents()
376
+ addWatchEvents(document.body)
377
+ // first show empty tooltip, so it will popup up in the right position
378
+ query(overlay.box).show()
379
+ overlay.tmp.observeTooltipResize.observe(overlay.box)
380
+ overlay.tmp.observeAnchorResize.observe(overlay.anchor)
381
+ overlay.tmp.observeAnchorMove.observe(overlay.anchor, { attributes: true })
382
+ // observer element removal from DOM
383
+ Tooltip.observeRemove.observe(document.body, { subtree: true, childList: true })
384
+ // then insert html and it will adjust
385
+ query(overlay.box)
386
+ .css('opacity', 1)
387
+ .find('.w2ui-overlay-body')
388
+ .html(options.html)
389
+ /**
390
+ * pointer-events: none is needed to avoid cases when popup is shown right under the cursor
391
+ * or it will trigger onmouseout, onmouseleave and other events.
392
+ */
393
+ setTimeout(() => { query(overlay.box).css({ 'pointer-events': 'auto' }).data('ready', 'yes') }, 100)
394
+
395
+ // bind events
396
+ w2utils.bindEvents(query(overlay.box).find('.w2ui-eaction'), this)
397
+
398
+ delete overlay.needsUpdate
399
+ // expose overlay to DOM element
400
+ overlay.box.overlay = overlay
401
+ // event after
402
+ if (edata) edata.finish()
403
+ return { overlay }
404
+
405
+ function addWatchEvents(el) {
406
+ let scope = 'tooltip-' + overlay.name
407
+ let queryEl = el
408
+ if (el.tagName == 'BODY') {
409
+ queryEl = el.ownerDocument
410
+ }
411
+ query(queryEl)
412
+ .off(`.${scope}`)
413
+ .on(`scroll.${scope}`, event => {
414
+ Object.assign(overlay.tmp, {
415
+ scrollLeft: el.scrollLeft,
416
+ scrollTop: el.scrollTop
417
+ })
418
+ self.resize(overlay.name)
419
+ })
420
+ }
421
+
422
+ function addHideEvents() {
423
+ let hide = (event) => { self.hide(overlay.name) }
424
+ let $anchor = query(overlay.anchor)
425
+ let scope = 'tooltip-' + overlay.name
426
+ // document click
427
+ query('html').off(`.${scope}`)
428
+ if (options.hideOn.includes('doc-click')) {
429
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
430
+ // otherwise hides on click to focus
431
+ $anchor
432
+ .off(`.${scope}-doc`)
433
+ .on(`click.${scope}-doc`, (event) => { event.stopPropagation() })
434
+ }
435
+ query('html').on(`click.${scope}`, hide)
436
+ }
437
+ if (options.hideOn.includes('focus-change') || options.hideOn.includes('blur')) {
438
+ query('html')
439
+ .on(`focusin.${scope}`, (e) => {
440
+ if (document.activeElement != overlay.anchor) {
441
+ self.hide(overlay.name)
442
+ }
443
+ })
444
+ }
445
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
446
+ $anchor.off(`.${scope}`)
447
+ options.hideOn.forEach(event => {
448
+ if (['doc-click', 'focus-change', 'blur'].indexOf(event) == -1) {
449
+ $anchor.on(`${event}.${scope}`, { once: true }, hide)
450
+ }
451
+ })
452
+ }
453
+ }
454
+ }
455
+
456
+ hide(name) {
457
+ let overlay
458
+ if (arguments.length == 0) {
459
+ // hide all tooltips
460
+ Object.keys(Tooltip.active).forEach(name => { this.hide(name) })
461
+ return
462
+ }
463
+ if (name instanceof HTMLElement) {
464
+ let names = query(name).data('tooltipName') ?? []
465
+ names.forEach(name => { this.hide(name) })
466
+ return
467
+ }
468
+ if (typeof name == 'string') {
469
+ name = name.replace(/[\s\.#]/g, '_')
470
+ overlay = Tooltip.active[name]
471
+ }
472
+ if (overlay?.tmp) overlay.tmp.hidden = true // it could be hidden before it is actually shown
473
+ if (!overlay || !overlay.box) return
474
+
475
+ // event before
476
+ let edata = this.trigger('hide', { target: name, overlay })
477
+ if (edata.isCancelled === true) return
478
+
479
+ // normal processing
480
+ if (!overlay.options._keep) delete Tooltip.active[name]
481
+ let scope = 'tooltip-' + overlay.name
482
+ overlay.tmp.observeTooltipResize?.disconnect()
483
+ overlay.tmp.observeAnchorResize?.disconnect()
484
+ overlay.tmp.observeAnchorMove?.disconnect()
485
+ if (overlay.options.watchScroll) {
486
+ query(overlay.options.watchScroll)
487
+ .off('.w2scroll-' + overlay.name)
488
+ }
489
+ // if no active tooltip then disable observeRemove
490
+ let cnt = 0
491
+ Object.keys(Tooltip.active).forEach(key => {
492
+ let overlay = Tooltip.active[key]
493
+ if (overlay.displayed) {
494
+ cnt++
495
+ }
496
+ })
497
+ if (cnt == 0) {
498
+ Tooltip.observeRemove.disconnect()
499
+ }
500
+ query('html').off(`.${scope}`) // hide to click event here
501
+ query(document).off(`.${scope}`) // scroll event here
502
+ // remove element
503
+ overlay.box?.remove()
504
+ overlay.box = null
505
+ overlay.displayed = false
506
+ // remove name from anchor properties
507
+ let names = query(overlay.anchor).data('tooltipName') ?? []
508
+ let ind = names.indexOf(overlay.name)
509
+ if (ind != -1) names.splice(names.indexOf(overlay.name), 1)
510
+ if (names.length == 0) {
511
+ query(overlay.anchor).removeData('tooltipName')
512
+ } else {
513
+ query(overlay.anchor).data('tooltipName', names)
514
+ }
515
+ // restore original CSS, only if anchor styles where extended
516
+ if (overlay.options.anchorStyle) {
517
+ overlay.anchor.style.cssText = overlay.tmp.originalCSS
518
+ }
519
+ query(overlay.anchor)
520
+ .off(`.${scope}`)
521
+ .removeClass(overlay.options.anchorClass)
522
+ // for remote data source
523
+ if (overlay.options.url) {
524
+ // remove all cached items
525
+ overlay.options.items.splice(0)
526
+ overlay.tmp.remote.hasMore = true
527
+ overlay.tmp.remote.search = null
528
+ }
529
+ // event after
530
+ edata.finish()
531
+ }
532
+
533
+ resize(name) {
534
+ let state = { moved: false, resize: false }
535
+ if (arguments.length == 0) {
536
+ Object.keys(Tooltip.active).forEach(key => {
537
+ let overlay = Tooltip.active[key]
538
+ if (overlay.displayed) this.resize(overlay.name)
539
+ })
540
+ return { multiple: true }
541
+ }
542
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
543
+ let pos = this.getPosition(overlay.name)
544
+ let newPos = pos.left + 'x' + pos.top
545
+ let newSize = pos.width + 'x' + pos.height
546
+ let edata1, edata2
547
+ if (overlay.tmp.lastPos != newPos) {
548
+ edata1 = this.trigger('move', { target: name, overlay, pos })
549
+ state.moved = true
550
+ }
551
+ if (overlay.tmp.lastSize != newSize) {
552
+ edata2 = this.trigger('resize', { target: name, overlay, pos })
553
+ state.moved = true
554
+ }
555
+ query(overlay.box)
556
+ .css({
557
+ left: pos.left + 'px',
558
+ top : pos.top + 'px'
559
+ })
560
+ .then(query => {
561
+ if (pos.width != null) {
562
+ query.css('width', pos.width + 'px')
563
+ .find('.w2ui-overlay-body')
564
+ .css('width', '100%')
565
+ }
566
+ if (pos.height != null) {
567
+ query.css('height', pos.height + 'px')
568
+ .find('.w2ui-overlay-body')
569
+ .css('height', '100%')
570
+ }
571
+ })
572
+ .find('.w2ui-overlay-body')
573
+ .removeClass('w2ui-arrow-right w2ui-arrow-left w2ui-arrow-top w2ui-arrow-bottom')
574
+ .addClass(pos.arrow.class)
575
+ .closest('.w2ui-overlay')
576
+ .find('style')
577
+ .text(pos.arrow.style)
578
+
579
+ if (overlay.tmp.lastPos != newPos && edata1) {
580
+ overlay.tmp.lastPos = newPos
581
+ edata1.finish()
582
+ }
583
+ if (overlay.tmp.lastSize != newSize && edata2) {
584
+ overlay.tmp.lastSize = newSize
585
+ edata2.finish()
586
+ }
587
+ return state
588
+ }
589
+
590
+ getPosition(name) {
591
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
592
+ if (!overlay || !overlay.box) {
593
+ return
594
+ }
595
+ let options = overlay.options
596
+ if (overlay.tmp.resizedY || overlay.tmp.resizedX) {
597
+ query(overlay.box).css({ width: '', height: '', scroll: 'auto' })
598
+ }
599
+ let scrollSize = w2utils.scrollBarSize()
600
+ let hasScrollBarX = !(document.body.scrollWidth == document.body.clientWidth)
601
+ let hasScrollBarY = !(document.body.scrollHeight == document.body.clientHeight)
602
+ let max = {
603
+ width: window.innerWidth - (hasScrollBarY ? scrollSize : 0),
604
+ height: window.innerHeight - (hasScrollBarX ? scrollSize : 0)
605
+ }
606
+ let position = options.position == 'auto'
607
+ ? 'top|bottom|right|left'.split('|')
608
+ : Array.isArray(options.position) ? options.position : options.position.split('|')
609
+ let isVertical = ['top', 'bottom'].includes(position[0])
610
+ let content = overlay.box.getBoundingClientRect()
611
+ let anchor = overlay.anchor?.getBoundingClientRect?.()
612
+
613
+ if (overlay.anchor == document.body) {
614
+ // context menu
615
+ let evt = options.originalEvent
616
+ while (evt?.originalEvent) { evt = evt.originalEvent }
617
+ let { x, y, width, height } = evt ?? { x: options.x, y: options.y, width: 0, height: 10 }
618
+ anchor = {
619
+ left: x - (options.contextMenu ? 4: 0),
620
+ top: y - (options.contextMenu ? 4: 0),
621
+ width: width ?? 0,
622
+ height: height ?? 10,
623
+ arrow: options.contextMenu ? 'none' : null
624
+ }
625
+ }
626
+ let arrowSize = options.arrowSize
627
+ if (anchor.arrow == 'none') arrowSize = 0
628
+ if (isNaN(arrowSize)) arrowSize = this.defaults.arrowSize
629
+
630
+ // space available
631
+ let available = { // tipsize adjustment should be here, not in max.width/max.height
632
+ top: anchor.top - arrowSize,
633
+ bottom: max.height - arrowSize - (anchor.top + anchor.height) - (hasScrollBarX ? scrollSize : 0) - 2,
634
+ left: anchor.left,
635
+ right: max.width - (anchor.left + anchor.width) + (hasScrollBarY ? scrollSize : 0),
636
+ }
637
+ // size of empty tooltip
638
+ if (content.width < 22) content.width = 22
639
+ if (content.height < 14) content.height = 14
640
+ let left, top, width, height // tooltip position
641
+ let found = ''
642
+ let arrow = {
643
+ offset: 0,
644
+ class: '',
645
+ style: `#${overlay.id} { --tip-size: ${arrowSize}px; }`
646
+ }
647
+ let adjust = { left: 0, top: 0 }
648
+ let bestFit = { posX: '', x: 0, posY: '', y: 0 }
649
+
650
+ // find best position
651
+ position.forEach(pos => {
652
+ if (['top', 'bottom'].includes(pos)) {
653
+ if (!found && (content.height + arrowSize/1.893) < available[pos]) { // 1.893 = 1 + sin(90)
654
+ found = pos
655
+ }
656
+ if (available[pos] > bestFit.y) {
657
+ Object.assign(bestFit, { posY: pos, y: available[pos] })
658
+ }
659
+ }
660
+ if (['left', 'right'].includes(pos)) {
661
+ if (!found && (content.width + arrowSize/1.893) < available[pos]) { // 1.893 = 1 + sin(90)
662
+ found = pos
663
+ }
664
+ if (available[pos] > bestFit.x) {
665
+ Object.assign(bestFit, { posX: pos, x: available[pos] })
666
+ }
667
+ }
668
+ })
669
+ // if not found, use best (greatest available space) position
670
+ if (!found) {
671
+ if (isVertical) {
672
+ found = bestFit.posY
673
+ } else {
674
+ found = bestFit.posX
675
+ }
676
+ }
677
+ if (options.autoResize) {
678
+ if (['top', 'bottom'].includes(found)) {
679
+ if (content.height > available[found]) {
680
+ height = available[found]
681
+ overlay.tmp.resizedY = true
682
+ } else {
683
+ overlay.tmp.resizedY = false
684
+ }
685
+ }
686
+ if (['left', 'right'].includes(found)) {
687
+ if (content.width > available[found]) {
688
+ width = available[found]
689
+ overlay.tmp.resizedX = true
690
+ } else {
691
+ overlay.tmp.resizedX = false
692
+ }
693
+ }
694
+ }
695
+ usePosition(found)
696
+ if (isVertical) anchorAlignment()
697
+
698
+ // user offset
699
+ top += parseFloat(options.offsetY * (found == 'top' ? -1 : 1)) // offset will always move from original position
700
+ left += parseFloat(options.offsetX * (found == 'left' ? -1 : 1)) // offset will always move from original position
701
+
702
+ // make sure it is inside visible screen area
703
+ screenAdjust()
704
+
705
+ // adjust for scrollbar
706
+ let extraTop = (found == 'top' ? -options.margin : (found == 'bottom' ? options.margin : 0))
707
+ let extraLeft = (found == 'left' ? -options.margin : (found == 'right' ? options.margin : 0))
708
+ top = Math.floor((top + parseFloat(extraTop)) * 100) / 100
709
+ left = Math.floor((left + parseFloat(extraLeft)) * 100) / 100
710
+
711
+ return { left, top, arrow, adjust, width, height, pos: found }
712
+
713
+ function usePosition(pos) {
714
+ arrow.class = anchor.arrow ? anchor.arrow : `w2ui-arrow-${pos}`
715
+ switch (pos) {
716
+ case 'top': {
717
+ left = anchor.left + (anchor.width - (width ?? content.width)) / 2
718
+ top = anchor.top - (height ?? content.height) - arrowSize / 1.5 + 1
719
+ break
720
+ }
721
+ case 'bottom': {
722
+ left = anchor.left + (anchor.width - (width ?? content.width)) / 2
723
+ top = anchor.top + anchor.height + arrowSize / 1.25 + 1
724
+ break
725
+ }
726
+ case 'left': {
727
+ left = anchor.left - (width ?? content.width) - arrowSize / 1.2 - 1
728
+ top = anchor.top + (anchor.height - (height ?? content.height)) / 2
729
+ break
730
+ }
731
+ case 'right': {
732
+ left = anchor.left + anchor.width + arrowSize / 1.2 + 1
733
+ top = anchor.top + (anchor.height - (height ?? content.height)) / 2
734
+ break
735
+ }
736
+ }
737
+ }
738
+
739
+ function anchorAlignment() {
740
+ // top/bottom alignments
741
+ if (options.align == 'left') {
742
+ adjust.left = anchor.left - left
743
+ left = anchor.left
744
+ }
745
+ if (options.align == 'right') {
746
+ adjust.left = (anchor.left + anchor.width - (width ?? content.width)) - left
747
+ left = anchor.left + anchor.width - (width ?? content.width)
748
+ }
749
+ if (['top', 'bottom'].includes(found) && options.align.startsWith('both')) {
750
+ let minWidth = options.align.split(':')[1] ?? 50
751
+ if (anchor.width >= minWidth) {
752
+ left = anchor.left
753
+ width = anchor.width
754
+ }
755
+ }
756
+ // left/right alignments
757
+ if (options.align == 'top') {
758
+ adjust.top = anchor.top - top
759
+ top = anchor.top
760
+ }
761
+ if (options.align == 'bottom') {
762
+ adjust.top = (anchor.top + anchor.height - (height ?? content.height)) - top
763
+ top = anchor.top + anchor.height - (height ?? content.height)
764
+ }
765
+ if (['left', 'right'].includes(found) && options.align.startsWith('both')) {
766
+ let minHeight = options.align.split(':')[1] ?? 50
767
+ if (anchor.height >= minHeight) {
768
+ top = anchor.top
769
+ height = anchor.height
770
+ }
771
+ }
772
+ }
773
+
774
+ function screenAdjust() {
775
+ let adjustArrow
776
+ // adjust tip if needed after alignment
777
+ if ((['left', 'right'].includes(options.align) && anchor.width < (width ?? content.width))
778
+ || (['top', 'bottom'].includes(options.align) && anchor.height < (height ?? content.height))
779
+ ) {
780
+ adjustArrow = true
781
+ }
782
+ // if off screen then adjust
783
+ let minLeft = (found == 'right' ? arrowSize : options.screenMargin)
784
+ let minTop = (found == 'bottom' ? arrowSize : options.screenMargin)
785
+ let maxLeft = max.width - (width ?? content.width) - (found == 'left' ? arrowSize : options.screenMargin)
786
+ let maxTop = max.height - (height ?? content.height) - (found == 'top' ? arrowSize : options.screenMargin) + 3
787
+ // adjust X
788
+ if (['top', 'bottom'].includes(found) || options.autoResize) {
789
+ if (left < minLeft) {
790
+ adjustArrow = true
791
+ adjust.left -= left
792
+ left = minLeft
793
+ }
794
+ if (left > maxLeft) {
795
+ adjustArrow = true
796
+ adjust.left -= left - maxLeft
797
+ left += maxLeft - left
798
+ }
799
+ }
800
+ // adjust Y
801
+ if (['left', 'right'].includes(found) || options.autoResize) {
802
+ if (top < minTop) {
803
+ adjustArrow = true
804
+ adjust.top -= top
805
+ top = minTop
806
+ }
807
+ if (top > maxTop) {
808
+ adjustArrow = true
809
+ adjust.top -= top - maxTop
810
+ top += maxTop - top
811
+ }
812
+ }
813
+ // moves carret to adjust it with element width
814
+ if (adjustArrow) {
815
+ let aType = isVertical ? 'left' : 'top'
816
+ let sType = isVertical ? 'width' : 'height'
817
+ arrow.offset = -adjust[aType]
818
+ let maxOffset = content[sType] / 2 - arrowSize
819
+ if (Math.abs(arrow.offset) > maxOffset + arrowSize) {
820
+ arrow.class = '' // no arrow
821
+ }
822
+ if (Math.abs(arrow.offset) > maxOffset) {
823
+ arrow.offset = arrow.offset < 0 ? -maxOffset : maxOffset
824
+ }
825
+ arrow.style = w2utils.stripSpaces(`#${overlay.id} .w2ui-overlay-body:after,
826
+ #${overlay.id} .w2ui-overlay-body:before {
827
+ --tip-size: ${arrowSize}px;
828
+ margin-${aType}: ${arrow.offset}px;
829
+ }`)
830
+ }
831
+ }
832
+ }
833
+
834
+ startDrag(event) {
835
+ let initial
836
+ let el = query(event.target).closest('.w2ui-overlay')
837
+ initial = {
838
+ el,
839
+ x: parseFloat(el.css('left')),
840
+ y: parseFloat(el.css('top')),
841
+ pageX: event.pageX,
842
+ pageY: event.pageY,
843
+ }
844
+ query('html')
845
+ .off('.w2color')
846
+ .on('mousemove.w2color', mouseMove)
847
+ .on('mouseup.w2color', mouseUp)
848
+
849
+ function mouseUp(event) {
850
+ query('html').off('.w2color')
851
+ }
852
+
853
+ function mouseMove(event) {
854
+ let divX = event.pageX - initial.pageX
855
+ let divY = event.pageY - initial.pageY
856
+ initial.el.css({
857
+ left: initial.x + divX + 'px',
858
+ top: initial.y + divY + 'px'
859
+ })
860
+ if (!initial._removed) {
861
+ initial._removed = true
862
+ initial.el.find(':scope > .w2ui-overlay-body')
863
+ .removeClass('w2ui-arrow-right w2ui-arrow-left w2ui-arrow-top w2ui-arrow-bottom')
864
+ }
865
+ }
866
+ }
867
+ }
868
+
869
+ class ColorTooltip extends Tooltip {
870
+ static custom_colors = []
871
+ constructor() {
872
+ super()
873
+ this.palette = [
874
+ ['000000', '333333', '555555', '777777', '888888', '999999', 'AAAAAA', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'F7F7F7', 'FFFFFF'],
875
+ ['FF011B', 'FF9838', 'FFC300', 'FFFD59', '86FF14', '14FF7A', '2EFFFC', '2693FF', '006CE7', '9B24F4', 'FF21F5', 'FF0099'],
876
+ ['FFEAEA', 'FCEFE1', 'FCF4DC', 'FFFECF', 'EBFFD9', 'D9FFE9', 'E0FFFF', 'E8F4FF', 'ECF4FC', 'EAE6F4', 'FFF5FE', 'FCF0F7'],
877
+ ['F4CCCC', 'FCE5CD', 'FFF1C2', 'FFFDA1', 'D5FCB1', 'B5F7D0', 'BFFFFF', 'D6ECFF', 'CFE2F3', 'D9D1E9', 'FFE3FD', 'FFD9F0'],
878
+ ['EA9899', 'F9CB9C', 'FFE48C', 'F7F56F', 'B9F77E', '84F0B1', '83F7F7', 'B5DAFF', '9FC5E8', 'B4A7D6', 'FAB9F6', 'FFADDE'],
879
+ ['E06666', 'F6B26B', 'DEB737', 'E0DE51', '8FDB48', '52D189', '4EDEDB', '76ACE3', '6FA8DC', '8E7CC3', 'E07EDA', 'F26DBD'],
880
+ ['CC0814', 'E69138', 'AB8816', 'B5B20E', '6BAB30', '27A85F', '1BA8A6', '3C81C7', '3D85C6', '674EA7', 'A14F9D', 'BF4990'],
881
+ ['99050C', 'B45F17', '80650E', '737103', '395E14', '10783D', '13615E', '094785', '0A5394', '351C75', '780172', '782C5A']
882
+ ]
883
+ this.defaults = w2utils.extend({}, this.defaults, {
884
+ advanced : false,
885
+ transparent : true,
886
+ position : 'top|bottom',
887
+ class : 'w2ui-white',
888
+ color : '',
889
+ updateInput : true,
890
+ arrowSize : 12,
891
+ autoResize : false,
892
+ anchorClass : 'w2ui-focus',
893
+ autoShowOn : 'focus',
894
+ hideOn : ['doc-click', 'focus-change'],
895
+ onSelect : null,
896
+ onLiveUpdate: null
897
+ })
898
+ }
899
+
900
+ attach(anchor, text) {
901
+ let options
902
+ if (arguments.length == 1 && anchor instanceof Object) {
903
+ options = anchor
904
+ anchor = options.anchor
905
+ } else if (arguments.length === 2 && text != null && typeof text === 'object') {
906
+ options = text
907
+ options.anchor = anchor
908
+ }
909
+ let prevHideOn = options.hideOn
910
+ options = w2utils.extend({}, this.defaults, options || {})
911
+ if (prevHideOn) {
912
+ options.hideOn = prevHideOn
913
+ }
914
+ options.style += '; padding: 0;'
915
+ // add remove transparent color
916
+ if (options.transparent && this.palette[0][1] == '333333') {
917
+ this.palette[0].splice(1, 1)
918
+ this.palette[0].push('TRANSPARENT')
919
+ }
920
+ if (!options.transparent && this.palette[0][1] != '333333') {
921
+ this.palette[0].splice(1, 0, '333333')
922
+ this.palette[0].pop()
923
+ }
924
+ if (options.color) options.color = String(options.color).toUpperCase()
925
+ if (typeof options.color === 'string' && options.color.substr(0,1) === '#') options.color = options.color.substr(1)
926
+ // needed for keyboard navigation
927
+ this.index = [-1, -1]
928
+ let ret = super.attach(options)
929
+ let overlay = ret.overlay
930
+ overlay.options.html = this.getColorHTML(overlay.name, options)
931
+ overlay.on('show.attach', event => {
932
+ let overlay = event.detail.overlay
933
+ let anchor = overlay.anchor
934
+ let options = overlay.options
935
+ if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && !options.color && anchor.value) {
936
+ overlay.tmp.initColor = anchor.value
937
+ }
938
+ delete overlay.newColor
939
+ })
940
+ overlay.on('show:after.attach', event => {
941
+ if (ret.overlay?.box) {
942
+ let actions = query(ret.overlay.box).find('.w2ui-eaction')
943
+ w2utils.bindEvents(actions, this)
944
+ this.initControls(ret.overlay)
945
+ }
946
+ })
947
+ overlay.on('update:after.attach', event => {
948
+ if (ret.overlay?.box) {
949
+ let actions = query(ret.overlay.box).find('.w2ui-eaction')
950
+ w2utils.bindEvents(actions, this)
951
+ this.initControls(ret.overlay)
952
+ }
953
+ })
954
+ overlay.on('hide.attach', event => {
955
+ let overlay = event.detail.overlay
956
+ let anchor = overlay.anchor
957
+ let color = overlay.newColor ?? overlay.options.color ?? ''
958
+ // color has been selected
959
+ if (color !== '') {
960
+ if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && anchor.value != color && overlay.options.updateInput) {
961
+ anchor.value = color
962
+ }
963
+ let edata = this.trigger('select', { color, target: overlay.name, overlay })
964
+ if (edata.isCancelled === true) return
965
+ // event after
966
+ edata.finish()
967
+ }
968
+ })
969
+ ret.liveUpdate = (callback) => {
970
+ overlay.on('liveUpdate.attach', (event) => { callback(event) })
971
+ return ret
972
+ }
973
+ ret.select = (callback) => {
974
+ overlay.on('select.attach', (event) => { callback(event) })
975
+ return ret
976
+ }
977
+ return ret
978
+ }
979
+
980
+ // regular panel handler, adds selection class
981
+ select(color, name) {
982
+ let target
983
+ this.index = [-1, -1]
984
+ if (typeof name != 'string') {
985
+ target = name.target
986
+ this.index = query(target).attr('index').split(':')
987
+ name = query(target).closest('.w2ui-overlay').attr('name')
988
+ }
989
+ let overlay = this.get(name)
990
+ // event before
991
+ let edata = this.trigger('liveUpdate', { color, target: name, overlay, param: arguments[1] })
992
+ if (edata.isCancelled === true) return
993
+ // if anchor is input - live update
994
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName) && overlay.options.updateInput) {
995
+ query(overlay.anchor).val(color)
996
+ }
997
+ overlay.newColor = color
998
+ query(overlay.box).find('.w2ui-color.w2ui-selected').removeClass('w2ui-selected')
999
+ if (target) {
1000
+ query(target).addClass('w2ui-selected')
1001
+ }
1002
+ // event after
1003
+ edata.finish()
1004
+ }
1005
+
1006
+ // used for keyboard navigation, if any
1007
+ nextColor(direction) { // TODO: check it
1008
+ let pal = this.palette
1009
+ switch (direction) {
1010
+ case 'up':
1011
+ this.index[0]--
1012
+ break
1013
+ case 'down':
1014
+ this.index[0]++
1015
+ break
1016
+ case 'right':
1017
+ this.index[1]++
1018
+ break
1019
+ case 'left':
1020
+ this.index[1]--
1021
+ break
1022
+ }
1023
+ if (this.index[0] < 0) this.index[0] = 0
1024
+ if (this.index[0] > pal.length - 2) this.index[0] = pal.length - 2
1025
+ if (this.index[1] < 0) this.index[1] = 0
1026
+ if (this.index[1] > pal[0].length - 1) this.index[1] = pal[0].length - 1
1027
+ return pal[this.index[0]][this.index[1]]
1028
+ }
1029
+
1030
+ tabClick(index, name) {
1031
+ if (typeof name != 'string') {
1032
+ name = query(name.target).closest('.w2ui-overlay').attr('name')
1033
+ }
1034
+ let overlay = this.get(name)
1035
+ let tab = query(overlay.box).find(`.w2ui-color-tab:nth-child(${index})`)
1036
+ query(overlay.box).find('.w2ui-color-tab').removeClass('w2ui-selected')
1037
+ query(tab).addClass('w2ui-selected')
1038
+ query(overlay.box)
1039
+ .find('.w2ui-tab-content')
1040
+ .hide()
1041
+ .closest('.w2ui-colors')
1042
+ .find('.tab-'+ index)
1043
+ .show()
1044
+ }
1045
+
1046
+ // generate HTML with color pallent and controls
1047
+ getColorHTML(name, options) {
1048
+ let html = `
1049
+ <div class="w2ui-colors-header w2ui-eaction" data-mousedown="startDrag|event">
1050
+ Colors
1051
+ </div>
1052
+ <div class="w2ui-colors">
1053
+ <div class="w2ui-tab-content tab-1">`
1054
+ for (let i = 0; i < this.palette.length; i++) {
1055
+ html += '<div class="w2ui-color-row">'
1056
+ for (let j = 0; j < this.palette[i].length; j++) {
1057
+ let color = this.palette[i][j]
1058
+ let border = ''
1059
+ if (color === 'FFFFFF') border = '; border: 1px solid #efefef'
1060
+ html += `
1061
+ <div class="w2ui-color w2ui-eaction ${color === 'TRANSPARENT' ? 'w2ui-no-color' : ''} ${options.color == color ? 'w2ui-selected' : ''}"
1062
+ style="background-color: #${color + border};" name="${color}" index="${i}:${j}"
1063
+ data-mousedown="select|'${color}'|event" data-mouseup="hide|${name}">&nbsp;
1064
+ </div>`
1065
+ }
1066
+ html += '</div>'
1067
+ if (i < 2) html += '<div style="height: 8px"></div>'
1068
+ }
1069
+ // custom colors
1070
+ html += `
1071
+ <div style="height: 8px"></div>
1072
+ <div class="w2ui-colors-custom">
1073
+ ${this.getCustomColorsHTML(name)}
1074
+ </div>`
1075
+ html += '</div>'
1076
+ // advanced tab
1077
+ html += `
1078
+ <div class="w2ui-tab-content tab-2" style="display: none">
1079
+ <div class="color-info">
1080
+ <div class="color-preview-bg"><div class="color-preview"></div><div class="color-original"></div></div>
1081
+ <div class="color-part">
1082
+ <span>H</span> <input class="w2ui-input" name="h" maxlength="3" max="360" tabindex="101">
1083
+ <span>R</span> <input class="w2ui-input" name="r" maxlength="3" max="255" tabindex="104">
1084
+ </div>
1085
+ <div class="color-part">
1086
+ <span>S</span> <input class="w2ui-input" name="s" maxlength="3" max="100" tabindex="102">
1087
+ <span>G</span> <input class="w2ui-input" name="g" maxlength="3" max="255" tabindex="105">
1088
+ </div>
1089
+ <div class="color-part">
1090
+ <span>V</span> <input class="w2ui-input" name="v" maxlength="3" max="100" tabindex="103">
1091
+ <span>B</span> <input class="w2ui-input" name="b" maxlength="3" max="255" tabindex="106">
1092
+ </div>
1093
+ <div class="color-part opacity">
1094
+ <span>${w2utils.lang('Opacity')}</span>
1095
+ <input class="w2ui-input" name="a" maxlength="5" max="1" tabindex="107">
1096
+ </div>
1097
+ </div>
1098
+ <div class="palette" name="palette">
1099
+ <div class="palette-bg"></div>
1100
+ <div class="value1 move-x move-y"></div>
1101
+ </div>
1102
+ <div class="rainbow" name="rainbow">
1103
+ <div class="value2 move-x"></div>
1104
+ </div>
1105
+ <div class="alpha" name="alpha">
1106
+ <div class="alpha-bg"></div>
1107
+ <div class="value2 move-x"></div>
1108
+ </div>
1109
+ <div class="final" name="final">
1110
+ <span style="text-align: right"> Hex </span>
1111
+ <input class="w2ui-input final" name="hex" tabindex="107" style="width: 70px" readonly>
1112
+ <div class="w2ui-color w2ui-color-picker w2ui-eaction" data-click="pickAndUse|${name}">
1113
+ <span class="w2ui-icon w2ui-icon-eye-dropper"></span>
1114
+ </div>
1115
+ </div>
1116
+ </div>`
1117
+ // color tabs on the bottom
1118
+ html += `
1119
+ <div class="w2ui-color-tabs">
1120
+ <div class="w2ui-color-tab w2ui-selected w2ui-eaction" data-click="tabClick|1|event|this"><span class="w2ui-icon w2ui-icon-colors"></span></div>
1121
+ <div class="w2ui-color-tab w2ui-eaction" data-click="tabClick|2|event|this"><span class="w2ui-icon w2ui-icon-settings"></span></div>
1122
+ <div style="padding: 5px; width: 100%; text-align: right;">
1123
+ ${(typeof options.html == 'string' ? options.html : '')}
1124
+ </div>
1125
+ </div>`
1126
+ return html
1127
+ }
1128
+
1129
+ getCustomColorsHTML(name) {
1130
+ let options = this.get(name)?.options
1131
+ let html = '<div class="w2ui-color-row" style="min-height: 21px">'
1132
+ ColorTooltip.custom_colors.forEach((color, i) => {
1133
+ let border = ''
1134
+ if (color === 'FFFFFF') border = '; border: 1px solid #efefef'
1135
+ html += `
1136
+ <div class="w2ui-color w2ui-eaction ${color === 'TRANSPARENT' ? 'w2ui-no-color' : ''} ${options.color == color ? 'w2ui-selected' : ''}"
1137
+ style="background-color: #${color + border};" name="${color}" index="c:${i}"
1138
+ data-mousedown="select|'${color}'|event" data-mouseup="hide|${name}">&nbsp;
1139
+ </div>`
1140
+
1141
+ })
1142
+ html += `
1143
+ <div class="w2ui-color w2ui-color-picker w2ui-eaction" data-click="pickAndSelect|${name}|event">
1144
+ <span class="w2ui-icon w2ui-icon-eye-dropper"></span>
1145
+ </div>
1146
+ </div>`
1147
+ return html
1148
+ }
1149
+
1150
+ // bind advanced tab controls
1151
+ initControls(overlay) {
1152
+ let initial // used for mouse events
1153
+ let self = this
1154
+ let options = overlay.options
1155
+ let color = options.color || overlay.tmp.initColor
1156
+ let rgb = w2utils.parseColor(color)
1157
+ if (rgb == null) {
1158
+ rgb = { r: 140, g: 150, b: 160, a: 1 }
1159
+ }
1160
+ let hsv = w2utils.rgb2hsv(rgb)
1161
+ if (options.advanced === true) {
1162
+ this.tabClick(2, overlay.name)
1163
+ }
1164
+ setColor(hsv, true, color ?? '') // should not be null or undefined
1165
+
1166
+ // even for rgb, hsv inputs
1167
+ query(overlay.box)
1168
+ .off('.w2color')
1169
+ .on('contextmenu.w2color', event => {
1170
+ event.preventDefault() // prevent browser context menu
1171
+ })
1172
+ .find('input')
1173
+ .off('.w2color')
1174
+ .on('change.w2color', (event) => {
1175
+ let el = query(event.target)
1176
+ let val = parseFloat(el.val())
1177
+ let max = parseFloat(el.attr('max'))
1178
+ if (isNaN(val)) {
1179
+ val = 0
1180
+ el.val(0)
1181
+ }
1182
+ if (max > 1) val = parseInt(val) // trancate fractions
1183
+ if (max > 0 && val > max) {
1184
+ el.val(max)
1185
+ val = max
1186
+ }
1187
+ if (val < 0) {
1188
+ el.val(0)
1189
+ val = 0
1190
+ }
1191
+ let name = el.attr('name')
1192
+ let color = {}
1193
+ if (['r', 'g', 'b', 'a'].indexOf(name) !== -1) {
1194
+ rgb[name] = val
1195
+ hsv = w2utils.rgb2hsv(rgb)
1196
+ } else if (['h', 's', 'v'].indexOf(name) !== -1) {
1197
+ color[name] = val
1198
+ }
1199
+ setColor(color, true)
1200
+ })
1201
+
1202
+ // click on original color resets it
1203
+ query(overlay.box).find('.color-original')
1204
+ .off('.w2color')
1205
+ .on('click.w2color', (event) => {
1206
+ let tmp = w2utils.parseColor(query(event.target).css('background-color'))
1207
+ if (tmp != null) {
1208
+ rgb = tmp
1209
+ hsv = w2utils.rgb2hsv(rgb)
1210
+ setColor(hsv, true)
1211
+ }
1212
+ })
1213
+
1214
+ // color sliders events
1215
+ let mDown = `${!w2utils.isMobile ? 'mousedown' : 'touchstart'}.w2color`
1216
+ let mUp = `${!w2utils.isMobile ? 'mouseup' : 'touchend'}.w2color`
1217
+ let mMove = `${!w2utils.isMobile ? 'mousemove' : 'touchmove'}.w2color`
1218
+ query(overlay.box).find('.palette, .rainbow, .alpha')
1219
+ .off('.w2color')
1220
+ .on(`${mDown}.w2color`, mouseDown)
1221
+
1222
+ this.setColor = setColor
1223
+ return
1224
+
1225
+ function setColor(color, fullUpdate, initial) {
1226
+ if (color.h != null) hsv.h = color.h
1227
+ if (color.s != null) hsv.s = color.s
1228
+ if (color.v != null) hsv.v = color.v
1229
+ if (color.a != null) { rgb.a = color.a; hsv.a = color.a }
1230
+ rgb = w2utils.hsv2rgb(hsv)
1231
+ let rgba = 'rgba('+ rgb.r +','+ rgb.g +','+ rgb.b +','+ rgb.a +')'
1232
+ let cl = [
1233
+ Number(rgb.r).toString(16).toUpperCase(),
1234
+ Number(rgb.g).toString(16).toUpperCase(),
1235
+ Number(rgb.b).toString(16).toUpperCase(),
1236
+ (Math.round(Number(rgb.a)*255)).toString(16).toUpperCase()
1237
+ ]
1238
+ cl.forEach((item, ind) => { if (item.length === 1) cl[ind] = '0' + item })
1239
+ let newColor = cl[0] + cl[1] + cl[2] + cl[3]
1240
+ if (rgb.a === 1) {
1241
+ newColor = cl[0] + cl[1] + cl[2]
1242
+ }
1243
+ query(overlay.box).find('.color-preview').css('background-color', '#' + newColor)
1244
+ query(overlay.box).find('input').each(el => {
1245
+ if (el.name) {
1246
+ if (rgb[el.name] != null) el.value = rgb[el.name]
1247
+ if (hsv[el.name] != null) el.value = hsv[el.name]
1248
+ if (el.name === 'a') el.value = rgb.a
1249
+ if (el.name == 'hex') el.value = newColor
1250
+ if (el.name == 'rgb') el.value = rgba
1251
+ }
1252
+ })
1253
+ // if it is in pallette
1254
+ if (initial != null) {
1255
+ let color = overlay.tmp.initColor || newColor
1256
+ query(overlay.box).find('.color-original')
1257
+ .css('background-color', '#' + color)
1258
+ query(overlay.box).find('.w2ui-color.w2ui-selected')
1259
+ .removeClass('w2ui-selected')
1260
+ query(overlay.box).find(`.w2ui-colors [name="${color}"], .w2ui-colors [name="${initial}"]`) // color conversion might be slightly off
1261
+ .addClass('w2ui-selected')
1262
+ // if has transparent color, open advanced tab
1263
+ if (newColor.length == 8) {
1264
+ self.tabClick(2, overlay.name)
1265
+ }
1266
+ } else {
1267
+ self.select(newColor, overlay.name)
1268
+ }
1269
+ if (fullUpdate) {
1270
+ updateSliders()
1271
+ refreshPalette()
1272
+ }
1273
+ }
1274
+
1275
+ function updateSliders() {
1276
+ let el1 = query(overlay.box).find('.palette .value1')
1277
+ let el2 = query(overlay.box).find('.rainbow .value2')
1278
+ let el3 = query(overlay.box).find('.alpha .value2')
1279
+ if (!el1[0] || !el2[0] || !el3[0]) return
1280
+ let offset1 = parseInt(el1[0].clientWidth) / 2
1281
+ let offset2 = parseInt(el2[0].clientWidth) / 2
1282
+ el1.css({
1283
+ 'left': (hsv.s * 150 / 100 - offset1) + 'px',
1284
+ 'top': ((100 - hsv.v) * 125 / 100 - offset1) + 'px'
1285
+ })
1286
+ el2.css('left', (hsv.h/(360/150) - offset2) + 'px')
1287
+ el3.css('left', (rgb.a*150 - offset2) + 'px')
1288
+ }
1289
+
1290
+ function refreshPalette() {
1291
+ let cl = w2utils.hsv2rgb(hsv.h, 100, 100)
1292
+ let rgb = `${cl.r},${cl.g},${cl.b}`
1293
+ query(overlay.box).find('.palette')
1294
+ .css('background-image', `linear-gradient(90deg, rgba(${rgb},0) 0%, rgba(${rgb},1) 100%)`)
1295
+ }
1296
+
1297
+ function mouseDown(event) {
1298
+ let el = query(this).find('.value1, .value2')
1299
+ let offset = parseInt(el.prop('clientWidth')) / 2
1300
+ if (el.hasClass('move-x')) el.css({ left: (event.offsetX - offset) + 'px' })
1301
+ if (el.hasClass('move-y')) el.css({ top: (event.offsetY - offset) + 'px' })
1302
+ initial = {
1303
+ el: el,
1304
+ x: event.pageX,
1305
+ y: event.pageY,
1306
+ width: el.prop('parentNode').clientWidth,
1307
+ height: el.prop('parentNode').clientHeight,
1308
+ left: parseInt(el.css('left')),
1309
+ top: parseInt(el.css('top'))
1310
+ }
1311
+ mouseMove(event)
1312
+ query('html')
1313
+ .off('.w2color')
1314
+ .on(mMove, mouseMove)
1315
+ .on(mUp, mouseUp)
1316
+ }
1317
+
1318
+ function mouseUp(event) {
1319
+ query('html').off('.w2color')
1320
+ }
1321
+
1322
+ function mouseMove(event) {
1323
+ let el = initial.el
1324
+ let divX = event.pageX - initial.x
1325
+ let divY = event.pageY - initial.y
1326
+ let newX = initial.left + divX
1327
+ let newY = initial.top + divY
1328
+ let offset = parseInt(el.prop('clientWidth')) / 2
1329
+ if (newX < -offset) newX = -offset
1330
+ if (newY < -offset) newY = -offset
1331
+ if (newX > initial.width - offset) newX = initial.width - offset
1332
+ if (newY > initial.height - offset) newY = initial.height - offset
1333
+ if (el.hasClass('move-x')) el.css({ left : newX + 'px' })
1334
+ if (el.hasClass('move-y')) el.css({ top : newY + 'px' })
1335
+
1336
+ // move
1337
+ let name = query(el.get(0).parentNode).attr('name')
1338
+ let x = parseInt(el.css('left')) + offset
1339
+ let y = parseInt(el.css('top')) + offset
1340
+ if (name === 'palette') {
1341
+ setColor({
1342
+ s: Math.round(x / initial.width * 100),
1343
+ v: Math.round(100 - (y / initial.height * 100))
1344
+ })
1345
+ }
1346
+ if (name === 'rainbow') {
1347
+ let h = Math.round(360 / 150 * x)
1348
+ setColor({ h: h })
1349
+ refreshPalette()
1350
+ }
1351
+ if (name === 'alpha') {
1352
+ setColor({ a: parseFloat(Number(x / 150).toFixed(2)) })
1353
+ }
1354
+ }
1355
+ }
1356
+
1357
+ addCustomColor(color, name) {
1358
+ if (typeof color == 'string' && color.substr(0, 1) == '#' && [7, 9].includes(color.length)) {
1359
+ color = color.substr(1).toUpperCase()
1360
+ let custom = ColorTooltip.custom_colors
1361
+ if (custom.includes(color)) {
1362
+ custom.splice(custom.indexOf(color), 1)
1363
+ }
1364
+ if (custom.length >= 5) {
1365
+ custom.pop() // removes last one
1366
+ }
1367
+ custom.unshift(color)
1368
+ }
1369
+ return color
1370
+ }
1371
+
1372
+ async pickAndSelect(name, event) {
1373
+ let color = await this.pickColor()
1374
+ if (typeof color == 'string' && color.substr(0, 1) == '#' && [7, 9].includes(color.length)) {
1375
+ this.addCustomColor(color, name)
1376
+ let cnt = query(event.target).closest('.w2ui-colors-custom')
1377
+ cnt.html(this.getCustomColorsHTML(name))
1378
+ w2utils.bindEvents(cnt.find('.w2ui-eaction'), this)
1379
+ this.select(color.substr(1), name)
1380
+ // this.hide(name)
1381
+ }
1382
+ }
1383
+
1384
+ async pickAndUse(name) {
1385
+ let color = await this.pickColor()
1386
+ if (typeof color == 'string' && color.substr(0, 1) == '#' && [7, 9].includes(color.length)) {
1387
+ let hsv = w2utils.rgb2hsv(w2utils.parseColor(color))
1388
+ this.setColor(hsv, true)
1389
+ }
1390
+ }
1391
+
1392
+ async pickColor() {
1393
+ if (!window.EyeDropper) {
1394
+ console.error('EyeDropper API is not supported in this browser.')
1395
+ return
1396
+ }
1397
+ let eyeDropper = new EyeDropper()
1398
+ try {
1399
+ const result = await eyeDropper.open()
1400
+ return result.sRGBHex
1401
+ } catch (err) {
1402
+ console.error('Error picking color:', err)
1403
+ }
1404
+ return ''
1405
+ }
1406
+ }
1407
+
1408
+ class MenuTooltip extends Tooltip {
1409
+ constructor() {
1410
+ super()
1411
+ // ITEM STRUCTURE
1412
+ // item : {
1413
+ // id : null,
1414
+ // text : '',
1415
+ // style : '',
1416
+ // icon : '',
1417
+ // count : '',
1418
+ // tooltip : '',
1419
+ // hotkey : '',
1420
+ // removable: false,
1421
+ // help : '', // text for help tooltip
1422
+ // hotkey ; '', // hotkey text for the items
1423
+ // items : []
1424
+ // indent : 0,
1425
+ // type : null, // check/radio
1426
+ // group : false, // groupping for checks
1427
+ // expanded : false,
1428
+ // hidden : false,
1429
+ // checked : null,
1430
+ // disabled : false
1431
+ // ...
1432
+ // }
1433
+ this.defaults = w2utils.extend({}, this.defaults, {
1434
+ type : 'normal', // can be normal, radio, check
1435
+ items : [],
1436
+ selected : null, // current selected
1437
+ render : null,
1438
+ spinner : false,
1439
+ msgNoItems : w2utils.lang('No items found'),
1440
+ topHTML : '',
1441
+ menuStyle : '',
1442
+ search : false, // search input inside tooltip
1443
+ filter : false, // will apply filter, if anchor is INPUT or TEXTAREA
1444
+ match : 'contains', // is, begins, ends, contains
1445
+ markSearch : false,
1446
+ prefilter : false,
1447
+ altRows : false,
1448
+ arrowSize : 10,
1449
+ align : 'left',
1450
+ position : 'bottom|top',
1451
+ class : 'w2ui-white',
1452
+ anchorClass : 'w2ui-focus',
1453
+ autoShowOn : 'focus',
1454
+ hideOn : ['doc-click', 'focus-change', 'select'], // also can 'item-remove'
1455
+ onSelect : null,
1456
+ onSubMenu : null,
1457
+ onRemove : null,
1458
+ onTooltip : null,
1459
+ onMouseEnter: null,
1460
+ onMouseLeave: null
1461
+ })
1462
+ }
1463
+
1464
+ attach(anchor, text) {
1465
+ let options
1466
+ if (arguments.length == 1 && anchor instanceof Object) {
1467
+ options = anchor
1468
+ anchor = options.anchor
1469
+ } else if (arguments.length === 2 && text != null && typeof text === 'object') {
1470
+ options = text
1471
+ options.anchor = anchor
1472
+ }
1473
+ let prevHideOn = options.hideOn
1474
+ options = w2utils.extend({}, this.defaults, options || {})
1475
+ if (prevHideOn) {
1476
+ options.hideOn = prevHideOn
1477
+ }
1478
+ options.style += '; padding: 0;'
1479
+ if (options.items == null) {
1480
+ options.items = []
1481
+ }
1482
+ if (options.cacheMax <= 0) {
1483
+ console.log(`The option "cacheMax" is ${options.cacheMax} but should be more than 0`)
1484
+ }
1485
+ options.items = w2utils.normMenu(options.items, options)
1486
+ options.html = this.getMenuHTML(options)
1487
+ let ret = super.attach(options)
1488
+ let overlay = ret.overlay
1489
+ overlay.on('show:after.attach, update:after.attach', event => {
1490
+ if (ret.overlay?.box) {
1491
+ let search = ''
1492
+ // reset selected and active chain
1493
+ overlay.selected = overlay.options.selected // this is needed so that menu item can be preselected
1494
+ let index = overlay.anchor.dataset?.selectedIndex
1495
+ if ((overlay.options.selected !== false && overlay.options.selected !== -1) || index != null) {
1496
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
1497
+ search = overlay.anchor.value
1498
+ overlay.selected = null // no element should be pre-selected
1499
+ if (index != null) {
1500
+ overlay.selected = index
1501
+ }
1502
+ }
1503
+ }
1504
+ let actions = query(ret.overlay.box).find('.w2ui-eaction')
1505
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
1506
+ overlay.tmp._new_search = false
1507
+ query(overlay.anchor).on('input.search-trigger', () => {
1508
+ overlay.tmp._new_search = true
1509
+ query(overlay.anchor).off('input.search-trigger')
1510
+ })
1511
+ }
1512
+ w2utils.bindEvents(actions, this)
1513
+ this.applyFilter(overlay.name, null, search)
1514
+ .then(data => {
1515
+ if (!Tooltip.active[overlay.name].displayed) {
1516
+ // if toolitp is not visible, do not proceed as it would make it visible
1517
+ return
1518
+ }
1519
+ this.getActiveChain(overlay.name, options.items) // need this to update chain for up/down key navigation
1520
+ overlay.tmp.searchCount = data.count
1521
+ overlay.tmp.search = data.search
1522
+ if (options.prefilter || search !== '') {
1523
+ // if selected is not in searched items
1524
+ if (data.count === 0 || !this.getActiveChain(overlay.name, options.items).includes(overlay.selected)) {
1525
+ overlay.selected = null
1526
+ }
1527
+ this.refreshSearch(overlay.name)
1528
+ }
1529
+ this.initControls(ret.overlay)
1530
+ this.refreshIndex(overlay.name, true)
1531
+ })
1532
+ }
1533
+ })
1534
+ overlay.next = () => {
1535
+ let chain = this.getActiveChain(overlay.name)
1536
+ if (overlay.selected == null || overlay.selected?.length == 0) {
1537
+ overlay.selected = chain[0]
1538
+ } else {
1539
+ let ind = chain.indexOf(String(overlay.selected)) // if nested menu, selected will be "2-2"
1540
+ // selected not in chain of items
1541
+ if (ind == -1) {
1542
+ overlay.selected = chain[0]
1543
+ }
1544
+ // not the last item
1545
+ if (ind < chain.length - 1) {
1546
+ overlay.selected = chain[ind + 1]
1547
+ }
1548
+ }
1549
+ this.refreshIndex(overlay.name)
1550
+ this.showTooltip(overlay.name)
1551
+ }
1552
+ overlay.prev = () => {
1553
+ let chain = this.getActiveChain(overlay.name)
1554
+ if (overlay.selected == null || overlay.selected?.length == 0) {
1555
+ overlay.selected = chain[chain.length-1]
1556
+ } else {
1557
+ let ind = chain.indexOf(String(overlay.selected)) // if nested menu, selected will be "2-2"
1558
+ // selected not in chain of items
1559
+ if (ind == -1) {
1560
+ overlay.selected = chain[chain.length-1]
1561
+ }
1562
+ // not first item
1563
+ if (ind > 0) {
1564
+ overlay.selected = chain[ind - 1]
1565
+ }
1566
+ }
1567
+ this.refreshIndex(overlay.name)
1568
+ this.showTooltip(overlay.name)
1569
+ }
1570
+ overlay.click = () => {
1571
+ $(overlay.box).find('.w2ui-selected').click()
1572
+ }
1573
+ overlay.on('hide:after.attach', event => {
1574
+ w2tooltip.hide(overlay.name + '-tooltip')
1575
+ })
1576
+ ret.select = (callback) => {
1577
+ overlay.on('select.attach', (event) => { callback(event) })
1578
+ return ret
1579
+ }
1580
+ ret.remove = (callback) => {
1581
+ overlay.on('remove.attach', (event) => { callback(event) })
1582
+ return ret
1583
+ }
1584
+ ret.subMenu = (callback) => {
1585
+ overlay.on('subMenu.attach', (event) => { callback(event) })
1586
+ return ret
1587
+ }
1588
+ return ret
1589
+ }
1590
+
1591
+ update(name, items) {
1592
+ let overlay = Tooltip.active[name]
1593
+ if (overlay) {
1594
+ let options = overlay.options
1595
+ if (options.items != items) {
1596
+ options.items = items
1597
+ }
1598
+ let menuHTML = this.getMenuHTML(options)
1599
+ if (options.html != menuHTML) {
1600
+ options.html = menuHTML
1601
+ overlay.needsUpdate = true
1602
+ this.show(name)
1603
+ }
1604
+ } else {
1605
+ console.log(`Tooltip "${name}" is not displayed. Cannot update it.`)
1606
+ }
1607
+ }
1608
+
1609
+ initControls(overlay) {
1610
+ let mdown = 'mousedown'
1611
+ let mclick = 'click'
1612
+ if (w2utils.isMobile) {
1613
+ mdown = 'touchstart'
1614
+ mclick = 'touchend'
1615
+ }
1616
+ query(overlay.box).find('.w2ui-menu:not(.w2ui-sub-menu)')
1617
+ .off('.w2menu')
1618
+ .on('contextmenu.w2menu', event => {
1619
+ event.preventDefault() // prevent browser context menu
1620
+ })
1621
+ .on(`${mdown}.w2menu`, { delegate: '.w2ui-menu-item' }, event => {
1622
+ let dt = event.delegate.dataset
1623
+ this.menuDown(overlay, event, dt.index, dt.parents)
1624
+ if (w2utils.isMobile) {
1625
+ // need it for mobile so that it would not generate onclick (items under menu receive focus)
1626
+ event.preventDefault()
1627
+ }
1628
+ })
1629
+ .on(`${mclick}.w2menu`, { delegate: '.w2ui-menu-item' }, event => {
1630
+ let dt = event.delegate.dataset
1631
+ this.menuClick(overlay, event, parseInt(dt.index), dt.parents)
1632
+ })
1633
+ .find('.w2ui-menu-item')
1634
+ .off('.w2menu')
1635
+ .on('mouseEnter.w2menu', event => {
1636
+ let dt = event.target.dataset
1637
+ let item = overlay.options.items[dt.index]
1638
+ let edata = this.trigger('mouseEnter', { overlay, item, originalEvent: event })
1639
+ if (edata.isCancelled) {
1640
+ return
1641
+ }
1642
+ let tooltip = item?.tooltip
1643
+ if (tooltip && dt.hassubmenu != 'yes') {
1644
+ this.showTooltip(overlay.name, { tooltip, anchor: event.target })
1645
+ }
1646
+ // hide previous sub-menu if any
1647
+ let _menu = query(event.target).closest('.w2ui-menu').get(0)
1648
+ if (_menu._evt && _menu._evt.target != event.target) {
1649
+ this.closeSubMenu(_menu._evt)
1650
+ }
1651
+ // show new sub-menu
1652
+ if (dt.hassubmenu == 'yes') {
1653
+ let _evt = {
1654
+ index: parseInt(dt.index),
1655
+ parents: _menu.dataset.parents !== '' ? _menu.dataset.parents.split('-').map(ind => parseInt(ind)) : [],
1656
+ target: event.target,
1657
+ originalEvent: event,
1658
+ overlay
1659
+ }
1660
+ _menu._evt = _evt
1661
+ this.openSubMenu(_evt)
1662
+ }
1663
+ edata.finish()
1664
+ })
1665
+ .on('mouseLeave.w2menu', event => {
1666
+ let dt = event.target.dataset
1667
+ let item = overlay.options.items[dt.index]
1668
+ let edata = this.trigger('mouseLeave', { overlay, item, originalEvent: event })
1669
+ if (edata.isCancelled) {
1670
+ return
1671
+ }
1672
+ w2tooltip.hide(overlay.name + '-tooltip')
1673
+ edata.finish()
1674
+ })
1675
+ .find('.menu-help')
1676
+ .off('.w2menu')
1677
+ .on('mouseEnter.w2menu', event => {
1678
+ let dt = event.target.parentNode.parentNode.dataset
1679
+ let tooltip = overlay.options.items[dt.index]?.help
1680
+ if (tooltip) {
1681
+ w2tooltip.show({
1682
+ name: overlay.name + '-help-tp',
1683
+ anchor: event.target,
1684
+ html: tooltip,
1685
+ position: 'right|left',
1686
+ hideOn: ['doc-click']
1687
+ })
1688
+ }
1689
+ })
1690
+ .on('mouseLeave.w2menu', event => {
1691
+ w2tooltip.hide(overlay.name + '-help-tp')
1692
+ })
1693
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
1694
+ query(overlay.anchor)
1695
+ .off('.w2menu')
1696
+ .on('input.w2menu', event => {
1697
+ // if user types, clear selection
1698
+ // let dt = event.target.dataset
1699
+ // delete dt.selected
1700
+ // delete dt.selectedIndex
1701
+ })
1702
+ .on('keyup.w2menu', event => {
1703
+ event._searchType = 'filter'
1704
+ this.keyUp(overlay, event)
1705
+ })
1706
+ }
1707
+ if (overlay.options.search) {
1708
+ query(overlay.box).find('#menu-search')
1709
+ .off('.w2menu')
1710
+ .on('keyup.w2menu', event => {
1711
+ event._searchType = 'search'
1712
+ this.keyUp(overlay, event)
1713
+ })
1714
+ }
1715
+ }
1716
+
1717
+ getCurrent(name, id) {
1718
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
1719
+ let options = overlay.options
1720
+ let selected = String(id ? id : (overlay.selected || '')).split('-')
1721
+ if (selected?.[0] === '') {
1722
+ selected.shift()
1723
+ }
1724
+ let last = selected.length - 1
1725
+ let index = selected[last]
1726
+ let parents = selected.slice(0, selected.length - 1).join('-')
1727
+ index = w2utils.isInt(index) ? parseInt(index) : 0
1728
+ // items
1729
+ let items = options.items
1730
+ selected.forEach((id, ind) => {
1731
+ // do not go to the last one
1732
+ if (ind < selected.length - 1) {
1733
+ items = items[id].items
1734
+ }
1735
+ })
1736
+ return { last, index, items, item: items[index], parents }
1737
+ }
1738
+
1739
+ getMenuHTML(options) {
1740
+ if (options.spinner) {
1741
+ return `
1742
+ <div class="w2ui-menu">
1743
+ <div class="w2ui-no-items">
1744
+ <div class="w2ui-spinner"></div>
1745
+ ${w2utils.lang('Loading...')}
1746
+ </div>
1747
+ </div>`
1748
+ }
1749
+ let parents = options.parents ?? []
1750
+ let items = options.items
1751
+ if (!Array.isArray(items)) items = []
1752
+ let count = 0
1753
+ let icon = null
1754
+ let topHTML = ''
1755
+ if (options.search) {
1756
+ topHTML += `
1757
+ <div class="w2ui-menu-search">
1758
+ <span class="w2ui-icon w2ui-icon-search"></span>
1759
+ <input id="menu-search" class="w2ui-input" type="text"/>
1760
+ </div>`
1761
+ items.forEach(item => item.hidden = false)
1762
+ }
1763
+ if (options.topHTML) {
1764
+ topHTML += `<div class="w2ui-menu-top">${options.topHTML}</div>`
1765
+ }
1766
+ let menu_html = `
1767
+ ${topHTML}
1768
+ <div class="w2ui-menu" style="${options.menuStyle}" data-parents="${parents.join('-')}">
1769
+ `
1770
+ items.forEach((mitem, f) => {
1771
+ icon = mitem.icon
1772
+ let index = (parents.length > 0 ? parents.join('-') + '-' : '') + f
1773
+ if (icon == null) icon = null // icon might be undefined
1774
+ if (['radio', 'check'].includes(options.type) && !Array.isArray(mitem.items) && mitem.group !== false) {
1775
+ if (mitem.checked === true) icon = 'w2ui-icon-check'; else icon = 'w2ui-icon-empty'
1776
+ }
1777
+ if (mitem.hidden !== true) {
1778
+ let txt = mitem.text
1779
+ let icon_dsp = ''
1780
+ if (typeof options.render === 'function') txt = options.render(mitem, options)
1781
+ if (typeof txt == 'function') txt = txt(mitem, options)
1782
+ if (icon) {
1783
+ let first = String(icon).trim().slice(0, 1)
1784
+ if (first == '#') {
1785
+ icon = `<span class="w2ui-icon w2ui-icon-empty" style="background-color: ${icon}"></span>`
1786
+ } else if (first !== '<') {
1787
+ icon = `<span class="w2ui-icon ${icon}"></span>`
1788
+ }
1789
+ icon_dsp = `<div class="menu-icon">${icon}</div>`
1790
+ }
1791
+ // for backward compatibility
1792
+ if (mitem.removable == null && mitem.remove != null) {
1793
+ mitem.removable = mitem.remove
1794
+ }
1795
+ // render only if non-empty
1796
+ if (mitem.type !== 'break' && txt != null && txt !== '' && String(txt).substr(0, 2) != '--') {
1797
+ let classes = ['w2ui-menu-item']
1798
+ if (options.altRows == true) {
1799
+ classes.push(count % 2 === 0 ? 'w2ui-even' : 'w2ui-odd')
1800
+ }
1801
+ let colspan = 1
1802
+ if (icon_dsp === '') colspan++
1803
+ if (mitem.count == null && mitem.hotkey == null && mitem.removable !== true && mitem.items == null) colspan++
1804
+ if (mitem.tooltip == null && mitem.hint != null) mitem.tooltip = mitem.hint // for backward compatibility
1805
+ let count_dsp = ''
1806
+ if (mitem.removable === true) {
1807
+ count_dsp = '<span class="menu-remove">x</span>'
1808
+ } else if (mitem.items != null) {
1809
+ classes.push('has-sub-menu')
1810
+ count_dsp = '<span style="background-color: transparent; border: transparent; box-shadow: none;"></span>' // used as drop arrow
1811
+ } else {
1812
+ if (mitem.count != null) count_dsp += '<span>' + mitem.count + '</span>'
1813
+ if (mitem.hotkey != null) count_dsp += '<span class="menu-hotkey">' + mitem.hotkey + '</span>'
1814
+ if (mitem.help != null) count_dsp += '<span class="menu-help">?</span>'
1815
+ }
1816
+ if (mitem.disabled === true) classes.push('w2ui-disabled')
1817
+ if (mitem._noSearchInside === true) classes.push('w2ui-no-search-inside')
1818
+ menu_html += `
1819
+ <div index="${index}" class="${classes.join(' ')}" style="${mitem.style ? mitem.style : ''}"
1820
+ data-index="${f}" data-hasSubmenu="${mitem.items != null ? 'yes' : ''}">
1821
+ <div style="width: ${parseInt(mitem.indent ?? 0)}px"></div>
1822
+ ${icon_dsp}
1823
+ <div class="menu-text" colspan="${colspan}">${w2utils.lang(txt)}</div>
1824
+ <div class="menu-extra">${mitem.extra ?? ''}${count_dsp}</div>
1825
+ </div>`
1826
+ count++
1827
+ } else {
1828
+ // horizontal line
1829
+ let divText = (txt ?? '').replace(/^-+/g, '')
1830
+ menu_html += `
1831
+ <div index="${index}" class="w2ui-menu-divider ${divText != '' ? 'has-text' : ''}">
1832
+ <div class="line"></div>
1833
+ ${divText ? `<div class="text">${divText}</div>` : ''}
1834
+ </div>`
1835
+ }
1836
+ }
1837
+ items[f] = mitem
1838
+ })
1839
+ if (count === 0 && options.msgNoItems) {
1840
+ let overlay = Tooltip.active[options.name.replace(/[\s\.#]/g, '_')]
1841
+ let remote = overlay?.tmp.remote
1842
+ let msg = options.msgNoItems
1843
+ if (options.url) {
1844
+ if (count == 0 && remote?.hasMore === false) {
1845
+ // if search is applied, but there are no items
1846
+ msg = options.msgNoItems
1847
+ } else {
1848
+ msg = options.msgSearch
1849
+ }
1850
+ }
1851
+ menu_html += `
1852
+ <div class="w2ui-no-items">
1853
+ ${w2utils.lang(msg)}
1854
+ </div>`
1855
+ }
1856
+ menu_html += '</div>'
1857
+ return menu_html
1858
+ }
1859
+
1860
+ openSubMenu(event) {
1861
+ let anchor = query(event.originalEvent.target).get(0)
1862
+ let { overlay } = event
1863
+ let { items } = overlay.options
1864
+ // build sub-items list
1865
+ let mitem = items[event.index]
1866
+ let _items = []
1867
+ if (typeof mitem.items == 'function') {
1868
+ _items = mitem.items(mitem)
1869
+ } else if (Array.isArray(mitem.items)) {
1870
+ _items = mitem.items
1871
+ }
1872
+ let prev = w2menu.get(overlay.name + '-submenu')
1873
+ if (prev) {
1874
+ prev.hide()
1875
+ }
1876
+ query(event.target).addClass('expanded')
1877
+ w2menu.show({
1878
+ name: overlay.name + '-submenu',
1879
+ anchor: anchor,
1880
+ items: _items,
1881
+ class: overlay.options.class + ' ' + mitem.overlay?.class,
1882
+ offsetX: -7,
1883
+ arrowSize: 0,
1884
+ parentOverlay: overlay,
1885
+ parents: [...event.parents, event.index],
1886
+ position: 'right|left',
1887
+ hideOn: ['doc-click', 'select']
1888
+ })
1889
+ .hide(evt => {
1890
+ query(event.target).removeClass('expanded')
1891
+ })
1892
+ // indicates if user cursor is over sub menu
1893
+ setTimeout(() => {
1894
+ query('#w2overlay-' + overlay.name + '-submenu')
1895
+ .on('mouseenter', event => { event.target._keepSubOpen = true })
1896
+ .on('mouseleave', event => { event.target._keepSubOpen = false })
1897
+ }, 10)
1898
+ }
1899
+
1900
+ closeSubMenu(event) {
1901
+ let { overlay } = event
1902
+ if (event.target._keepSubOpen !== true) {
1903
+ let prev = w2menu.get(overlay.name + '-submenu')
1904
+ if (prev) {
1905
+ prev.hide()
1906
+ }
1907
+ }
1908
+ }
1909
+
1910
+ // Refreshed only selected item highligh, used in keyboard navigation
1911
+ refreshIndex(name, instant) {
1912
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
1913
+ if (!overlay) return
1914
+ if (!overlay.displayed) {
1915
+ this.show(overlay.name)
1916
+ }
1917
+ let view = query(overlay.box).find('.w2ui-overlay-body').get(0)
1918
+ let search = query(overlay.box).find('.w2ui-menu-search, .w2ui-menu-top').get(0)
1919
+ query(overlay.box).find('.w2ui-menu-item.w2ui-selected')
1920
+ .removeClass('w2ui-selected')
1921
+ let el = query(overlay.box).find(`.w2ui-menu-item[index="${overlay.selected}"]`)
1922
+ .addClass('w2ui-selected')
1923
+ .get(0)
1924
+ if (el) {
1925
+ if (el.offsetTop + el.clientHeight > view.clientHeight + view.scrollTop) {
1926
+ el.scrollIntoView({
1927
+ behavior: instant ? 'instant' : 'smooth',
1928
+ block: instant ? 'center' : 'start',
1929
+ inline: instant ? 'center' : 'start'
1930
+ })
1931
+ }
1932
+ if (el.offsetTop < view.scrollTop + (search ? search.clientHeight : 0)) {
1933
+ el.scrollIntoView({
1934
+ behavior: instant ? 'instant' : 'smooth',
1935
+ block: instant ? 'center' : 'end',
1936
+ inline: instant ? 'center' : 'end'
1937
+ })
1938
+ }
1939
+ }
1940
+ w2tooltip.hide(overlay.name + '-tooltip')
1941
+ }
1942
+
1943
+ showTooltip(name, options) {
1944
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
1945
+ if (!overlay || !overlay.displayed) return
1946
+ let anchor = options?.anchor ?? query(overlay.box).find(`.w2ui-menu-item[index="${overlay.selected}"]`).get(0)
1947
+ let tooltip = options?.tooltip ?? overlay.options.items[overlay.selected]?.tooltip
1948
+ if (tooltip) {
1949
+ let html = tooltip.html ?? tooltip
1950
+ w2tooltip.show(Object.assign({
1951
+ name: overlay.name + '-tooltip',
1952
+ anchor,
1953
+ html,
1954
+ position: 'right|left',
1955
+ hideOn: ['doc-click'],
1956
+ onShow(event) {
1957
+ overlay.self.trigger('tooltip', { overlay, action: 'show', originalEvent: event })
1958
+ },
1959
+ onHide(event) {
1960
+ overlay.self.trigger('tooltip', { overlay, action: 'hide', originalEvent: event })
1961
+ }
1962
+ }, typeof tooltip == 'object' && tooltip != null ? tooltip : {}))
1963
+ }
1964
+ }
1965
+
1966
+ // show/hide searched items
1967
+ refreshSearch(name) {
1968
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
1969
+ if (!overlay) return
1970
+ if (!overlay.displayed) {
1971
+ this.show(overlay.name)
1972
+ }
1973
+ w2tooltip.hide(overlay.name + '-tooltip')
1974
+ query(overlay.box).find('.w2ui-no-items').hide()
1975
+ query(overlay.box).find('.w2ui-menu-item, .w2ui-menu-divider').each(el => {
1976
+ let cur = this.getCurrent(name, el.getAttribute('index'))
1977
+ if (cur.item?.hidden) {
1978
+ query(el).hide()
1979
+ } else {
1980
+ let search = overlay.tmp?.search
1981
+ if (overlay.options.markSearch) {
1982
+ w2utils.marker(el, search, { onlyFirst: overlay.options.match == 'begins' })
1983
+ }
1984
+ query(el).show()
1985
+ }
1986
+ })
1987
+ // hide empty menus
1988
+ query(overlay.box).find('.w2ui-sub-menu').each(sub => {
1989
+ let hasItems = query(sub).find('.w2ui-menu-item').get().some(el => {
1990
+ return el.style.display != 'none' ? true : false
1991
+ })
1992
+ let parent = this.getCurrent(name, sub.dataset.parent)
1993
+ // only if parent is expaneded
1994
+ if (parent.item.expanded) {
1995
+ if (!hasItems) {
1996
+ query(sub).parent().hide()
1997
+ } else {
1998
+ query(sub).parent().show()
1999
+ }
2000
+ }
2001
+ })
2002
+ // show empty message
2003
+ if (overlay.tmp.searchCount == 0 || overlay.options?.items?.length == 0) {
2004
+ if (query(overlay.box).find('.w2ui-no-items').length == 0) {
2005
+ query(overlay.box).find('.w2ui-menu:not(.w2ui-sub-menu)').append(`
2006
+ <div class="w2ui-no-items">
2007
+ ${w2utils.lang(overlay.options.msgNoItems)}
2008
+ </div>`)
2009
+ }
2010
+ query(overlay.box).find('.w2ui-no-items').show()
2011
+ }
2012
+ }
2013
+
2014
+ /**
2015
+ * Loops through the items and markes item.hidden = true for those that need to be hidden, and item.hidden = false
2016
+ * for those that are visible. Return a promise (since items can be on the server) with the number of visible items.
2017
+ */
2018
+ applyFilter(name, items, search, debounce) {
2019
+ let count = 0
2020
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
2021
+ let options = overlay.options
2022
+ let resolve, reject
2023
+ let prom = new Promise((res, rej) => {
2024
+ resolve = res
2025
+ reject = rej
2026
+ })
2027
+ if (overlay.tmp._skip_filter === true) {
2028
+ return prom
2029
+ }
2030
+ if (search == null) {
2031
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
2032
+ search = overlay.anchor.value
2033
+ } else {
2034
+ search = ''
2035
+ }
2036
+ }
2037
+ /**
2038
+ * If it is input control that has a menu, and if filter is true, then show all the items unfiltered, unless user
2039
+ * starts filtering again.
2040
+ */
2041
+ if (overlay.tmp._new_search === false) {
2042
+ search = ''
2043
+ }
2044
+ let selectedIds = []
2045
+ if (options.selected) {
2046
+ if (Array.isArray(options.selected)) {
2047
+ selectedIds = options.selected.map(item => {
2048
+ return item?.id ?? item
2049
+ })
2050
+ } else if (options.selected?.id) {
2051
+ selectedIds = [options.selected.id]
2052
+ }
2053
+ }
2054
+ overlay.tmp.activeChain = null
2055
+ // if url is defined, get items from it
2056
+ let remote = overlay.tmp.remote ?? { hasMore: true, emptySet: false, search: null, cached: -1 }
2057
+ if (remote.hasMore == false) {
2058
+ let len = remote.hasMore_search.length
2059
+ if (search.substr(0, len) != remote.hasMore_search) {
2060
+ remote.hasMore = true
2061
+ }
2062
+ }
2063
+ if (items == null && options.url && remote.hasMore && remote.search !== search) {
2064
+ let proceed = true
2065
+ // only when items == null because it is case of nested items
2066
+ let msg = w2utils.lang('Loading...')
2067
+ if (search.length < options.minLength && remote.emptySet !== true) {
2068
+ msg = w2utils.lang('${count} letters or more...', { count: options.minLength })
2069
+ proceed = false
2070
+ if (search === '') {
2071
+ msg = w2utils.lang(options.msgSearch)
2072
+ }
2073
+ // if there are items - then clear them
2074
+ if (options.items?.length > 0) {
2075
+ this.update(name, [])
2076
+ this.applyFilter(name, null, search)
2077
+ }
2078
+ }
2079
+ query(overlay.box).find('.w2ui-no-items').html(msg)
2080
+ remote.search = search
2081
+ options.items = []
2082
+ overlay.tmp.remote = remote
2083
+ if (proceed) {
2084
+ this.request(overlay, search, debounce)
2085
+ .then(remoteItems => {
2086
+ overlay.tmp._skip_filter = true
2087
+ this.update(name, remoteItems)
2088
+ delete overlay.tmp._skip_filter
2089
+ overlay.tmp._new_search = true
2090
+ this.applyFilter(name, remoteItems, search).then(data => {
2091
+ this.getActiveChain(overlay.name, options.items) // need this to update chain for up/down key navigation
2092
+ overlay.tmp.searchCount = data.count
2093
+ overlay.tmp.search = data.search
2094
+ if (options.prefilter || search !== '') {
2095
+ // if selected is not in searched items
2096
+ if (data.count === 0 || !this.getActiveChain(overlay.name, options.items).includes(overlay.selected)) {
2097
+ overlay.selected = null
2098
+ }
2099
+ this.refreshSearch(overlay.name)
2100
+ }
2101
+ this.initControls(overlay)
2102
+ this.refreshIndex(overlay.name, true)
2103
+
2104
+ resolve(data)
2105
+ })
2106
+ })
2107
+ .catch(error => {
2108
+ console.log('Server Request error', error)
2109
+ })
2110
+ }
2111
+ return prom
2112
+ }
2113
+ let edata
2114
+ // only trigger search event when data is present and for the top level
2115
+ if (items == null) {
2116
+ edata = this.trigger('search', { search, overlay, prom, resolve, reject })
2117
+ if (edata.isCancelled === true) {
2118
+ return prom
2119
+ }
2120
+ }
2121
+ if (items == null) {
2122
+ items = overlay.options.items
2123
+ }
2124
+ if (options.filter === false) {
2125
+ resolve({ count: -1, search })
2126
+ return prom
2127
+ }
2128
+ items.forEach(item => {
2129
+ let prefix = ''
2130
+ let suffix = ''
2131
+ if (['is', 'begins', 'begins with'].indexOf(options.match) !== -1) prefix = '^'
2132
+ if (['is', 'ends', 'ends with'].indexOf(options.match) !== -1) suffix = '$'
2133
+ try {
2134
+ let re = new RegExp(prefix + search + suffix, 'i')
2135
+ if (re.test(item.text) || item.text === '...') {
2136
+ item.hidden = false
2137
+ } else {
2138
+ item.hidden = true
2139
+ }
2140
+ } catch (e) {}
2141
+ // do not show selected items
2142
+ if (options.hideSelected && selectedIds.includes(item.id)) {
2143
+ item.hidden = true
2144
+ }
2145
+ // search nested items
2146
+ if (Array.isArray(item.items) && item.items.length > 0) {
2147
+ delete item._noSearchInside
2148
+ this.applyFilter(name, item.items, search).then(data => {
2149
+ let subCount = data.count
2150
+ if (subCount > 0) {
2151
+ count += subCount
2152
+ if (item.hidden) item._noSearchInside = true
2153
+ // only expand items if search is not empty
2154
+ if (search) item.expanded = true
2155
+ item.hidden = false
2156
+ }
2157
+ })
2158
+ }
2159
+ if (item.hidden !== true) count++
2160
+ })
2161
+ resolve({ count, search })
2162
+ edata?.finish()
2163
+ return prom
2164
+ }
2165
+
2166
+ request(overlay, search, debounce) {
2167
+ let options = overlay.options
2168
+ let remote = overlay.tmp.remote
2169
+ let resolve, reject // promise functions
2170
+ if ((options.items.length === 0 && remote.cached !== 0)
2171
+ || (remote.cached == options.cacheMax && search.length > remote.search.length)
2172
+ || (search.length >= remote.search.length && search.substr(0, remote.search.length) !== remote.search)
2173
+ || (search.length < remote.search.length))
2174
+ {
2175
+ // Aabort previous request if any
2176
+ if (remote.controller) {
2177
+ remote.controller.abort()
2178
+ }
2179
+ remote.loading = true
2180
+ clearTimeout(remote.timeout)
2181
+ remote.timeout = setTimeout(() => {
2182
+ let url = options.url
2183
+ let postData = { search, max: options.cacheMax }
2184
+ Object.assign(postData, options.postData)
2185
+ // trigger event
2186
+ let edata = this.trigger('request', {
2187
+ search, overlay, url, postData,
2188
+ httpMethod: options.method ?? 'GET',
2189
+ httpHeaders: {}
2190
+ })
2191
+ if (edata.isCancelled === true) return
2192
+ // if event updated url and postData, use it
2193
+ url = new URL(edata.detail.url, location)
2194
+ let fetchOptions = w2utils.prepareParams(url, {
2195
+ method: edata.detail.httpMethod,
2196
+ headers: edata.detail.httpHeaders,
2197
+ body: edata.detail.postData
2198
+ }, { caller: this, overlay, search })
2199
+ // Create new abort controller
2200
+ remote.controller = new AbortController()
2201
+ fetchOptions.signal = remote.controller.signal
2202
+ // send request
2203
+ fetch(url, fetchOptions)
2204
+ .then(resp => resp.json())
2205
+ .then(data => {
2206
+ remote.controller = null
2207
+ // trigger event
2208
+ let edata = overlay.trigger('load', { search: postData.search, overlay, data })
2209
+ if (edata.isCancelled === true) return
2210
+ // default behavior
2211
+ data = edata.detail.data
2212
+ if (typeof data === 'string') data = JSON.parse(data)
2213
+ // if server just returns array
2214
+ if (Array.isArray(data)) {
2215
+ data = { records: data }
2216
+ }
2217
+ // needed for backward compatibility
2218
+ if (data.records == null && data.items != null) {
2219
+ data.records = data.items
2220
+ delete data.items
2221
+ }
2222
+ // handles Golang marshal of empty arrays to null
2223
+ if (!data.error && data.records == null) {
2224
+ data.records = []
2225
+ }
2226
+ if (!Array.isArray(data.records)) {
2227
+ console.error('ERROR: server did not return proper JSON data structure', '\n',
2228
+ ' - it should return', { records: [{ id: 1, text: 'item' }] }, '\n',
2229
+ ' - or just an array ', [{ id: 1, text: 'item' }], '\n',
2230
+ ' - or if errorr ', { error: true, message: 'error message' })
2231
+ return
2232
+ }
2233
+ // remove all extra items if more then needed for cache
2234
+ if (data.records.length >= options.cacheMax) {
2235
+ data.records.splice(options.cacheMax, data.records.length)
2236
+ remote.hasMore = true
2237
+ } else {
2238
+ remote.hasMore = false
2239
+ remote.hasMore_search = search
2240
+ }
2241
+ // map id and text
2242
+ if (options.recId == null && options.recid != null) options.recId = options.recid // since lower-case recid is used in grid
2243
+ if (options.recId || options.recText) {
2244
+ data.records.forEach((item) => {
2245
+ if (typeof options.recId === 'string') item.id = item[options.recId]
2246
+ if (typeof options.recId === 'function') item.id = options.recId(item)
2247
+ if (typeof options.recText === 'string') item.text = item[options.recText]
2248
+ if (typeof options.recText === 'function') item.text = options.recText(item)
2249
+ })
2250
+ }
2251
+ // remember stats
2252
+ remote.loading = false
2253
+ remote.search = search
2254
+ remote.cached = data.records.length == 0 ? -1 : data.records.length
2255
+ remote.lastError = ''
2256
+ remote.emptySet = (search === '' && data.records.length === 0 ? true : false)
2257
+ // event after
2258
+ edata.finish()
2259
+ resolve(w2utils.normMenu(data.records, data))
2260
+ })
2261
+ .catch(error => {
2262
+ let edata = this.trigger('error', { overlay, search, error })
2263
+ if (edata.isCancelled === true) return
2264
+ // default behavior
2265
+ if (error?.name !== 'AbortError') {
2266
+ console.error('ERROR: Server communication failed.', '\n',
2267
+ ' - it should return', { records: [{ id: 1, text: 'item' }] }, '\n',
2268
+ ' - or just an array ', [{ id: 1, text: 'item' }], '\n',
2269
+ ' - or if errorr ', { error: true, message: 'error message' })
2270
+ }
2271
+ // reset stats
2272
+ remote.loading = false
2273
+ remote.search = ''
2274
+ remote.cached = -1
2275
+ remote.emptySet = true
2276
+ remote.lastError = (edata.detail.error || 'Server communication failed')
2277
+ options.items = []
2278
+ // event after
2279
+ edata.finish()
2280
+ reject()
2281
+ })
2282
+ // event after
2283
+ edata.finish()
2284
+ }, debounce ? (options.debounce ?? 350) : 0)
2285
+ }
2286
+ return new Promise((res, rej) => {
2287
+ resolve = res
2288
+ reject = rej
2289
+ })
2290
+ }
2291
+
2292
+ /**
2293
+ * Builds an array of item ids that sequencial order for navigation with up/down keys. Skips hidden and disabled items
2294
+ * and goes into nested structures. It will remember last active chain in 'overlay.tmp.activeChain'
2295
+ */
2296
+ getActiveChain(name, items, parents = [], res = [], noSave) {
2297
+ let overlay = Tooltip.active[name.replace(/[\s\.#]/g, '_')]
2298
+ if (overlay.tmp.activeChain != null) {
2299
+ return overlay.tmp.activeChain
2300
+ }
2301
+ if (items == null) items = overlay.options.items
2302
+ items.forEach((item, ind) => {
2303
+ if (!item.hidden && !item.disabled && !item?.text?.startsWith?.('--')) {
2304
+ res.push(parents.concat([ind]).join('-'))
2305
+ if (Array.isArray(item.items) && item.items.length > 0 && item.expanded) {
2306
+ parents.push(ind)
2307
+ this.getActiveChain(name, item.items, parents, res, true)
2308
+ parents.pop()
2309
+ }
2310
+ }
2311
+ })
2312
+ if (noSave == null) {
2313
+ overlay.tmp.activeChain = res
2314
+ }
2315
+ return res
2316
+ }
2317
+
2318
+ menuDown(overlay, event, index, parents) {
2319
+ let options = overlay.options
2320
+ let items = options.items
2321
+ let icon = query(event.delegate).find('.w2ui-icon')
2322
+ let menu = query(event.target).closest('.w2ui-menu:not(.w2ui-sub-menu)')
2323
+ if (typeof parents == 'string' && parents !== '') {
2324
+ let ids = parents.split('-')
2325
+ ids.forEach(id => {
2326
+ items = items[id].items
2327
+ })
2328
+ }
2329
+ if (typeof items == 'function') {
2330
+ items = items({ overlay, index, parents, event })
2331
+ }
2332
+ let item = items[index]
2333
+ if (item == null || item.disabled) {
2334
+ return
2335
+ }
2336
+ let uncheck = (items, parent) => {
2337
+ items.forEach((other, ind) => {
2338
+ if (other.id == item.id) return
2339
+ if (other.group === item.group && other.checked) {
2340
+ menu
2341
+ .find(`.w2ui-menu-item[index="${(parent ? parent + '-' : '') + ind}"] .w2ui-icon`)
2342
+ .removeClass('w2ui-icon-check')
2343
+ .addClass('w2ui-icon-empty')
2344
+ items[ind].checked = false
2345
+ }
2346
+ if (Array.isArray(other.items)) {
2347
+ uncheck(other.items, ind)
2348
+ }
2349
+ })
2350
+ }
2351
+ if ((options.type === 'check' || options.type === 'radio') && item.group !== false
2352
+ && !query(event.target).hasClass('menu-remove')
2353
+ && !query(event.target).hasClass('menu-help')
2354
+ && !query(event.target).closest('.w2ui-menu-item').hasClass('has-sub-menu')) {
2355
+ item.checked = options.type == 'radio' ? true : !item.checked
2356
+ if (item.checked) {
2357
+ if (options.type === 'radio') {
2358
+ query(event.target).closest('.w2ui-menu').find('.w2ui-icon')
2359
+ .removeClass('w2ui-icon-check')
2360
+ .addClass('w2ui-icon-empty')
2361
+ }
2362
+ if (options.type === 'check' && item.group != null) {
2363
+ uncheck(options.items)
2364
+ }
2365
+ icon.removeClass('w2ui-icon-empty').addClass('w2ui-icon-check')
2366
+ } else if (options.type === 'check') {
2367
+ icon.removeClass('w2ui-icon-check').addClass('w2ui-icon-empty')
2368
+ }
2369
+ }
2370
+ // highlight record
2371
+ if (!query(event.target).hasClass('menu-remove') && !query(event.target).hasClass('menu-help')) {
2372
+ menu.find('.w2ui-menu-item').removeClass('w2ui-selected')
2373
+ // click on the item that has submenu will not select the item
2374
+ if (!query(event.delegate).hasClass('has-sub-menu')) {
2375
+ query(event.delegate).addClass('w2ui-selected')
2376
+ }
2377
+ }
2378
+ }
2379
+
2380
+ menuClick(overlay, event, index, parents) {
2381
+ let options = overlay.options
2382
+ let items = options.items
2383
+ let $item = query(event.delegate).closest('.w2ui-menu-item')
2384
+ let keepOpen = options.hideOn.includes('select') ? false : true
2385
+ if (event.shiftKey || event.metaKey || event.ctrlKey) {
2386
+ keepOpen = true
2387
+ }
2388
+ if (typeof parents == 'string' && parents !== '') {
2389
+ parents = parents.split('-')
2390
+ } else if (!Array.isArray(parents)) {
2391
+ parents = null
2392
+ }
2393
+ // parse through parents to get to right items
2394
+ if (parents) {
2395
+ parents.forEach(id => {
2396
+ items = items[id].items
2397
+ })
2398
+ }
2399
+ if (typeof items == 'function') {
2400
+ items = items({ overlay, index, parents, event })
2401
+ }
2402
+ let item = items[index]
2403
+ if (!item || (item.disabled && !query(event.target).hasClass('menu-remove'))) {
2404
+ return
2405
+ }
2406
+ let edata
2407
+ let overlays = [overlay]
2408
+ let topOverlay = overlay
2409
+ let parentOverlay
2410
+ while (topOverlay.options.parentOverlay) {
2411
+ topOverlay = topOverlay.options.parentOverlay
2412
+ parentOverlay ??= topOverlay // not top overlay, but parent overlay items
2413
+ overlays.push(topOverlay)
2414
+ }
2415
+
2416
+ if (query(event.target).hasClass('menu-remove')) {
2417
+ edata = topOverlay.trigger('remove', {
2418
+ originalEvent: event, target: overlay.name, overlay, topOverlay, parentOverlay,
2419
+ item, index, el: $item[0]
2420
+ })
2421
+ if (edata.isCancelled === true) {
2422
+ return
2423
+ }
2424
+ // delete from items
2425
+ let items = options.items
2426
+ let ind = items.findIndex(it => it.id == item.id)
2427
+ if (ind != -1) {
2428
+ let tmp = items.splice(ind, 1)
2429
+ // delete from the parent too
2430
+ if (overlay.options.parents) {
2431
+ let pind = overlay.options.parents[overlay.options.parents.length -1]
2432
+ let pitems = parentOverlay.options.items[pind].items
2433
+ if (pitems[ind].id == tmp[0].id) {
2434
+ pitems.splice(ind, 1)
2435
+ }
2436
+ }
2437
+ }
2438
+ keepOpen = !options.hideOn.includes('item-remove')
2439
+ let name = $item.closest('.w2ui-overlay').attr('name')
2440
+ overlay.self.update(name, items)
2441
+
2442
+ } else if ($item.hasClass('has-sub-menu')) {
2443
+
2444
+ edata = topOverlay.trigger('subMenu', {
2445
+ originalEvent: event, target: overlay.name, overlay, topOverlay, parentOverlay,
2446
+ item, index, el: $item[0]
2447
+ })
2448
+ if (edata.isCancelled === true) {
2449
+ return
2450
+ }
2451
+ keepOpen = true
2452
+
2453
+ } else {
2454
+
2455
+ // find items that are selected
2456
+ let selected = this.findChecked(options.items)
2457
+ let a_index = $item.attr('index')
2458
+ overlay.selected = isNaN(a_index) ? a_index: parseInt(a_index)
2459
+ edata = topOverlay.trigger('select', {
2460
+ originalEvent: event, target: overlay.name, overlay, topOverlay, parentOverlay,
2461
+ item, index, selected, keepOpen, el: $item[0]
2462
+ })
2463
+ if (edata.isCancelled === true) {
2464
+ return
2465
+ }
2466
+ if (item.keepOpen != null) {
2467
+ keepOpen = item.keepOpen
2468
+ }
2469
+ if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
2470
+ overlay.anchor.dataset.selected = item.id
2471
+ overlay.anchor.dataset.selectedIndex = overlay.selected
2472
+ }
2473
+ }
2474
+ if (!keepOpen) {
2475
+ overlays.forEach(overlay => this.hide(overlay.name))
2476
+ }
2477
+ // if (['INPUT', 'TEXTAREA'].includes(overlay.anchor.tagName)) {
2478
+ // overlay.anchor.focus()
2479
+ // }
2480
+ // event after
2481
+ edata.finish()
2482
+ }
2483
+
2484
+ findChecked(items) {
2485
+ let found = []
2486
+ items.forEach(item => {
2487
+ if (item.checked) found.push(item)
2488
+ if (Array.isArray(item.items)) {
2489
+ found = found.concat(this.findChecked(item.items))
2490
+ }
2491
+ })
2492
+ return found
2493
+ }
2494
+
2495
+ keyUp(overlay, event) {
2496
+ let options = overlay.options
2497
+ let search = event.target.value
2498
+ let filter = true
2499
+ let refreshIndex = false
2500
+ switch (event.keyCode) {
2501
+ case 46: // delete
2502
+ case 8: { // backspace
2503
+ // if search empty and delete is clicked, do not filter nor show overlay
2504
+ if (search === '' && !overlay.displayed) filter = false
2505
+ break
2506
+ }
2507
+ case 13: { // enter
2508
+ if (!overlay.displayed || !overlay.selected) return
2509
+ let { index, parents } = this.getCurrent(overlay.name)
2510
+ event.delegate = query(overlay.box).find('.w2ui-selected').get(0)
2511
+ // reset active chain for folders
2512
+ this.menuClick(overlay, event, parseInt(index), parents)
2513
+ filter = false
2514
+ break
2515
+ }
2516
+ case 27: { // escape
2517
+ filter = false
2518
+ if (overlay.displayed) {
2519
+ this.hide(overlay.name)
2520
+ } else {
2521
+ // clear selected
2522
+ let el = overlay.anchor
2523
+ if (['INPUT', 'TEXTAREA'].includes(el.tagName)) {
2524
+ el.value = ''
2525
+ delete el.dataset.selected
2526
+ delete el.dataset.selectedIndex
2527
+ }
2528
+ }
2529
+ break
2530
+ }
2531
+ case 37: { // left
2532
+ if (!overlay.displayed) return
2533
+ let { item, index, parents } = this.getCurrent(overlay.name)
2534
+ // collapse parent if any
2535
+ if (parents) {
2536
+ item = options.items[parents]
2537
+ index = parseInt(parents)
2538
+ parents = ''
2539
+ refreshIndex = true
2540
+ }
2541
+ if (Array.isArray(item?.items) && item.items.length > 0 && item.expanded) {
2542
+ event.delegate = query(overlay.box).find(`.w2ui-menu-item[index="${index}"]`).get(0)
2543
+ overlay.selected = index
2544
+ this.menuClick(overlay, event, parseInt(index), parents)
2545
+ }
2546
+ filter = false
2547
+ break
2548
+ }
2549
+ case 39: { // right
2550
+ if (!overlay.displayed) return
2551
+ let { item, index, parents } = this.getCurrent(overlay.name)
2552
+ if (Array.isArray(item?.items) && item.items.length > 0 && !item.expanded) {
2553
+ event.delegate = query(overlay.box).find('.w2ui-selected').get(0)
2554
+ this.menuClick(overlay, event, parseInt(index), parents)
2555
+ }
2556
+ filter = false
2557
+ break
2558
+ }
2559
+ case 38: { // up
2560
+ if (!overlay.displayed) {
2561
+ break
2562
+ }
2563
+ overlay.prev()
2564
+ filter = false
2565
+ event.preventDefault()
2566
+ break
2567
+ }
2568
+ case 40: { // down
2569
+ if (!overlay.displayed) {
2570
+ break
2571
+ }
2572
+ overlay.next()
2573
+ filter = false
2574
+ event.preventDefault()
2575
+ break
2576
+ }
2577
+ }
2578
+ // filter
2579
+ if (filter && overlay.displayed
2580
+ && ((options.filter && event._searchType == 'filter') || (options.search && event._searchType == 'search'))) {
2581
+ this.applyFilter(overlay.name, null, search, true)
2582
+ .then(data => {
2583
+ overlay.tmp.searchCount = data.count
2584
+ overlay.tmp.search = data.search
2585
+ // if selected is not in searched items
2586
+ if (data.count === 0 || !this.getActiveChain(overlay.name).includes(overlay.selected)) {
2587
+ overlay.selected = null
2588
+ }
2589
+ this.refreshSearch(overlay.name)
2590
+ })
2591
+ }
2592
+ if (refreshIndex) {
2593
+ this.refreshIndex(overlay.name)
2594
+ }
2595
+ }
2596
+ }
2597
+
2598
+ class DateTooltip extends Tooltip {
2599
+ constructor() {
2600
+ super()
2601
+ let td = new Date()
2602
+ this.daysCount = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
2603
+ this.today = td.getFullYear() + '/' + (Number(td.getMonth()) + 1) + '/' + td.getDate()
2604
+ this.defaults = w2utils.extend({}, this.defaults, {
2605
+ position : 'top|bottom',
2606
+ class : 'w2ui-calendar',
2607
+ type : 'date', // can be date/time/datetime
2608
+ value : '', // initial date (in w2utils.settings format)
2609
+ format : '',
2610
+ start : null,
2611
+ end : null,
2612
+ btnNow : false,
2613
+ blockDates : [], // array of blocked dates
2614
+ blockWeekdays : [], // blocked weekdays 0 - sunday, 1 - monday, etc
2615
+ colored : {}, // ex: { '3/13/2022': 'bg-color|text-color' }
2616
+ arrowSize : 12,
2617
+ autoResize : false,
2618
+ anchorClass : 'w2ui-focus',
2619
+ autoShowOn : 'focus',
2620
+ hideOn : ['doc-click', 'focus-change'],
2621
+ onSelect : null
2622
+ })
2623
+ }
2624
+
2625
+ attach(anchor, text) {
2626
+ let options
2627
+ if (arguments.length == 1 && anchor instanceof Object) {
2628
+ options = anchor
2629
+ anchor = options.anchor
2630
+ } else if (arguments.length === 2 && text != null && typeof text === 'object') {
2631
+ options = text
2632
+ options.anchor = anchor
2633
+ }
2634
+ let prevHideOn = options.hideOn
2635
+ options = w2utils.extend({}, this.defaults, options || {})
2636
+ if (prevHideOn) {
2637
+ options.hideOn = prevHideOn
2638
+ }
2639
+ if (!options.format) {
2640
+ let df = w2utils.settings.dateFormat
2641
+ let tf = w2utils.settings.timeFormat
2642
+ if (options.type == 'date') {
2643
+ options.format = df
2644
+ } else if (options.type == 'time') {
2645
+ options.format = tf
2646
+ } else {
2647
+ options.format = df + '|' + tf
2648
+ }
2649
+ }
2650
+ let cal = options.type == 'time' ? this.getHourHTML(options) : this.getMonthHTML(options)
2651
+ options.style += '; padding: 0;'
2652
+ options.html = cal.html
2653
+ let ret = super.attach(options)
2654
+ let overlay = ret.overlay
2655
+ Object.assign(overlay.tmp, cal)
2656
+ overlay.on('show.attach', event => {
2657
+ let overlay = event.detail.overlay
2658
+ let anchor = overlay.anchor
2659
+ let options = overlay.options
2660
+ if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && !options.value && anchor.value) {
2661
+ overlay.tmp.initValue = anchor.value
2662
+ }
2663
+ delete overlay.newValue
2664
+ delete overlay.newDate
2665
+ })
2666
+ overlay.on('show:after.attach', event => {
2667
+ if (ret.overlay?.box) {
2668
+ this.initControls(ret.overlay)
2669
+ }
2670
+ })
2671
+ overlay.on('update:after.attach', event => {
2672
+ if (ret.overlay?.box) {
2673
+ this.initControls(ret.overlay)
2674
+ }
2675
+ })
2676
+ overlay.on('hide.attach', event => {
2677
+ let overlay = event.detail.overlay
2678
+ let anchor = overlay.anchor
2679
+ if (overlay.newValue != null) {
2680
+ if (overlay.newDate) {
2681
+ overlay.newValue = overlay.newDate + ' ' + overlay.newValue
2682
+ }
2683
+ if (['INPUT', 'TEXTAREA'].includes(anchor.tagName) && anchor.value != overlay.newValue) {
2684
+ anchor.value = overlay.newValue
2685
+ }
2686
+ let edata = this.trigger('select', { date: overlay.newValue, target: overlay.name, overlay })
2687
+ if (edata.isCancelled === true) return
2688
+ // event after
2689
+ edata.finish()
2690
+ }
2691
+ })
2692
+ ret.select = (callback) => {
2693
+ overlay.on('select.attach', (event) => { callback(event) })
2694
+ return ret
2695
+ }
2696
+ return ret
2697
+ }
2698
+
2699
+ initControls(overlay) {
2700
+ let options = overlay.options
2701
+ let moveMonth = (inc) => {
2702
+ let { month, year } = overlay.tmp
2703
+ month += inc
2704
+ if (month > 12) {
2705
+ month = 1
2706
+ year++
2707
+ }
2708
+ if (month < 1 ) {
2709
+ month = 12
2710
+ year--
2711
+ }
2712
+ let cal = this.getMonthHTML(options, month, year)
2713
+ Object.assign(overlay.tmp, cal)
2714
+ query(overlay.box).find('.w2ui-overlay-body').html(cal.html)
2715
+ this.initControls(overlay)
2716
+ }
2717
+ let checkJump = (event, dblclick) => {
2718
+ query(event.target).parent().find('.w2ui-jump-month, .w2ui-jump-year')
2719
+ .removeClass('w2ui-selected')
2720
+ query(event.target).addClass('w2ui-selected')
2721
+ let dt = new Date()
2722
+ let { jumpMonth, jumpYear } = overlay.tmp
2723
+ if (dblclick) {
2724
+ if (jumpYear == null) jumpYear = dt.getFullYear()
2725
+ if (jumpMonth == null) jumpMonth = dt.getMonth() + 1
2726
+ }
2727
+ if (jumpMonth && jumpYear) {
2728
+ let cal = this.getMonthHTML(options, jumpMonth, jumpYear)
2729
+ Object.assign(overlay.tmp, cal)
2730
+ query(overlay.box).find('.w2ui-overlay-body').html(cal.html)
2731
+ overlay.tmp.jump = false
2732
+ this.initControls(overlay)
2733
+ }
2734
+ }
2735
+
2736
+ // events for next/prev buttons and title
2737
+ query(overlay.box)
2738
+ .find('.w2ui-cal-title')
2739
+ .off('.calendar')
2740
+ // click on title
2741
+ .on('click.calendar', event => {
2742
+ Object.assign(overlay.tmp, { jumpYear: null, jumpMonth: null })
2743
+ if (overlay.tmp.jump) {
2744
+ let { month, year } = overlay.tmp
2745
+ let cal = this.getMonthHTML(options, month, year)
2746
+ query(overlay.box).find('.w2ui-overlay-body').html(cal.html)
2747
+ overlay.tmp.jump = false
2748
+ } else {
2749
+ query(overlay.box).find('.w2ui-overlay-body .w2ui-cal-days')
2750
+ .replace(this.getYearHTML())
2751
+ let el = query(overlay.box).find(`[name="${overlay.tmp.year}"]`).get(0)
2752
+ if (el) el.scrollIntoView(true)
2753
+ overlay.tmp.jump = true
2754
+ }
2755
+ this.initControls(overlay)
2756
+ event.stopPropagation()
2757
+ })
2758
+ // prev button
2759
+ .find('.w2ui-cal-previous')
2760
+ .off('.calendar')
2761
+ .on('click.calendar', event => {
2762
+ moveMonth(-1)
2763
+ event.stopPropagation()
2764
+ })
2765
+ .parent()
2766
+ // next button
2767
+ .find('.w2ui-cal-next')
2768
+ .off('.calendar')
2769
+ .on('click.calendar', event => {
2770
+ moveMonth(1)
2771
+ event.stopPropagation()
2772
+ })
2773
+ // now button
2774
+ query(overlay.box).find('.w2ui-cal-now')
2775
+ .off('.calendar')
2776
+ .on('click.calendar', event => {
2777
+ if (options.type == 'datetime') {
2778
+ if (overlay.newDate) {
2779
+ overlay.newValue = w2utils.formatTime(new Date(), options.format.split('|')[1])
2780
+ } else {
2781
+ overlay.newValue = w2utils.formatDateTime(new Date(), options.format)
2782
+ }
2783
+ } else if (options.type == 'date') {
2784
+ overlay.newValue = w2utils.formatDate(new Date(), options.format)
2785
+ } else if (options.type == 'time') {
2786
+ overlay.newValue = w2utils.formatTime(new Date(), options.format)
2787
+ }
2788
+ this.hide(overlay.name)
2789
+ })
2790
+ // events for dates
2791
+ query(overlay.box)
2792
+ .off('.calendar')
2793
+ .on('contextmenu.calendar', event => {
2794
+ event.preventDefault() // prevent browser context menu
2795
+ })
2796
+ .on('click.calendar', { delegate: '.w2ui-day.w2ui-date' }, event => {
2797
+ if (options.type == 'datetime') {
2798
+ overlay.newDate = query(event.target).attr('date')
2799
+ query(overlay.box).find('.w2ui-overlay-body').html(this.getHourHTML(overlay.options).html)
2800
+ this.initControls(overlay)
2801
+ } else {
2802
+ overlay.newValue = query(event.target).attr('date')
2803
+ this.hide(overlay.name)
2804
+ }
2805
+ })
2806
+ // click on month
2807
+ .on('click.calendar', { delegate: '.w2ui-jump-month' }, event => {
2808
+ overlay.tmp.jumpMonth = parseInt(query(event.target).attr('name'))
2809
+ checkJump(event)
2810
+ })
2811
+ // double click on month
2812
+ .on('dblclick.calendar', { delegate: '.w2ui-jump-month' }, event => {
2813
+ overlay.tmp.jumpMonth = parseInt(query(event.target).attr('name'))
2814
+ checkJump(event, true)
2815
+ })
2816
+ // click on year
2817
+ .on('click.calendar', { delegate: '.w2ui-jump-year' }, event => {
2818
+ overlay.tmp.jumpYear = parseInt(query(event.target).attr('name'))
2819
+ checkJump(event)
2820
+ })
2821
+ // dbl click on year
2822
+ .on('dblclick.calendar', { delegate: '.w2ui-jump-year' }, event => {
2823
+ overlay.tmp.jumpYear = parseInt(query(event.target).attr('name'))
2824
+ checkJump(event, true)
2825
+ })
2826
+ // click on hour
2827
+ .on('click.calendar', { delegate: '.w2ui-time.hour' }, event => {
2828
+ let hour = query(event.target).attr('hour')
2829
+ let min = this.str2min(options.value) % 60
2830
+ if (overlay.tmp.initValue && !options.value) {
2831
+ min = this.str2min(overlay.tmp.initValue) % 60
2832
+ }
2833
+ if (options.noMinutes) {
2834
+ overlay.newValue = this.min2str(hour * 60, options.format)
2835
+ this.hide(overlay.name)
2836
+ } else {
2837
+ overlay.newValue = hour + ':' + min
2838
+ let html = this.getMinHTML(hour, options).html
2839
+ query(overlay.box).find('.w2ui-overlay-body').html(html)
2840
+ this.initControls(overlay)
2841
+ }
2842
+ })
2843
+ // click on minute
2844
+ .on('click.calendar', { delegate: '.w2ui-time.min' }, event => {
2845
+ let hour = Math.floor(this.str2min(overlay.newValue) / 60)
2846
+ let time = (hour * 60) + parseInt(query(event.target).attr('min'))
2847
+ overlay.newValue = this.min2str(time, options.format)
2848
+ this.hide(overlay.name)
2849
+ })
2850
+ }
2851
+
2852
+ getMonthHTML(options, month, year) {
2853
+ let days = w2utils.settings.fulldays.slice() // creates copy of the array
2854
+ let sdays = w2utils.settings.shortdays.slice() // creates copy of the array
2855
+ if (w2utils.settings.weekStarts !== 'M') {
2856
+ days.unshift(days.pop())
2857
+ sdays.unshift(sdays.pop())
2858
+ }
2859
+
2860
+ let DT = new Date()
2861
+ let dayLengthMil = 1000 * 60 * 60 * 24
2862
+
2863
+ let selected = options.type === 'datetime'
2864
+ ? w2utils.isDateTime(options.value, options.format, true)
2865
+ : w2utils.isDate(options.value, options.format, true)
2866
+ let selected_dsp = w2utils.formatDate(selected)
2867
+
2868
+ // normalize date
2869
+ if (month == null || year == null) {
2870
+ year = selected ? selected.getFullYear() : DT.getFullYear()
2871
+ month = selected ? selected.getMonth() + 1 : DT.getMonth() + 1
2872
+ }
2873
+ if (month > 12) { month -= 12; year++ }
2874
+ if (month < 1 || month === 0) { month += 12; year-- }
2875
+ if (year/4 == Math.floor(year/4)) { this.daysCount[1] = 29 } else { this.daysCount[1] = 28 }
2876
+ options.current = month + '/' + year
2877
+
2878
+ let weekDaysHeaderHTML = ''
2879
+ let st = w2utils.settings.weekStarts
2880
+ for (let i = 0; i < sdays.length; i++) {
2881
+ let isSat = (st == 'M' && i == 5) || (st != 'M' && i == 6) ? true : false
2882
+ let isSun = (st == 'M' && i == 6) || (st != 'M' && i == 0) ? true : false
2883
+ weekDaysHeaderHTML += `<div class="w2ui-day w2ui-weekday ${isSat ? 'w2ui-sunday' : ''} ${isSun ? 'w2ui-saturday' : ''}">${sdays[i]}</div>`
2884
+ }
2885
+
2886
+ let html = `
2887
+ <div class="w2ui-cal-title">
2888
+ <div class="w2ui-cal-previous">
2889
+ <div></div>
2890
+ </div>
2891
+ <div class="w2ui-cal-next">
2892
+ <div></div>
2893
+ </div>
2894
+ ${w2utils.settings.fullmonths[month-1]}, ${year}
2895
+ <span class="arrow-down"></span>
2896
+ </div>
2897
+ <div class="w2ui-cal-days">
2898
+ ${weekDaysHeaderHTML}
2899
+ `
2900
+
2901
+ // start with the required date
2902
+ DT = new Date(year, month-1, 1)
2903
+
2904
+ /**
2905
+ * Move to noon, instead of midnight. If not, then the date when time saving happens
2906
+ * will be duplicated in the calendar
2907
+ */
2908
+ DT = new Date(DT.getTime() + dayLengthMil * 0.5)
2909
+
2910
+ // calendar offset
2911
+ let weekDayOffset = DT.getDay()
2912
+ if (w2utils.settings.weekStarts == 'M') {
2913
+ // offset should be 1 day more, but not negative (Sunday)
2914
+ weekDayOffset = weekDayOffset > 0 ? weekDayOffset - 1 : 6
2915
+ }
2916
+
2917
+ // apply the offset for the first day in the calendar
2918
+ DT = new Date(DT.getTime() - (weekDayOffset * dayLengthMil))
2919
+
2920
+ const DaySat = 6, DaySun = 0
2921
+ for (let ci = 0; ci < 42; ci++) {
2922
+ let className = []
2923
+ let dt = `${DT.getFullYear()}/${DT.getMonth()+1}/${DT.getDate()}`
2924
+ if (DT.getDay() === DaySat) className.push('w2ui-saturday')
2925
+ if (DT.getDay() === DaySun) className.push('w2ui-sunday')
2926
+ if (DT.getMonth() + 1 !== month) className.push('outside')
2927
+ if (dt == this.today) className.push('w2ui-today')
2928
+
2929
+ let dspDay = DT.getDate()
2930
+ let col = ''
2931
+ let bgcol = ''
2932
+ let tmp_dt, tmp_dt_fmt
2933
+ if (options.type === 'datetime') {
2934
+ tmp_dt = w2utils.formatDateTime(dt, options.format)
2935
+ tmp_dt_fmt = w2utils.formatDate(dt, w2utils.settings.dateFormat)
2936
+ } else {
2937
+ tmp_dt = w2utils.formatDate(dt, options.format)
2938
+ tmp_dt_fmt = tmp_dt
2939
+ }
2940
+ if (options.colored && options.colored[tmp_dt_fmt] !== undefined) { // if there is predefined colors for dates
2941
+ let tmp = options.colored[tmp_dt_fmt].split('|')
2942
+ bgcol = 'background-color: ' + tmp[0] + ';'
2943
+ col = 'color: ' + tmp[1] + ';'
2944
+ }
2945
+ html += `<div class="w2ui-day ${this.inRange(tmp_dt, options, true)
2946
+ ? 'w2ui-date ' + (tmp_dt_fmt == selected_dsp ? 'w2ui-selected' : '')
2947
+ : 'w2ui-blocked'
2948
+ } ${className.join(' ')}"
2949
+ style="${col + bgcol}" date="${tmp_dt_fmt}" data-date="${DT.getTime()}">
2950
+ ${dspDay}
2951
+ </div>`
2952
+ DT = new Date(DT.getTime() + dayLengthMil)
2953
+ }
2954
+ html += '</div>'
2955
+ if (options.btnNow) {
2956
+ let label = w2utils.lang('Today' + (options.type == 'datetime' ? ' & Now' : ''))
2957
+ html += `<div class="w2ui-cal-now">${label}</div>`
2958
+ }
2959
+ return { html, month, year }
2960
+ }
2961
+
2962
+ getYearHTML() {
2963
+ let mhtml = ''
2964
+ let yhtml = ''
2965
+ for (let m = 0; m < w2utils.settings.fullmonths.length; m++) {
2966
+ mhtml += `<div class="w2ui-jump-month" name="${m+1}">${w2utils.settings.shortmonths[m]}</div>`
2967
+ }
2968
+ for (let y = w2utils.settings.dateStartYear; y <= w2utils.settings.dateEndYear; y++) {
2969
+ yhtml += `<div class="w2ui-jump-year" name="${y}">${y}</div>`
2970
+ }
2971
+ return `<div class="w2ui-cal-jump">
2972
+ <div id="w2ui-jump-month">${mhtml}</div>
2973
+ <div id="w2ui-jump-year">${yhtml}</div>
2974
+ </div>`
2975
+ }
2976
+
2977
+ getHourHTML(options) {
2978
+ options = options ?? {}
2979
+ if (!options.format) options.format = w2utils.settings.timeFormat
2980
+ let h24 = (options.format.indexOf('h24') > -1)
2981
+ let value = options.value ? options.value : (options.anchor ? options.anchor.value : '')
2982
+
2983
+ let tmp = []
2984
+ for (let a = 0; a < 24; a++) {
2985
+ let time = (a >= 12 && !h24 ? a - 12 : a) + ':00' + (!h24 ? (a < 12 ? ' am' : ' pm') : '')
2986
+ if (a == 12 && !h24) time = '12:00 pm'
2987
+ if (!tmp[Math.floor(a/8)]) tmp[Math.floor(a/8)] = ''
2988
+ let tm1 = this.min2str(this.str2min(time))
2989
+ let tm2 = this.min2str(this.str2min(time) + 59)
2990
+ if (options.type === 'datetime') {
2991
+ let dt = w2utils.isDateTime(value, options.format, true)
2992
+ let fm = options.format.split('|')[0].trim()
2993
+ tm1 = w2utils.formatDate(dt, fm) + ' ' + tm1
2994
+ tm2 = w2utils.formatDate(dt, fm) + ' ' + tm2
2995
+ }
2996
+ let valid = this.inRange(tm1, options) || this.inRange(tm2, options)
2997
+ tmp[Math.floor(a/8)] += `<span hour="${a}"
2998
+ class="hour ${valid ? 'w2ui-time ' : 'w2ui-blocked'}">${time}</span>`
2999
+ }
3000
+ let html = `<div class="w2ui-calendar">
3001
+ <div class="w2ui-time-title">${w2utils.lang('Select Hour')}</div>
3002
+ <div class="w2ui-cal-time">
3003
+ <div class="w2ui-cal-column">${tmp[0]}</div>
3004
+ <div class="w2ui-cal-column">${tmp[1]}</div>
3005
+ <div class="w2ui-cal-column">${tmp[2]}</div>
3006
+ </div>
3007
+ ${options.btnNow ? `<div class="w2ui-cal-now">${w2utils.lang('Now')}</div>` : '' }
3008
+ </div>`
3009
+ return { html }
3010
+ }
3011
+
3012
+ getMinHTML(hour, options) {
3013
+ if (hour == null) hour = 0
3014
+ options = options ?? {}
3015
+ if (!options.format) options.format = w2utils.settings.timeFormat
3016
+ let h24 = (options.format.indexOf('h24') > -1)
3017
+ let value = options.value ? options.value : (options.anchor ? options.anchor.value : '')
3018
+
3019
+ let tmp = []
3020
+ for (let a = 0; a < 60; a += 5) {
3021
+ let time = (hour > 12 && !h24 ? hour - 12 : hour) + ':' + (a < 10 ? 0 : '') + a + ' ' + (!h24 ? (hour < 12 ? 'am' : 'pm') : '')
3022
+ let tm = time
3023
+ let ind = a < 20 ? 0 : (a < 40 ? 1 : 2)
3024
+ if (!tmp[ind]) tmp[ind] = ''
3025
+ if (options.type === 'datetime') {
3026
+ let dt = w2utils.isDateTime(value, options.format, true)
3027
+ let fm = options.format.split('|')[0].trim()
3028
+ tm = w2utils.formatDate(dt, fm) + ' ' + tm
3029
+ }
3030
+ tmp[ind] += `<span min="${a}" class="min ${(this.inRange(tm, options) ? 'w2ui-time ' : 'w2ui-blocked')}">${time}</span>`
3031
+ }
3032
+ let html = `<div class="w2ui-calendar">
3033
+ <div class="w2ui-time-title">${w2utils.lang('Select Minute')}</div>
3034
+ <div class="w2ui-cal-time">
3035
+ <div class="w2ui-cal-column">${tmp[0]}</div>
3036
+ <div class="w2ui-cal-column">${tmp[1]}</div>
3037
+ <div class="w2ui-cal-column">${tmp[2]}</div>
3038
+ </div>
3039
+ ${options.btnNow ? `<div class="w2ui-cal-now">${w2utils.lang('Now')}</div>` : '' }
3040
+ </div>`
3041
+ return { html }
3042
+ }
3043
+
3044
+ // checks if date is in range (loost at start, end, blockDates, blockWeekdays)
3045
+ inRange(str, options, dateOnly) {
3046
+ let inRange = false
3047
+ if (options.type === 'date') {
3048
+ let dt = w2utils.isDate(str, options.format, true)
3049
+ if (dt) {
3050
+ // enable range
3051
+ if (options.start || options.end) {
3052
+ let st = (typeof options.start === 'string' ? options.start : query(options.start).val())
3053
+ let en = (typeof options.end === 'string' ? options.end : query(options.end).val())
3054
+ let start = w2utils.isDate(st, options.format, true)
3055
+ let end = w2utils.isDate(en, options.format, true)
3056
+ let current = new Date(dt)
3057
+ if (!start) start = current
3058
+ if (!end) end = current
3059
+ if (current >= start && current <= end) inRange = true
3060
+ } else {
3061
+ inRange = true
3062
+ }
3063
+ // block predefined dates
3064
+ if (Array.isArray(options.blockDates) && options.blockDates.includes(str)) inRange = false
3065
+ // block weekdays
3066
+ if (Array.isArray(options.blockWeekdays) && options.blockWeekdays.includes(dt.getDay())) inRange = false
3067
+ }
3068
+ } else if (options.type === 'time') {
3069
+ if (options.start || options.end) {
3070
+ let tm = this.str2min(str)
3071
+ let tm1 = this.str2min(options.start)
3072
+ let tm2 = this.str2min(options.end)
3073
+ if (!tm1) tm1 = tm
3074
+ if (!tm2) tm2 = tm
3075
+ if (tm >= tm1 && tm <= tm2) inRange = true
3076
+ } else {
3077
+ inRange = true
3078
+ }
3079
+ } else if (options.type === 'datetime') {
3080
+ let dt = w2utils.isDateTime(str, options.format, true)
3081
+ if (dt) {
3082
+ let format = options.format.split('|').map(format => format.trim())
3083
+ if (dateOnly) {
3084
+ let date = w2utils.formatDate(dt, format[0])
3085
+ let opts = w2utils.extend({}, options, { type: 'date', format: format[0] })
3086
+ if (this.inRange(date, opts)) inRange = true
3087
+ } else {
3088
+ let time = w2utils.formatTime(dt, format[1])
3089
+ let opts = { type: 'time', format: format[1], start: options.startTime, end: options.endTime }
3090
+ if (this.inRange(time, opts)) inRange = true
3091
+ }
3092
+ }
3093
+ }
3094
+ return inRange
3095
+ }
3096
+
3097
+ // converts time into number of minutes since midnight -- '11:50am' => 710
3098
+ str2min(str) {
3099
+ if (typeof str !== 'string') return null
3100
+ let tmp = str.split(':')
3101
+ if (tmp.length === 2) {
3102
+ tmp[0] = parseInt(tmp[0])
3103
+ tmp[1] = parseInt(tmp[1])
3104
+ if (str.indexOf('pm') !== -1 && tmp[0] !== 12) tmp[0] += 12
3105
+ if (str.includes('am') && tmp[0] == 12) tmp[0] = 0 // 12:00am - is midnight
3106
+ } else {
3107
+ return null
3108
+ }
3109
+ return tmp[0] * 60 + tmp[1]
3110
+ }
3111
+
3112
+ // converts minutes since midnight into time str -- 710 => '11:50am'
3113
+ min2str(time, format) {
3114
+ let ret = ''
3115
+ if (time >= 24 * 60) time = time % (24 * 60)
3116
+ if (time < 0) time = 24 * 60 + time
3117
+ let hour = Math.floor(time/60)
3118
+ let min = ((time % 60) < 10 ? '0' : '') + (time % 60)
3119
+ if (!format) { format = w2utils.settings.timeFormat}
3120
+ if (format.indexOf('h24') !== -1) {
3121
+ ret = hour + ':' + min
3122
+ } else {
3123
+ ret = (hour <= 12 ? hour : hour - 12) + ':' + min + ' ' + (hour >= 12 ? 'pm' : 'am')
3124
+ }
3125
+ return ret
3126
+ }
3127
+ }
3128
+
3129
+ let w2tooltip = new Tooltip()
3130
+ let w2menu = new MenuTooltip()
3131
+ let w2color = new ColorTooltip()
3132
+ let w2date = new DateTooltip()
3133
+
3134
+ export { w2tooltip, w2color, w2menu, w2date, Tooltip }