bard-tag_field 0.4.2 → 0.5.0

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: 7a90b26ef508ec001cae14170b6ce96c543d6610c984622a1a2f3d9722c3e713
4
- data.tar.gz: 5bafffc31b38325321042c768d96cb9d4b7f829a8d05f803d9f6bb96d38c85e1
3
+ metadata.gz: 41638b348c696d0f3f95ae6c6c485c53d83c24df043c705cbfb52045410eee6f
4
+ data.tar.gz: 581a77505a56ee7c545ec14f1fb856a99af95346f02af98ddddb4703fbbbcf3f
5
5
  SHA512:
6
- metadata.gz: ca88e6931e551b19f280e6ecec610251c44b7778ebc88b93693ce3b9a3f05df800d9743b5bf58bf9d97f45bb542be0fae5e8a64ad6d04a5dbd813dac455eeb79
7
- data.tar.gz: 61013f52ca3daadefc24a4d1a7f3346d392a31ebefd2c144ddd2490975d4a51a6d27d840fb818bdbdf463256661f1dd655915e7e6f2c237c5b4f99f16ad7e402
6
+ metadata.gz: c7056e3b6f96f40a4f4b704e10b626012e5d6df73b8c6f91e189e9238bf3a69260f768e2bbe2cdcde18b9a9c493f036e3e6b5b9b999be35e098ab617308e8681
7
+ data.tar.gz: ea50ae8634d86c7f796fbf636475dd6805e4446abdb0b486ad2b4e63b5d8c20b63d0fc1db76e6fed23f2c48a75733aa18ddc62ded5698676929e5d799b4db75b
data/Appraisals CHANGED
@@ -10,4 +10,8 @@ end
10
10
 
11
11
  appraise "rails-8.0" do
12
12
  gem "rails", "~> 8.0.0"
13
- end
13
+ end
14
+
15
+ appraise "rails-8.1" do
16
+ gem "rails", "~> 8.1.0"
17
+ end
data/CLAUDE.md ADDED
@@ -0,0 +1,109 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Rails form helper gem that provides `tag_field` for creating interactive tag input fields. The gem wraps the [@botandrose/input-tag](https://github.com/botandrose/input-tag) custom element and integrates it with Rails form builders.
8
+
9
+ **Key Architecture:**
10
+ - `lib/bard/tag_field/form_builder.rb` - Rails form builder integration that handles method signature variants (like Rails' `select` helper)
11
+ - `lib/bard/tag_field/field.rb` - Core field rendering logic, extends `ActionView::Helpers::Tags::TextField`
12
+ - `lib/bard/tag_field.rb` - Rails Engine that auto-registers the form builder and precompiles JavaScript assets
13
+ - `input-tag/` - JavaScript build directory using Rollup to bundle the `@botandrose/input-tag` package with Bun
14
+ - `app/assets/javascripts/input-tag.js` - Compiled JavaScript output for Rails asset pipeline
15
+
16
+ ## Development Commands
17
+
18
+ ### Testing
19
+ ```bash
20
+ # Run all tests
21
+ bundle exec rspec
22
+
23
+ # Run tests for specific Rails version
24
+ bundle exec appraisal rails-7.1 rspec
25
+ bundle exec appraisal rails-7.2 rspec
26
+ bundle exec appraisal rails-8.0 rspec
27
+
28
+ # Generate appraisal gemfiles after updating Appraisals
29
+ bundle exec appraisal install
30
+ ```
31
+
32
+ ### JavaScript Assets
33
+ ```bash
34
+ # Build JavaScript assets (required before running tests or releasing)
35
+ cd input-tag && bun run build
36
+
37
+ # Install Bun dependencies
38
+ cd input-tag && bun install
39
+
40
+ # Clean compiled assets
41
+ cd input-tag && bun run clean
42
+ ```
43
+
44
+ ### Gem Management
45
+ ```bash
46
+ # Install the gem locally for testing
47
+ bundle exec rake install
48
+
49
+ # Build gem package
50
+ bundle exec rake build
51
+ ```
52
+
53
+ ## Form Builder Method Signatures
54
+
55
+ The `tag_field` method supports multiple signatures to match Rails conventions:
56
+
57
+ ```ruby
58
+ # Basic usage
59
+ form.tag_field :tags
60
+
61
+ # With HTML options only
62
+ form.tag_field :tags, class: "form-control"
63
+
64
+ # With choices (like form.select)
65
+ form.tag_field :tags, ["ruby", "rails", "javascript"]
66
+
67
+ # With choices and HTML options
68
+ form.tag_field :tags, ["ruby", "rails"], {}, { class: "form-control" }
69
+
70
+ # With nested choices [display, value]
71
+ form.tag_field :categories, [["Web Dev", "web"], ["ML", "ml"]]
72
+
73
+ # With block for custom rendering
74
+ form.tag_field :tags do |options|
75
+ # Custom tag-option rendering
76
+ end
77
+ ```
78
+
79
+ The FormBuilder handles signature detection in lib/bard/tag_field/form_builder.rb:6-21.
80
+
81
+ ## Rendering Logic
82
+
83
+ The Field class (lib/bard/tag_field/field.rb) handles three rendering scenarios:
84
+
85
+ 1. **Object values only** - Renders current object's tags as `<tag-option>` elements
86
+ 2. **With choices** - Renders object values as `<tag-option>` and choices in a nested `<datalist>`
87
+ 3. **With block** - Delegates content rendering to the provided block
88
+
89
+ The `build_choice_map` method (lib/bard/tag_field/field.rb:58-74) maps choice values to display labels for proper tag rendering.
90
+
91
+ ## Testing
92
+
93
+ Tests use RSpec with a custom HTML matcher that supports wildcards (`...`) for flexible HTML comparison. The matcher is defined in spec/spec_helper.rb:55-152 and allows testing HTML structure without exact whitespace or attribute order matching.
94
+
95
+ Test setup includes a mock Rails application (TestApp) initialized in spec/spec_helper.rb:12-17.
96
+
97
+ ## JavaScript Build Process
98
+
99
+ The gem bundles the `@botandrose/input-tag` package using Rollup with Bun:
100
+ 1. Source: `input-tag/index.js` imports from `@botandrose/input-tag`
101
+ 2. Build: `cd input-tag && bun run build` runs Rollup
102
+ 3. Output: `app/assets/javascripts/input-tag.js` for Rails asset pipeline
103
+ 4. The Engine precompiles this asset (lib/bard/tag_field.rb:11)
104
+
105
+ **Important:** Always rebuild JavaScript assets after updating the `@botandrose/input-tag` dependency.
106
+
107
+ ## Multi-Rails Version Support
108
+
109
+ Uses Appraisal gem to test against Rails 7.1, 7.2, and 8.0. Gemfiles are in `gemfiles/` directory. CI tests all combinations of Ruby 3.2/3.3/3.4 with each Rails version.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Ruby](https://img.shields.io/badge/ruby-3.2%2B-red)](https://www.ruby-lang.org)
5
5
  [![Rails](https://img.shields.io/badge/rails-7.1%2B-red)](https://rubyonrails.org)
6
6
 
7
- A Rails form helper gem that adds `bard_tag_field` to your forms, creating interactive tag input fields using the [@botandrose/input-tag](https://github.com/botandrose/input-tag) custom element.
7
+ A Rails form helper gem that adds `tag_field` to your forms, creating interactive tag input fields using the [@botandrose/input-tag](https://github.com/botandrose/input-tag) custom element.
8
8
 
9
9
  Perfect for adding tag functionality to your Rails forms with a clean, modern interface that works seamlessly with your existing Rails form helpers.
10
10
 
@@ -22,11 +22,11 @@ Perfect for adding tag functionality to your Rails forms with a clean, modern in
22
22
 
23
23
  ### Basic Usage
24
24
 
25
- After installing and requiring the gem, Use `bard_tag_field` in your Rails forms just like any other form helper:
25
+ After installing and requiring the gem, Use `tag_field` in your Rails forms just like any other form helper:
26
26
 
27
27
  ```erb
28
28
  <%= form_with model: @post do |form| %>
29
- <%= form.bard_tag_field :tags %>
29
+ <%= form.tag_field :tags %>
30
30
  <% end %>
31
31
  ```
32
32
 
@@ -37,7 +37,7 @@ This generates an interactive tag field that binds to your model's `tags` attrib
37
37
  Add CSS classes, data attributes, and other HTML options:
38
38
 
39
39
  ```erb
40
- <%= form.bard_tag_field :tags,
40
+ <%= form.tag_field :tags,
41
41
  class: "form-control",
42
42
  id: "post-tags",
43
43
  data: { placeholder: "Add tags..." } %>
@@ -54,7 +54,7 @@ The field automatically displays existing tags from your model:
54
54
 
55
55
  ```erb
56
56
  <!-- Tags will be pre-populated in the form -->
57
- <%= form.bard_tag_field :tags %>
57
+ <%= form.tag_field :tags %>
58
58
  ```
59
59
 
60
60
  ### With Predefined Choices (Rails select-style)
@@ -62,13 +62,13 @@ The field automatically displays existing tags from your model:
62
62
  Like `form.select`, you can provide predefined choices for users to select from:
63
63
 
64
64
  ```erb
65
- <%= form.bard_tag_field :tags, ["ruby", "rails", "javascript", "css"] %>
65
+ <%= form.tag_field :tags, ["ruby", "rails", "javascript", "css"] %>
66
66
  ```
67
67
 
68
68
  Or use nested arrays for display vs submit values:
69
69
 
70
70
  ```erb
71
- <%= form.bard_tag_field :categories, [
71
+ <%= form.tag_field :categories, [
72
72
  ["Web Development", "web-dev"],
73
73
  ["Machine Learning", "ml"],
74
74
  ["Database Design", "db"]
@@ -82,7 +82,7 @@ This creates a datalist with available options while still showing current objec
82
82
  Use blocks for custom tag rendering:
83
83
 
84
84
  ```erb
85
- <%= form.bard_tag_field :tags do |options| %>
85
+ <%= form.tag_field :tags do |options| %>
86
86
  <% @post.tags.each do |tag| %>
87
87
  <tag-option value="<%= tag %>" class="custom-tag"><%= tag %></tag-option>
88
88
  <% end %>
@@ -128,7 +128,7 @@ end
128
128
  ```
129
129
 
130
130
  ```erb
131
- <%= form.bard_tag_field :tags_array %>
131
+ <%= form.tag_field :tags_array %>
132
132
  ```
133
133
 
134
134
  ## Generated HTML
@@ -155,7 +155,7 @@ The gem generates semantic HTML using custom elements:
155
155
 
156
156
  ## JavaScript Integration
157
157
 
158
- This gem works with the [@botandrose/input-tag](https://github.com/botandrose/input-tag) custom element. Make sure to include it in your asset pipeline:
158
+ This gem works with the [@botandrose/input-tag](https://github.com/botandrose/input-tag) custom element.
159
159
 
160
160
  ```javascript
161
161
  // In your application.js or wherever you manage JS
@@ -176,7 +176,7 @@ Or include the precompiled asset (automatically added by this gem):
176
176
 
177
177
  ## API Reference
178
178
 
179
- ### `bard_tag_field(method, choices = nil, options = {}, html_options = {}, &block)`
179
+ ### `tag_field(method, choices = nil, options = {}, html_options = {}, &block)`
180
180
 
181
181
  **Parameters:**
182
182
  - `method` - The attribute name (symbol)
@@ -190,19 +190,19 @@ Or include the precompiled asset (automatically added by this gem):
190
190
  **Examples:**
191
191
  ```ruby
192
192
  # Basic usage
193
- form.bard_tag_field :tags
193
+ form.tag_field :tags
194
194
 
195
195
  # With choices
196
- form.bard_tag_field :tags, ["ruby", "rails", "javascript"]
196
+ form.tag_field :tags, ["ruby", "rails", "javascript"]
197
197
 
198
198
  # With nested choices (display vs value)
199
- form.bard_tag_field :categories, [["Web Dev", "web"], ["ML", "ml"]]
199
+ form.tag_field :categories, [["Web Dev", "web"], ["ML", "ml"]]
200
200
 
201
201
  # With HTML options
202
- form.bard_tag_field :tags, class: "form-control", data: { max_tags: 5 }
202
+ form.tag_field :tags, class: "form-control", data: { max_tags: 5 }
203
203
 
204
204
  # With choices and HTML options
205
- form.bard_tag_field :tags, ["ruby", "rails"], {}, { class: "form-control" }
205
+ form.tag_field :tags, ["ruby", "rails"], {}, { class: "form-control" }
206
206
  ```
207
207
 
208
208
  ## Development
data/Rakefile CHANGED
@@ -9,10 +9,10 @@ task default: :spec
9
9
 
10
10
  desc "Build JavaScript assets"
11
11
  task :build_js do
12
- sh "cd bard-tag && npm run build"
12
+ sh "cd input-tag && bun run build"
13
13
  end
14
14
 
15
- desc "Install npm dependencies"
15
+ desc "Install Bun dependencies"
16
16
  task :install_deps do
17
- sh "cd bard-tag && npm install"
17
+ sh "cd input-tag && bun install"
18
18
  end
@@ -543,459 +543,459 @@ class Taggle {
543
543
  }
544
544
  }
545
545
 
546
- /**
547
- * Copyright (c) 2016 Denis Taran
548
- *
549
- * Homepage: https://smartscheduling.com/en/documentation/autocomplete
550
- * Source: https://github.com/denis-taran/autocomplete
551
- *
552
- * MIT License
553
- */
554
- function autocomplete(settings) {
555
- // just an alias to minimize JS file size
556
- var doc = document;
557
- var container = settings.container || doc.createElement('div');
558
- var preventSubmit = settings.preventSubmit || 0 /* Never */;
559
- container.id = container.id || 'autocomplete-' + uid();
560
- var containerStyle = container.style;
561
- var debounceWaitMs = settings.debounceWaitMs || 0;
562
- var disableAutoSelect = settings.disableAutoSelect || false;
563
- var customContainerParent = container.parentElement;
564
- var items = [];
565
- var inputValue = '';
566
- var minLen = 2;
567
- var showOnFocus = settings.showOnFocus;
568
- var selected;
569
- var fetchCounter = 0;
570
- var debounceTimer;
571
- var destroyed = false;
572
- // Fixes #104: autocomplete selection is broken on Firefox for Android
573
- var suppressAutocomplete = false;
574
- if (settings.minLength !== undefined) {
575
- minLen = settings.minLength;
576
- }
577
- if (!settings.input) {
578
- throw new Error('input undefined');
579
- }
580
- var input = settings.input;
581
- container.className = [container.className, 'autocomplete', settings.className || ''].join(' ').trim();
582
- container.setAttribute('role', 'listbox');
583
- input.setAttribute('role', 'combobox');
584
- input.setAttribute('aria-expanded', 'false');
585
- input.setAttribute('aria-autocomplete', 'list');
586
- input.setAttribute('aria-controls', container.id);
587
- input.setAttribute('aria-owns', container.id);
588
- input.setAttribute('aria-activedescendant', '');
589
- input.setAttribute('aria-haspopup', 'listbox');
590
- // IOS implementation for fixed positioning has many bugs, so we will use absolute positioning
591
- containerStyle.position = 'absolute';
592
- /**
593
- * Generate a very complex textual ID that greatly reduces the chance of a collision with another ID or text.
594
- */
595
- function uid() {
596
- return Date.now().toString(36) + Math.random().toString(36).substring(2);
597
- }
598
- /**
599
- * Detach the container from DOM
600
- */
601
- function detach() {
602
- var parent = container.parentNode;
603
- if (parent) {
604
- parent.removeChild(container);
605
- }
606
- }
607
- /**
608
- * Clear debouncing timer if assigned
609
- */
610
- function clearDebounceTimer() {
611
- if (debounceTimer) {
612
- window.clearTimeout(debounceTimer);
613
- }
614
- }
615
- /**
616
- * Attach the container to DOM
617
- */
618
- function attach() {
619
- if (!container.parentNode) {
620
- (customContainerParent || doc.body).appendChild(container);
621
- }
622
- }
623
- /**
624
- * Check if container for autocomplete is displayed
625
- */
626
- function containerDisplayed() {
627
- return !!container.parentNode;
628
- }
629
- /**
630
- * Clear autocomplete state and hide container
631
- */
632
- function clear() {
633
- // prevent the update call if there are pending AJAX requests
634
- fetchCounter++;
635
- items = [];
636
- inputValue = '';
637
- selected = undefined;
638
- input.setAttribute('aria-activedescendant', '');
639
- input.setAttribute('aria-expanded', 'false');
640
- detach();
641
- }
642
- /**
643
- * Update autocomplete position
644
- */
645
- function updatePosition() {
646
- if (!containerDisplayed()) {
647
- return;
648
- }
649
- input.setAttribute('aria-expanded', 'true');
650
- containerStyle.height = 'auto';
651
- containerStyle.width = input.offsetWidth + 'px';
652
- var maxHeight = 0;
653
- var inputRect;
654
- function calc() {
655
- var docEl = doc.documentElement;
656
- var clientTop = docEl.clientTop || doc.body.clientTop || 0;
657
- var clientLeft = docEl.clientLeft || doc.body.clientLeft || 0;
658
- var scrollTop = window.pageYOffset || docEl.scrollTop;
659
- var scrollLeft = window.pageXOffset || docEl.scrollLeft;
660
- inputRect = input.getBoundingClientRect();
661
- var top = inputRect.top + input.offsetHeight + scrollTop - clientTop;
662
- var left = inputRect.left + scrollLeft - clientLeft;
663
- containerStyle.top = top + 'px';
664
- containerStyle.left = left + 'px';
665
- maxHeight = window.innerHeight - (inputRect.top + input.offsetHeight);
666
- if (maxHeight < 0) {
667
- maxHeight = 0;
668
- }
669
- containerStyle.top = top + 'px';
670
- containerStyle.bottom = '';
671
- containerStyle.left = left + 'px';
672
- containerStyle.maxHeight = maxHeight + 'px';
673
- }
674
- // the calc method must be called twice, otherwise the calculation may be wrong on resize event (chrome browser)
675
- calc();
676
- calc();
677
- if (settings.customize && inputRect) {
678
- settings.customize(input, inputRect, container, maxHeight);
679
- }
680
- }
681
- /**
682
- * Redraw the autocomplete div element with suggestions
683
- */
684
- function update() {
685
- container.textContent = '';
686
- input.setAttribute('aria-activedescendant', '');
687
- // function for rendering autocomplete suggestions
688
- var render = function (item, _, __) {
689
- var itemElement = doc.createElement('div');
690
- itemElement.textContent = item.label || '';
691
- return itemElement;
692
- };
693
- if (settings.render) {
694
- render = settings.render;
695
- }
696
- // function to render autocomplete groups
697
- var renderGroup = function (groupName, _) {
698
- var groupDiv = doc.createElement('div');
699
- groupDiv.textContent = groupName;
700
- return groupDiv;
701
- };
702
- if (settings.renderGroup) {
703
- renderGroup = settings.renderGroup;
704
- }
705
- var fragment = doc.createDocumentFragment();
706
- var prevGroup = uid();
707
- items.forEach(function (item, index) {
708
- if (item.group && item.group !== prevGroup) {
709
- prevGroup = item.group;
710
- var groupDiv = renderGroup(item.group, inputValue);
711
- if (groupDiv) {
712
- groupDiv.className += ' group';
713
- fragment.appendChild(groupDiv);
714
- }
715
- }
716
- var div = render(item, inputValue, index);
717
- if (div) {
718
- div.id = container.id + "_" + index;
719
- div.setAttribute('role', 'option');
720
- div.addEventListener('click', function (ev) {
721
- suppressAutocomplete = true;
722
- try {
723
- settings.onSelect(item, input);
724
- }
725
- finally {
726
- suppressAutocomplete = false;
727
- }
728
- clear();
729
- ev.preventDefault();
730
- ev.stopPropagation();
731
- });
732
- if (item === selected) {
733
- div.className += ' selected';
734
- div.setAttribute('aria-selected', 'true');
735
- input.setAttribute('aria-activedescendant', div.id);
736
- }
737
- fragment.appendChild(div);
738
- }
739
- });
740
- container.appendChild(fragment);
741
- if (items.length < 1) {
742
- if (settings.emptyMsg) {
743
- var empty = doc.createElement('div');
744
- empty.id = container.id + "_" + uid();
745
- empty.className = 'empty';
746
- empty.textContent = settings.emptyMsg;
747
- container.appendChild(empty);
748
- input.setAttribute('aria-activedescendant', empty.id);
749
- }
750
- else {
751
- clear();
752
- return;
753
- }
754
- }
755
- attach();
756
- updatePosition();
757
- updateScroll();
758
- }
759
- function updateIfDisplayed() {
760
- if (containerDisplayed()) {
761
- update();
762
- }
763
- }
764
- function resizeEventHandler() {
765
- updateIfDisplayed();
766
- }
767
- function scrollEventHandler(e) {
768
- if (e.target !== container) {
769
- updateIfDisplayed();
770
- }
771
- else {
772
- e.preventDefault();
773
- }
774
- }
775
- function inputEventHandler() {
776
- if (!suppressAutocomplete) {
777
- fetch(0 /* Keyboard */);
778
- }
779
- }
780
- /**
781
- * Automatically move scroll bar if selected item is not visible
782
- */
783
- function updateScroll() {
784
- var elements = container.getElementsByClassName('selected');
785
- if (elements.length > 0) {
786
- var element = elements[0];
787
- // make group visible
788
- var previous = element.previousElementSibling;
789
- if (previous && previous.className.indexOf('group') !== -1 && !previous.previousElementSibling) {
790
- element = previous;
791
- }
792
- if (element.offsetTop < container.scrollTop) {
793
- container.scrollTop = element.offsetTop;
794
- }
795
- else {
796
- var selectBottom = element.offsetTop + element.offsetHeight;
797
- var containerBottom = container.scrollTop + container.offsetHeight;
798
- if (selectBottom > containerBottom) {
799
- container.scrollTop += selectBottom - containerBottom;
800
- }
801
- }
802
- }
803
- }
804
- function selectPreviousSuggestion() {
805
- var index = items.indexOf(selected);
806
- selected = index === -1
807
- ? undefined
808
- : items[(index + items.length - 1) % items.length];
809
- updateSelectedSuggestion(index);
810
- }
811
- function selectNextSuggestion() {
812
- var index = items.indexOf(selected);
813
- selected = items.length < 1
814
- ? undefined
815
- : index === -1
816
- ? items[0]
817
- : items[(index + 1) % items.length];
818
- updateSelectedSuggestion(index);
819
- }
820
- function updateSelectedSuggestion(index) {
821
- if (items.length > 0) {
822
- unselectSuggestion(index);
823
- selectSuggestion(items.indexOf(selected));
824
- updateScroll();
825
- }
826
- }
827
- function selectSuggestion(index) {
828
- var element = doc.getElementById(container.id + "_" + index);
829
- if (element) {
830
- element.classList.add('selected');
831
- element.setAttribute('aria-selected', 'true');
832
- input.setAttribute('aria-activedescendant', element.id);
833
- }
834
- }
835
- function unselectSuggestion(index) {
836
- var element = doc.getElementById(container.id + "_" + index);
837
- if (element) {
838
- element.classList.remove('selected');
839
- element.removeAttribute('aria-selected');
840
- input.removeAttribute('aria-activedescendant');
841
- }
842
- }
843
- function handleArrowAndEscapeKeys(ev, key) {
844
- var containerIsDisplayed = containerDisplayed();
845
- if (key === 'Escape') {
846
- clear();
847
- }
848
- else {
849
- if (!containerIsDisplayed || items.length < 1) {
850
- return;
851
- }
852
- key === 'ArrowUp'
853
- ? selectPreviousSuggestion()
854
- : selectNextSuggestion();
855
- }
856
- ev.preventDefault();
857
- if (containerIsDisplayed) {
858
- ev.stopPropagation();
859
- }
860
- }
861
- function handleEnterKey(ev) {
862
- if (selected) {
863
- if (preventSubmit === 2 /* OnSelect */) {
864
- ev.preventDefault();
865
- }
866
- suppressAutocomplete = true;
867
- try {
868
- settings.onSelect(selected, input);
869
- }
870
- finally {
871
- suppressAutocomplete = false;
872
- }
873
- clear();
874
- }
875
- if (preventSubmit === 1 /* Always */) {
876
- ev.preventDefault();
877
- }
878
- }
879
- function keydownEventHandler(ev) {
880
- var key = ev.key;
881
- switch (key) {
882
- case 'ArrowUp':
883
- case 'ArrowDown':
884
- case 'Escape':
885
- handleArrowAndEscapeKeys(ev, key);
886
- break;
887
- case 'Enter':
888
- handleEnterKey(ev);
889
- break;
890
- }
891
- }
892
- function focusEventHandler() {
893
- if (showOnFocus) {
894
- fetch(1 /* Focus */);
895
- }
896
- }
897
- function fetch(trigger) {
898
- if (input.value.length >= minLen || trigger === 1 /* Focus */) {
899
- clearDebounceTimer();
900
- debounceTimer = window.setTimeout(function () { return startFetch(input.value, trigger, input.selectionStart || 0); }, trigger === 0 /* Keyboard */ || trigger === 2 /* Mouse */ ? debounceWaitMs : 0);
901
- }
902
- else {
903
- clear();
904
- }
905
- }
906
- function startFetch(inputText, trigger, cursorPos) {
907
- if (destroyed)
908
- return;
909
- var savedFetchCounter = ++fetchCounter;
910
- settings.fetch(inputText, function (elements) {
911
- if (fetchCounter === savedFetchCounter && elements) {
912
- items = elements;
913
- inputValue = inputText;
914
- selected = (items.length < 1 || disableAutoSelect) ? undefined : items[0];
915
- update();
916
- }
917
- }, trigger, cursorPos);
918
- }
919
- function keyupEventHandler(e) {
920
- if (settings.keyup) {
921
- settings.keyup({
922
- event: e,
923
- fetch: function () { return fetch(0 /* Keyboard */); }
924
- });
925
- return;
926
- }
927
- if (!containerDisplayed() && e.key === 'ArrowDown') {
928
- fetch(0 /* Keyboard */);
929
- }
930
- }
931
- function clickEventHandler(e) {
932
- settings.click && settings.click({
933
- event: e,
934
- fetch: function () { return fetch(2 /* Mouse */); }
935
- });
936
- }
937
- function blurEventHandler() {
938
- // when an item is selected by mouse click, the blur event will be initiated before the click event and remove DOM elements,
939
- // so that the click event will never be triggered. In order to avoid this issue, DOM removal should be delayed.
940
- setTimeout(function () {
941
- if (doc.activeElement !== input) {
942
- clear();
943
- }
944
- }, 200);
945
- }
946
- function manualFetch() {
947
- startFetch(input.value, 3 /* Manual */, input.selectionStart || 0);
948
- }
949
- /**
950
- * Fixes #26: on long clicks focus will be lost and onSelect method will not be called
951
- */
952
- container.addEventListener('mousedown', function (evt) {
953
- evt.stopPropagation();
954
- evt.preventDefault();
955
- });
956
- /**
957
- * Fixes #30: autocomplete closes when scrollbar is clicked in IE
958
- * See: https://stackoverflow.com/a/9210267/13172349
959
- */
960
- container.addEventListener('focus', function () { return input.focus(); });
961
- // If the custom autocomplete container is already appended to the DOM during widget initialization, detach it.
962
- detach();
963
- /**
964
- * This function will remove DOM elements and clear event handlers
965
- */
966
- function destroy() {
967
- input.removeEventListener('focus', focusEventHandler);
968
- input.removeEventListener('keyup', keyupEventHandler);
969
- input.removeEventListener('click', clickEventHandler);
970
- input.removeEventListener('keydown', keydownEventHandler);
971
- input.removeEventListener('input', inputEventHandler);
972
- input.removeEventListener('blur', blurEventHandler);
973
- window.removeEventListener('resize', resizeEventHandler);
974
- doc.removeEventListener('scroll', scrollEventHandler, true);
975
- input.removeAttribute('role');
976
- input.removeAttribute('aria-expanded');
977
- input.removeAttribute('aria-autocomplete');
978
- input.removeAttribute('aria-controls');
979
- input.removeAttribute('aria-activedescendant');
980
- input.removeAttribute('aria-owns');
981
- input.removeAttribute('aria-haspopup');
982
- clearDebounceTimer();
983
- clear();
984
- destroyed = true;
985
- }
986
- // setup event handlers
987
- input.addEventListener('keyup', keyupEventHandler);
988
- input.addEventListener('click', clickEventHandler);
989
- input.addEventListener('keydown', keydownEventHandler);
990
- input.addEventListener('input', inputEventHandler);
991
- input.addEventListener('blur', blurEventHandler);
992
- input.addEventListener('focus', focusEventHandler);
993
- window.addEventListener('resize', resizeEventHandler);
994
- doc.addEventListener('scroll', scrollEventHandler, true);
995
- return {
996
- destroy: destroy,
997
- fetch: manualFetch
998
- };
546
+ /**
547
+ * Copyright (c) 2016 Denis Taran
548
+ *
549
+ * Homepage: https://smartscheduling.com/en/documentation/autocomplete
550
+ * Source: https://github.com/denis-taran/autocomplete
551
+ *
552
+ * MIT License
553
+ */
554
+ function autocomplete(settings) {
555
+ // just an alias to minimize JS file size
556
+ var doc = document;
557
+ var container = settings.container || doc.createElement('div');
558
+ var preventSubmit = settings.preventSubmit || 0 /* Never */;
559
+ container.id = container.id || 'autocomplete-' + uid();
560
+ var containerStyle = container.style;
561
+ var debounceWaitMs = settings.debounceWaitMs || 0;
562
+ var disableAutoSelect = settings.disableAutoSelect || false;
563
+ var customContainerParent = container.parentElement;
564
+ var items = [];
565
+ var inputValue = '';
566
+ var minLen = 2;
567
+ var showOnFocus = settings.showOnFocus;
568
+ var selected;
569
+ var fetchCounter = 0;
570
+ var debounceTimer;
571
+ var destroyed = false;
572
+ // Fixes #104: autocomplete selection is broken on Firefox for Android
573
+ var suppressAutocomplete = false;
574
+ if (settings.minLength !== undefined) {
575
+ minLen = settings.minLength;
576
+ }
577
+ if (!settings.input) {
578
+ throw new Error('input undefined');
579
+ }
580
+ var input = settings.input;
581
+ container.className = [container.className, 'autocomplete', settings.className || ''].join(' ').trim();
582
+ container.setAttribute('role', 'listbox');
583
+ input.setAttribute('role', 'combobox');
584
+ input.setAttribute('aria-expanded', 'false');
585
+ input.setAttribute('aria-autocomplete', 'list');
586
+ input.setAttribute('aria-controls', container.id);
587
+ input.setAttribute('aria-owns', container.id);
588
+ input.setAttribute('aria-activedescendant', '');
589
+ input.setAttribute('aria-haspopup', 'listbox');
590
+ // IOS implementation for fixed positioning has many bugs, so we will use absolute positioning
591
+ containerStyle.position = 'absolute';
592
+ /**
593
+ * Generate a very complex textual ID that greatly reduces the chance of a collision with another ID or text.
594
+ */
595
+ function uid() {
596
+ return Date.now().toString(36) + Math.random().toString(36).substring(2);
597
+ }
598
+ /**
599
+ * Detach the container from DOM
600
+ */
601
+ function detach() {
602
+ var parent = container.parentNode;
603
+ if (parent) {
604
+ parent.removeChild(container);
605
+ }
606
+ }
607
+ /**
608
+ * Clear debouncing timer if assigned
609
+ */
610
+ function clearDebounceTimer() {
611
+ if (debounceTimer) {
612
+ window.clearTimeout(debounceTimer);
613
+ }
614
+ }
615
+ /**
616
+ * Attach the container to DOM
617
+ */
618
+ function attach() {
619
+ if (!container.parentNode) {
620
+ (customContainerParent || doc.body).appendChild(container);
621
+ }
622
+ }
623
+ /**
624
+ * Check if container for autocomplete is displayed
625
+ */
626
+ function containerDisplayed() {
627
+ return !!container.parentNode;
628
+ }
629
+ /**
630
+ * Clear autocomplete state and hide container
631
+ */
632
+ function clear() {
633
+ // prevent the update call if there are pending AJAX requests
634
+ fetchCounter++;
635
+ items = [];
636
+ inputValue = '';
637
+ selected = undefined;
638
+ input.setAttribute('aria-activedescendant', '');
639
+ input.setAttribute('aria-expanded', 'false');
640
+ detach();
641
+ }
642
+ /**
643
+ * Update autocomplete position
644
+ */
645
+ function updatePosition() {
646
+ if (!containerDisplayed()) {
647
+ return;
648
+ }
649
+ input.setAttribute('aria-expanded', 'true');
650
+ containerStyle.height = 'auto';
651
+ containerStyle.width = input.offsetWidth + 'px';
652
+ var maxHeight = 0;
653
+ var inputRect;
654
+ function calc() {
655
+ var docEl = doc.documentElement;
656
+ var clientTop = docEl.clientTop || doc.body.clientTop || 0;
657
+ var clientLeft = docEl.clientLeft || doc.body.clientLeft || 0;
658
+ var scrollTop = window.pageYOffset || docEl.scrollTop;
659
+ var scrollLeft = window.pageXOffset || docEl.scrollLeft;
660
+ inputRect = input.getBoundingClientRect();
661
+ var top = inputRect.top + input.offsetHeight + scrollTop - clientTop;
662
+ var left = inputRect.left + scrollLeft - clientLeft;
663
+ containerStyle.top = top + 'px';
664
+ containerStyle.left = left + 'px';
665
+ maxHeight = window.innerHeight - (inputRect.top + input.offsetHeight);
666
+ if (maxHeight < 0) {
667
+ maxHeight = 0;
668
+ }
669
+ containerStyle.top = top + 'px';
670
+ containerStyle.bottom = '';
671
+ containerStyle.left = left + 'px';
672
+ containerStyle.maxHeight = maxHeight + 'px';
673
+ }
674
+ // the calc method must be called twice, otherwise the calculation may be wrong on resize event (chrome browser)
675
+ calc();
676
+ calc();
677
+ if (settings.customize && inputRect) {
678
+ settings.customize(input, inputRect, container, maxHeight);
679
+ }
680
+ }
681
+ /**
682
+ * Redraw the autocomplete div element with suggestions
683
+ */
684
+ function update() {
685
+ container.textContent = '';
686
+ input.setAttribute('aria-activedescendant', '');
687
+ // function for rendering autocomplete suggestions
688
+ var render = function (item, _, __) {
689
+ var itemElement = doc.createElement('div');
690
+ itemElement.textContent = item.label || '';
691
+ return itemElement;
692
+ };
693
+ if (settings.render) {
694
+ render = settings.render;
695
+ }
696
+ // function to render autocomplete groups
697
+ var renderGroup = function (groupName, _) {
698
+ var groupDiv = doc.createElement('div');
699
+ groupDiv.textContent = groupName;
700
+ return groupDiv;
701
+ };
702
+ if (settings.renderGroup) {
703
+ renderGroup = settings.renderGroup;
704
+ }
705
+ var fragment = doc.createDocumentFragment();
706
+ var prevGroup = uid();
707
+ items.forEach(function (item, index) {
708
+ if (item.group && item.group !== prevGroup) {
709
+ prevGroup = item.group;
710
+ var groupDiv = renderGroup(item.group, inputValue);
711
+ if (groupDiv) {
712
+ groupDiv.className += ' group';
713
+ fragment.appendChild(groupDiv);
714
+ }
715
+ }
716
+ var div = render(item, inputValue, index);
717
+ if (div) {
718
+ div.id = container.id + "_" + index;
719
+ div.setAttribute('role', 'option');
720
+ div.addEventListener('click', function (ev) {
721
+ suppressAutocomplete = true;
722
+ try {
723
+ settings.onSelect(item, input);
724
+ }
725
+ finally {
726
+ suppressAutocomplete = false;
727
+ }
728
+ clear();
729
+ ev.preventDefault();
730
+ ev.stopPropagation();
731
+ });
732
+ if (item === selected) {
733
+ div.className += ' selected';
734
+ div.setAttribute('aria-selected', 'true');
735
+ input.setAttribute('aria-activedescendant', div.id);
736
+ }
737
+ fragment.appendChild(div);
738
+ }
739
+ });
740
+ container.appendChild(fragment);
741
+ if (items.length < 1) {
742
+ if (settings.emptyMsg) {
743
+ var empty = doc.createElement('div');
744
+ empty.id = container.id + "_" + uid();
745
+ empty.className = 'empty';
746
+ empty.textContent = settings.emptyMsg;
747
+ container.appendChild(empty);
748
+ input.setAttribute('aria-activedescendant', empty.id);
749
+ }
750
+ else {
751
+ clear();
752
+ return;
753
+ }
754
+ }
755
+ attach();
756
+ updatePosition();
757
+ updateScroll();
758
+ }
759
+ function updateIfDisplayed() {
760
+ if (containerDisplayed()) {
761
+ update();
762
+ }
763
+ }
764
+ function resizeEventHandler() {
765
+ updateIfDisplayed();
766
+ }
767
+ function scrollEventHandler(e) {
768
+ if (e.target !== container) {
769
+ updateIfDisplayed();
770
+ }
771
+ else {
772
+ e.preventDefault();
773
+ }
774
+ }
775
+ function inputEventHandler() {
776
+ if (!suppressAutocomplete) {
777
+ fetch(0 /* Keyboard */);
778
+ }
779
+ }
780
+ /**
781
+ * Automatically move scroll bar if selected item is not visible
782
+ */
783
+ function updateScroll() {
784
+ var elements = container.getElementsByClassName('selected');
785
+ if (elements.length > 0) {
786
+ var element = elements[0];
787
+ // make group visible
788
+ var previous = element.previousElementSibling;
789
+ if (previous && previous.className.indexOf('group') !== -1 && !previous.previousElementSibling) {
790
+ element = previous;
791
+ }
792
+ if (element.offsetTop < container.scrollTop) {
793
+ container.scrollTop = element.offsetTop;
794
+ }
795
+ else {
796
+ var selectBottom = element.offsetTop + element.offsetHeight;
797
+ var containerBottom = container.scrollTop + container.offsetHeight;
798
+ if (selectBottom > containerBottom) {
799
+ container.scrollTop += selectBottom - containerBottom;
800
+ }
801
+ }
802
+ }
803
+ }
804
+ function selectPreviousSuggestion() {
805
+ var index = items.indexOf(selected);
806
+ selected = index === -1
807
+ ? undefined
808
+ : items[(index + items.length - 1) % items.length];
809
+ updateSelectedSuggestion(index);
810
+ }
811
+ function selectNextSuggestion() {
812
+ var index = items.indexOf(selected);
813
+ selected = items.length < 1
814
+ ? undefined
815
+ : index === -1
816
+ ? items[0]
817
+ : items[(index + 1) % items.length];
818
+ updateSelectedSuggestion(index);
819
+ }
820
+ function updateSelectedSuggestion(index) {
821
+ if (items.length > 0) {
822
+ unselectSuggestion(index);
823
+ selectSuggestion(items.indexOf(selected));
824
+ updateScroll();
825
+ }
826
+ }
827
+ function selectSuggestion(index) {
828
+ var element = doc.getElementById(container.id + "_" + index);
829
+ if (element) {
830
+ element.classList.add('selected');
831
+ element.setAttribute('aria-selected', 'true');
832
+ input.setAttribute('aria-activedescendant', element.id);
833
+ }
834
+ }
835
+ function unselectSuggestion(index) {
836
+ var element = doc.getElementById(container.id + "_" + index);
837
+ if (element) {
838
+ element.classList.remove('selected');
839
+ element.removeAttribute('aria-selected');
840
+ input.removeAttribute('aria-activedescendant');
841
+ }
842
+ }
843
+ function handleArrowAndEscapeKeys(ev, key) {
844
+ var containerIsDisplayed = containerDisplayed();
845
+ if (key === 'Escape') {
846
+ clear();
847
+ }
848
+ else {
849
+ if (!containerIsDisplayed || items.length < 1) {
850
+ return;
851
+ }
852
+ key === 'ArrowUp'
853
+ ? selectPreviousSuggestion()
854
+ : selectNextSuggestion();
855
+ }
856
+ ev.preventDefault();
857
+ if (containerIsDisplayed) {
858
+ ev.stopPropagation();
859
+ }
860
+ }
861
+ function handleEnterKey(ev) {
862
+ if (selected) {
863
+ if (preventSubmit === 2 /* OnSelect */) {
864
+ ev.preventDefault();
865
+ }
866
+ suppressAutocomplete = true;
867
+ try {
868
+ settings.onSelect(selected, input);
869
+ }
870
+ finally {
871
+ suppressAutocomplete = false;
872
+ }
873
+ clear();
874
+ }
875
+ if (preventSubmit === 1 /* Always */) {
876
+ ev.preventDefault();
877
+ }
878
+ }
879
+ function keydownEventHandler(ev) {
880
+ var key = ev.key;
881
+ switch (key) {
882
+ case 'ArrowUp':
883
+ case 'ArrowDown':
884
+ case 'Escape':
885
+ handleArrowAndEscapeKeys(ev, key);
886
+ break;
887
+ case 'Enter':
888
+ handleEnterKey(ev);
889
+ break;
890
+ }
891
+ }
892
+ function focusEventHandler() {
893
+ if (showOnFocus) {
894
+ fetch(1 /* Focus */);
895
+ }
896
+ }
897
+ function fetch(trigger) {
898
+ if (input.value.length >= minLen || trigger === 1 /* Focus */) {
899
+ clearDebounceTimer();
900
+ debounceTimer = window.setTimeout(function () { return startFetch(input.value, trigger, input.selectionStart || 0); }, trigger === 0 /* Keyboard */ || trigger === 2 /* Mouse */ ? debounceWaitMs : 0);
901
+ }
902
+ else {
903
+ clear();
904
+ }
905
+ }
906
+ function startFetch(inputText, trigger, cursorPos) {
907
+ if (destroyed)
908
+ return;
909
+ var savedFetchCounter = ++fetchCounter;
910
+ settings.fetch(inputText, function (elements) {
911
+ if (fetchCounter === savedFetchCounter && elements) {
912
+ items = elements;
913
+ inputValue = inputText;
914
+ selected = (items.length < 1 || disableAutoSelect) ? undefined : items[0];
915
+ update();
916
+ }
917
+ }, trigger, cursorPos);
918
+ }
919
+ function keyupEventHandler(e) {
920
+ if (settings.keyup) {
921
+ settings.keyup({
922
+ event: e,
923
+ fetch: function () { return fetch(0 /* Keyboard */); }
924
+ });
925
+ return;
926
+ }
927
+ if (!containerDisplayed() && e.key === 'ArrowDown') {
928
+ fetch(0 /* Keyboard */);
929
+ }
930
+ }
931
+ function clickEventHandler(e) {
932
+ settings.click && settings.click({
933
+ event: e,
934
+ fetch: function () { return fetch(2 /* Mouse */); }
935
+ });
936
+ }
937
+ function blurEventHandler() {
938
+ // when an item is selected by mouse click, the blur event will be initiated before the click event and remove DOM elements,
939
+ // so that the click event will never be triggered. In order to avoid this issue, DOM removal should be delayed.
940
+ setTimeout(function () {
941
+ if (doc.activeElement !== input) {
942
+ clear();
943
+ }
944
+ }, 200);
945
+ }
946
+ function manualFetch() {
947
+ startFetch(input.value, 3 /* Manual */, input.selectionStart || 0);
948
+ }
949
+ /**
950
+ * Fixes #26: on long clicks focus will be lost and onSelect method will not be called
951
+ */
952
+ container.addEventListener('mousedown', function (evt) {
953
+ evt.stopPropagation();
954
+ evt.preventDefault();
955
+ });
956
+ /**
957
+ * Fixes #30: autocomplete closes when scrollbar is clicked in IE
958
+ * See: https://stackoverflow.com/a/9210267/13172349
959
+ */
960
+ container.addEventListener('focus', function () { return input.focus(); });
961
+ // If the custom autocomplete container is already appended to the DOM during widget initialization, detach it.
962
+ detach();
963
+ /**
964
+ * This function will remove DOM elements and clear event handlers
965
+ */
966
+ function destroy() {
967
+ input.removeEventListener('focus', focusEventHandler);
968
+ input.removeEventListener('keyup', keyupEventHandler);
969
+ input.removeEventListener('click', clickEventHandler);
970
+ input.removeEventListener('keydown', keydownEventHandler);
971
+ input.removeEventListener('input', inputEventHandler);
972
+ input.removeEventListener('blur', blurEventHandler);
973
+ window.removeEventListener('resize', resizeEventHandler);
974
+ doc.removeEventListener('scroll', scrollEventHandler, true);
975
+ input.removeAttribute('role');
976
+ input.removeAttribute('aria-expanded');
977
+ input.removeAttribute('aria-autocomplete');
978
+ input.removeAttribute('aria-controls');
979
+ input.removeAttribute('aria-activedescendant');
980
+ input.removeAttribute('aria-owns');
981
+ input.removeAttribute('aria-haspopup');
982
+ clearDebounceTimer();
983
+ clear();
984
+ destroyed = true;
985
+ }
986
+ // setup event handlers
987
+ input.addEventListener('keyup', keyupEventHandler);
988
+ input.addEventListener('click', clickEventHandler);
989
+ input.addEventListener('keydown', keydownEventHandler);
990
+ input.addEventListener('input', inputEventHandler);
991
+ input.addEventListener('blur', blurEventHandler);
992
+ input.addEventListener('focus', focusEventHandler);
993
+ window.addEventListener('resize', resizeEventHandler);
994
+ doc.addEventListener('scroll', scrollEventHandler, true);
995
+ return {
996
+ destroy: destroy,
997
+ fetch: manualFetch
998
+ };
999
999
  }
1000
1000
 
1001
1001
  class TagOption extends HTMLElement {
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 8.1.0"
6
+
7
+ gemspec path: "../"
Binary file
@@ -11,5 +11,6 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@botandrose/input-tag": "0.4.2"
14
- }
14
+ },
15
+ "packageManager": "bun@latest"
15
16
  }
@@ -3,19 +3,19 @@ require_relative "field"
3
3
  module Bard
4
4
  module TagField
5
5
  module FormBuilder
6
- def bard_tag_field method, choices = nil, options = {}, html_options = {}, &block
6
+ def tag_field method, choices = nil, options = {}, html_options = {}, &block
7
7
  # Handle different method signatures to match Rails select helper
8
8
  case choices
9
9
  when Hash
10
- # bard_tag_field(:method, { class: "form-control" })
10
+ # tag_field(:method, { class: "form-control" })
11
11
  html_options = options
12
12
  options = choices
13
13
  choices = nil
14
14
  when Array
15
- # bard_tag_field(:method, choices_array, { class: "form-control" })
15
+ # tag_field(:method, choices_array, { class: "form-control" })
16
16
  html_options = options if options.is_a?(Hash)
17
17
  when NilClass
18
- # bard_tag_field(:method)
18
+ # tag_field(:method)
19
19
  html_options = options
20
20
  options = {}
21
21
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module TagField
5
- VERSION = "0.4.2"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bard-tag_field
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-19 00:00:00.000000000 Z
11
+ date: 2025-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -75,22 +75,24 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - ".rspec"
77
77
  - Appraisals
78
+ - CLAUDE.md
78
79
  - Gemfile
79
80
  - LICENSE
80
81
  - README.md
81
82
  - Rakefile
82
83
  - app/assets/javascripts/input-tag.js
83
- - bard-tag/.gitignore
84
- - bard-tag/bun.lockb
85
- - bard-tag/index.js
86
- - bard-tag/package.json
87
- - bard-tag/rollup.config.js
88
84
  - bard-tag_field.gemspec
89
85
  - bin/console
90
86
  - bin/setup
91
87
  - gemfiles/rails_7.1.gemfile
92
88
  - gemfiles/rails_7.2.gemfile
93
89
  - gemfiles/rails_8.0.gemfile
90
+ - gemfiles/rails_8.1.gemfile
91
+ - input-tag/.gitignore
92
+ - input-tag/bun.lockb
93
+ - input-tag/index.js
94
+ - input-tag/package.json
95
+ - input-tag/rollup.config.js
94
96
  - lib/bard/tag_field.rb
95
97
  - lib/bard/tag_field/cucumber.rb
96
98
  - lib/bard/tag_field/field.rb
File without changes
File without changes
File without changes