hotwire_combobox 0.3.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb0eaf3c6593f518473d24ffdb931dc2519adc97c74eb4539055920c3ffd5ef9
4
- data.tar.gz: 888c826e85dc6e77b601c89fc4a573a3f45706e51203aadc7edc14b431a50245
3
+ metadata.gz: 5bdfc61a78393aa3f25828d5a09b0146038b1e4c74aeeb1189eb3d9cb3da1bc4
4
+ data.tar.gz: be98cd6da831d77f7401127bfc6409cd93a07dbe13d57b4198427ca0d662e146
5
5
  SHA512:
6
- metadata.gz: 5c2725f7a89aff33a4fa649e37f03f363c28fab51ca930337593897068c5c60732568f8fb6b43e8c7f9cdc9309fdea9dd55298baed2774c50dc0e6041c59b989
7
- data.tar.gz: b63c3b6031f3d74190115f5f01152085491125bc5ee0a721c3c1cb0be493993f3dec83b8132424bd471b93ff36f6b248f7f27d0abb34a3821959b6b9a2900fe7
6
+ metadata.gz: 9a6556e4c39ae6b6ae721e1e0fc587c5c1224365ca5e1feffdf32750b1a69085e3ec531ea8eafc3857cbc841d12c2a361321a295bbf90197ce3054547fe1abed
7
+ data.tar.gz: e4d81183deceede7b786abe9732d12c1ce76c2399ae8e44d4f3c4ebfa6822fef041b661358ea0ce254200985dd3643a056b50fa992e7ad94f3e5d6522ceaa7f4
@@ -1,5 +1,6 @@
1
1
  import Combobox from "hw_combobox/models/combobox"
2
2
  import { Concerns, sleep } from "hw_combobox/helpers"
3
+ import { nextRepaint } from "hw_combobox/helpers"
3
4
  import { Controller } from "@hotwired/stimulus"
4
5
 
5
6
  window.HOTWIRE_COMBOBOX_STREAM_DELAY = 0 // ms, for testing purposes
@@ -10,6 +11,7 @@ const concerns = [
10
11
  Combobox.Announcements,
11
12
  Combobox.AsyncLoading,
12
13
  Combobox.Autocomplete,
14
+ Combobox.Callbacks,
13
15
  Combobox.Dialog,
14
16
  Combobox.Events,
15
17
  Combobox.Filtering,
@@ -61,6 +63,7 @@ export default class HwComboboxController extends Concerns(...concerns) {
61
63
  initialize() {
62
64
  this._initializeActors()
63
65
  this._initializeFiltering()
66
+ this._initializeCallbacks()
64
67
  }
65
68
 
66
69
  connect() {
@@ -87,18 +90,39 @@ export default class HwComboboxController extends Concerns(...concerns) {
87
90
  }
88
91
 
89
92
  async endOfOptionsStreamTargetConnected(element) {
90
- const inputType = element.dataset.inputType
91
- const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY
93
+ if (element.dataset.callbackId) {
94
+ this._runCallback(element)
95
+ } else {
96
+ this._preselectSingle()
97
+ }
98
+ }
99
+
100
+ async _runCallback(element) {
101
+ const callbackId = element.dataset.callbackId
92
102
 
93
- this._resetMultiselectionMarks()
103
+ if (this._callbackAttemptsExceeded(callbackId)) {
104
+ this._dequeueCallback(callbackId)
105
+ return
106
+ } else {
107
+ this._recordCallbackAttempt(callbackId)
108
+ }
109
+
110
+ if (this._isNextCallback(callbackId)) {
111
+ const inputType = element.dataset.inputType
112
+ const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY
94
113
 
95
- if (inputType === "hw:multiselectSync") {
96
- this.openByFocusing()
97
- } else if (inputType && inputType !== "hw:lockInSelection") {
98
114
  if (delay) await sleep(delay)
99
- this._selectOnQuery(inputType)
115
+ this._dequeueCallback(callbackId)
116
+ this._resetMultiselectionMarks()
117
+
118
+ if (inputType === "hw:multiselectSync") {
119
+ this.openByFocusing()
120
+ } else if (inputType !== "hw:lockInSelection") {
121
+ this._selectOnQuery(inputType)
122
+ }
100
123
  } else {
101
- this._preselectSingle()
124
+ await nextRepaint()
125
+ this._runCallback(element)
102
126
  }
103
127
  }
104
128
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- HotwireCombobox 0.3.1
2
+ HotwireCombobox 0.3.2
3
3
  */
4
4
  import { Controller } from '@hotwired/stimulus';
5
5
 
@@ -204,6 +204,50 @@ Combobox.Autocomplete = Base => class extends Base {
204
204
  }
205
205
  };
206
206
 
207
+ const MAX_CALLBACK_ATTEMPTS = 3;
208
+
209
+ Combobox.Callbacks = Base => class extends Base {
210
+ _initializeCallbacks() {
211
+ this.callbackQueue = [];
212
+ this.callbackExecutionAttempts = {};
213
+ }
214
+
215
+ _enqueueCallback() {
216
+ const callbackId = crypto.randomUUID();
217
+ this.callbackQueue.push(callbackId);
218
+ return callbackId
219
+ }
220
+
221
+ _isNextCallback(callbackId) {
222
+ return this._nextCallback === callbackId
223
+ }
224
+
225
+ _callbackAttemptsExceeded(callbackId) {
226
+ return this._callbackAttempts(callbackId) > MAX_CALLBACK_ATTEMPTS
227
+ }
228
+
229
+ _callbackAttempts(callbackId) {
230
+ return this.callbackExecutionAttempts[callbackId] || 0
231
+ }
232
+
233
+ _recordCallbackAttempt(callbackId) {
234
+ this.callbackExecutionAttempts[callbackId] = this._callbackAttempts(callbackId) + 1;
235
+ }
236
+
237
+ _dequeueCallback(callbackId) {
238
+ this.callbackQueue = this.callbackQueue.filter(id => id !== callbackId);
239
+ this._forgetCallbackExecutionAttempts(callbackId);
240
+ }
241
+
242
+ _forgetCallbackExecutionAttempts(callbackId) {
243
+ delete this.callbackExecutionAttempts[callbackId];
244
+ }
245
+
246
+ get _nextCallback() {
247
+ return this.callbackQueue[0]
248
+ }
249
+ };
250
+
207
251
  Combobox.Dialog = Base => class extends Base {
208
252
  rerouteListboxStreamToDialog({ detail: { newStream } }) {
209
253
  if (newStream.target == this.listboxTarget.id && this._dialogIsOpen) {
@@ -592,6 +636,12 @@ Combobox.Filtering = Base => class extends Base {
592
636
  }
593
637
  }
594
638
 
639
+ clear(event) {
640
+ this._clearQuery();
641
+ this.chipDismisserTargets.forEach(el => el.click());
642
+ if (event && !event.defaultPrevented) event.target.focus();
643
+ }
644
+
595
645
  _initializeFiltering() {
596
646
  this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
597
647
  }
@@ -614,7 +664,8 @@ Combobox.Filtering = Base => class extends Base {
614
664
  const query = {
615
665
  q: this._fullQuery,
616
666
  input_type: inputType,
617
- for_id: this.element.dataset.asyncId
667
+ for_id: this.element.dataset.asyncId,
668
+ callback_id: this._enqueueCallback()
618
669
  };
619
670
 
620
671
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
@@ -635,7 +686,7 @@ Combobox.Filtering = Base => class extends Base {
635
686
  }
636
687
 
637
688
  _markQueried() {
638
- this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
689
+ this._forAllComboboxes(el => el.toggleAttribute("data-queried", this._isQueried));
639
690
  }
640
691
 
641
692
  get _isQueried() {
@@ -1657,6 +1708,7 @@ const concerns = [
1657
1708
  Combobox.Announcements,
1658
1709
  Combobox.AsyncLoading,
1659
1710
  Combobox.Autocomplete,
1711
+ Combobox.Callbacks,
1660
1712
  Combobox.Dialog,
1661
1713
  Combobox.Events,
1662
1714
  Combobox.Filtering,
@@ -1708,6 +1760,7 @@ class HwComboboxController extends Concerns(...concerns) {
1708
1760
  initialize() {
1709
1761
  this._initializeActors();
1710
1762
  this._initializeFiltering();
1763
+ this._initializeCallbacks();
1711
1764
  }
1712
1765
 
1713
1766
  connect() {
@@ -1734,18 +1787,39 @@ class HwComboboxController extends Concerns(...concerns) {
1734
1787
  }
1735
1788
 
1736
1789
  async endOfOptionsStreamTargetConnected(element) {
1737
- const inputType = element.dataset.inputType;
1738
- const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
1790
+ if (element.dataset.callbackId) {
1791
+ this._runCallback(element);
1792
+ } else {
1793
+ this._preselectSingle();
1794
+ }
1795
+ }
1739
1796
 
1740
- this._resetMultiselectionMarks();
1797
+ async _runCallback(element) {
1798
+ const callbackId = element.dataset.callbackId;
1799
+
1800
+ if (this._callbackAttemptsExceeded(callbackId)) {
1801
+ this._dequeueCallback(callbackId);
1802
+ return
1803
+ } else {
1804
+ this._recordCallbackAttempt(callbackId);
1805
+ }
1806
+
1807
+ if (this._isNextCallback(callbackId)) {
1808
+ const inputType = element.dataset.inputType;
1809
+ const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
1741
1810
 
1742
- if (inputType === "hw:multiselectSync") {
1743
- this.openByFocusing();
1744
- } else if (inputType && inputType !== "hw:lockInSelection") {
1745
1811
  if (delay) await sleep(delay);
1746
- this._selectOnQuery(inputType);
1812
+ this._dequeueCallback(callbackId);
1813
+ this._resetMultiselectionMarks();
1814
+
1815
+ if (inputType === "hw:multiselectSync") {
1816
+ this.openByFocusing();
1817
+ } else if (inputType !== "hw:lockInSelection") {
1818
+ this._selectOnQuery(inputType);
1819
+ }
1747
1820
  } else {
1748
- this._preselectSingle();
1821
+ await nextRepaint();
1822
+ this._runCallback(element);
1749
1823
  }
1750
1824
  }
1751
1825
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- HotwireCombobox 0.3.1
2
+ HotwireCombobox 0.3.2
3
3
  */
4
4
  (function (global, factory) {
5
5
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('@hotwired/stimulus')) :
@@ -208,6 +208,50 @@ HotwireCombobox 0.3.1
208
208
  }
209
209
  };
210
210
 
211
+ const MAX_CALLBACK_ATTEMPTS = 3;
212
+
213
+ Combobox.Callbacks = Base => class extends Base {
214
+ _initializeCallbacks() {
215
+ this.callbackQueue = [];
216
+ this.callbackExecutionAttempts = {};
217
+ }
218
+
219
+ _enqueueCallback() {
220
+ const callbackId = crypto.randomUUID();
221
+ this.callbackQueue.push(callbackId);
222
+ return callbackId
223
+ }
224
+
225
+ _isNextCallback(callbackId) {
226
+ return this._nextCallback === callbackId
227
+ }
228
+
229
+ _callbackAttemptsExceeded(callbackId) {
230
+ return this._callbackAttempts(callbackId) > MAX_CALLBACK_ATTEMPTS
231
+ }
232
+
233
+ _callbackAttempts(callbackId) {
234
+ return this.callbackExecutionAttempts[callbackId] || 0
235
+ }
236
+
237
+ _recordCallbackAttempt(callbackId) {
238
+ this.callbackExecutionAttempts[callbackId] = this._callbackAttempts(callbackId) + 1;
239
+ }
240
+
241
+ _dequeueCallback(callbackId) {
242
+ this.callbackQueue = this.callbackQueue.filter(id => id !== callbackId);
243
+ this._forgetCallbackExecutionAttempts(callbackId);
244
+ }
245
+
246
+ _forgetCallbackExecutionAttempts(callbackId) {
247
+ delete this.callbackExecutionAttempts[callbackId];
248
+ }
249
+
250
+ get _nextCallback() {
251
+ return this.callbackQueue[0]
252
+ }
253
+ };
254
+
211
255
  Combobox.Dialog = Base => class extends Base {
212
256
  rerouteListboxStreamToDialog({ detail: { newStream } }) {
213
257
  if (newStream.target == this.listboxTarget.id && this._dialogIsOpen) {
@@ -596,6 +640,12 @@ HotwireCombobox 0.3.1
596
640
  }
597
641
  }
598
642
 
643
+ clear(event) {
644
+ this._clearQuery();
645
+ this.chipDismisserTargets.forEach(el => el.click());
646
+ if (event && !event.defaultPrevented) event.target.focus();
647
+ }
648
+
599
649
  _initializeFiltering() {
600
650
  this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
601
651
  }
@@ -618,7 +668,8 @@ HotwireCombobox 0.3.1
618
668
  const query = {
619
669
  q: this._fullQuery,
620
670
  input_type: inputType,
621
- for_id: this.element.dataset.asyncId
671
+ for_id: this.element.dataset.asyncId,
672
+ callback_id: this._enqueueCallback()
622
673
  };
623
674
 
624
675
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
@@ -639,7 +690,7 @@ HotwireCombobox 0.3.1
639
690
  }
640
691
 
641
692
  _markQueried() {
642
- this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
693
+ this._forAllComboboxes(el => el.toggleAttribute("data-queried", this._isQueried));
643
694
  }
644
695
 
645
696
  get _isQueried() {
@@ -1661,6 +1712,7 @@ HotwireCombobox 0.3.1
1661
1712
  Combobox.Announcements,
1662
1713
  Combobox.AsyncLoading,
1663
1714
  Combobox.Autocomplete,
1715
+ Combobox.Callbacks,
1664
1716
  Combobox.Dialog,
1665
1717
  Combobox.Events,
1666
1718
  Combobox.Filtering,
@@ -1712,6 +1764,7 @@ HotwireCombobox 0.3.1
1712
1764
  initialize() {
1713
1765
  this._initializeActors();
1714
1766
  this._initializeFiltering();
1767
+ this._initializeCallbacks();
1715
1768
  }
1716
1769
 
1717
1770
  connect() {
@@ -1738,18 +1791,39 @@ HotwireCombobox 0.3.1
1738
1791
  }
1739
1792
 
1740
1793
  async endOfOptionsStreamTargetConnected(element) {
1741
- const inputType = element.dataset.inputType;
1742
- const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
1794
+ if (element.dataset.callbackId) {
1795
+ this._runCallback(element);
1796
+ } else {
1797
+ this._preselectSingle();
1798
+ }
1799
+ }
1743
1800
 
1744
- this._resetMultiselectionMarks();
1801
+ async _runCallback(element) {
1802
+ const callbackId = element.dataset.callbackId;
1803
+
1804
+ if (this._callbackAttemptsExceeded(callbackId)) {
1805
+ this._dequeueCallback(callbackId);
1806
+ return
1807
+ } else {
1808
+ this._recordCallbackAttempt(callbackId);
1809
+ }
1810
+
1811
+ if (this._isNextCallback(callbackId)) {
1812
+ const inputType = element.dataset.inputType;
1813
+ const delay = window.HOTWIRE_COMBOBOX_STREAM_DELAY;
1745
1814
 
1746
- if (inputType === "hw:multiselectSync") {
1747
- this.openByFocusing();
1748
- } else if (inputType && inputType !== "hw:lockInSelection") {
1749
1815
  if (delay) await sleep(delay);
1750
- this._selectOnQuery(inputType);
1816
+ this._dequeueCallback(callbackId);
1817
+ this._resetMultiselectionMarks();
1818
+
1819
+ if (inputType === "hw:multiselectSync") {
1820
+ this.openByFocusing();
1821
+ } else if (inputType !== "hw:lockInSelection") {
1822
+ this._selectOnQuery(inputType);
1823
+ }
1751
1824
  } else {
1752
- this._preselectSingle();
1825
+ await nextRepaint();
1826
+ this._runCallback(element);
1753
1827
  }
1754
1828
  }
1755
1829
 
@@ -0,0 +1,45 @@
1
+ import Combobox from "hw_combobox/models/combobox/base"
2
+
3
+ const MAX_CALLBACK_ATTEMPTS = 3
4
+
5
+ Combobox.Callbacks = Base => class extends Base {
6
+ _initializeCallbacks() {
7
+ this.callbackQueue = []
8
+ this.callbackExecutionAttempts = {}
9
+ }
10
+
11
+ _enqueueCallback() {
12
+ const callbackId = crypto.randomUUID()
13
+ this.callbackQueue.push(callbackId)
14
+ return callbackId
15
+ }
16
+
17
+ _isNextCallback(callbackId) {
18
+ return this._nextCallback === callbackId
19
+ }
20
+
21
+ _callbackAttemptsExceeded(callbackId) {
22
+ return this._callbackAttempts(callbackId) > MAX_CALLBACK_ATTEMPTS
23
+ }
24
+
25
+ _callbackAttempts(callbackId) {
26
+ return this.callbackExecutionAttempts[callbackId] || 0
27
+ }
28
+
29
+ _recordCallbackAttempt(callbackId) {
30
+ this.callbackExecutionAttempts[callbackId] = this._callbackAttempts(callbackId) + 1
31
+ }
32
+
33
+ _dequeueCallback(callbackId) {
34
+ this.callbackQueue = this.callbackQueue.filter(id => id !== callbackId)
35
+ this._forgetCallbackExecutionAttempts(callbackId)
36
+ }
37
+
38
+ _forgetCallbackExecutionAttempts(callbackId) {
39
+ delete this.callbackExecutionAttempts[callbackId]
40
+ }
41
+
42
+ get _nextCallback() {
43
+ return this.callbackQueue[0]
44
+ }
45
+ }
@@ -14,6 +14,12 @@ Combobox.Filtering = Base => class extends Base {
14
14
  }
15
15
  }
16
16
 
17
+ clear(event) {
18
+ this._clearQuery()
19
+ this.chipDismisserTargets.forEach(el => el.click())
20
+ if (event && !event.defaultPrevented) event.target.focus()
21
+ }
22
+
17
23
  _initializeFiltering() {
18
24
  this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
19
25
  }
@@ -36,7 +42,8 @@ Combobox.Filtering = Base => class extends Base {
36
42
  const query = {
37
43
  q: this._fullQuery,
38
44
  input_type: inputType,
39
- for_id: this.element.dataset.asyncId
45
+ for_id: this.element.dataset.asyncId,
46
+ callback_id: this._enqueueCallback()
40
47
  }
41
48
 
42
49
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
@@ -57,7 +64,7 @@ Combobox.Filtering = Base => class extends Base {
57
64
  }
58
65
 
59
66
  _markQueried() {
60
- this._actingCombobox.toggleAttribute("data-queried", this._isQueried)
67
+ this._forAllComboboxes(el => el.toggleAttribute("data-queried", this._isQueried))
61
68
  }
62
69
 
63
70
  get _isQueried() {
@@ -4,6 +4,7 @@ import "hw_combobox/models/combobox/actors"
4
4
  import "hw_combobox/models/combobox/announcements"
5
5
  import "hw_combobox/models/combobox/async_loading"
6
6
  import "hw_combobox/models/combobox/autocomplete"
7
+ import "hw_combobox/models/combobox/callbacks"
7
8
  import "hw_combobox/models/combobox/dialog"
8
9
  import "hw_combobox/models/combobox/events"
9
10
  import "hw_combobox/models/combobox/filtering"
@@ -209,7 +209,7 @@
209
209
  align-items: center;
210
210
  display: flex;
211
211
  flex-direction: column;
212
- justify-content: start;
212
+ justify-content: flex-start;
213
213
  }
214
214
 
215
215
  &::backdrop {
@@ -246,7 +246,7 @@ class HotwireCombobox::Component
246
246
  elsif async_src && form_object&.respond_to?(name)
247
247
  form_object.public_send name
248
248
  else
249
- options.find_by_value(hidden_field_value)&.autocompletable_as
249
+ options.find_by_value(hidden_field_value)&.autocompletable_as || hidden_field_value
250
250
  end
251
251
  end
252
252
 
@@ -26,6 +26,7 @@ class HotwireCombobox::Listbox::Option
26
26
  {
27
27
  id: id,
28
28
  role: :option,
29
+ tabindex: "-1",
29
30
  class: [ "hw-combobox__option", { "hw-combobox__option--blank": blank? } ],
30
31
  data: data,
31
32
  aria: aria
@@ -1,7 +1,7 @@
1
1
  <%# locals: (for_id:, src:) -%>
2
2
 
3
3
  <%= tag.li id: hw_pagination_frame_wrapper_id(for_id), class: "hw_combobox__pagination__wrapper",
4
- data: { hw_combobox_target: "endOfOptionsStream", input_type: params[:input_type] },
4
+ data: { hw_combobox_target: "endOfOptionsStream", input_type: params[:input_type], callback_id: params[:callback_id] },
5
5
  aria: { hidden: true } do %>
6
6
  <%= turbo_frame_tag hw_pagination_frame_id(for_id), src: src, loading: :lazy %>
7
7
  <% end %>
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
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.3.1
4
+ version: 0.3.2
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-05-31 00:00:00.000000000 Z
11
+ date: 2024-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -87,6 +87,7 @@ files:
87
87
  - app/assets/javascripts/hw_combobox/models/combobox/async_loading.js
88
88
  - app/assets/javascripts/hw_combobox/models/combobox/autocomplete.js
89
89
  - app/assets/javascripts/hw_combobox/models/combobox/base.js
90
+ - app/assets/javascripts/hw_combobox/models/combobox/callbacks.js
90
91
  - app/assets/javascripts/hw_combobox/models/combobox/dialog.js
91
92
  - app/assets/javascripts/hw_combobox/models/combobox/events.js
92
93
  - app/assets/javascripts/hw_combobox/models/combobox/filtering.js
@@ -144,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
145
  - !ruby/object:Gem::Version
145
146
  version: '0'
146
147
  requirements: []
147
- rubygems_version: 3.5.10
148
+ rubygems_version: 3.5.11
148
149
  signing_key:
149
150
  specification_version: 4
150
151
  summary: Accessible Autocomplete for Rails apps