bard-tag_field 0.6.1 → 0.7.1
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 +4 -4
- data/README.md +57 -0
- data/app/assets/javascripts/input-tag.js +60 -18
- data/input-tag/src/input-tag.js +60 -18
- data/input-tag/test/autocomplete.test.js +163 -0
- data/lib/bard/tag_field/version.rb +1 -1
- data/lib/bard-tag_field.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 273e3dda2a790b12547aa1aecc3b897fcf114f667fe5e3b5c0d991b41e61c24f
|
|
4
|
+
data.tar.gz: 12174ee0c8167b487dd2081b32220ac257874bf26e1cbaab1d51f7b422497b55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ae7bf9a3ff6260c925d22a890581e1995dd6e1327002abb91f70f9064c3c339f34517b92e5cc2ef24d78fcad67873c5fbb991240815f85bb2acf74333c1ccf1
|
|
7
|
+
data.tar.gz: 43cbe858f1ecad6feed1fd80006bb3939af4c3c1963921973ae24c9cffb52eb67e3620d346b97a46d572325e7a889af07a1aedb78aa62b92899afb54227cafc7
|
data/README.md
CHANGED
|
@@ -168,6 +168,63 @@ Or include the precompiled asset (automatically added by this gem):
|
|
|
168
168
|
//= require input-tag
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
## Styling
|
|
172
|
+
|
|
173
|
+
The custom element uses Shadow DOM, so its internals can't be reached with normal selectors. Theme it by overriding these CSS custom properties on `input-tag` (or any ancestor).
|
|
174
|
+
|
|
175
|
+
### Tag (`<tag-option>`)
|
|
176
|
+
|
|
177
|
+
| Variable | Default | Styles |
|
|
178
|
+
| --- | --- | --- |
|
|
179
|
+
| `--tag-option-bg` | `#588a00` | Tag background |
|
|
180
|
+
| `--tag-option-color` | `#fff` | Tag label color |
|
|
181
|
+
| `--tag-option-button-color` | `rgba(255, 255, 255, 0.6)` | Tag close-button color |
|
|
182
|
+
|
|
183
|
+
### Container
|
|
184
|
+
|
|
185
|
+
| Variable | Default | Styles |
|
|
186
|
+
| --- | --- | --- |
|
|
187
|
+
| `--container-bg` | `rgba(255, 255, 255, 0.8)` | Background of the input area |
|
|
188
|
+
| `--container-border` | `#d0d0d0` | Border color (all sides) |
|
|
189
|
+
| `--container-border-left-width` | `1px` | Left border width (override to add a focus/error accent stripe) |
|
|
190
|
+
| `--container-border-left-color` | `#d0d0d0` | Left border color |
|
|
191
|
+
| `--container-shadow` | `#ccc` | Inset shadow color |
|
|
192
|
+
|
|
193
|
+
### Input
|
|
194
|
+
|
|
195
|
+
| Variable | Default | Styles |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `--input-border` | `#d0d0d0` | Dashed border around the input |
|
|
198
|
+
| `--input-bg` | `#fff` | Input background |
|
|
199
|
+
| `--input-color` | `#333` | Input text color |
|
|
200
|
+
|
|
201
|
+
### Suggestions toggle button
|
|
202
|
+
|
|
203
|
+
| Variable | Default | Styles |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `--button-border` | `#e0e0e0` | Button border color |
|
|
206
|
+
| `--button-color` | `#666` | Caret color |
|
|
207
|
+
|
|
208
|
+
### Autocomplete menu
|
|
209
|
+
|
|
210
|
+
| Variable | Default | Styles |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| `--menu-shadow` | `#ccc` | Drop shadow color |
|
|
213
|
+
| `--menu-bg` | `#fff` | Menu background |
|
|
214
|
+
| `--menu-color` | `#555` | Menu item text color |
|
|
215
|
+
| `--menu-hover` | `#e0e0e0` | Menu item hover background |
|
|
216
|
+
|
|
217
|
+
### Example
|
|
218
|
+
|
|
219
|
+
```css
|
|
220
|
+
input-tag {
|
|
221
|
+
--tag-option-bg: #1f6feb;
|
|
222
|
+
--container-border: #ccd;
|
|
223
|
+
--container-border-left-width: 3px;
|
|
224
|
+
--container-border-left-color: #1f6feb;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
171
228
|
## Browser Support
|
|
172
229
|
|
|
173
230
|
- Modern browsers that support custom elements
|
|
@@ -1001,7 +1001,7 @@ function autocomplete(settings) {
|
|
|
1001
1001
|
const tagOptionStyleSheet = new CSSStyleSheet();
|
|
1002
1002
|
tagOptionStyleSheet.replaceSync(`
|
|
1003
1003
|
:host {
|
|
1004
|
-
background: #588a00;
|
|
1004
|
+
background: var(--tag-option-bg, #588a00);
|
|
1005
1005
|
padding: 3px 10px 3px 10px !important;
|
|
1006
1006
|
margin-right: 4px !important;
|
|
1007
1007
|
margin-bottom: 2px !important;
|
|
@@ -1011,7 +1011,7 @@ tagOptionStyleSheet.replaceSync(`
|
|
|
1011
1011
|
font-size: 14px;
|
|
1012
1012
|
line-height: 1;
|
|
1013
1013
|
min-height: 32px;
|
|
1014
|
-
color: #fff;
|
|
1014
|
+
color: var(--tag-option-color, #fff);
|
|
1015
1015
|
text-transform: none;
|
|
1016
1016
|
border-radius: 3px;
|
|
1017
1017
|
position: relative;
|
|
@@ -1023,7 +1023,7 @@ tagOptionStyleSheet.replaceSync(`
|
|
|
1023
1023
|
background: none;
|
|
1024
1024
|
font-size: 20px;
|
|
1025
1025
|
display: inline-block;
|
|
1026
|
-
color: rgba(255, 255, 255, 0.6);
|
|
1026
|
+
color: var(--tag-option-button-color, rgba(255, 255, 255, 0.6));
|
|
1027
1027
|
right: 10px;
|
|
1028
1028
|
height: 100%;
|
|
1029
1029
|
cursor: pointer;
|
|
@@ -1040,7 +1040,7 @@ inputTagStyleSheet.replaceSync(`
|
|
|
1040
1040
|
padding: 0;
|
|
1041
1041
|
}
|
|
1042
1042
|
#container {
|
|
1043
|
-
background: rgba(255, 255, 255, 0.8);
|
|
1043
|
+
background: var(--container-bg, rgba(255, 255, 255, 0.8));
|
|
1044
1044
|
padding: 6px 6px 3px;
|
|
1045
1045
|
max-height: none;
|
|
1046
1046
|
display: flex;
|
|
@@ -1050,9 +1050,11 @@ inputTagStyleSheet.replaceSync(`
|
|
|
1050
1050
|
min-height: 48px;
|
|
1051
1051
|
line-height: 48px;
|
|
1052
1052
|
width: 100%;
|
|
1053
|
-
border: 1px solid #d0d0d0;
|
|
1053
|
+
border: 1px solid var(--container-border, #d0d0d0);
|
|
1054
|
+
border-left-width: var(--container-border-left-width, 1px);
|
|
1055
|
+
border-left-color: var(--container-border-left-color, #d0d0d0);
|
|
1054
1056
|
outline: 1px solid transparent;
|
|
1055
|
-
box-shadow: #ccc 0 1px 4px 0 inset;
|
|
1057
|
+
box-shadow: var(--container-shadow, #ccc) 0 1px 4px 0 inset;
|
|
1056
1058
|
border-radius: 2px;
|
|
1057
1059
|
cursor: text;
|
|
1058
1060
|
color: #333;
|
|
@@ -1072,21 +1074,21 @@ inputTagStyleSheet.replaceSync(`
|
|
|
1072
1074
|
width: 100%;
|
|
1073
1075
|
line-height: 2;
|
|
1074
1076
|
padding: 0 0 0 10px;
|
|
1075
|
-
border: 1px dashed #d0d0d0;
|
|
1077
|
+
border: 1px dashed var(--input-border, #d0d0d0);
|
|
1076
1078
|
outline: 1px solid transparent;
|
|
1077
|
-
background: #fff;
|
|
1079
|
+
background: var(--input-bg, #fff);
|
|
1078
1080
|
box-shadow: none;
|
|
1079
1081
|
border-radius: 2px;
|
|
1080
1082
|
cursor: text;
|
|
1081
|
-
color: #333;
|
|
1083
|
+
color: var(--input-color, #333);
|
|
1082
1084
|
}
|
|
1083
1085
|
button {
|
|
1084
1086
|
width: 38px;
|
|
1085
1087
|
text-align: center;
|
|
1086
1088
|
line-height: 36px;
|
|
1087
|
-
border: 1px solid #e0e0e0;
|
|
1089
|
+
border: 1px solid var(--button-border, #e0e0e0);
|
|
1088
1090
|
font-size: 20px;
|
|
1089
|
-
color: #666;
|
|
1091
|
+
color: var(--button-color, #666);
|
|
1090
1092
|
position: absolute !important;
|
|
1091
1093
|
z-index: 10;
|
|
1092
1094
|
right: 0px;
|
|
@@ -1111,11 +1113,11 @@ inputTagStyleSheet.replaceSync(`
|
|
|
1111
1113
|
.ui-menu{
|
|
1112
1114
|
margin: 0;
|
|
1113
1115
|
padding: 6px;
|
|
1114
|
-
box-shadow: #ccc 0 1px 6px;
|
|
1116
|
+
box-shadow: var(--menu-shadow, #ccc) 0 1px 6px;
|
|
1115
1117
|
z-index: 2;
|
|
1116
1118
|
display: flex;
|
|
1117
1119
|
flex-wrap: wrap;
|
|
1118
|
-
background: #fff;
|
|
1120
|
+
background: var(--menu-bg, #fff);
|
|
1119
1121
|
list-style: none;
|
|
1120
1122
|
font-size: 14px;
|
|
1121
1123
|
min-width: 200px;
|
|
@@ -1130,10 +1132,10 @@ inputTagStyleSheet.replaceSync(`
|
|
|
1130
1132
|
border-radius: 2px;
|
|
1131
1133
|
width: auto;
|
|
1132
1134
|
cursor: pointer;
|
|
1133
|
-
color: #555;
|
|
1135
|
+
color: var(--menu-color, #555);
|
|
1134
1136
|
}
|
|
1135
1137
|
.ui-menu .ui-menu-item::before{ display: none; }
|
|
1136
|
-
.ui-menu .ui-menu-item:hover{ background: #e0e0e0; }
|
|
1138
|
+
.ui-menu .ui-menu-item:hover{ background: var(--menu-hover, #e0e0e0); }
|
|
1137
1139
|
.ui-state-active{
|
|
1138
1140
|
padding: 0;
|
|
1139
1141
|
border: none;
|
|
@@ -1337,6 +1339,7 @@ class InputTag extends HTMLElement {
|
|
|
1337
1339
|
reset() {
|
|
1338
1340
|
this._taggle.removeAll();
|
|
1339
1341
|
this._taggleInputTarget.value = '';
|
|
1342
|
+
this._updateButtonContent();
|
|
1340
1343
|
}
|
|
1341
1344
|
|
|
1342
1345
|
get options() {
|
|
@@ -1437,11 +1440,15 @@ class InputTag extends HTMLElement {
|
|
|
1437
1440
|
this.checkRequired();
|
|
1438
1441
|
|
|
1439
1442
|
this.buttonTarget = document.createElement("button");
|
|
1443
|
+
this.buttonTarget.type = "button";
|
|
1440
1444
|
this.buttonTarget.className = "add";
|
|
1441
1445
|
this.buttonTarget.textContent = "+";
|
|
1446
|
+
this.buttonTarget.addEventListener("mousedown", e => e.preventDefault());
|
|
1442
1447
|
this.buttonTarget.addEventListener("click", e => this._add(e));
|
|
1443
1448
|
this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget);
|
|
1444
1449
|
|
|
1450
|
+
this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
|
|
1451
|
+
|
|
1445
1452
|
this.autocompleteContainerTarget = document.createElement("ul");
|
|
1446
1453
|
this._wrapperTarget.appendChild(this.autocompleteContainerTarget);
|
|
1447
1454
|
|
|
@@ -1452,10 +1459,11 @@ class InputTag extends HTMLElement {
|
|
|
1452
1459
|
|
|
1453
1460
|
// Update visibility based on current state
|
|
1454
1461
|
this.updateInputVisibility();
|
|
1462
|
+
this._updateButtonContent();
|
|
1455
1463
|
}
|
|
1456
1464
|
|
|
1457
1465
|
setupAutocomplete() {
|
|
1458
|
-
autocomplete({
|
|
1466
|
+
this._autocompleteResult = autocomplete({
|
|
1459
1467
|
input: this._taggleInputTarget,
|
|
1460
1468
|
container: this.autocompleteContainerTarget,
|
|
1461
1469
|
className: "ui-menu ui-autocomplete",
|
|
@@ -1478,6 +1486,7 @@ class InputTag extends HTMLElement {
|
|
|
1478
1486
|
// Prevent adding multiple tags in single mode
|
|
1479
1487
|
if (!this.multiple && this._taggle.getTagValues().length > 0) {
|
|
1480
1488
|
this._taggleInputTarget.value = '';
|
|
1489
|
+
this._updateButtonContent();
|
|
1481
1490
|
return
|
|
1482
1491
|
}
|
|
1483
1492
|
|
|
@@ -1489,8 +1498,9 @@ class InputTag extends HTMLElement {
|
|
|
1489
1498
|
|
|
1490
1499
|
// Clear input
|
|
1491
1500
|
this._taggleInputTarget.value = '';
|
|
1501
|
+
this._updateButtonContent();
|
|
1492
1502
|
},
|
|
1493
|
-
minLength:
|
|
1503
|
+
minLength: 0,
|
|
1494
1504
|
customize: (input, inputRect, container, maxHeight) => {
|
|
1495
1505
|
// Position autocomplete below the input-tag container, accounting for dynamic height
|
|
1496
1506
|
this._updateAutocompletePosition(container);
|
|
@@ -1565,8 +1575,35 @@ class InputTag extends HTMLElement {
|
|
|
1565
1575
|
|
|
1566
1576
|
_add(event) {
|
|
1567
1577
|
event.preventDefault();
|
|
1568
|
-
this.
|
|
1578
|
+
const value = this._taggleInputTarget.value;
|
|
1579
|
+
if (value === '') {
|
|
1580
|
+
if (this._isAutocompleteOpen()) {
|
|
1581
|
+
this._closeAutocomplete();
|
|
1582
|
+
} else {
|
|
1583
|
+
this._taggleInputTarget.focus();
|
|
1584
|
+
this._autocompleteResult.fetch();
|
|
1585
|
+
}
|
|
1586
|
+
return
|
|
1587
|
+
}
|
|
1588
|
+
this._taggle.add(value);
|
|
1569
1589
|
this._taggleInputTarget.value = '';
|
|
1590
|
+
this._updateButtonContent();
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
_isAutocompleteOpen() {
|
|
1594
|
+
return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
_closeAutocomplete() {
|
|
1598
|
+
this._taggleInputTarget.setAttribute("aria-expanded", "false");
|
|
1599
|
+
this.autocompleteContainerTarget.remove();
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
_updateButtonContent() {
|
|
1603
|
+
const isEmpty = !this._taggleInputTarget.value;
|
|
1604
|
+
const currentTags = this._taggle.getTagValues();
|
|
1605
|
+
const hasAvailableOption = this.options.some(value => !currentTags.includes(value));
|
|
1606
|
+
this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+";
|
|
1570
1607
|
}
|
|
1571
1608
|
|
|
1572
1609
|
onTagAdd(event, tag) {
|
|
@@ -1581,6 +1618,8 @@ class InputTag extends HTMLElement {
|
|
|
1581
1618
|
this.syncValue();
|
|
1582
1619
|
this.checkRequired();
|
|
1583
1620
|
this.updateInputVisibility();
|
|
1621
|
+
// Defer button update: taggle clears input.value after calling this callback
|
|
1622
|
+
setTimeout(() => this._updateButtonContent(), 0);
|
|
1584
1623
|
|
|
1585
1624
|
// Update autocomplete position if it's currently open
|
|
1586
1625
|
if (this._autocompleteContainer) {
|
|
@@ -1600,6 +1639,7 @@ class InputTag extends HTMLElement {
|
|
|
1600
1639
|
this.syncValue();
|
|
1601
1640
|
this.checkRequired();
|
|
1602
1641
|
this.updateInputVisibility();
|
|
1642
|
+
this._updateButtonContent();
|
|
1603
1643
|
|
|
1604
1644
|
// Update autocomplete position if it's currently open
|
|
1605
1645
|
if (this._autocompleteContainer) {
|
|
@@ -1821,9 +1861,11 @@ class InputTag extends HTMLElement {
|
|
|
1821
1861
|
this._taggleInputTarget.autocomplete = "off";
|
|
1822
1862
|
this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
|
|
1823
1863
|
this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
|
|
1864
|
+
this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
|
|
1824
1865
|
|
|
1825
1866
|
// Re-setup autocomplete
|
|
1826
1867
|
this.setupAutocomplete();
|
|
1868
|
+
this._updateButtonContent();
|
|
1827
1869
|
|
|
1828
1870
|
// Re-process existing tag options
|
|
1829
1871
|
this.processTagOptions();
|
data/input-tag/src/input-tag.js
CHANGED
|
@@ -5,7 +5,7 @@ import autocomplete from "autocompleter"
|
|
|
5
5
|
const tagOptionStyleSheet = new CSSStyleSheet()
|
|
6
6
|
tagOptionStyleSheet.replaceSync(`
|
|
7
7
|
:host {
|
|
8
|
-
background: #588a00;
|
|
8
|
+
background: var(--tag-option-bg, #588a00);
|
|
9
9
|
padding: 3px 10px 3px 10px !important;
|
|
10
10
|
margin-right: 4px !important;
|
|
11
11
|
margin-bottom: 2px !important;
|
|
@@ -15,7 +15,7 @@ tagOptionStyleSheet.replaceSync(`
|
|
|
15
15
|
font-size: 14px;
|
|
16
16
|
line-height: 1;
|
|
17
17
|
min-height: 32px;
|
|
18
|
-
color: #fff;
|
|
18
|
+
color: var(--tag-option-color, #fff);
|
|
19
19
|
text-transform: none;
|
|
20
20
|
border-radius: 3px;
|
|
21
21
|
position: relative;
|
|
@@ -27,7 +27,7 @@ tagOptionStyleSheet.replaceSync(`
|
|
|
27
27
|
background: none;
|
|
28
28
|
font-size: 20px;
|
|
29
29
|
display: inline-block;
|
|
30
|
-
color: rgba(255, 255, 255, 0.6);
|
|
30
|
+
color: var(--tag-option-button-color, rgba(255, 255, 255, 0.6));
|
|
31
31
|
right: 10px;
|
|
32
32
|
height: 100%;
|
|
33
33
|
cursor: pointer;
|
|
@@ -44,7 +44,7 @@ inputTagStyleSheet.replaceSync(`
|
|
|
44
44
|
padding: 0;
|
|
45
45
|
}
|
|
46
46
|
#container {
|
|
47
|
-
background: rgba(255, 255, 255, 0.8);
|
|
47
|
+
background: var(--container-bg, rgba(255, 255, 255, 0.8));
|
|
48
48
|
padding: 6px 6px 3px;
|
|
49
49
|
max-height: none;
|
|
50
50
|
display: flex;
|
|
@@ -54,9 +54,11 @@ inputTagStyleSheet.replaceSync(`
|
|
|
54
54
|
min-height: 48px;
|
|
55
55
|
line-height: 48px;
|
|
56
56
|
width: 100%;
|
|
57
|
-
border: 1px solid #d0d0d0;
|
|
57
|
+
border: 1px solid var(--container-border, #d0d0d0);
|
|
58
|
+
border-left-width: var(--container-border-left-width, 1px);
|
|
59
|
+
border-left-color: var(--container-border-left-color, #d0d0d0);
|
|
58
60
|
outline: 1px solid transparent;
|
|
59
|
-
box-shadow: #ccc 0 1px 4px 0 inset;
|
|
61
|
+
box-shadow: var(--container-shadow, #ccc) 0 1px 4px 0 inset;
|
|
60
62
|
border-radius: 2px;
|
|
61
63
|
cursor: text;
|
|
62
64
|
color: #333;
|
|
@@ -76,21 +78,21 @@ inputTagStyleSheet.replaceSync(`
|
|
|
76
78
|
width: 100%;
|
|
77
79
|
line-height: 2;
|
|
78
80
|
padding: 0 0 0 10px;
|
|
79
|
-
border: 1px dashed #d0d0d0;
|
|
81
|
+
border: 1px dashed var(--input-border, #d0d0d0);
|
|
80
82
|
outline: 1px solid transparent;
|
|
81
|
-
background: #fff;
|
|
83
|
+
background: var(--input-bg, #fff);
|
|
82
84
|
box-shadow: none;
|
|
83
85
|
border-radius: 2px;
|
|
84
86
|
cursor: text;
|
|
85
|
-
color: #333;
|
|
87
|
+
color: var(--input-color, #333);
|
|
86
88
|
}
|
|
87
89
|
button {
|
|
88
90
|
width: 38px;
|
|
89
91
|
text-align: center;
|
|
90
92
|
line-height: 36px;
|
|
91
|
-
border: 1px solid #e0e0e0;
|
|
93
|
+
border: 1px solid var(--button-border, #e0e0e0);
|
|
92
94
|
font-size: 20px;
|
|
93
|
-
color: #666;
|
|
95
|
+
color: var(--button-color, #666);
|
|
94
96
|
position: absolute !important;
|
|
95
97
|
z-index: 10;
|
|
96
98
|
right: 0px;
|
|
@@ -115,11 +117,11 @@ inputTagStyleSheet.replaceSync(`
|
|
|
115
117
|
.ui-menu{
|
|
116
118
|
margin: 0;
|
|
117
119
|
padding: 6px;
|
|
118
|
-
box-shadow: #ccc 0 1px 6px;
|
|
120
|
+
box-shadow: var(--menu-shadow, #ccc) 0 1px 6px;
|
|
119
121
|
z-index: 2;
|
|
120
122
|
display: flex;
|
|
121
123
|
flex-wrap: wrap;
|
|
122
|
-
background: #fff;
|
|
124
|
+
background: var(--menu-bg, #fff);
|
|
123
125
|
list-style: none;
|
|
124
126
|
font-size: 14px;
|
|
125
127
|
min-width: 200px;
|
|
@@ -134,10 +136,10 @@ inputTagStyleSheet.replaceSync(`
|
|
|
134
136
|
border-radius: 2px;
|
|
135
137
|
width: auto;
|
|
136
138
|
cursor: pointer;
|
|
137
|
-
color: #555;
|
|
139
|
+
color: var(--menu-color, #555);
|
|
138
140
|
}
|
|
139
141
|
.ui-menu .ui-menu-item::before{ display: none; }
|
|
140
|
-
.ui-menu .ui-menu-item:hover{ background: #e0e0e0; }
|
|
142
|
+
.ui-menu .ui-menu-item:hover{ background: var(--menu-hover, #e0e0e0); }
|
|
141
143
|
.ui-state-active{
|
|
142
144
|
padding: 0;
|
|
143
145
|
border: none;
|
|
@@ -341,6 +343,7 @@ class InputTag extends HTMLElement {
|
|
|
341
343
|
reset() {
|
|
342
344
|
this._taggle.removeAll()
|
|
343
345
|
this._taggleInputTarget.value = ''
|
|
346
|
+
this._updateButtonContent()
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
get options() {
|
|
@@ -441,11 +444,15 @@ class InputTag extends HTMLElement {
|
|
|
441
444
|
this.checkRequired()
|
|
442
445
|
|
|
443
446
|
this.buttonTarget = document.createElement("button")
|
|
447
|
+
this.buttonTarget.type = "button"
|
|
444
448
|
this.buttonTarget.className = "add"
|
|
445
449
|
this.buttonTarget.textContent = "+"
|
|
450
|
+
this.buttonTarget.addEventListener("mousedown", e => e.preventDefault())
|
|
446
451
|
this.buttonTarget.addEventListener("click", e => this._add(e))
|
|
447
452
|
this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget)
|
|
448
453
|
|
|
454
|
+
this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent())
|
|
455
|
+
|
|
449
456
|
this.autocompleteContainerTarget = document.createElement("ul");
|
|
450
457
|
this._wrapperTarget.appendChild(this.autocompleteContainerTarget)
|
|
451
458
|
|
|
@@ -456,10 +463,11 @@ class InputTag extends HTMLElement {
|
|
|
456
463
|
|
|
457
464
|
// Update visibility based on current state
|
|
458
465
|
this.updateInputVisibility()
|
|
466
|
+
this._updateButtonContent()
|
|
459
467
|
}
|
|
460
468
|
|
|
461
469
|
setupAutocomplete() {
|
|
462
|
-
autocomplete({
|
|
470
|
+
this._autocompleteResult = autocomplete({
|
|
463
471
|
input: this._taggleInputTarget,
|
|
464
472
|
container: this.autocompleteContainerTarget,
|
|
465
473
|
className: "ui-menu ui-autocomplete",
|
|
@@ -482,6 +490,7 @@ class InputTag extends HTMLElement {
|
|
|
482
490
|
// Prevent adding multiple tags in single mode
|
|
483
491
|
if (!this.multiple && this._taggle.getTagValues().length > 0) {
|
|
484
492
|
this._taggleInputTarget.value = ''
|
|
493
|
+
this._updateButtonContent()
|
|
485
494
|
return
|
|
486
495
|
}
|
|
487
496
|
|
|
@@ -493,8 +502,9 @@ class InputTag extends HTMLElement {
|
|
|
493
502
|
|
|
494
503
|
// Clear input
|
|
495
504
|
this._taggleInputTarget.value = ''
|
|
505
|
+
this._updateButtonContent()
|
|
496
506
|
},
|
|
497
|
-
minLength:
|
|
507
|
+
minLength: 0,
|
|
498
508
|
customize: (input, inputRect, container, maxHeight) => {
|
|
499
509
|
// Position autocomplete below the input-tag container, accounting for dynamic height
|
|
500
510
|
this._updateAutocompletePosition(container);
|
|
@@ -569,8 +579,35 @@ class InputTag extends HTMLElement {
|
|
|
569
579
|
|
|
570
580
|
_add(event) {
|
|
571
581
|
event.preventDefault()
|
|
572
|
-
this.
|
|
582
|
+
const value = this._taggleInputTarget.value
|
|
583
|
+
if (value === '') {
|
|
584
|
+
if (this._isAutocompleteOpen()) {
|
|
585
|
+
this._closeAutocomplete()
|
|
586
|
+
} else {
|
|
587
|
+
this._taggleInputTarget.focus()
|
|
588
|
+
this._autocompleteResult.fetch()
|
|
589
|
+
}
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
this._taggle.add(value)
|
|
573
593
|
this._taggleInputTarget.value = ''
|
|
594
|
+
this._updateButtonContent()
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_isAutocompleteOpen() {
|
|
598
|
+
return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
_closeAutocomplete() {
|
|
602
|
+
this._taggleInputTarget.setAttribute("aria-expanded", "false")
|
|
603
|
+
this.autocompleteContainerTarget.remove()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
_updateButtonContent() {
|
|
607
|
+
const isEmpty = !this._taggleInputTarget.value
|
|
608
|
+
const currentTags = this._taggle.getTagValues()
|
|
609
|
+
const hasAvailableOption = this.options.some(value => !currentTags.includes(value))
|
|
610
|
+
this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+"
|
|
574
611
|
}
|
|
575
612
|
|
|
576
613
|
onTagAdd(event, tag) {
|
|
@@ -585,6 +622,8 @@ class InputTag extends HTMLElement {
|
|
|
585
622
|
this.syncValue()
|
|
586
623
|
this.checkRequired()
|
|
587
624
|
this.updateInputVisibility()
|
|
625
|
+
// Defer button update: taggle clears input.value after calling this callback
|
|
626
|
+
setTimeout(() => this._updateButtonContent(), 0)
|
|
588
627
|
|
|
589
628
|
// Update autocomplete position if it's currently open
|
|
590
629
|
if (this._autocompleteContainer) {
|
|
@@ -604,6 +643,7 @@ class InputTag extends HTMLElement {
|
|
|
604
643
|
this.syncValue()
|
|
605
644
|
this.checkRequired()
|
|
606
645
|
this.updateInputVisibility()
|
|
646
|
+
this._updateButtonContent()
|
|
607
647
|
|
|
608
648
|
// Update autocomplete position if it's currently open
|
|
609
649
|
if (this._autocompleteContainer) {
|
|
@@ -825,9 +865,11 @@ class InputTag extends HTMLElement {
|
|
|
825
865
|
this._taggleInputTarget.autocomplete = "off";
|
|
826
866
|
this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
|
|
827
867
|
this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
|
|
868
|
+
this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
|
|
828
869
|
|
|
829
870
|
// Re-setup autocomplete
|
|
830
871
|
this.setupAutocomplete();
|
|
872
|
+
this._updateButtonContent();
|
|
831
873
|
|
|
832
874
|
// Re-process existing tag options
|
|
833
875
|
this.processTagOptions();
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
waitForBasicInitialization,
|
|
8
8
|
waitForUpdate,
|
|
9
9
|
simulateInput,
|
|
10
|
+
simulateKeyup,
|
|
11
|
+
simulateClick,
|
|
10
12
|
getTagElements,
|
|
11
13
|
getTagValues
|
|
12
14
|
} from './lib/test-utils.js'
|
|
@@ -633,4 +635,165 @@ describe('Autocomplete', () => {
|
|
|
633
635
|
expect(getTagValues(inputTag)).to.deep.equal(['option1'])
|
|
634
636
|
})
|
|
635
637
|
})
|
|
638
|
+
|
|
639
|
+
describe('Dropdown on Empty Input', () => {
|
|
640
|
+
async function setupDropdownTest() {
|
|
641
|
+
document.body.innerHTML = `
|
|
642
|
+
<input-tag name="frameworks" list="suggestions" multiple></input-tag>
|
|
643
|
+
<datalist id="suggestions">
|
|
644
|
+
<option value="react">React</option>
|
|
645
|
+
<option value="vue">Vue</option>
|
|
646
|
+
<option value="angular">Angular</option>
|
|
647
|
+
</datalist>
|
|
648
|
+
`
|
|
649
|
+
const inputTag = document.querySelector('input-tag')
|
|
650
|
+
await waitForBasicInitialization(inputTag)
|
|
651
|
+
return inputTag
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
it('should show all options on ArrowDown when input is empty', async () => {
|
|
655
|
+
const inputTag = await setupDropdownTest()
|
|
656
|
+
const input = inputTag._taggleInputTarget
|
|
657
|
+
|
|
658
|
+
simulateKeyup(input, 40, { key: 'ArrowDown' })
|
|
659
|
+
await waitForUpdate()
|
|
660
|
+
|
|
661
|
+
expect(inputTag._autocompleteSuggestions).to.include.members(['React', 'Vue', 'Angular'])
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('should exclude already-selected tags from ArrowDown dropdown', async () => {
|
|
665
|
+
const inputTag = await setupDropdownTest()
|
|
666
|
+
inputTag.add('react')
|
|
667
|
+
await waitForUpdate()
|
|
668
|
+
const input = inputTag._taggleInputTarget
|
|
669
|
+
|
|
670
|
+
simulateKeyup(input, 40, { key: 'ArrowDown' })
|
|
671
|
+
await waitForUpdate()
|
|
672
|
+
|
|
673
|
+
expect(inputTag._autocompleteSuggestions).to.include.members(['Vue', 'Angular'])
|
|
674
|
+
expect(inputTag._autocompleteSuggestions).to.not.include('React')
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('should show down chevron on the button when input is empty', async () => {
|
|
678
|
+
const inputTag = await setupDropdownTest()
|
|
679
|
+
|
|
680
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it('should keep + button when there are no autocomplete options', async () => {
|
|
684
|
+
const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
|
|
685
|
+
|
|
686
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('should keep + button when datalist is empty', async () => {
|
|
690
|
+
document.body.innerHTML = `
|
|
691
|
+
<input-tag name="tags" list="empty" multiple></input-tag>
|
|
692
|
+
<datalist id="empty"></datalist>
|
|
693
|
+
`
|
|
694
|
+
const inputTag = document.querySelector('input-tag')
|
|
695
|
+
await waitForBasicInitialization(inputTag)
|
|
696
|
+
|
|
697
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('should swap to + when all datalist options are already selected', async () => {
|
|
701
|
+
const inputTag = await setupDropdownTest()
|
|
702
|
+
|
|
703
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
704
|
+
|
|
705
|
+
inputTag.add('react')
|
|
706
|
+
inputTag.add('vue')
|
|
707
|
+
inputTag.add('angular')
|
|
708
|
+
await waitForUpdate()
|
|
709
|
+
|
|
710
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('should close the dropdown when chevron is clicked while open', async () => {
|
|
714
|
+
const inputTag = await setupDropdownTest()
|
|
715
|
+
const input = inputTag._taggleInputTarget
|
|
716
|
+
|
|
717
|
+
simulateClick(inputTag.buttonTarget)
|
|
718
|
+
await waitForUpdate()
|
|
719
|
+
expect(input.getAttribute('aria-expanded')).to.equal('true')
|
|
720
|
+
|
|
721
|
+
simulateClick(inputTag.buttonTarget)
|
|
722
|
+
await waitForUpdate()
|
|
723
|
+
|
|
724
|
+
expect(input.getAttribute('aria-expanded')).to.equal('false')
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it('should swap back to chevron after removing a tag frees an option', async () => {
|
|
728
|
+
const inputTag = await setupDropdownTest()
|
|
729
|
+
|
|
730
|
+
inputTag.add('react')
|
|
731
|
+
inputTag.add('vue')
|
|
732
|
+
inputTag.add('angular')
|
|
733
|
+
await waitForUpdate()
|
|
734
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
735
|
+
|
|
736
|
+
inputTag.remove('react')
|
|
737
|
+
await waitForUpdate()
|
|
738
|
+
|
|
739
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('should show + on the button when input has text', async () => {
|
|
743
|
+
const inputTag = await setupDropdownTest()
|
|
744
|
+
const input = inputTag._taggleInputTarget
|
|
745
|
+
|
|
746
|
+
await simulateInput(input, 'r')
|
|
747
|
+
|
|
748
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('should swap button content as input changes', async () => {
|
|
752
|
+
const inputTag = await setupDropdownTest()
|
|
753
|
+
const input = inputTag._taggleInputTarget
|
|
754
|
+
|
|
755
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
756
|
+
|
|
757
|
+
await simulateInput(input, 'react')
|
|
758
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
759
|
+
|
|
760
|
+
await simulateInput(input, '')
|
|
761
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it('should open autocomplete dropdown when chevron is clicked', async () => {
|
|
765
|
+
const inputTag = await setupDropdownTest()
|
|
766
|
+
|
|
767
|
+
simulateClick(inputTag.buttonTarget)
|
|
768
|
+
await waitForUpdate()
|
|
769
|
+
|
|
770
|
+
expect(inputTag._autocompleteSuggestions).to.include.members(['React', 'Vue', 'Angular'])
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('should still add typed text as tag when + button is clicked', async () => {
|
|
774
|
+
const inputTag = await setupDropdownTest()
|
|
775
|
+
const input = inputTag._taggleInputTarget
|
|
776
|
+
|
|
777
|
+
await simulateInput(input, 'custom-tag')
|
|
778
|
+
simulateClick(inputTag.buttonTarget)
|
|
779
|
+
await waitForUpdate()
|
|
780
|
+
|
|
781
|
+
expect(getTagValues(inputTag)).to.include('custom-tag')
|
|
782
|
+
expect(input.value).to.equal('')
|
|
783
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
it('should show chevron again after a tag is added', async () => {
|
|
787
|
+
const inputTag = await setupDropdownTest()
|
|
788
|
+
const input = inputTag._taggleInputTarget
|
|
789
|
+
|
|
790
|
+
await simulateInput(input, 'react')
|
|
791
|
+
expect(inputTag.buttonTarget.textContent).to.equal('+')
|
|
792
|
+
|
|
793
|
+
inputTag.add('react')
|
|
794
|
+
await waitForUpdate()
|
|
795
|
+
|
|
796
|
+
expect(inputTag.buttonTarget.textContent).to.equal('▾')
|
|
797
|
+
})
|
|
798
|
+
})
|
|
636
799
|
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bard/tag_field"
|
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
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -183,6 +183,7 @@ files:
|
|
|
183
183
|
- input-tag/test/nested-datalist.test.js
|
|
184
184
|
- input-tag/test/value-label-separation.test.js
|
|
185
185
|
- input-tag/web-test-runner.config.mjs
|
|
186
|
+
- lib/bard-tag_field.rb
|
|
186
187
|
- lib/bard/tag_field.rb
|
|
187
188
|
- lib/bard/tag_field/cucumber.rb
|
|
188
189
|
- lib/bard/tag_field/field.rb
|