hotwire_combobox 0.1.34 → 0.1.36
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/controllers/hw_combobox_controller.js +9 -5
- data/app/assets/javascripts/hw_combobox/helpers.js +12 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js +6 -6
- data/app/assets/javascripts/hw_combobox/models/combobox/dialog.js +2 -2
- data/app/assets/javascripts/hw_combobox/models/combobox/filtering.js +11 -9
- data/app/assets/javascripts/hw_combobox/models/combobox/selection.js +17 -14
- data/app/assets/javascripts/hw_combobox/models/combobox/toggle.js +1 -6
- data/lib/hotwire_combobox/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 446cd3c4beeb66029cab7cd6b92ac04c2a8c7a1f08ffd874728da2a3e605ead7
|
4
|
+
data.tar.gz: 9783eb83824ebeff3fa55536002c24e129fc66ed0b9d82e33c808a8d65c6c3ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4d5e3d5fddd0017fc6bb6e838a60550d0e7dc2da3fac8999befa092ad9a8a7d35753cc785271fdfb8fe43e63edc535dc97f441594c8be2a3eece394622b97d6
|
7
|
+
data.tar.gz: 1d60ba35f6f043b9698d7b9bd48aadd702e67c00cb626068e797890ebcc3cfd370af14c33479da153f57096c1015c062f7539676da55aac559d76b38234a4d24
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox"
|
2
|
-
import { Concerns } from "hw_combobox/helpers"
|
2
|
+
import { Concerns, sleep } from "hw_combobox/helpers"
|
3
3
|
import { Controller } from "@hotwired/stimulus"
|
4
4
|
|
5
|
+
window.HOTWIRE_COMBOBOX_STREAM_DELAY = 0 // ms, for testing purposes
|
6
|
+
|
5
7
|
const concerns = [
|
6
8
|
Controller,
|
7
9
|
Combobox.Actors,
|
@@ -70,13 +72,15 @@ export default class HwComboboxController extends Concerns(...concerns) {
|
|
70
72
|
}
|
71
73
|
}
|
72
74
|
|
73
|
-
endOfOptionsStreamTargetConnected(element) {
|
75
|
+
async endOfOptionsStreamTargetConnected(element) {
|
74
76
|
const inputType = element.dataset.inputType
|
77
|
+
const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY
|
75
78
|
|
76
|
-
|
77
|
-
|
78
|
-
if (inputType) {
|
79
|
+
if (inputType && inputType !== "hw:lockInSelection") {
|
80
|
+
if (delay) await sleep(delay)
|
79
81
|
this._commitFilter({ inputType })
|
82
|
+
} else {
|
83
|
+
this._preselectOption()
|
80
84
|
}
|
81
85
|
}
|
82
86
|
}
|
@@ -1,5 +1,3 @@
|
|
1
|
-
export const nullEvent = new Event("NULL")
|
2
|
-
|
3
1
|
export function Concerns(Base, ...mixins) {
|
4
2
|
return mixins.reduce((accumulator, current) => current(accumulator), Base)
|
5
3
|
}
|
@@ -52,3 +50,15 @@ export function debounce(fn, delay = 150) {
|
|
52
50
|
export function isDeleteEvent(event) {
|
53
51
|
return event.inputType === "deleteContentBackward" || event.inputType === "deleteWordBackward"
|
54
52
|
}
|
53
|
+
|
54
|
+
export function sleep(ms) {
|
55
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
56
|
+
}
|
57
|
+
|
58
|
+
export function unselectedPortion(element) {
|
59
|
+
if (element.selectionStart === element.selectionEnd) {
|
60
|
+
return element.value
|
61
|
+
} else {
|
62
|
+
return element.value.substring(0, element.selectionStart)
|
63
|
+
}
|
64
|
+
}
|
@@ -11,14 +11,14 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
11
11
|
_autocompleteWith(option, { force }) {
|
12
12
|
if (!this._autocompletesInline && !force) return
|
13
13
|
|
14
|
-
const typedValue = this.
|
14
|
+
const typedValue = this._typedQuery
|
15
15
|
const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
|
16
16
|
|
17
17
|
if (force) {
|
18
|
-
this.
|
18
|
+
this._fullQuery = autocompletedValue
|
19
19
|
this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
|
20
20
|
} else if (startsWith(autocompletedValue, typedValue)) {
|
21
|
-
this.
|
21
|
+
this._fullQuery = autocompletedValue
|
22
22
|
this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length)
|
23
23
|
}
|
24
24
|
}
|
@@ -30,14 +30,14 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
30
30
|
}
|
31
31
|
|
32
32
|
get _isExactAutocompleteMatch() {
|
33
|
-
return this._immediatelyAutocompletableValue === this.
|
33
|
+
return this._immediatelyAutocompletableValue === this._fullQuery
|
34
34
|
}
|
35
35
|
|
36
36
|
// All `_isExactAutocompleteMatch` matches are `_isPartialAutocompleteMatch` matches
|
37
37
|
// but not all `_isPartialAutocompleteMatch` matches are `_isExactAutocompleteMatch` matches.
|
38
38
|
get _isPartialAutocompleteMatch() {
|
39
39
|
return !!this._immediatelyAutocompletableValue &&
|
40
|
-
startsWith(this._immediatelyAutocompletableValue, this.
|
40
|
+
startsWith(this._immediatelyAutocompletableValue, this._fullQuery)
|
41
41
|
}
|
42
42
|
|
43
43
|
get _autocompletesList() {
|
@@ -49,6 +49,6 @@ Combobox.Autocomplete = Base => class extends Base {
|
|
49
49
|
}
|
50
50
|
|
51
51
|
get _immediatelyAutocompletableValue() {
|
52
|
-
return this.
|
52
|
+
return this._ensurableOption?.getAttribute(this.autocompletableAttributeValue)
|
53
53
|
}
|
54
54
|
}
|
@@ -14,7 +14,7 @@ Combobox.Dialog = Base => class extends Base {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
_moveArtifactsToDialog() {
|
17
|
-
this.dialogComboboxTarget.value = this.
|
17
|
+
this.dialogComboboxTarget.value = this._fullQuery
|
18
18
|
|
19
19
|
this._actingCombobox = this.dialogComboboxTarget
|
20
20
|
this._actingListbox = this.dialogListboxTarget
|
@@ -23,7 +23,7 @@ Combobox.Dialog = Base => class extends Base {
|
|
23
23
|
}
|
24
24
|
|
25
25
|
_moveArtifactsInline() {
|
26
|
-
this.comboboxTarget.value = this.
|
26
|
+
this.comboboxTarget.value = this._fullQuery
|
27
27
|
|
28
28
|
this._actingCombobox = this.comboboxTarget
|
29
29
|
this._actingListbox = this.listboxTarget
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
import Combobox from "hw_combobox/models/combobox/base"
|
3
|
-
import { applyFilter, debounce, isDeleteEvent } from "hw_combobox/helpers"
|
3
|
+
import { applyFilter, debounce, isDeleteEvent, unselectedPortion } from "hw_combobox/helpers"
|
4
4
|
import { get } from "hw_combobox/vendor/requestjs"
|
5
5
|
|
6
6
|
Combobox.Filtering = Base => class extends Base {
|
@@ -21,13 +21,13 @@ Combobox.Filtering = Base => class extends Base {
|
|
21
21
|
}
|
22
22
|
|
23
23
|
async _filterAsync(event) {
|
24
|
-
const query = { q: this.
|
24
|
+
const query = { q: this._fullQuery, input_type: event.inputType }
|
25
25
|
await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
|
26
26
|
}
|
27
27
|
|
28
28
|
_filterSync(event) {
|
29
29
|
this.open()
|
30
|
-
this._allOptionElements.forEach(applyFilter(this.
|
30
|
+
this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }))
|
31
31
|
this._commitFilter(event)
|
32
32
|
}
|
33
33
|
|
@@ -42,16 +42,18 @@ Combobox.Filtering = Base => class extends Base {
|
|
42
42
|
}
|
43
43
|
|
44
44
|
get _isQueried() {
|
45
|
-
return this.
|
45
|
+
return this._fullQuery.length > 0
|
46
46
|
}
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
get _query() {
|
51
|
-
return this._actingCombobox.value.trim()
|
48
|
+
get _fullQuery() {
|
49
|
+
return this._actingCombobox.value
|
52
50
|
}
|
53
51
|
|
54
|
-
set
|
52
|
+
set _fullQuery(value) {
|
55
53
|
this._actingCombobox.value = value
|
56
54
|
}
|
55
|
+
|
56
|
+
get _typedQuery() {
|
57
|
+
return unselectedPortion(this._actingCombobox)
|
58
|
+
}
|
57
59
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Combobox from "hw_combobox/models/combobox/base"
|
2
|
-
import { wrapAroundAccess
|
2
|
+
import { wrapAroundAccess } from "hw_combobox/helpers"
|
3
3
|
|
4
4
|
Combobox.Selection = Base => class extends Base {
|
5
5
|
selectOption(event) {
|
@@ -10,16 +10,16 @@ Combobox.Selection = Base => class extends Base {
|
|
10
10
|
|
11
11
|
_connectSelection() {
|
12
12
|
if (this.hasPrefilledDisplayValue) {
|
13
|
-
this.
|
13
|
+
this._fullQuery = this.prefilledDisplayValue
|
14
14
|
}
|
15
15
|
}
|
16
16
|
|
17
|
-
_select(option, {
|
17
|
+
_select(option, { forceAutocomplete = false } = {}) {
|
18
18
|
this._resetOptions()
|
19
19
|
|
20
20
|
if (option) {
|
21
21
|
this._markValid()
|
22
|
-
this._autocompleteWith(option, { force })
|
22
|
+
this._autocompleteWith(option, { force: forceAutocomplete })
|
23
23
|
this._commitSelection(option, { selected: true })
|
24
24
|
} else {
|
25
25
|
this._markInvalid()
|
@@ -32,8 +32,6 @@ Combobox.Selection = Base => class extends Base {
|
|
32
32
|
if (selected) {
|
33
33
|
this.hiddenFieldTarget.value = option.dataset.value
|
34
34
|
option.scrollIntoView({ block: "nearest" })
|
35
|
-
} else {
|
36
|
-
this.hiddenFieldTarget.value = null
|
37
35
|
}
|
38
36
|
}
|
39
37
|
|
@@ -48,17 +46,18 @@ Combobox.Selection = Base => class extends Base {
|
|
48
46
|
_deselect() {
|
49
47
|
const option = this._selectedOptionElement
|
50
48
|
if (option) this._commitSelection(option, { selected: false })
|
49
|
+
this.hiddenFieldTarget.value = null
|
51
50
|
}
|
52
51
|
|
53
52
|
_selectNew() {
|
54
53
|
this._resetOptions()
|
55
|
-
this.hiddenFieldTarget.value = this.
|
54
|
+
this.hiddenFieldTarget.value = this._fullQuery
|
56
55
|
this.hiddenFieldTarget.name = this.nameWhenNewValue
|
57
56
|
}
|
58
57
|
|
59
58
|
_selectIndex(index) {
|
60
59
|
const option = wrapAroundAccess(this._visibleOptionElements, index)
|
61
|
-
this._select(option, {
|
60
|
+
this._select(option, { forceAutocomplete: true })
|
62
61
|
}
|
63
62
|
|
64
63
|
_preselectOption() {
|
@@ -71,10 +70,10 @@ Combobox.Selection = Base => class extends Base {
|
|
71
70
|
}
|
72
71
|
}
|
73
72
|
|
74
|
-
|
75
|
-
if (this.
|
76
|
-
this._select(this.
|
77
|
-
this.filter(
|
73
|
+
_lockInSelection() {
|
74
|
+
if (this._shouldLockInSelection) {
|
75
|
+
this._select(this._ensurableOption, { forceAutocomplete: true })
|
76
|
+
this.filter({ inputType: "hw:lockInSelection" })
|
78
77
|
}
|
79
78
|
}
|
80
79
|
|
@@ -82,7 +81,11 @@ Combobox.Selection = Base => class extends Base {
|
|
82
81
|
return this.hiddenFieldTarget.value && !this._selectedOptionElement
|
83
82
|
}
|
84
83
|
|
85
|
-
get
|
86
|
-
return this._isQueried && !!this.
|
84
|
+
get _shouldLockInSelection() {
|
85
|
+
return this._isQueried && !!this._ensurableOption && !this._isNewOptionWithPotentialMatches
|
86
|
+
}
|
87
|
+
|
88
|
+
get _ensurableOption() {
|
89
|
+
return this._selectedOptionElement || this._visibleOptionElements[0]
|
87
90
|
}
|
88
91
|
}
|
@@ -8,7 +8,7 @@ Combobox.Toggle = Base => class extends Base {
|
|
8
8
|
|
9
9
|
close() {
|
10
10
|
if (this._isOpen) {
|
11
|
-
this.
|
11
|
+
this._lockInSelection()
|
12
12
|
this.expandedValue = false
|
13
13
|
}
|
14
14
|
}
|
@@ -34,7 +34,6 @@ Combobox.Toggle = Base => class extends Base {
|
|
34
34
|
closeOnFocusOutside({ target }) {
|
35
35
|
if (!this._isOpen) return
|
36
36
|
if (this.element.contains(target)) return
|
37
|
-
if (target.matches("main")) return
|
38
37
|
|
39
38
|
this.close()
|
40
39
|
}
|
@@ -50,10 +49,6 @@ Combobox.Toggle = Base => class extends Base {
|
|
50
49
|
return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom
|
51
50
|
}
|
52
51
|
|
53
|
-
_ensureSelection() {
|
54
|
-
this._selectFuzzyMatch()
|
55
|
-
}
|
56
|
-
|
57
52
|
_openByFocusing() {
|
58
53
|
this._actingCombobox.focus()
|
59
54
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hotwire_combobox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.36
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jose Farias
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02
|
11
|
+
date: 2024-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -52,7 +52,8 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.2'
|
55
|
-
description:
|
55
|
+
description: An autocomplete combobox implementation for Ruby on Rails apps using
|
56
|
+
Hotwire.
|
56
57
|
email:
|
57
58
|
- jose@farias.mx
|
58
59
|
executables: []
|