hotwire_combobox 0.3.1 → 0.3.2

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: 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