@qtoggle/qui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +492 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
  6. package/.github/workflows/main.yml +74 -0
  7. package/.pre-commit-config.yaml +8 -0
  8. package/LICENSE.txt +177 -0
  9. package/README.md +4 -0
  10. package/font/dejavusans-bold.woff +0 -0
  11. package/font/dejavusans-bolditalic.woff +0 -0
  12. package/font/dejavusans-italic.woff +0 -0
  13. package/font/dejavusans-regular.woff +0 -0
  14. package/img/qui-icons.svg +1937 -0
  15. package/js/base/base.js +47 -0
  16. package/js/base/condition-variable.js +92 -0
  17. package/js/base/errors.js +36 -0
  18. package/js/base/i18n.js +20 -0
  19. package/js/base/mixwith.js +135 -0
  20. package/js/base/require-js-compat.js +78 -0
  21. package/js/base/signal.js +91 -0
  22. package/js/base/singleton.js +66 -0
  23. package/js/base/timer.js +126 -0
  24. package/js/config.js +184 -0
  25. package/js/forms/common-fields/check-field.js +42 -0
  26. package/js/forms/common-fields/choice-buttons-field.js +30 -0
  27. package/js/forms/common-fields/color-combo-field.js +37 -0
  28. package/js/forms/common-fields/combo-field.js +108 -0
  29. package/js/forms/common-fields/common-fields.js +23 -0
  30. package/js/forms/common-fields/composite-field.js +132 -0
  31. package/js/forms/common-fields/custom-html-field.js +51 -0
  32. package/js/forms/common-fields/email-field.js +30 -0
  33. package/js/forms/common-fields/file-picker-field.js +46 -0
  34. package/js/forms/common-fields/jquery-ui-field.js +111 -0
  35. package/js/forms/common-fields/labels-field.js +69 -0
  36. package/js/forms/common-fields/numeric-field.js +39 -0
  37. package/js/forms/common-fields/password-field.js +28 -0
  38. package/js/forms/common-fields/phone-field.js +26 -0
  39. package/js/forms/common-fields/progress-disk-field.js +69 -0
  40. package/js/forms/common-fields/push-button-field.js +138 -0
  41. package/js/forms/common-fields/slider-field.js +51 -0
  42. package/js/forms/common-fields/text-area-field.js +34 -0
  43. package/js/forms/common-fields/text-field.js +89 -0
  44. package/js/forms/common-fields/up-down-field.js +85 -0
  45. package/js/forms/common-forms/common-forms.js +16 -0
  46. package/js/forms/common-forms/options-form.js +77 -0
  47. package/js/forms/common-forms/page-form.js +115 -0
  48. package/js/forms/form-button.js +202 -0
  49. package/js/forms/form-field.js +1183 -0
  50. package/js/forms/form.js +1181 -0
  51. package/js/forms/forms.js +68 -0
  52. package/js/global-glass.js +100 -0
  53. package/js/icons/default-stock.js +173 -0
  54. package/js/icons/icon.js +64 -0
  55. package/js/icons/icons.js +16 -0
  56. package/js/icons/multi-state-sprites-icon.js +362 -0
  57. package/js/icons/stock-icon.js +219 -0
  58. package/js/icons/stock.js +98 -0
  59. package/js/icons/stocks.js +57 -0
  60. package/js/index.js +232 -0
  61. package/js/lib/jquery.longpress.js +79 -0
  62. package/js/lib/jquery.module.js +4 -0
  63. package/js/lib/logger.module.js +4 -0
  64. package/js/lib/pep.module.js +4 -0
  65. package/js/lists/common-items/common-items.js +5 -0
  66. package/js/lists/common-items/icon-label-list-item.js +86 -0
  67. package/js/lists/common-lists/common-lists.js +5 -0
  68. package/js/lists/common-lists/page-list.js +53 -0
  69. package/js/lists/list-item.js +147 -0
  70. package/js/lists/list.js +636 -0
  71. package/js/lists/lists.js +26 -0
  72. package/js/main-ui/main-ui.js +64 -0
  73. package/js/main-ui/menu-bar.js +144 -0
  74. package/js/main-ui/options-bar.js +181 -0
  75. package/js/main-ui/status.js +185 -0
  76. package/js/main-ui/top-bar.js +59 -0
  77. package/js/messages/common-message-forms/common-message-forms.js +7 -0
  78. package/js/messages/common-message-forms/confirm-message-form.js +81 -0
  79. package/js/messages/common-message-forms/simple-message-form.js +67 -0
  80. package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
  81. package/js/messages/message-form.js +107 -0
  82. package/js/messages/messages.js +21 -0
  83. package/js/messages/sticky-modal-page.js +98 -0
  84. package/js/messages/sticky-modal-progress-message.js +27 -0
  85. package/js/messages/toast.js +164 -0
  86. package/js/navigation.js +654 -0
  87. package/js/pages/breadcrumbs.js +124 -0
  88. package/js/pages/common-pages/common-pages.js +6 -0
  89. package/js/pages/common-pages/modal-progress-page.js +83 -0
  90. package/js/pages/common-pages/structured-page.js +46 -0
  91. package/js/pages/page.js +1018 -0
  92. package/js/pages/pages-context.js +154 -0
  93. package/js/pages/pages.js +252 -0
  94. package/js/pwa.js +337 -0
  95. package/js/sections/section.js +612 -0
  96. package/js/sections/sections.js +300 -0
  97. package/js/tables/common-cells/common-cells.js +7 -0
  98. package/js/tables/common-cells/icon-label-table-cell.js +68 -0
  99. package/js/tables/common-cells/push-button-table-cell.js +133 -0
  100. package/js/tables/common-cells/simple-table-cell.js +37 -0
  101. package/js/tables/common-tables/common-tables.js +5 -0
  102. package/js/tables/common-tables/page-table.js +55 -0
  103. package/js/tables/table-cell.js +198 -0
  104. package/js/tables/table-row.js +126 -0
  105. package/js/tables/table.js +492 -0
  106. package/js/tables/tables.js +36 -0
  107. package/js/theme.js +304 -0
  108. package/js/utils/ajax.js +126 -0
  109. package/js/utils/array.js +194 -0
  110. package/js/utils/colors.js +445 -0
  111. package/js/utils/cookies.js +65 -0
  112. package/js/utils/crypto.js +439 -0
  113. package/js/utils/css.js +234 -0
  114. package/js/utils/date.js +300 -0
  115. package/js/utils/files.js +27 -0
  116. package/js/utils/gestures.js +165 -0
  117. package/js/utils/html.js +76 -0
  118. package/js/utils/misc.js +81 -0
  119. package/js/utils/object.js +324 -0
  120. package/js/utils/promise.js +49 -0
  121. package/js/utils/string.js +192 -0
  122. package/js/utils/url.js +187 -0
  123. package/js/utils/utils.js +3 -0
  124. package/js/utils/visibility-manager.js +211 -0
  125. package/js/views/common-views/common-views.js +7 -0
  126. package/js/views/common-views/icon-label-view.js +210 -0
  127. package/js/views/common-views/progress-view.js +89 -0
  128. package/js/views/common-views/structured-view.js +368 -0
  129. package/js/views/view.js +467 -0
  130. package/js/views/views.js +3 -0
  131. package/js/widgets/base-widget.js +23 -0
  132. package/js/widgets/common-widgets/check-button.js +109 -0
  133. package/js/widgets/common-widgets/choice-buttons.js +322 -0
  134. package/js/widgets/common-widgets/color-combo.js +104 -0
  135. package/js/widgets/common-widgets/combo.js +645 -0
  136. package/js/widgets/common-widgets/common-widgets.js +17 -0
  137. package/js/widgets/common-widgets/email-input.js +7 -0
  138. package/js/widgets/common-widgets/file-picker.js +133 -0
  139. package/js/widgets/common-widgets/labels.js +132 -0
  140. package/js/widgets/common-widgets/numeric-input.js +49 -0
  141. package/js/widgets/common-widgets/password-input.js +91 -0
  142. package/js/widgets/common-widgets/phone-input.js +7 -0
  143. package/js/widgets/common-widgets/progress-disk.js +174 -0
  144. package/js/widgets/common-widgets/push-button.js +155 -0
  145. package/js/widgets/common-widgets/slider.js +455 -0
  146. package/js/widgets/common-widgets/text-area.js +52 -0
  147. package/js/widgets/common-widgets/text-input.js +174 -0
  148. package/js/widgets/common-widgets/up-down.js +351 -0
  149. package/js/widgets/widgets.js +57 -0
  150. package/js/window.js +557 -0
  151. package/jsdoc.conf.json +20 -0
  152. package/less/base.less +123 -0
  153. package/less/forms/common-fields.less +101 -0
  154. package/less/forms/common-forms.less +5 -0
  155. package/less/forms/form-button.less +21 -0
  156. package/less/forms/form-field.less +266 -0
  157. package/less/forms/form.less +131 -0
  158. package/less/global-glass.less +64 -0
  159. package/less/icon-label-view.less +82 -0
  160. package/less/icons.less +144 -0
  161. package/less/lists.less +105 -0
  162. package/less/main-ui.less +328 -0
  163. package/less/messages.less +189 -0
  164. package/less/no-effects.less +24 -0
  165. package/less/pages/breadcrumbs.less +98 -0
  166. package/less/pages/common-pages.less +36 -0
  167. package/less/pages/page.less +70 -0
  168. package/less/progress-view.less +51 -0
  169. package/less/stock-icons.less +43 -0
  170. package/less/structured-view.less +245 -0
  171. package/less/tables.less +84 -0
  172. package/less/theme-dark.less +133 -0
  173. package/less/theme-light.less +132 -0
  174. package/less/theme.less +419 -0
  175. package/less/visibility-manager.less +11 -0
  176. package/less/widgets/check-button.less +96 -0
  177. package/less/widgets/choice-buttons.less +160 -0
  178. package/less/widgets/color-combo.less +33 -0
  179. package/less/widgets/combo.less +230 -0
  180. package/less/widgets/common-buttons.less +120 -0
  181. package/less/widgets/common.less +24 -0
  182. package/less/widgets/input.less +258 -0
  183. package/less/widgets/labels.less +81 -0
  184. package/less/widgets/progress-disk.less +70 -0
  185. package/less/widgets/slider.less +199 -0
  186. package/less/widgets/updown.less +115 -0
  187. package/less/widgets/various.less +36 -0
  188. package/package.json +47 -0
  189. package/pyproject.toml +45 -0
  190. package/qui/__init__.py +110 -0
  191. package/qui/constants.py +1 -0
  192. package/qui/exceptions.py +2 -0
  193. package/qui/j2template.py +71 -0
  194. package/qui/settings.py +60 -0
  195. package/qui/templates/manifest.json +25 -0
  196. package/qui/templates/qui.html +126 -0
  197. package/qui/templates/service-worker.js +188 -0
  198. package/qui/web/__init__.py +0 -0
  199. package/qui/web/tornado.py +220 -0
  200. package/scripts/postinstall.sh +10 -0
  201. package/webpack/webpack-adjust-css-urls-loader.js +36 -0
  202. package/webpack/webpack-common.js +384 -0
@@ -0,0 +1,636 @@
1
+
2
+ import $ from '$qui/lib/jquery.module.js'
3
+ import Logger from '$qui/lib/logger.module.js'
4
+
5
+ import {gettext} from '$qui/base/i18n.js'
6
+ import {mix} from '$qui/base/mixwith.js'
7
+ import StockIcon from '$qui/icons/stock-icon.js'
8
+ import * as Lists from '$qui/lists/lists.js'
9
+ import {asap} from '$qui/utils/misc.js'
10
+ import * as ObjectUtils from '$qui/utils/object.js'
11
+ import {ProgressViewMixin} from '$qui/views/common-views/common-views.js'
12
+ import {StructuredViewMixin} from '$qui/views/common-views/common-views.js'
13
+ import ViewMixin from '$qui/views/view.js'
14
+
15
+
16
+ const logger = Logger.get('qui.lists.list')
17
+
18
+
19
+ /**
20
+ * A list view.
21
+ * @alias qui.lists.List
22
+ * @mixes qui.views.ViewMixin
23
+ * @mixes qui.views.commonviews.StructuredViewMixin
24
+ * @mixes qui.views.commonviews.ProgressViewMixin
25
+ */
26
+ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin) {
27
+
28
+ /**
29
+ * @constructs
30
+ * @param {qui.lists.ListItem[]} [initialItems] initial list items
31
+ * @param {Boolean} [searchEnabled] set to `true` to enable the search feature (defaults to `false`)
32
+ * @param {Boolean} [addEnabled] set to `true` to enable the add item feature (defaults to `false`)
33
+ * @param {String} [selectMode] one of:
34
+ * * {@link qui.lists.LIST_SELECT_MODE_DISABLED}
35
+ * * {@link qui.lists.LIST_SELECT_MODE_SINGLE} (default)
36
+ * * {@link qui.lists.LIST_SELECT_MODE_MULTIPLE}
37
+ * @param {Boolean} longPressMultipleSelection set to `true` to enable toggling between single and multiple select
38
+ * modes by long pressing items
39
+ * @param {...*} args parent class parameters
40
+ */
41
+ constructor({
42
+ initialItems = null,
43
+ searchEnabled = false,
44
+ addEnabled = false,
45
+ selectMode = Lists.LIST_SELECT_MODE_SINGLE,
46
+ longPressMultipleSelection = false,
47
+ ...args
48
+ } = {}) {
49
+
50
+ super(args)
51
+
52
+ this._items = initialItems || []
53
+ this._searchEnabled = searchEnabled
54
+ this._addEnabled = addEnabled
55
+ this._selectMode = selectMode
56
+ this._longPressMultipleSelection = longPressMultipleSelection
57
+
58
+ this._addElem = null
59
+ this._searchElem = null
60
+ this._filterInput = null
61
+ }
62
+
63
+ makeHTML() {
64
+ return $('<div></div>', {class: 'qui-list'})
65
+ }
66
+
67
+ initHTML(html) {
68
+ super.initHTML(html)
69
+
70
+ html.addClass(`select-mode-${this._selectMode}`)
71
+ }
72
+
73
+ init() {
74
+ super.init()
75
+
76
+ /* Set initial items */
77
+ if (this._items.length) {
78
+ this.setItems(this._items)
79
+ }
80
+ }
81
+
82
+ makeBody() {
83
+ let bodyDiv = $('<div></div>', {class: 'qui-list-body'})
84
+
85
+ if (this._searchEnabled) {
86
+ this._enableSearch(bodyDiv)
87
+ }
88
+
89
+ if (this._addEnabled) {
90
+ this._enableAdd(bodyDiv)
91
+ }
92
+
93
+ return bodyDiv
94
+ }
95
+
96
+
97
+ /* Items */
98
+
99
+ /**
100
+ * Return all items.
101
+ * @returns {qui.lists.ListItem[]}
102
+ */
103
+ getItems() {
104
+ return this._items.slice()
105
+ }
106
+
107
+ /**
108
+ * Set the items of the list.
109
+ * @param {qui.lists.ListItem[]} items list items
110
+ */
111
+ setItems(items) {
112
+ this._items.forEach(i => i.getHTML().remove())
113
+
114
+ items.forEach(i => this.prepareItem(i))
115
+ this._items = items
116
+
117
+ if (this._searchEnabled) {
118
+ this._applySearchFilter()
119
+ }
120
+
121
+ this._items.forEach(function (item) {
122
+ if (this._addElem) {
123
+ this._addElem.before(item.getHTML())
124
+ }
125
+ else {
126
+ this.getBody().append(item.getHTML())
127
+ }
128
+ }, this)
129
+ }
130
+
131
+ /**
132
+ * Update one item.
133
+ * @param {Number} index the index where to perform the update
134
+ * @param {qui.lists.ListItem} item the item to update
135
+ */
136
+ setItem(index, item) {
137
+ this.prepareItem(item)
138
+
139
+ if (this._searchEnabled) {
140
+ this._applySearchFilter(item)
141
+ }
142
+
143
+ this._items[index].getHTML().replaceWith(item.getHTML())
144
+ this._items[index] = item
145
+
146
+ }
147
+
148
+ /**
149
+ * Add one item to the list.
150
+ * @param {Number} index the index where the item should be added; `-1` will add the item at the end
151
+ * @param {qui.lists.ListItem} item the item
152
+ */
153
+ addItem(index, item) {
154
+ this.prepareItem(item)
155
+
156
+ if (this._searchEnabled) {
157
+ this._applySearchFilter(item)
158
+ }
159
+
160
+ if (index < 0 || !this._items.length) {
161
+ if (this._addElem) {
162
+ this._addElem.before(item.getHTML())
163
+ }
164
+ else {
165
+ this.getBody().append(item.getHTML())
166
+ }
167
+
168
+ this._items.push(item)
169
+ }
170
+ else {
171
+ this._items[index].getHTML().before(item.getHTML())
172
+ this._items.splice(index, 0, item)
173
+ }
174
+
175
+ }
176
+
177
+ /**
178
+ * Remove the item at a given index.
179
+ * @param {Number} index the index of the item to remove
180
+ * @returns {?qui.lists.ListItem} the removed item
181
+ */
182
+ removeItemAt(index) {
183
+ if (this._items[index]) {
184
+ this._items[index].getHTML().remove()
185
+ }
186
+
187
+ return this._items.splice(index, 1)[0] || null
188
+ }
189
+
190
+ /**
191
+ * Remove a specific item.
192
+ * @param {qui.lists.ListItem} item the item to remove
193
+ * @returns {Boolean} `true` if item found and removed, `false` otherwise
194
+ */
195
+ removeItem(item) {
196
+ return this.removeItems(i => i === item).length > 0
197
+ }
198
+
199
+ /**
200
+ * Remove all items that match a condition.
201
+ * @param {qui.lists.ListItemMatchFunc} matchFunc
202
+ * @returns {qui.lists.ListItem[]} the removed items
203
+ */
204
+ removeItems(matchFunc) {
205
+ let removedItems = []
206
+
207
+ for (let i = 0; i < this._items.length; i++) {
208
+ if (matchFunc(this._items[i])) {
209
+ removedItems.push(this.removeItemAt(i--))
210
+ }
211
+ }
212
+
213
+ return removedItems
214
+ }
215
+
216
+ /**
217
+ * Prepare item to be part of this list.
218
+ * @param {qui.lists.ListItem} item
219
+ */
220
+ prepareItem(item) {
221
+ item.setList(this)
222
+
223
+ let html = item.getHTML()
224
+
225
+ html.on('click', this._handleItemClick.bind(this, item))
226
+ html.longpress(this._handleLongPress.bind(this))
227
+
228
+ item.setSelectMode(this._selectMode)
229
+ }
230
+
231
+ _handleItemClick(item) {
232
+ /* Flag to prevent handling clicks on long press */
233
+ if (item._wasLongPressed) {
234
+ item._wasLongPressed = false
235
+ return
236
+ }
237
+
238
+ if (this._selectMode === Lists.LIST_SELECT_MODE_DISABLED) {
239
+ return
240
+ }
241
+
242
+ let oldItems = this._items.filter(i => i.isSelected())
243
+ let newItems = []
244
+ let addedItems = []
245
+ let removedItems = []
246
+
247
+ if (this._selectMode === Lists.LIST_SELECT_MODE_MULTIPLE) {
248
+ /* In multi-selection mode, simply add/remove new item to/from selection */
249
+ if (oldItems.includes(item)) {
250
+ newItems = oldItems.filter(i => i !== item)
251
+ removedItems.push(item)
252
+ }
253
+ else {
254
+ newItems = oldItems.concat([item])
255
+ addedItems.push(item)
256
+ }
257
+ }
258
+ else { /* Assuming Lists.LIST_SELECT_MODE_SINGLE */
259
+ newItems.push(item)
260
+ removedItems = oldItems
261
+ addedItems.push(item)
262
+ }
263
+
264
+ if (ObjectUtils.deepEquals(oldItems, newItems)) {
265
+ return /* Selection unchanged */
266
+ }
267
+
268
+ let promise = this.onSelectionChange(oldItems, newItems) || Promise.resolve()
269
+
270
+ promise.then(function () {
271
+ try {
272
+ removedItems.forEach(i => i.setSelected(false))
273
+ addedItems.forEach(i => i.setSelected(true))
274
+ }
275
+ catch (e) {
276
+ logger.errorStack('setSelected failed', e)
277
+ }
278
+ }).catch(function (e) {
279
+ if (e == null) {
280
+ logger.debug('selection change rejected')
281
+ }
282
+ else {
283
+ throw e
284
+ }
285
+ })
286
+ }
287
+
288
+ _handleLongPress(item) {
289
+ if (!this._longPressMultipleSelection) {
290
+ return
291
+ }
292
+
293
+ // TODO: replace jQuery longpress plugin with a simple, more integrated long press event manager
294
+ if (this._selectMode === Lists.LIST_SELECT_MODE_SINGLE) {
295
+ this.setSelectMode(Lists.LIST_SELECT_MODE_MULTIPLE)
296
+ let selectedItems = this.getSelectedItems()
297
+ if (!selectedItems.includes(item)) {
298
+ selectedItems.push(item)
299
+ this.setSelectedItems(selectedItems)
300
+ }
301
+ }
302
+ else if (this._selectMode === Lists.LIST_SELECT_MODE_MULTIPLE) {
303
+ this.setSelectMode(Lists.LIST_SELECT_MODE_SINGLE)
304
+ this.setSelectedItems([item])
305
+ }
306
+
307
+ item._wasLongPressed = true
308
+ }
309
+
310
+
311
+ /* Add feature */
312
+
313
+ /**
314
+ * Tell if the add feature is enabled
315
+ * @returns {Boolean}
316
+ */
317
+ isAddEnabled() {
318
+ return this._addEnabled
319
+ }
320
+
321
+ /**
322
+ * Enable the search feature.
323
+ */
324
+ enableAdd() {
325
+ if (this._addEnabled) {
326
+ return
327
+ }
328
+
329
+ this._addEnabled = true
330
+ this._enableAdd(this.getBody())
331
+ }
332
+
333
+ _enableAdd(element) {
334
+ this._addElem = this._makeAddElem()
335
+ element.append(this._addElem)
336
+ element.addClass('add-enabled')
337
+ }
338
+
339
+ /**
340
+ * Disable the add feature.
341
+ */
342
+ disableAdd() {
343
+ if (!this._addEnabled) {
344
+ return
345
+ }
346
+
347
+ this._addEnabled = false
348
+ this._disableAdd()
349
+ }
350
+
351
+ _disableAdd() {
352
+ this._addElem.remove()
353
+ this._addElem = null
354
+ this.getBody().removeClass('add-enabled')
355
+ }
356
+
357
+ _makeAddElem() {
358
+ let addElem = $('<div></div>', {class: 'qui-base-button qui-list-child qui-list-add'})
359
+ let addIcon = $('<div></div>', {class: 'qui-icon'})
360
+ addElem.append(addIcon)
361
+ new StockIcon({name: 'plus', variant: 'interactive'}).applyTo(addIcon)
362
+
363
+ addElem.on('click', function () {
364
+
365
+ let promise = this.onAdd()
366
+ promise = promise || Promise.resolve()
367
+ promise.then(function () {
368
+ try {
369
+ this._items.forEach(i => i.setSelected(false))
370
+ }
371
+ catch (e) {
372
+ logger.errorStack('setSelected failed', e)
373
+ }
374
+ }.bind(this)).catch(function (e) {
375
+ if (e == null) {
376
+ logger.debug('add rejected')
377
+ }
378
+ else {
379
+ throw e
380
+ }
381
+ })
382
+
383
+ }.bind(this))
384
+
385
+ return addElem
386
+ }
387
+
388
+ /**
389
+ * Override this to define the behavior of the list when the add button is pressed.
390
+ * @returns {?Promise} an optional promise which, if rejected with no argument, will cancel adding
391
+ */
392
+ onAdd() {
393
+ }
394
+
395
+
396
+ /* Search feature */
397
+
398
+ _makeSearchElem() {
399
+ let list = this
400
+
401
+ let searchElem = $('<div></div>', {class: 'qui-list-child qui-list-search'})
402
+
403
+ let searchInput = $('<input>', {type: 'text'})
404
+ searchInput.attr('placeholder', gettext('search...'))
405
+
406
+ let searchWrapper = $('<div></div>', {class: 'qui-list-search-wrapper'})
407
+ searchWrapper.append(searchInput)
408
+ searchElem.append(searchWrapper)
409
+
410
+ let searchIcon = $('<div></div>', {class: 'qui-icon'})
411
+ new StockIcon({
412
+ name: 'magnifier', variant: 'interactive',
413
+ activeName: 'magnifier', activeVariant: 'interactive',
414
+ focusedName: 'close', focusedVariant: 'background'
415
+ }).applyTo(searchIcon)
416
+
417
+ searchWrapper.append(searchIcon)
418
+
419
+ searchInput.on('keydown', function (e) {
420
+ if (e.which === 27) {
421
+ if (list._filterInput.val().length) {
422
+ list._clearSearch()
423
+ }
424
+ else {
425
+ list._filterInput.blur()
426
+ }
427
+ }
428
+ })
429
+
430
+ searchInput.on('keyup', function () {
431
+ list._applySearchFilter()
432
+ })
433
+
434
+ searchInput.on('paste', function () {
435
+ list._applySearchFilter()
436
+ })
437
+
438
+ searchIcon.on('pointerdown', function () {
439
+ if (searchInput.is(':focus')) {
440
+ searchInput.blur()
441
+ list._clearSearch()
442
+ return false
443
+ }
444
+ else {
445
+ asap(function () {
446
+ searchInput.focus()
447
+ })
448
+ }
449
+ })
450
+
451
+ return searchElem
452
+ }
453
+
454
+ _applySearchFilter(item = null) {
455
+ let searchText = this._filterInput.val().trim().toLowerCase()
456
+
457
+ searchText = searchText.replace(/\s\s+/g, ' ')
458
+ let searchTextParts = searchText.split(' ')
459
+
460
+ /* If item is specified, apply filtering only to given item */
461
+ if (item) {
462
+ if (!this._filterInput) {
463
+ if (item.isHidden()) {
464
+ item.show()
465
+ }
466
+ }
467
+ else {
468
+ let match = searchTextParts.every(s => item.isMatch(s))
469
+ if (match) {
470
+ if (item.isHidden()) {
471
+ item.show()
472
+ }
473
+ }
474
+ else {
475
+ if (!item.isHidden()) {
476
+ item.hide()
477
+ }
478
+ }
479
+ }
480
+
481
+ return
482
+ }
483
+
484
+ if (!this._filterInput) {
485
+ this._items.filter(i => i.isHidden()).forEach(i => i.show())
486
+ return
487
+ }
488
+
489
+ this._items.forEach(function (item) {
490
+ let match = searchTextParts.every(s => item.isMatch(s))
491
+ if (match) {
492
+ if (item.isHidden()) {
493
+ item.show()
494
+ }
495
+ }
496
+ else {
497
+ if (!item.isHidden()) {
498
+ item.hide()
499
+ }
500
+ }
501
+ })
502
+ }
503
+
504
+ _clearSearch() {
505
+ this._filterInput.val('')
506
+ this._applySearchFilter()
507
+ }
508
+
509
+ /**
510
+ * Tell if the search feature is enabled
511
+ * @returns {Boolean}
512
+ */
513
+ isSearchEnabled() {
514
+ return this._searchEnabled
515
+ }
516
+
517
+ /**
518
+ * Enable the search feature.
519
+ */
520
+ enableSearch() {
521
+ if (this._searchEnabled) {
522
+ return
523
+ }
524
+
525
+ this._searchEnabled = true
526
+ this._enableSearch(this.getBody())
527
+ }
528
+
529
+ _enableSearch(element) {
530
+ this._searchElem = this._makeSearchElem()
531
+ this._filterInput = this._searchElem.find('input[type=text]')
532
+ element.prepend(this._searchElem)
533
+ element.addClass('search-enabled')
534
+ }
535
+
536
+ /**
537
+ * Disable the search feature.
538
+ */
539
+ disableSearch() {
540
+ if (!this._searchEnabled) {
541
+ return
542
+ }
543
+
544
+ this._searchEnabled = false
545
+ this._disableSearch()
546
+ }
547
+
548
+ _disableSearch() {
549
+ this._searchElem.remove()
550
+ this._searchElem = null
551
+ this._filterInput = null
552
+ this.getBody().removeClass('search-enabled')
553
+ this._applySearchFilter()
554
+ }
555
+
556
+
557
+ /* Selection */
558
+
559
+ /**
560
+ * Set selection mode.
561
+ * @param {String} selectMode one of:
562
+ * * {@link qui.lists.LIST_SELECT_MODE_DISABLED}
563
+ * * {@link qui.lists.LIST_SELECT_MODE_SINGLE} (default)
564
+ * * {@link qui.lists.LIST_SELECT_MODE_MULTIPLE}
565
+ */
566
+ setSelectMode(selectMode) {
567
+ this._selectMode = selectMode
568
+
569
+ let selectedItems = this._items.filter(i => i.isSelected())
570
+
571
+ if (this._selectMode === Lists.LIST_SELECT_MODE_DISABLED) {
572
+ selectedItems.forEach(i => i.setSelected(false))
573
+ }
574
+ else if (this._selectMode === Lists.LIST_SELECT_MODE_SINGLE) {
575
+ if (selectedItems.length > 1) {
576
+ selectedItems.slice(1).forEach(i => i.setSelected(false))
577
+ }
578
+ }
579
+
580
+ /* Update HTML class according to new select mode */
581
+ let html = this.getHTML()
582
+ html.removeClass([
583
+ Lists.LIST_SELECT_MODE_DISABLED,
584
+ Lists.LIST_SELECT_MODE_SINGLE,
585
+ Lists.LIST_SELECT_MODE_MULTIPLE
586
+ ].map(m => `select-mode-${m}`).join(' '))
587
+ html.addClass(`select-mode-${this._selectMode}`)
588
+
589
+ /* Update items select mode */
590
+ this.getItems().forEach(i => i.setSelectMode(this._selectMode))
591
+ }
592
+
593
+ /**
594
+ * Return the currently selected items.
595
+ * @returns {qui.lists.ListItem[]}
596
+ */
597
+ getSelectedItems() {
598
+ return this._items.filter(i => i.isSelected())
599
+ }
600
+
601
+ /**
602
+ * Update current selection.
603
+ * @param {qui.lists.ListItem[]} items the list of new items to select; empty list clears selection
604
+ */
605
+ setSelectedItems(items) {
606
+ if (this._selectMode === Lists.LIST_SELECT_MODE_DISABLED) {
607
+ return
608
+ }
609
+ else if (this._selectMode === Lists.LIST_SELECT_MODE_SINGLE) {
610
+ if (items.length > 1) {
611
+ items = items.slice(0, 1) /* Keep only first element in single selection mode */
612
+ }
613
+ }
614
+
615
+ let selectedItems = this._items.filter(i => i.isSelected())
616
+
617
+ /* Remove selection from items no longer selected */
618
+ selectedItems.filter(i => !items.includes(i)).forEach(i => i.setSelected(false))
619
+
620
+ /* Add selection to newly selected items */
621
+ items.filter(i => !selectedItems.includes(i)).forEach(i => i.setSelected(true))
622
+ }
623
+
624
+ /**
625
+ * Called when the current selection is changed by user.
626
+ * @param {qui.lists.ListItem[]} oldItems the previously selected items (can be empty)
627
+ * @param {qui.lists.ListItem[]} newItems the new selected items (can be empty)
628
+ * @returns {?Promise} an optional promise which, if rejected with no argument, will cancel the selection change
629
+ */
630
+ onSelectionChange(oldItems, newItems) {
631
+ }
632
+
633
+ }
634
+
635
+
636
+ export default List
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @namespace qui.lists
3
+ */
4
+
5
+ /**
6
+ * @alias qui.lists.LIST_SELECT_MODE_DISABLED
7
+ */
8
+ export const LIST_SELECT_MODE_DISABLED = 'disabled'
9
+
10
+ /**
11
+ * @alias qui.lists.LIST_SELECT_MODE_SINGLE
12
+ */
13
+ export const LIST_SELECT_MODE_SINGLE = 'single'
14
+
15
+ /**
16
+ * @alias qui.lists.LIST_SELECT_MODE_MULTIPLE
17
+ */
18
+ export const LIST_SELECT_MODE_MULTIPLE = 'multiple'
19
+
20
+
21
+ /**
22
+ * List item match function.
23
+ * @callback qui.lists.ListItemMatchFunc
24
+ * @param {qui.lists.ListItem} item the item to be tested
25
+ * @returns {Boolean} `true` if the item matches the condition, `false` otherwise
26
+ */