@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.
- package/.eslintignore +2 -0
- package/.eslintrc.json +492 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
- package/.github/workflows/main.yml +74 -0
- package/.pre-commit-config.yaml +8 -0
- package/LICENSE.txt +177 -0
- package/README.md +4 -0
- package/font/dejavusans-bold.woff +0 -0
- package/font/dejavusans-bolditalic.woff +0 -0
- package/font/dejavusans-italic.woff +0 -0
- package/font/dejavusans-regular.woff +0 -0
- package/img/qui-icons.svg +1937 -0
- package/js/base/base.js +47 -0
- package/js/base/condition-variable.js +92 -0
- package/js/base/errors.js +36 -0
- package/js/base/i18n.js +20 -0
- package/js/base/mixwith.js +135 -0
- package/js/base/require-js-compat.js +78 -0
- package/js/base/signal.js +91 -0
- package/js/base/singleton.js +66 -0
- package/js/base/timer.js +126 -0
- package/js/config.js +184 -0
- package/js/forms/common-fields/check-field.js +42 -0
- package/js/forms/common-fields/choice-buttons-field.js +30 -0
- package/js/forms/common-fields/color-combo-field.js +37 -0
- package/js/forms/common-fields/combo-field.js +108 -0
- package/js/forms/common-fields/common-fields.js +23 -0
- package/js/forms/common-fields/composite-field.js +132 -0
- package/js/forms/common-fields/custom-html-field.js +51 -0
- package/js/forms/common-fields/email-field.js +30 -0
- package/js/forms/common-fields/file-picker-field.js +46 -0
- package/js/forms/common-fields/jquery-ui-field.js +111 -0
- package/js/forms/common-fields/labels-field.js +69 -0
- package/js/forms/common-fields/numeric-field.js +39 -0
- package/js/forms/common-fields/password-field.js +28 -0
- package/js/forms/common-fields/phone-field.js +26 -0
- package/js/forms/common-fields/progress-disk-field.js +69 -0
- package/js/forms/common-fields/push-button-field.js +138 -0
- package/js/forms/common-fields/slider-field.js +51 -0
- package/js/forms/common-fields/text-area-field.js +34 -0
- package/js/forms/common-fields/text-field.js +89 -0
- package/js/forms/common-fields/up-down-field.js +85 -0
- package/js/forms/common-forms/common-forms.js +16 -0
- package/js/forms/common-forms/options-form.js +77 -0
- package/js/forms/common-forms/page-form.js +115 -0
- package/js/forms/form-button.js +202 -0
- package/js/forms/form-field.js +1183 -0
- package/js/forms/form.js +1181 -0
- package/js/forms/forms.js +68 -0
- package/js/global-glass.js +100 -0
- package/js/icons/default-stock.js +173 -0
- package/js/icons/icon.js +64 -0
- package/js/icons/icons.js +16 -0
- package/js/icons/multi-state-sprites-icon.js +362 -0
- package/js/icons/stock-icon.js +219 -0
- package/js/icons/stock.js +98 -0
- package/js/icons/stocks.js +57 -0
- package/js/index.js +232 -0
- package/js/lib/jquery.longpress.js +79 -0
- package/js/lib/jquery.module.js +4 -0
- package/js/lib/logger.module.js +4 -0
- package/js/lib/pep.module.js +4 -0
- package/js/lists/common-items/common-items.js +5 -0
- package/js/lists/common-items/icon-label-list-item.js +86 -0
- package/js/lists/common-lists/common-lists.js +5 -0
- package/js/lists/common-lists/page-list.js +53 -0
- package/js/lists/list-item.js +147 -0
- package/js/lists/list.js +636 -0
- package/js/lists/lists.js +26 -0
- package/js/main-ui/main-ui.js +64 -0
- package/js/main-ui/menu-bar.js +144 -0
- package/js/main-ui/options-bar.js +181 -0
- package/js/main-ui/status.js +185 -0
- package/js/main-ui/top-bar.js +59 -0
- package/js/messages/common-message-forms/common-message-forms.js +7 -0
- package/js/messages/common-message-forms/confirm-message-form.js +81 -0
- package/js/messages/common-message-forms/simple-message-form.js +67 -0
- package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
- package/js/messages/message-form.js +107 -0
- package/js/messages/messages.js +21 -0
- package/js/messages/sticky-modal-page.js +98 -0
- package/js/messages/sticky-modal-progress-message.js +27 -0
- package/js/messages/toast.js +164 -0
- package/js/navigation.js +654 -0
- package/js/pages/breadcrumbs.js +124 -0
- package/js/pages/common-pages/common-pages.js +6 -0
- package/js/pages/common-pages/modal-progress-page.js +83 -0
- package/js/pages/common-pages/structured-page.js +46 -0
- package/js/pages/page.js +1018 -0
- package/js/pages/pages-context.js +154 -0
- package/js/pages/pages.js +252 -0
- package/js/pwa.js +337 -0
- package/js/sections/section.js +612 -0
- package/js/sections/sections.js +300 -0
- package/js/tables/common-cells/common-cells.js +7 -0
- package/js/tables/common-cells/icon-label-table-cell.js +68 -0
- package/js/tables/common-cells/push-button-table-cell.js +133 -0
- package/js/tables/common-cells/simple-table-cell.js +37 -0
- package/js/tables/common-tables/common-tables.js +5 -0
- package/js/tables/common-tables/page-table.js +55 -0
- package/js/tables/table-cell.js +198 -0
- package/js/tables/table-row.js +126 -0
- package/js/tables/table.js +492 -0
- package/js/tables/tables.js +36 -0
- package/js/theme.js +304 -0
- package/js/utils/ajax.js +126 -0
- package/js/utils/array.js +194 -0
- package/js/utils/colors.js +445 -0
- package/js/utils/cookies.js +65 -0
- package/js/utils/crypto.js +439 -0
- package/js/utils/css.js +234 -0
- package/js/utils/date.js +300 -0
- package/js/utils/files.js +27 -0
- package/js/utils/gestures.js +165 -0
- package/js/utils/html.js +76 -0
- package/js/utils/misc.js +81 -0
- package/js/utils/object.js +324 -0
- package/js/utils/promise.js +49 -0
- package/js/utils/string.js +192 -0
- package/js/utils/url.js +187 -0
- package/js/utils/utils.js +3 -0
- package/js/utils/visibility-manager.js +211 -0
- package/js/views/common-views/common-views.js +7 -0
- package/js/views/common-views/icon-label-view.js +210 -0
- package/js/views/common-views/progress-view.js +89 -0
- package/js/views/common-views/structured-view.js +368 -0
- package/js/views/view.js +467 -0
- package/js/views/views.js +3 -0
- package/js/widgets/base-widget.js +23 -0
- package/js/widgets/common-widgets/check-button.js +109 -0
- package/js/widgets/common-widgets/choice-buttons.js +322 -0
- package/js/widgets/common-widgets/color-combo.js +104 -0
- package/js/widgets/common-widgets/combo.js +645 -0
- package/js/widgets/common-widgets/common-widgets.js +17 -0
- package/js/widgets/common-widgets/email-input.js +7 -0
- package/js/widgets/common-widgets/file-picker.js +133 -0
- package/js/widgets/common-widgets/labels.js +132 -0
- package/js/widgets/common-widgets/numeric-input.js +49 -0
- package/js/widgets/common-widgets/password-input.js +91 -0
- package/js/widgets/common-widgets/phone-input.js +7 -0
- package/js/widgets/common-widgets/progress-disk.js +174 -0
- package/js/widgets/common-widgets/push-button.js +155 -0
- package/js/widgets/common-widgets/slider.js +455 -0
- package/js/widgets/common-widgets/text-area.js +52 -0
- package/js/widgets/common-widgets/text-input.js +174 -0
- package/js/widgets/common-widgets/up-down.js +351 -0
- package/js/widgets/widgets.js +57 -0
- package/js/window.js +557 -0
- package/jsdoc.conf.json +20 -0
- package/less/base.less +123 -0
- package/less/forms/common-fields.less +101 -0
- package/less/forms/common-forms.less +5 -0
- package/less/forms/form-button.less +21 -0
- package/less/forms/form-field.less +266 -0
- package/less/forms/form.less +131 -0
- package/less/global-glass.less +64 -0
- package/less/icon-label-view.less +82 -0
- package/less/icons.less +144 -0
- package/less/lists.less +105 -0
- package/less/main-ui.less +328 -0
- package/less/messages.less +189 -0
- package/less/no-effects.less +24 -0
- package/less/pages/breadcrumbs.less +98 -0
- package/less/pages/common-pages.less +36 -0
- package/less/pages/page.less +70 -0
- package/less/progress-view.less +51 -0
- package/less/stock-icons.less +43 -0
- package/less/structured-view.less +245 -0
- package/less/tables.less +84 -0
- package/less/theme-dark.less +133 -0
- package/less/theme-light.less +132 -0
- package/less/theme.less +419 -0
- package/less/visibility-manager.less +11 -0
- package/less/widgets/check-button.less +96 -0
- package/less/widgets/choice-buttons.less +160 -0
- package/less/widgets/color-combo.less +33 -0
- package/less/widgets/combo.less +230 -0
- package/less/widgets/common-buttons.less +120 -0
- package/less/widgets/common.less +24 -0
- package/less/widgets/input.less +258 -0
- package/less/widgets/labels.less +81 -0
- package/less/widgets/progress-disk.less +70 -0
- package/less/widgets/slider.less +199 -0
- package/less/widgets/updown.less +115 -0
- package/less/widgets/various.less +36 -0
- package/package.json +47 -0
- package/pyproject.toml +45 -0
- package/qui/__init__.py +110 -0
- package/qui/constants.py +1 -0
- package/qui/exceptions.py +2 -0
- package/qui/j2template.py +71 -0
- package/qui/settings.py +60 -0
- package/qui/templates/manifest.json +25 -0
- package/qui/templates/qui.html +126 -0
- package/qui/templates/service-worker.js +188 -0
- package/qui/web/__init__.py +0 -0
- package/qui/web/tornado.py +220 -0
- package/scripts/postinstall.sh +10 -0
- package/webpack/webpack-adjust-css-urls-loader.js +36 -0
- package/webpack/webpack-common.js +384 -0
package/js/lists/list.js
ADDED
|
@@ -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
|
+
*/
|