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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3bf6f83f984a36fb6e0491f68a71c9386c3f2a00e6b6082cd490b73e58d652d
4
- data.tar.gz: db089a58628fa02b4a173f18053a9a81a21402eeafdcfb943d403989597bf314
3
+ metadata.gz: 446cd3c4beeb66029cab7cd6b92ac04c2a8c7a1f08ffd874728da2a3e605ead7
4
+ data.tar.gz: 9783eb83824ebeff3fa55536002c24e129fc66ed0b9d82e33c808a8d65c6c3ee
5
5
  SHA512:
6
- metadata.gz: '0483ae80475b1d52b9b74d21c5749ee0300914954f74e40267de50cfb96dbb77e44a8bdfb5448caf9d743798e2621d678986927c5f201c40052e049c2844a13f'
7
- data.tar.gz: 594db1df1c85dd096ca2cebf2ff9a7dc6564e061aee5f4403729eb94eaf55ab587f32ea620b0f359bd5f1c41e2e4c70096fb5ef04230a07ee13d6cda7873cd33
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
- this._preselectOption()
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._query
14
+ const typedValue = this._typedQuery
15
15
  const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
16
16
 
17
17
  if (force) {
18
- this._query = autocompletedValue
18
+ this._fullQuery = autocompletedValue
19
19
  this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
20
20
  } else if (startsWith(autocompletedValue, typedValue)) {
21
- this._query = autocompletedValue
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._query
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._query)
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._visibleOptionElements[0]?.getAttribute(this.autocompletableAttributeValue)
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._query
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._query
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._query, input_type: event.inputType }
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._query, { matching: this.filterableAttributeValue }))
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._query.length > 0
45
+ return this._fullQuery.length > 0
46
46
  }
47
47
 
48
- // Consider +_query+ will contain the full autocompleted value
49
- // after a certain point in the call chain.
50
- get _query() {
51
- return this._actingCombobox.value.trim()
48
+ get _fullQuery() {
49
+ return this._actingCombobox.value
52
50
  }
53
51
 
54
- set _query(value) {
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, nullEvent } from "hw_combobox/helpers"
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._query = this.prefilledDisplayValue
13
+ this._fullQuery = this.prefilledDisplayValue
14
14
  }
15
15
  }
16
16
 
17
- _select(option, { force = false } = {}) {
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._query
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, { force: true })
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
- _selectFuzzyMatch() {
75
- if (this._isFuzzyMatch) {
76
- this._select(this._visibleOptionElements[0], { force: true })
77
- this.filter(nullEvent)
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 _isFuzzyMatch() {
86
- return this._isQueried && !!this._visibleOptionElements[0] && !this._isNewOptionWithPotentialMatches
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._ensureSelection()
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
  }
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.1.34"
2
+ VERSION = "0.1.36"
3
3
  end
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.34
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-29 00:00:00.000000000 Z
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: A combobox implementation for Ruby on Rails apps using Hotwire.
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: []