@qtoggle/qui 1.19.11 → 1.20.0-alpha.2
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/.github/workflows/main.yml +15 -4
- package/js/forms/common-fields/combo-field.js +3 -1
- package/js/index.js +1 -2
- package/js/lists/common-items/icon-label-list-item.js +7 -12
- package/js/lists/list-item.js +18 -4
- package/js/lists/list.js +175 -49
- package/js/pages/pages.js +54 -6
- package/js/utils/gestures.js +137 -0
- package/js/utils/string.js +35 -8
- package/js/widgets/base-widget.js +1 -1
- package/js/widgets/common-widgets/combo.js +5 -4
- package/js/widgets/common-widgets/up-down.js +18 -13
- package/less/lists.less +18 -0
- package/package.json +4 -4
- package/pyproject.toml +2 -1
- package/scripts/postinstall.sh +1 -1
- package/js/lib/jquery.longpress.js +0 -79
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
- name: Setup NodeJS
|
|
32
32
|
uses: actions/setup-node@v4
|
|
33
33
|
with:
|
|
34
|
-
node-version: '
|
|
34
|
+
node-version: '24'
|
|
35
35
|
- name: Install
|
|
36
36
|
run: |
|
|
37
37
|
npm install && rm -rf docs/* js/lib/* && npx jsdoc -c jsdoc.conf.json
|
|
@@ -63,11 +63,22 @@ jobs:
|
|
|
63
63
|
- name: Setup NodeJS
|
|
64
64
|
uses: actions/setup-node@v4
|
|
65
65
|
with:
|
|
66
|
-
node-version: '
|
|
66
|
+
node-version: '24'
|
|
67
67
|
- name: Publish to NPM
|
|
68
68
|
run: |
|
|
69
|
-
npm install -g npm@
|
|
70
|
-
|
|
69
|
+
npm install -g npm@12
|
|
70
|
+
version=$(node -p "require('./package.json').version")
|
|
71
|
+
if [[ "${version}" == *-* ]]; then
|
|
72
|
+
# A prerelease must not take over the "latest" dist-tag, and npm insists on an explicit
|
|
73
|
+
# --tag for one. Name the tag after the prerelease identifier: 1.20.0-alpha.1 -> alpha.
|
|
74
|
+
npm_tag="${version#*-}"
|
|
75
|
+
npm_tag="${npm_tag%%.*}"
|
|
76
|
+
echo "Publishing ${version} under dist-tag ${npm_tag}"
|
|
77
|
+
npm publish --tag "${npm_tag}"
|
|
78
|
+
else
|
|
79
|
+
echo "Publishing ${version} under dist-tag latest"
|
|
80
|
+
npm publish
|
|
81
|
+
fi
|
|
71
82
|
- name: Fetch python dist folder
|
|
72
83
|
uses: actions/download-artifact@v4
|
|
73
84
|
with:
|
|
@@ -80,7 +80,9 @@ class ComboField extends JQueryUIField {
|
|
|
80
80
|
/**
|
|
81
81
|
* Tell if a choice matches a search text or not.
|
|
82
82
|
* @param {Object} choice
|
|
83
|
-
* @param {String} searchText
|
|
83
|
+
* @param {String|RegExp} searchText the search text, compiled with
|
|
84
|
+
* {@link qui.utils.string.intelliSearchRegExp} and suitable for passing straight to
|
|
85
|
+
* {@link qui.utils.string.intelliSearch}
|
|
84
86
|
* @returns {Boolean}
|
|
85
87
|
*/
|
|
86
88
|
filterFunc(choice, searchText) {
|
package/js/index.js
CHANGED
|
@@ -6,9 +6,8 @@ import * as RequireJSCompat from '$qui/base/require-js-compat.js'
|
|
|
6
6
|
import $ from '$qui/lib/jquery.module.js'
|
|
7
7
|
import Logger from '$qui/lib/logger.module.js'
|
|
8
8
|
|
|
9
|
-
import '$qui/lib/jquery-ui.js'
|
|
9
|
+
import '$qui/lib/jquery-ui-widget.js'
|
|
10
10
|
import '$qui/lib/jquery.mousewheel.js'
|
|
11
|
-
import '$qui/lib/jquery.longpress.js'
|
|
12
11
|
import '$qui/lib/pep.js'
|
|
13
12
|
|
|
14
13
|
import {globalize} from '$qui/base/base.js'
|
|
@@ -38,26 +38,21 @@ class IconLabelListItem extends mix(ListItem).with(IconLabelViewMixin) {
|
|
|
38
38
|
this.setClickable(selectMode !== Lists.LIST_SELECT_MODE_DISABLED)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Return the text a search filter is matched against: the label and the sub-label joined in the order in which
|
|
43
|
+
* they are displayed, so that a filter can span both.
|
|
44
|
+
* @returns {String}
|
|
45
|
+
*/
|
|
41
46
|
getMatchPhrase() {
|
|
42
47
|
if (this._matchPhrase == null) {
|
|
43
|
-
this._matchPhrase = []
|
|
44
|
-
|
|
45
|
-
/* Consider the entire label as is */
|
|
46
|
-
if (this.getLabel()) {
|
|
47
|
-
this._matchPhrase.push(this.getLabel().toLowerCase())
|
|
48
|
-
}
|
|
49
|
-
if (this.getSubLabel()) {
|
|
50
|
-
this._matchPhrase.push(this.getSubLabel().toLowerCase())
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
this._matchPhrase = this._matchPhrase.filter(p => Boolean(p))
|
|
48
|
+
this._matchPhrase = [this.getLabel(), this.getSubLabel()].filter(Boolean).join(' ')
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
return this._matchPhrase
|
|
57
52
|
}
|
|
58
53
|
|
|
59
54
|
isMatch(filter) {
|
|
60
|
-
return this.getMatchPhrase()
|
|
55
|
+
return StringUtils.intelliSearch(this.getMatchPhrase(), filter) != null
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
|
package/js/lists/list-item.js
CHANGED
|
@@ -93,10 +93,24 @@ class ListItem extends mix().with(ViewMixin) {
|
|
|
93
93
|
/* Visibility */
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
* Tell if the item is hidden
|
|
96
|
+
* Tell if the item is hidden, either because it has been explicitly hidden or because it is currently filtered
|
|
97
|
+
* out by its list's search filter.
|
|
97
98
|
* @returns {Boolean}
|
|
98
99
|
*/
|
|
99
100
|
isHidden() {
|
|
101
|
+
if (this._list != null && this._list.isItemFilteredOut(this)) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.isExplicitlyHidden()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tell if the item has been hidden with {@link qui.lists.ListItem#hide}, as opposed to being filtered out by its
|
|
110
|
+
* list's search filter.
|
|
111
|
+
* @returns {Boolean}
|
|
112
|
+
*/
|
|
113
|
+
isExplicitlyHidden() {
|
|
100
114
|
return !this._visibilityManager.isElementVisible()
|
|
101
115
|
}
|
|
102
116
|
|
|
@@ -117,12 +131,12 @@ class ListItem extends mix().with(ViewMixin) {
|
|
|
117
131
|
/**
|
|
118
132
|
* Tell if item matches a search filter. By default, uses {@link qui.utils.string.intelliSearch} on textual content
|
|
119
133
|
* of the HTML element.
|
|
120
|
-
* @param {String} filter search filter
|
|
134
|
+
* @param {String|RegExp} filter search filter, possibly precompiled with
|
|
135
|
+
* {@link qui.utils.string.intelliSearchRegExp}
|
|
121
136
|
* @returns {Boolean}
|
|
122
137
|
*/
|
|
123
138
|
isMatch(filter) {
|
|
124
|
-
|
|
125
|
-
return StringUtils.intelliSearch(text, filter) != null
|
|
139
|
+
return StringUtils.intelliSearch(this.getHTML().text().trim(), filter) != null
|
|
126
140
|
}
|
|
127
141
|
|
|
128
142
|
/**
|
package/js/lists/list.js
CHANGED
|
@@ -6,13 +6,23 @@ import {gettext} from '$qui/base/i18n.js'
|
|
|
6
6
|
import {mix} from '$qui/base/mixwith.js'
|
|
7
7
|
import StockIcon from '$qui/icons/stock-icon.js'
|
|
8
8
|
import * as Lists from '$qui/lists/lists.js'
|
|
9
|
+
import * as Theme from '$qui/theme.js'
|
|
10
|
+
import Debouncer from '$qui/utils/debouncer.js'
|
|
11
|
+
import * as Gestures from '$qui/utils/gestures.js'
|
|
9
12
|
import {asap} from '$qui/utils/misc.js'
|
|
10
|
-
import * as
|
|
13
|
+
import * as StringUtils from '$qui/utils/string.js'
|
|
11
14
|
import {ProgressViewMixin} from '$qui/views/common-views/common-views.js'
|
|
12
15
|
import {StructuredViewMixin} from '$qui/views/common-views/common-views.js'
|
|
13
16
|
import ViewMixin from '$qui/views/view.js'
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
/* Associates item elements with their items. A WeakMap is used rather than jQuery's element data, which would
|
|
20
|
+
* store the item in an expando on the DOM element itself, creating an item -> element -> item reference cycle. */
|
|
21
|
+
const itemsByElement = new WeakMap()
|
|
22
|
+
|
|
23
|
+
/* How long to wait after the last keystroke before filtering the list, in milliseconds */
|
|
24
|
+
const SEARCH_FILTER_DELAY = 100
|
|
25
|
+
|
|
16
26
|
const logger = Logger.get('qui.lists.list')
|
|
17
27
|
|
|
18
28
|
|
|
@@ -58,6 +68,13 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
58
68
|
this._addElem = null
|
|
59
69
|
this._searchElem = null
|
|
60
70
|
this._filterInput = null
|
|
71
|
+
|
|
72
|
+
/* Search filtering state */
|
|
73
|
+
this._filteredOutItems = new Set()
|
|
74
|
+
this._pendingReveal = new Set()
|
|
75
|
+
this._filterCollapseTimeout = null
|
|
76
|
+
this._revealFrameHandle = null
|
|
77
|
+
this._applySearchFilterDebouncer = new Debouncer(() => this._applySearchFilter(), SEARCH_FILTER_DELAY)
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
makeHTML() {
|
|
@@ -82,6 +99,28 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
82
99
|
makeBody() {
|
|
83
100
|
let bodyDiv = $('<div></div>', {class: 'qui-list-body'})
|
|
84
101
|
|
|
102
|
+
/* Item events are delegated to the list body rather than bound to each item. A list of a few hundred items
|
|
103
|
+
* would otherwise install a few thousand event handlers, including a mousemove handler per item. */
|
|
104
|
+
|
|
105
|
+
bodyDiv.on('click', 'div.qui-list-item', function (e) {
|
|
106
|
+
let item = this._itemFromElement($(e.currentTarget))
|
|
107
|
+
if (item) {
|
|
108
|
+
this._handleItemClick(item)
|
|
109
|
+
}
|
|
110
|
+
}.bind(this))
|
|
111
|
+
|
|
112
|
+
if (this._longPressMultipleSelection) {
|
|
113
|
+
Gestures.enableLongPress(bodyDiv, {
|
|
114
|
+
selector: 'div.qui-list-item',
|
|
115
|
+
onLongPress: function (element) {
|
|
116
|
+
let item = this._itemFromElement(element)
|
|
117
|
+
if (item) {
|
|
118
|
+
this._handleLongPress(item)
|
|
119
|
+
}
|
|
120
|
+
}.bind(this)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
if (this._searchEnabled) {
|
|
86
125
|
this._enableSearch(bodyDiv)
|
|
87
126
|
}
|
|
@@ -109,7 +148,10 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
109
148
|
* @param {qui.lists.ListItem[]} items list items
|
|
110
149
|
*/
|
|
111
150
|
setItems(items) {
|
|
112
|
-
this._items.forEach(
|
|
151
|
+
this._items.forEach(function (i) {
|
|
152
|
+
this._forgetItemFilter(i)
|
|
153
|
+
i.getHTML().remove()
|
|
154
|
+
}, this)
|
|
113
155
|
|
|
114
156
|
items.forEach(i => this.prepareItem(i))
|
|
115
157
|
this._items = items
|
|
@@ -140,7 +182,10 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
140
182
|
this._applySearchFilter(item)
|
|
141
183
|
}
|
|
142
184
|
|
|
143
|
-
this._items[index]
|
|
185
|
+
let oldItem = this._items[index]
|
|
186
|
+
this._forgetItemFilter(oldItem)
|
|
187
|
+
|
|
188
|
+
oldItem.getHTML().replaceWith(item.getHTML())
|
|
144
189
|
this._items[index] = item
|
|
145
190
|
|
|
146
191
|
}
|
|
@@ -180,8 +225,10 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
180
225
|
* @returns {?qui.lists.ListItem} the removed item
|
|
181
226
|
*/
|
|
182
227
|
removeItemAt(index) {
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
let item = this._items[index]
|
|
229
|
+
if (item) {
|
|
230
|
+
this._forgetItemFilter(item)
|
|
231
|
+
item.getHTML().remove()
|
|
185
232
|
}
|
|
186
233
|
|
|
187
234
|
return this._items.splice(index, 1)[0] || null
|
|
@@ -220,14 +267,26 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
220
267
|
prepareItem(item) {
|
|
221
268
|
item.setList(this)
|
|
222
269
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
html.on('click', this._handleItemClick.bind(this, item))
|
|
226
|
-
html.longpress(this._handleLongPress.bind(this))
|
|
270
|
+
/* Associate the item with its element, so that delegated event handlers can find it back */
|
|
271
|
+
itemsByElement.set(item.getHTML()[0], item)
|
|
227
272
|
|
|
228
273
|
item.setSelectMode(this._selectMode)
|
|
229
274
|
}
|
|
230
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Return the item that owns a given element, if it belongs to this list.
|
|
278
|
+
* @param {jQuery} element
|
|
279
|
+
* @returns {?qui.lists.ListItem}
|
|
280
|
+
*/
|
|
281
|
+
_itemFromElement(element) {
|
|
282
|
+
let item = itemsByElement.get(element[0])
|
|
283
|
+
if (!item || item.getList() !== this) {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return item
|
|
288
|
+
}
|
|
289
|
+
|
|
231
290
|
_handleItemClick(item) {
|
|
232
291
|
/* Flag to prevent handling clicks on long press */
|
|
233
292
|
if (item._wasLongPressed) {
|
|
@@ -261,7 +320,9 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
261
320
|
addedItems.push(item)
|
|
262
321
|
}
|
|
263
322
|
|
|
264
|
-
|
|
323
|
+
/* Items are compared by identity: deep comparison would walk each item's entire object graph, including its
|
|
324
|
+
* HTML element and everything reachable from it */
|
|
325
|
+
if (oldItems.length === newItems.length && oldItems.every((item, i) => item === newItems[i])) {
|
|
265
326
|
return /* Selection unchanged */
|
|
266
327
|
}
|
|
267
328
|
|
|
@@ -290,7 +351,6 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
290
351
|
return
|
|
291
352
|
}
|
|
292
353
|
|
|
293
|
-
// TODO: replace jQuery longpress plugin with a simple, more integrated long press event manager
|
|
294
354
|
if (this._selectMode === Lists.LIST_SELECT_MODE_SINGLE) {
|
|
295
355
|
this.setSelectMode(Lists.LIST_SELECT_MODE_MULTIPLE)
|
|
296
356
|
let selectedItems = this.getSelectedItems()
|
|
@@ -428,11 +488,11 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
428
488
|
})
|
|
429
489
|
|
|
430
490
|
searchInput.on('keyup', function () {
|
|
431
|
-
list.
|
|
491
|
+
list._applySearchFilterDebouncer.call()
|
|
432
492
|
})
|
|
433
493
|
|
|
434
494
|
searchInput.on('paste', function () {
|
|
435
|
-
list.
|
|
495
|
+
list._applySearchFilterDebouncer.call()
|
|
436
496
|
})
|
|
437
497
|
|
|
438
498
|
searchIcon.on('pointerdown', function () {
|
|
@@ -451,54 +511,120 @@ class List extends mix().with(ViewMixin, StructuredViewMixin, ProgressViewMixin)
|
|
|
451
511
|
return searchElem
|
|
452
512
|
}
|
|
453
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Tell if an item is currently filtered out by the search filter.
|
|
516
|
+
* @param {qui.lists.ListItem} item
|
|
517
|
+
* @returns {Boolean}
|
|
518
|
+
*/
|
|
519
|
+
isItemFilteredOut(item) {
|
|
520
|
+
return this._filteredOutItems.has(item)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
_forgetItemFilter(item) {
|
|
524
|
+
this._pendingReveal.delete(item)
|
|
525
|
+
|
|
526
|
+
if (!this._filteredOutItems.delete(item)) {
|
|
527
|
+
return /* The filter never touched this item's element */
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* Undo what the filter did, so that an item that is added to a list again does not stay invisible */
|
|
531
|
+
item.getHTML().css({opacity: '', display: ''})
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
_makeSearchExpression() {
|
|
535
|
+
if (!this._filterInput) {
|
|
536
|
+
return null
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
let searchText = this._filterInput.val().trim()
|
|
540
|
+
if (!searchText) {
|
|
541
|
+
return null
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* The whole search text is compiled into a single expression, which also takes care of splitting it into
|
|
545
|
+
* groups. Compiling it here means compiling it once per list rather than once per item. */
|
|
546
|
+
return StringUtils.intelliSearchRegExp(searchText)
|
|
547
|
+
}
|
|
548
|
+
|
|
454
549
|
_applySearchFilter(item = null) {
|
|
455
|
-
let
|
|
550
|
+
let searchExpression = this._makeSearchExpression()
|
|
551
|
+
let items = item ? [item] : this._items
|
|
552
|
+
|
|
553
|
+
/* Filtering is applied to the whole list at once: items are faded together, collapsed together by a single
|
|
554
|
+
* timer, and revealed together on a single frame. Going through each item's visibility manager instead would
|
|
555
|
+
* schedule two timeouts per item whose visibility changes.
|
|
556
|
+
*
|
|
557
|
+
* Visibility is driven by inline styles rather than by classes, so that filtering does not depend on a
|
|
558
|
+
* stylesheet built from the same sources as this file. The fade comes from the opacity transition that
|
|
559
|
+
* div.qui-list-child already carries. show() and hide() drive the same inline properties through the item's
|
|
560
|
+
* visibility manager, so revealing an item leaves it hidden if it has also been explicitly hidden. */
|
|
561
|
+
|
|
562
|
+
let toReveal = []
|
|
563
|
+
let toCollapse = []
|
|
564
|
+
|
|
565
|
+
items.forEach(function (item) {
|
|
566
|
+
|
|
567
|
+
let filteredOut = searchExpression != null && !item.isMatch(searchExpression)
|
|
568
|
+
if (filteredOut === this._filteredOutItems.has(item)) {
|
|
569
|
+
return /* Nothing to do for this item */
|
|
570
|
+
}
|
|
456
571
|
|
|
457
|
-
|
|
458
|
-
let searchTextParts = searchText.split(' ')
|
|
572
|
+
let html = item.getHTML()
|
|
459
573
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
574
|
+
if (filteredOut) {
|
|
575
|
+
this._filteredOutItems.add(item)
|
|
576
|
+
this._pendingReveal.delete(item)
|
|
577
|
+
|
|
578
|
+
if (html[0].isConnected) {
|
|
579
|
+
html.css('opacity', '0') /* Starts fading out */
|
|
580
|
+
toCollapse.push(item)
|
|
581
|
+
}
|
|
582
|
+
else { /* Not part of the document yet, so there is nothing to transition from */
|
|
583
|
+
html.css({opacity: '0', display: 'none'})
|
|
465
584
|
}
|
|
466
585
|
}
|
|
467
586
|
else {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (!item.isHidden()) {
|
|
476
|
-
item.hide()
|
|
477
|
-
}
|
|
478
|
-
}
|
|
587
|
+
this._filteredOutItems.delete(item)
|
|
588
|
+
|
|
589
|
+
/* Take up layout again, still transparent, and start fading in on the next frame, unless the item
|
|
590
|
+
* has been explicitly hidden, in which case its visibility manager owns the display property */
|
|
591
|
+
html.css('display', item.isExplicitlyHidden() ? 'none' : '')
|
|
592
|
+
this._pendingReveal.add(item)
|
|
593
|
+
toReveal.push(item)
|
|
479
594
|
}
|
|
480
595
|
|
|
481
|
-
|
|
596
|
+
}, this)
|
|
597
|
+
|
|
598
|
+
if (toCollapse.length) {
|
|
599
|
+
this._scheduleFilterCollapse()
|
|
482
600
|
}
|
|
483
601
|
|
|
484
|
-
if (
|
|
485
|
-
this.
|
|
486
|
-
|
|
602
|
+
if (toReveal.length && this._revealFrameHandle == null) {
|
|
603
|
+
this._revealFrameHandle = window.requestAnimationFrame(function () {
|
|
604
|
+
|
|
605
|
+
this._revealFrameHandle = null
|
|
606
|
+
this._pendingReveal.forEach(function (item) {
|
|
607
|
+
if (!this._filteredOutItems.has(item)) {
|
|
608
|
+
item.getHTML().css('opacity', '')
|
|
609
|
+
}
|
|
610
|
+
}, this)
|
|
611
|
+
this._pendingReveal.clear()
|
|
612
|
+
|
|
613
|
+
}.bind(this))
|
|
487
614
|
}
|
|
615
|
+
}
|
|
488
616
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
})
|
|
617
|
+
_scheduleFilterCollapse() {
|
|
618
|
+
if (this._filterCollapseTimeout != null) {
|
|
619
|
+
return /* Items added to the batch in the meantime are collapsed by the pending timeout */
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
this._filterCollapseTimeout = setTimeout(function () {
|
|
623
|
+
|
|
624
|
+
this._filterCollapseTimeout = null
|
|
625
|
+
this._filteredOutItems.forEach(i => i.getHTML().css('display', 'none'))
|
|
626
|
+
|
|
627
|
+
}.bind(this), Theme.getTransitionDuration())
|
|
502
628
|
}
|
|
503
629
|
|
|
504
630
|
_clearSearch() {
|
package/js/pages/pages.js
CHANGED
|
@@ -17,6 +17,12 @@ import * as Breadcrumbs from './breadcrumbs.js'
|
|
|
17
17
|
let pagesContainer = null
|
|
18
18
|
let currentContext = null
|
|
19
19
|
|
|
20
|
+
/* Scroll handling state. Scroll events are coalesced into one update per frame, and that update reads before it
|
|
21
|
+
* writes, so that a write never sits between two reads and forces a synchronous layout. */
|
|
22
|
+
let scrollUpdatePending = false
|
|
23
|
+
let scrolledPages = new Set()
|
|
24
|
+
let contentScrolled = null
|
|
25
|
+
|
|
20
26
|
|
|
21
27
|
/* Page context */
|
|
22
28
|
|
|
@@ -151,9 +157,8 @@ function updatePagesVisibility() {
|
|
|
151
157
|
GlobalGlass.updateVisibility()
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
let scrolled = currentContext.getPages().some(function (p) {
|
|
160
|
+
function isContentScrolled() {
|
|
161
|
+
return currentContext.getPages().some(function (p) {
|
|
157
162
|
if (!p.isVisible()) {
|
|
158
163
|
return false
|
|
159
164
|
}
|
|
@@ -161,19 +166,61 @@ function updateContentScroll() {
|
|
|
161
166
|
return p.getPageHTML()[0].scrollTop !== 0
|
|
162
167
|
|
|
163
168
|
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function applyContentScrolled(scrolled) {
|
|
172
|
+
if (scrolled === contentScrolled) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
contentScrolled = scrolled
|
|
164
177
|
Window.$body.toggleClass('content-scrolled', scrolled)
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
function
|
|
168
|
-
|
|
180
|
+
function updateContentScroll() {
|
|
181
|
+
applyContentScrolled(isContentScrolled())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function runScrollUpdate() {
|
|
185
|
+
scrollUpdatePending = false
|
|
186
|
+
|
|
187
|
+
let pages = Array.from(scrolledPages)
|
|
188
|
+
scrolledPages.clear()
|
|
189
|
+
|
|
190
|
+
/* Read phase: nothing has been written yet in this frame, so these reads are served from a clean layout */
|
|
191
|
+
let scrolled = isContentScrolled()
|
|
169
192
|
|
|
193
|
+
/* Page handlers read their own scroll state before writing to their own elements. They run before the body class
|
|
194
|
+
* is updated, so that no write precedes their reads. */
|
|
195
|
+
pages.forEach(function (page) {
|
|
196
|
+
/* The page may have been removed from its context, or its whole context may have been swapped out by
|
|
197
|
+
* setCurrentContext(), between the scroll event and this frame */
|
|
198
|
+
if (page.getContext() !== currentContext) {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
page.handleVertScroll()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
/* Write phase */
|
|
206
|
+
applyContentScrolled(scrolled)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handlePageScroll() {
|
|
170
210
|
let $this = $(this)
|
|
171
211
|
let page = $this.data('page')
|
|
172
212
|
if (!page) {
|
|
173
213
|
throw AssertionError('page scroll event from a non-page HTML element')
|
|
174
214
|
}
|
|
175
215
|
|
|
176
|
-
|
|
216
|
+
scrolledPages.add(page)
|
|
217
|
+
|
|
218
|
+
if (scrollUpdatePending) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
scrollUpdatePending = true
|
|
223
|
+
window.requestAnimationFrame(runScrollUpdate)
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
function triggerPageResize() {
|
|
@@ -202,6 +249,7 @@ function attachPageHTMLHandlers(page) {
|
|
|
202
249
|
}
|
|
203
250
|
|
|
204
251
|
function detachPageHTMLHandlers(page) {
|
|
252
|
+
scrolledPages.delete(page)
|
|
205
253
|
page.getPageHTML().off('scroll', handlePageScroll)
|
|
206
254
|
page.getPageHTML().off('transitionend', triggerPageResizeOnTransitionEnd)
|
|
207
255
|
}
|
package/js/utils/gestures.js
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
* @namespace qui.utils.gestures
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import $ from '$qui/lib/jquery.module.js'
|
|
6
|
+
|
|
5
7
|
import * as Window from '$qui/window.js'
|
|
6
8
|
|
|
7
9
|
|
|
10
|
+
const LONG_PRESS_DATA_KEY = 'qui.utils.gestures.longPress'
|
|
11
|
+
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Drag Move Callback Function.
|
|
10
15
|
* @callback qui.utils.gestures.DragMoveCallback
|
|
@@ -163,3 +168,135 @@ export function disableDragging(element) {
|
|
|
163
168
|
element.attr('touch-action', '') /* Required for pep.js (on iOS) */
|
|
164
169
|
element.off('pointerdown', draggingData.pointerDown)
|
|
165
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Long Press Callback Function.
|
|
174
|
+
* @callback qui.utils.gestures.LongPressCallback
|
|
175
|
+
* @param {jQuery} element the pressed element
|
|
176
|
+
* @param {jQuery.Event} event the pointer event that started the press
|
|
177
|
+
*/
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Setup long press detection on an HTML element.
|
|
181
|
+
*
|
|
182
|
+
* Detection is based on pointer events, which never block scrolling; a press is automatically abandoned as soon as
|
|
183
|
+
* the browser starts scrolling with the same pointer, or as soon as the pointer moves further than `moveThreshold`.
|
|
184
|
+
*
|
|
185
|
+
* When a `selector` is given, presses are detected for matching descendants of `element` rather than for `element`
|
|
186
|
+
* itself. This allows handling long presses on a large number of children with a single set of event handlers.
|
|
187
|
+
*
|
|
188
|
+
* @alias qui.utils.gestures.enableLongPress
|
|
189
|
+
* @param {jQuery} element the element on which presses are detected
|
|
190
|
+
* @param {qui.utils.gestures.LongPressCallback} onLongPress called when the element has been pressed for at least
|
|
191
|
+
* `duration` milliseconds
|
|
192
|
+
* @param {?qui.utils.gestures.LongPressCallback} [onShortPress] called when the element has been released before
|
|
193
|
+
* `duration` milliseconds have elapsed
|
|
194
|
+
* @param {?String} [selector] an optional selector restricting detection to matching descendants of `element`
|
|
195
|
+
* @param {Number} [duration] how long the element must be pressed for a long press, in milliseconds (defaults to
|
|
196
|
+
* `500`)
|
|
197
|
+
* @param {Number} [moveThreshold] how far the pointer may move before the press is abandoned, in pixels (defaults to
|
|
198
|
+
* `10`)
|
|
199
|
+
*/
|
|
200
|
+
export function enableLongPress(
|
|
201
|
+
element,
|
|
202
|
+
{onLongPress, onShortPress = null, selector = null, duration = 500, moveThreshold = 10}
|
|
203
|
+
) {
|
|
204
|
+
let timeoutHandle = null
|
|
205
|
+
let pressedElement = null
|
|
206
|
+
let pressedPointerId = null
|
|
207
|
+
let startPageX = 0
|
|
208
|
+
let startPageY = 0
|
|
209
|
+
|
|
210
|
+
function abandon() {
|
|
211
|
+
if (timeoutHandle != null) {
|
|
212
|
+
clearTimeout(timeoutHandle)
|
|
213
|
+
timeoutHandle = null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pressedElement = null
|
|
217
|
+
pressedPointerId = null
|
|
218
|
+
|
|
219
|
+
Window.$body.off('pointermove', pointerMove)
|
|
220
|
+
.off('pointerup pointercancel', pointerUp)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isPrimaryPointer(e) {
|
|
224
|
+
/* jQuery normalizes pointerId and pointerType, but not isPrimary, which must be read from the original
|
|
225
|
+
* event; events triggered programmatically have no original event and are considered primary */
|
|
226
|
+
return !e.originalEvent || e.originalEvent.isPrimary !== false
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function pointerDown(e) {
|
|
230
|
+
/* Only the primary pointer (first finger, left mouse button) can start a press */
|
|
231
|
+
if (!isPrimaryPointer(e) || (e.pointerType === 'mouse' && e.button !== 0)) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
abandon() /* Any previous press is implicitly abandoned */
|
|
236
|
+
|
|
237
|
+
pressedElement = $(this)
|
|
238
|
+
pressedPointerId = e.pointerId
|
|
239
|
+
startPageX = e.pageX
|
|
240
|
+
startPageY = e.pageY
|
|
241
|
+
|
|
242
|
+
timeoutHandle = setTimeout(function () {
|
|
243
|
+
|
|
244
|
+
timeoutHandle = null
|
|
245
|
+
|
|
246
|
+
let element = pressedElement
|
|
247
|
+
abandon()
|
|
248
|
+
onLongPress(element, e)
|
|
249
|
+
|
|
250
|
+
}, duration)
|
|
251
|
+
|
|
252
|
+
Window.$body.on('pointermove', pointerMove)
|
|
253
|
+
.on('pointerup pointercancel', pointerUp)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pointerMove(e) {
|
|
257
|
+
/* Ignore any pointer other than the one that started the press */
|
|
258
|
+
if (!pressedElement || e.pointerId !== pressedPointerId) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (Math.abs(e.pageX - startPageX) > moveThreshold || Math.abs(e.pageY - startPageY) > moveThreshold) {
|
|
263
|
+
abandon()
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function pointerUp(e) {
|
|
268
|
+
/* Ignore any pointer other than the one that started the press */
|
|
269
|
+
if (!pressedElement || e.pointerId !== pressedPointerId) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* A pending timeout means the press has been released before becoming a long press */
|
|
274
|
+
let shortPress = timeoutHandle != null
|
|
275
|
+
|
|
276
|
+
let element = pressedElement
|
|
277
|
+
abandon()
|
|
278
|
+
|
|
279
|
+
if (shortPress && onShortPress) {
|
|
280
|
+
onShortPress(element, e)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
element.data(LONG_PRESS_DATA_KEY, {pointerDown: pointerDown, abandon: abandon, selector: selector})
|
|
285
|
+
element.on('pointerdown', selector, pointerDown)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Disable previously configured long press support on an HTML element.
|
|
290
|
+
* @alias qui.utils.gestures.disableLongPress
|
|
291
|
+
* @param {jQuery} element the element on which presses are detected
|
|
292
|
+
*/
|
|
293
|
+
export function disableLongPress(element) {
|
|
294
|
+
let longPressData = element.data(LONG_PRESS_DATA_KEY)
|
|
295
|
+
if (!longPressData) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
longPressData.abandon()
|
|
300
|
+
element.off('pointerdown', longPressData.selector, longPressData.pointerDown)
|
|
301
|
+
element.removeData(LONG_PRESS_DATA_KEY)
|
|
302
|
+
}
|
package/js/utils/string.js
CHANGED
|
@@ -174,19 +174,46 @@ export function fromUTF8(s) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
* Intelligently search for an input sequence in a string.
|
|
178
|
-
*
|
|
177
|
+
* Intelligently search for an input sequence in a string.
|
|
178
|
+
*
|
|
179
|
+
* The input sequence is made of groups of characters separated by whitespace. Each group must be present in the
|
|
180
|
+
* searched string as it was given, without anything in between, and the groups must appear in the order in which they
|
|
181
|
+
* were given, each one starting after the previous one ends. Searching is case insensitive.
|
|
182
|
+
*
|
|
183
|
+
* For example, `"temp sens"` matches `"temperature sensor"`, while neither `"tmp"` nor `"sens temp"` does.
|
|
184
|
+
*
|
|
179
185
|
* @alias qui.utils.string.intelliSearch
|
|
180
186
|
* @param {String} s string to search into
|
|
181
|
-
* @param {String} search string to search for
|
|
187
|
+
* @param {String|RegExp} search string to search for, or an expression previously compiled with
|
|
188
|
+
* {@link qui.utils.string.intelliSearchRegExp}
|
|
182
189
|
* @returns {?RegExpMatchArray}
|
|
183
190
|
*/
|
|
184
191
|
export function intelliSearch(s, search) {
|
|
185
|
-
let
|
|
186
|
-
return `${REGEX_ESCAPE_CHARS.includes(c) ? '\\' : ''}${c}.*`
|
|
187
|
-
}).join('')
|
|
188
|
-
|
|
189
|
-
let rex = new RegExp(rexStr, 'i')
|
|
192
|
+
let rex = (search instanceof RegExp) ? search : intelliSearchRegExp(search)
|
|
190
193
|
|
|
191
194
|
return s.match(rex)
|
|
192
195
|
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Compile the regular expression used by {@link qui.utils.string.intelliSearch}.
|
|
199
|
+
*
|
|
200
|
+
* Compiling once and passing the result to {@link qui.utils.string.intelliSearch} avoids recompiling the same
|
|
201
|
+
* expression when matching a single search string against many candidates.
|
|
202
|
+
*
|
|
203
|
+
* @alias qui.utils.string.intelliSearchRegExp
|
|
204
|
+
* @param {String} search the search string
|
|
205
|
+
* @returns {RegExp}
|
|
206
|
+
*/
|
|
207
|
+
export function intelliSearchRegExp(search) {
|
|
208
|
+
let groups = search.split(/\s+/).filter(group => group.length > 0)
|
|
209
|
+
|
|
210
|
+
/* Characters of a group must be adjacent in the searched string, while groups themselves may be arbitrarily far
|
|
211
|
+
* apart, as long as they follow each other */
|
|
212
|
+
let rexStr = groups.map(function (group) {
|
|
213
|
+
return Array.prototype.map.call(group, function (c) {
|
|
214
|
+
return `${REGEX_ESCAPE_CHARS.includes(c) ? '\\' : ''}${c}`
|
|
215
|
+
}).join('')
|
|
216
|
+
}).join('.*')
|
|
217
|
+
|
|
218
|
+
return new RegExp(rexStr, 'i')
|
|
219
|
+
}
|
|
@@ -555,9 +555,10 @@ $.widget('qui.combo', $.qui.basewidget, {
|
|
|
555
555
|
})
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
let
|
|
558
|
+
/* The whole search text is compiled into a single expression, which also takes care of splitting it into
|
|
559
|
+
* groups. Compiling it here means compiling it once per combo rather than once per choice. */
|
|
560
|
+
let searchText = this._filterInput.val().trim()
|
|
561
|
+
let searchExpression = searchText ? StringUtils.intelliSearchRegExp(searchText) : null
|
|
561
562
|
let children = this._itemContainer.children('div.qui-combo-item')
|
|
562
563
|
|
|
563
564
|
let filterFunc = this.options.filterFunc
|
|
@@ -580,7 +581,7 @@ $.widget('qui.combo', $.qui.basewidget, {
|
|
|
580
581
|
let visibleCount = 0
|
|
581
582
|
this._getChoices().forEach(function (choice, i) {
|
|
582
583
|
|
|
583
|
-
let visible = !
|
|
584
|
+
let visible = !searchExpression || filterFunc(choice, searchExpression)
|
|
584
585
|
if (visibleCount > MAX_VISIBLE_ITEMS && this._maxHeightSet) {
|
|
585
586
|
visible = false
|
|
586
587
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import $ from '$qui/lib/jquery.module.js'
|
|
3
3
|
|
|
4
|
-
import {gettext}
|
|
4
|
+
import {gettext} from '$qui/base/i18n.js'
|
|
5
|
+
import * as Gestures from '$qui/utils/gestures.js'
|
|
5
6
|
|
|
6
7
|
import * as BaseWidget from '../base-widget.js' /* Needed */
|
|
7
8
|
|
|
@@ -146,25 +147,29 @@ $.widget('qui.updown', $.qui.basewidget, {
|
|
|
146
147
|
|
|
147
148
|
/* Install up/down buttons long press handlers */
|
|
148
149
|
|
|
149
|
-
this._downButton
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
Gestures.enableLongPress(this._downButton, {
|
|
151
|
+
onLongPress: function () {
|
|
152
|
+
if (widget.options.readonly || widget.options.disabled) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
incDecLoopActive = true
|
|
157
|
+
decLoop()
|
|
158
|
+
}
|
|
156
159
|
})
|
|
157
160
|
this._downButton.on('pointerup pointerleave pointerout pointercancel', function () {
|
|
158
161
|
incDecLoopActive = false
|
|
159
162
|
})
|
|
160
163
|
|
|
161
|
-
this._upButton
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
Gestures.enableLongPress(this._upButton, {
|
|
165
|
+
onLongPress: function () {
|
|
166
|
+
if (widget.options.readonly || widget.options.disabled) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
incDecLoopActive = true
|
|
171
|
+
incLoop()
|
|
172
|
+
}
|
|
168
173
|
})
|
|
169
174
|
this._upButton.on('pointerup pointerleave pointerout pointercancel', function () {
|
|
170
175
|
incDecLoopActive = false
|
package/less/lists.less
CHANGED
|
@@ -29,10 +29,28 @@ div.qui-list-child {
|
|
|
29
29
|
|
|
30
30
|
div.qui-list-child.qui-list-item {
|
|
31
31
|
|
|
32
|
+
/* Nothing inside an item is ever painted outside of it, since div.qui-list-child above already clips with
|
|
33
|
+
* overflow: hidden. Containment is therefore visually inert here, but it lets the engine scope layout and style
|
|
34
|
+
* invalidation to the single row that changed, instead of the whole list, and skip painting off-screen rows. */
|
|
35
|
+
contain: layout style paint;
|
|
36
|
+
|
|
32
37
|
&.hidden {
|
|
33
38
|
opacity: 0;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
/* Only an item whose whole content is an icon-label view has a height we can predict: min-height: 3em in
|
|
42
|
+
* icon-label-view.less, with the content kept to a single ellipsised line. Such a view can have its off-screen
|
|
43
|
+
* rendering skipped against a reliable size estimate. Everything else built on ListItem is deliberately left out,
|
|
44
|
+
* table rows above all -- their content is a set of cells and their height is unconstrained.
|
|
45
|
+
*
|
|
46
|
+
* The plain contain-intrinsic-size comes first for engines that predate the auto keyword and would otherwise drop
|
|
47
|
+
* the declaration, collapsing a skipped view to nothing. */
|
|
48
|
+
& > div.qui-icon-label-view {
|
|
49
|
+
content-visibility: auto;
|
|
50
|
+
contain-intrinsic-size: 0 3em;
|
|
51
|
+
contain-intrinsic-size: auto 3em;
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qtoggle/qui",
|
|
3
3
|
"description": "A JavaScript UI library with batteries included.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.20.0-alpha.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Calin Crisan",
|
|
7
7
|
"email": "ccrisan@gmail.com"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
|
-
"url": "https://github.com/qtoggle/qui.git"
|
|
10
|
+
"url": "git+https://github.com/qtoggle/qui.git"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [],
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"jquery": "^3",
|
|
16
16
|
"jquery-mousewheel": "*",
|
|
17
|
-
"jquery-ui
|
|
17
|
+
"jquery-ui": "^1",
|
|
18
18
|
"js-logger": "*",
|
|
19
19
|
"pepjs": "*"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@babel/core": "^7.27.0",
|
|
23
|
+
"@babel/eslint-parser": "^7.27.0",
|
|
23
24
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
24
25
|
"@babel/preset-env": "^7.27.0",
|
|
25
|
-
"@babel/eslint-parser": "^7.27.0",
|
|
26
26
|
"babel-loader": "^8",
|
|
27
27
|
"css-loader": "^3",
|
|
28
28
|
"eslint": "^9",
|
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "qui-server"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.20.0-alpha.2"
|
|
4
4
|
description = "A fully fledged qToggle implementation written in Python"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Calin Crisan", email = "ccrisan@gmail.com"},
|
|
@@ -29,6 +29,7 @@ package = true
|
|
|
29
29
|
[tool.ruff]
|
|
30
30
|
line-length = 120
|
|
31
31
|
lint.extend-select = ["I", "RUF022"]
|
|
32
|
+
lint.extend-ignore = ["RUF012"]
|
|
32
33
|
lint.isort.lines-after-imports = 2
|
|
33
34
|
lint.isort.lines-between-types = 1
|
|
34
35
|
lint.isort.force-wrap-aliases = true
|
package/scripts/postinstall.sh
CHANGED
|
@@ -5,6 +5,6 @@ echo "Creating symlinks to libs"
|
|
|
5
5
|
cd js/lib
|
|
6
6
|
ln -sf ../../../../jquery/dist/jquery.js .
|
|
7
7
|
ln -sf ../../../../jquery-mousewheel/jquery.mousewheel.js .
|
|
8
|
-
ln -sf ../../../../jquery-ui
|
|
8
|
+
ln -sf ../../../../jquery-ui/ui/widget.js jquery-ui-widget.js
|
|
9
9
|
ln -sf ../../../../js-logger/src/logger.js
|
|
10
10
|
ln -sf ../../../../pepjs/dist/pep.js
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Longpress is a jQuery plugin that makes it easy to support long press
|
|
3
|
-
* events on mobile devices and desktop borwsers.
|
|
4
|
-
*
|
|
5
|
-
* @name longpress
|
|
6
|
-
* @version 0.1.2
|
|
7
|
-
* @requires jQuery v1.2.3+
|
|
8
|
-
* @author Vaidik Kapoor
|
|
9
|
-
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
|
10
|
-
*
|
|
11
|
-
* For usage and examples, check out the README at:
|
|
12
|
-
* http://github.com/vaidik/jquery-longpress/
|
|
13
|
-
*
|
|
14
|
-
* Copyright (c) 2008-2013, Vaidik Kapoor (kapoor [*dot*] vaidik -[at]- gmail [*dot*] com)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
(function($) {
|
|
18
|
-
$.fn.longpress = function(longCallback, shortCallback, duration) {
|
|
19
|
-
if (typeof duration === "undefined") {
|
|
20
|
-
duration = 500;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return this.each(function() {
|
|
24
|
-
var $this = $(this);
|
|
25
|
-
|
|
26
|
-
// to keep track of how long something was pressed
|
|
27
|
-
var mouse_down_time;
|
|
28
|
-
var timeout;
|
|
29
|
-
|
|
30
|
-
// mousedown or touchstart callback
|
|
31
|
-
function mousedown_callback(e) {
|
|
32
|
-
mouse_down_time = new Date().getTime();
|
|
33
|
-
var context = $(this);
|
|
34
|
-
|
|
35
|
-
// set a timeout to call the longpress callback when time elapses
|
|
36
|
-
timeout = setTimeout(function() {
|
|
37
|
-
if (typeof longCallback === "function") {
|
|
38
|
-
longCallback.call(context, e);
|
|
39
|
-
} else {
|
|
40
|
-
$.error('Callback required for long press. You provided: ' + typeof longCallback);
|
|
41
|
-
}
|
|
42
|
-
}, duration);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// mouseup or touchend callback
|
|
46
|
-
function mouseup_callback(e) {
|
|
47
|
-
var press_time = new Date().getTime() - mouse_down_time;
|
|
48
|
-
if (press_time < duration) {
|
|
49
|
-
// cancel the timeout
|
|
50
|
-
clearTimeout(timeout);
|
|
51
|
-
|
|
52
|
-
// call the shortCallback if provided
|
|
53
|
-
if (typeof shortCallback === "function") {
|
|
54
|
-
shortCallback.call($(this), e);
|
|
55
|
-
} else if (typeof shortCallback === "undefined") {
|
|
56
|
-
;
|
|
57
|
-
} else {
|
|
58
|
-
$.error('Optional callback for short press should be a function.');
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// cancel long press event if the finger or mouse was moved
|
|
64
|
-
function move_callback(e) {
|
|
65
|
-
clearTimeout(timeout);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Browser Support
|
|
69
|
-
$this.on('mousedown', mousedown_callback);
|
|
70
|
-
$this.on('mouseup', mouseup_callback);
|
|
71
|
-
$this.on('mousemove', move_callback);
|
|
72
|
-
|
|
73
|
-
// Mobile Support
|
|
74
|
-
$this.on('touchstart', mousedown_callback);
|
|
75
|
-
$this.on('touchend', mouseup_callback);
|
|
76
|
-
$this.on('touchmove', move_callback);
|
|
77
|
-
});
|
|
78
|
-
};
|
|
79
|
-
}(jQuery));
|