tam_select 1.1.1 → 1.2.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77477a755bd92b2c88c2f6909b1cd30fc80f4586cc06b047a60a524e95b08b8f
|
|
4
|
+
data.tar.gz: 55381cbe7a64e822f2f2ea6d48afc933d00f353b4f5563d733606b1520bc09df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 749ebc117bccbcb42ada9476629f5897cec512fbd80c669be9bf64150554e2e217907efbd4d4cbb636707bb9650921f630c164ad6fa1cca12141309d1bfcba67
|
|
7
|
+
data.tar.gz: b2b39df96276b3255dce1ab5903ec53ede199a0e741f51b36d243bc129708992a09035150c4f946dfd2da2a065dc43ec9267886d985fcc39a8fff496d95014d4
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
# Tam Select
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
|
-
[](https://github.com/tamiru/tam_select)
|
|
5
5
|
|
|
6
6
|
**Tam Select** is an accessible, searchable select component for Ruby on Rails, built for Simple Form, Stimulus, Turbo, and Tailwind CSS. It keeps the native `<select>` as the source of truth, so Rails form submission, validation, selected values, and browser autofill continue to work.
|
|
7
7
|
|
|
@@ -73,6 +73,7 @@ Review `git diff` afterward because `--force` overwrites application-specific cu
|
|
|
73
73
|
- Local search and user-created tags
|
|
74
74
|
- Remote JSON search with debouncing and incremental pagination
|
|
75
75
|
- Loading, empty, and error states
|
|
76
|
+
- Polished search control with clear, search, selected, and open-state affordances
|
|
76
77
|
- Keyboard navigation: arrows, Enter, Escape, Tab, and Backspace
|
|
77
78
|
- Combobox/listbox ARIA semantics
|
|
78
79
|
- Light and dark Tailwind themes
|
|
@@ -104,6 +105,18 @@ When the Rails generator is used, scan the generated component source:
|
|
|
104
105
|
@source "../../javascript/tam_select/**/*.js";
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
Tam Select includes adaptive light and dark styles for every state. It follows a `dark` class on the document or any ancestor and also sets the appropriate native `color-scheme`:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
document.documentElement.classList.toggle("dark")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For Tailwind CSS 4 class-based theming, define the variant in the application stylesheet if it is not already present:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
118
|
+
```
|
|
119
|
+
|
|
107
120
|
## Rails and Stimulus
|
|
108
121
|
|
|
109
122
|
The install generator copies the Stimulus controller into your application, where Stimulus normally discovers it automatically. If controllers are registered manually:
|
|
@@ -193,8 +206,8 @@ Options are passed under `input_html[:tam_options]`. Common options include `sea
|
|
|
193
206
|
```json
|
|
194
207
|
{
|
|
195
208
|
"items": [
|
|
196
|
-
{ "value": "1", "label": "
|
|
197
|
-
{ "value": "2", "label": "Afar" }
|
|
209
|
+
{ "value": "1", "label": "Hana Bekele", "detail": "Admission no. UG/1024/26", "meta": "Active", "image": "/avatars/hana.jpg" },
|
|
210
|
+
{ "value": "2", "label": "Afar", "detail": "Semera", "meta": "AF" }
|
|
198
211
|
],
|
|
199
212
|
"pagination": {
|
|
200
213
|
"page": 1,
|
|
@@ -221,6 +234,8 @@ class RegionsController < ApplicationController
|
|
|
221
234
|
model: Region,
|
|
222
235
|
label: :name,
|
|
223
236
|
value: :id,
|
|
237
|
+
detail: :description,
|
|
238
|
+
meta: :code,
|
|
224
239
|
search_by: %i[name code],
|
|
225
240
|
scope: -> { Region.order(:name) },
|
|
226
241
|
per_page: 20
|
|
@@ -255,51 +270,43 @@ Point Simple Form to that collection action:
|
|
|
255
270
|
|
|
256
271
|
Typing sends `GET /regions/tam_select_options.json?q=addis&page=1` and receives the standard Tam Select JSON payload.
|
|
257
272
|
|
|
258
|
-
|
|
273
|
+
Remote items may include optional `detail`, `meta`, and `image` fields. Tam Select renders the primary label with the detail on a second line and an optional circular image on the left. The selected value keeps the same image, label, and detail, including an initial value rendered before remote search completes. `meta` remains a badge on the right.
|
|
259
274
|
|
|
260
|
-
|
|
275
|
+
This works well for program and student searches. For example, return the program name as `label` and admission as `detail`:
|
|
261
276
|
|
|
262
277
|
```ruby
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def tam_select_scope(config)
|
|
271
|
-
current_account.regions.order(:name)
|
|
272
|
-
end
|
|
273
|
-
end
|
|
278
|
+
tam_select_remote(
|
|
279
|
+
model: Estudent::Program,
|
|
280
|
+
label: :name,
|
|
281
|
+
detail: ->(program) { program.admission.to_s },
|
|
282
|
+
search_by: %i[name],
|
|
283
|
+
scope: -> { Estudent::Program.includes(admission: %i[admission_type enrollment_type enrollment_mode]).order(:name) }
|
|
284
|
+
)
|
|
274
285
|
```
|
|
275
286
|
|
|
276
|
-
|
|
287
|
+
For student search, add the admission number and photo URL:
|
|
277
288
|
|
|
278
289
|
```ruby
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
tam_select_remote(
|
|
291
|
+
model: Estudent::Student,
|
|
292
|
+
label: ->(student) { student.full_name },
|
|
293
|
+
detail: ->(student) { "Admission no. #{student.applicant.registration_number}" },
|
|
294
|
+
image: ->(student) { url_for(student.applicant.person.avatar) if student.applicant.person.avatar.attached? },
|
|
295
|
+
search_by: %i[id_number],
|
|
296
|
+
scope: -> { Estudent::Student.includes(applicant: { person: { avatar_attachment: :blob } }) }
|
|
297
|
+
)
|
|
281
298
|
```
|
|
282
299
|
|
|
300
|
+
For a local native select, provide the same values as option data attributes:
|
|
301
|
+
|
|
283
302
|
```erb
|
|
284
|
-
|
|
285
|
-
as: :tam_select,
|
|
286
|
-
collection: [form.object.region].compact,
|
|
287
|
-
label_method: :name,
|
|
288
|
-
value_method: :id,
|
|
289
|
-
input_html: {
|
|
290
|
-
tam_options: {
|
|
291
|
-
remoteUrl: region_options_path(format: :json),
|
|
292
|
-
minQueryLength: 1
|
|
293
|
-
}
|
|
294
|
-
} %>
|
|
303
|
+
<option value="1" data-detail="Admission no. UG/1024/26" data-meta="Active" data-image="/avatars/hana.jpg">Hana Bekele</option>
|
|
295
304
|
```
|
|
296
305
|
|
|
297
|
-
Do not accept a model name from request parameters. Declaring each source in a controller subclass prevents clients from querying arbitrary application models.
|
|
298
|
-
|
|
299
306
|
The browser sends requests such as:
|
|
300
307
|
|
|
301
308
|
```text
|
|
302
|
-
GET /
|
|
309
|
+
GET /regions/tam_select_options.json?q=addis&page=1
|
|
303
310
|
Accept: application/json
|
|
304
311
|
```
|
|
305
312
|
|
|
@@ -307,7 +314,7 @@ Test an endpoint independently with:
|
|
|
307
314
|
|
|
308
315
|
```bash
|
|
309
316
|
curl -H "Accept: application/json" \
|
|
310
|
-
"http://localhost:3000/
|
|
317
|
+
"http://localhost:3000/regions/tam_select_options.json?q=addis&page=1"
|
|
311
318
|
```
|
|
312
319
|
|
|
313
320
|
A `406 Not Acceptable` response means the route or controller rejected JSON. Keep `defaults: { format: :json }` on the route, use a `.json` URL, and ensure the controller does not restrict responses to HTML only.
|
|
@@ -345,6 +352,7 @@ instance.destroy()
|
|
|
345
352
|
| `minQueryLength` | `0` | Characters required before requesting |
|
|
346
353
|
| `valueField` | `value` | Remote item value key |
|
|
347
354
|
| `labelField` | `label` | Remote item label key |
|
|
355
|
+
| `imageField` | `image` | Remote item image URL key |
|
|
348
356
|
| `classes` | `{}` | Overrides any Tailwind class group |
|
|
349
357
|
|
|
350
358
|
## Events
|
|
@@ -3,7 +3,7 @@ module TamSelectPaginatable
|
|
|
3
3
|
|
|
4
4
|
private
|
|
5
5
|
|
|
6
|
-
def tam_select_payload(scope, label:, value: :id, per_page: 20)
|
|
6
|
+
def tam_select_payload(scope, label:, value: :id, detail: nil, meta: nil, image: nil, per_page: 20)
|
|
7
7
|
page = [params.fetch(:page, 1).to_i, 1].max
|
|
8
8
|
records = scope.limit(per_page + 1).offset((page - 1) * per_page).to_a
|
|
9
9
|
has_more = records.length > per_page
|
|
@@ -11,9 +11,12 @@ module TamSelectPaginatable
|
|
|
11
11
|
{
|
|
12
12
|
items: records.first(per_page).map do |record|
|
|
13
13
|
{
|
|
14
|
-
value: record
|
|
15
|
-
label:
|
|
16
|
-
|
|
14
|
+
value: tam_select_record_value(record, value).to_s,
|
|
15
|
+
label: tam_select_record_value(record, label).to_s,
|
|
16
|
+
detail: detail && tam_select_record_value(record, detail).to_s,
|
|
17
|
+
meta: meta && tam_select_record_value(record, meta).to_s,
|
|
18
|
+
image: image && tam_select_record_value(record, image).to_s
|
|
19
|
+
}.compact
|
|
17
20
|
end,
|
|
18
21
|
pagination: {
|
|
19
22
|
page: page,
|
|
@@ -22,4 +25,8 @@ module TamSelectPaginatable
|
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
end
|
|
28
|
+
|
|
29
|
+
def tam_select_record_value(record, resolver)
|
|
30
|
+
resolver.respond_to?(:call) ? resolver.call(record) : record.public_send(resolver)
|
|
31
|
+
end
|
|
25
32
|
end
|
|
@@ -7,11 +7,14 @@ module TamSelectRemote
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
class_methods do
|
|
10
|
-
def tam_select_remote(model:, label:, value: :id, search_by: nil, scope: nil, per_page: 20)
|
|
10
|
+
def tam_select_remote(model:, label:, value: :id, detail: nil, meta: nil, image: nil, search_by: nil, scope: nil, per_page: 20)
|
|
11
11
|
self.tam_select_remote_config = {
|
|
12
12
|
model: model,
|
|
13
13
|
label: label,
|
|
14
14
|
value: value,
|
|
15
|
+
detail: detail,
|
|
16
|
+
meta: meta,
|
|
17
|
+
image: image,
|
|
15
18
|
search_by: Array(search_by || label),
|
|
16
19
|
scope: scope,
|
|
17
20
|
per_page: per_page
|
|
@@ -27,6 +30,9 @@ module TamSelectRemote
|
|
|
27
30
|
records,
|
|
28
31
|
label: config[:label],
|
|
29
32
|
value: config[:value],
|
|
33
|
+
detail: config[:detail],
|
|
34
|
+
meta: config[:meta],
|
|
35
|
+
image: config[:image],
|
|
30
36
|
per_page: config[:per_page]
|
|
31
37
|
)
|
|
32
38
|
end
|
data/lib/tam_select/version.rb
CHANGED
data/src/tam-select.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
const DEFAULT_CLASSES = {
|
|
2
|
-
wrapper: "tam-select relative w-full",
|
|
3
|
-
control: "flex min-h-
|
|
4
|
-
controlDisabled: "cursor-not-allowed bg-zinc-100 opacity-60 dark:bg-zinc-800",
|
|
5
|
-
input: "min-w-16 flex-1 border-0 bg-transparent p-0 text-sm text-zinc-900 outline-none placeholder:text-zinc-400 focus:ring-0 dark:text-zinc-100 dark:placeholder:text-zinc-500",
|
|
2
|
+
wrapper: "tam-select relative w-full text-zinc-900 [color-scheme:light] dark:text-zinc-100 dark:[color-scheme:dark]",
|
|
3
|
+
control: "flex min-h-11 w-full cursor-text flex-wrap items-center gap-2 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm transition-colors hover:border-zinc-400 focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500/20 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:shadow-black/20 dark:hover:border-zinc-600 dark:focus-within:border-blue-400 dark:focus-within:ring-blue-400/20",
|
|
4
|
+
controlDisabled: "cursor-not-allowed bg-zinc-100 opacity-60 dark:bg-zinc-800 dark:text-zinc-400",
|
|
5
|
+
input: "min-w-16 flex-1 border-0 bg-transparent p-0 text-left text-sm text-zinc-900 outline-none placeholder:text-zinc-400 focus:ring-0 dark:text-zinc-100 dark:placeholder:text-zinc-500",
|
|
6
|
+
searchIcon: "size-4 shrink-0 text-zinc-400 dark:text-zinc-500",
|
|
6
7
|
placeholder: "pointer-events-none text-zinc-400 dark:text-zinc-500",
|
|
7
8
|
tag: "inline-flex max-w-full items-center gap-1 rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-200 dark:bg-blue-950/50 dark:text-blue-300 dark:ring-blue-800",
|
|
8
9
|
tagRemove: "rounded p-0.5 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:hover:bg-blue-900",
|
|
9
|
-
clear: "ml-auto rounded p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200",
|
|
10
|
+
clear: "ml-auto rounded p-1 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 dark:focus:ring-blue-400",
|
|
10
11
|
chevron: "ml-1 size-4 shrink-0 text-zinc-400 transition-transform",
|
|
11
12
|
chevronOpen: "rotate-180",
|
|
12
|
-
dropdown: "absolute z-50 mt-1 max-h-72 w-full overflow-auto rounded-lg border border-zinc-200 bg-white p-1 shadow-xl ring-1 ring-black/5 dark:border-zinc-700 dark:bg-zinc-900",
|
|
13
|
-
option: "flex cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2 text-sm text-zinc-700 outline-none dark:text-zinc-200",
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
dropdown: "absolute z-50 mt-1.5 max-h-72 w-full overflow-auto rounded-lg border border-zinc-200 bg-white p-1.5 shadow-xl ring-1 ring-black/5 dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-black/40 dark:ring-white/10",
|
|
14
|
+
option: "flex min-h-11 cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2 text-sm text-zinc-700 outline-none transition-colors dark:text-zinc-200",
|
|
15
|
+
optionContent: "flex min-w-0 flex-1 items-center gap-3",
|
|
16
|
+
optionText: "flex min-w-0 flex-1 flex-col",
|
|
17
|
+
optionLabel: "font-normal",
|
|
18
|
+
optionDetail: "text-xs font-normal text-zinc-500 dark:text-zinc-400",
|
|
19
|
+
optionImage: "size-9 shrink-0 rounded-full bg-zinc-100 object-cover dark:bg-zinc-800",
|
|
20
|
+
optionMeta: "shrink-0 rounded bg-zinc-100 px-1.5 py-0.5 text-xs font-medium text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300",
|
|
21
|
+
optionActive: "bg-zinc-100 text-zinc-950 dark:bg-zinc-800 dark:text-white",
|
|
22
|
+
optionSelected: "bg-blue-50 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300",
|
|
16
23
|
optionDisabled: "cursor-not-allowed opacity-50",
|
|
17
24
|
message: "px-3 py-6 text-center text-sm text-zinc-500 dark:text-zinc-400",
|
|
18
25
|
spinner: "size-4 animate-spin rounded-full border-2 border-zinc-300 border-t-blue-600",
|
|
@@ -20,6 +27,7 @@ const DEFAULT_CLASSES = {
|
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const ICONS = {
|
|
30
|
+
search: '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path stroke-linecap="round" d="m14.5 14.5 3 3m-1.25-8.25a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg>',
|
|
23
31
|
chevron: '<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M5.22 7.22a.75.75 0 011.06 0L10 10.94l3.72-3.72a.75.75 0 111.06 1.06l-4.25 4.25a.75.75 0 01-1.06 0L5.22 8.28a.75.75 0 010-1.06z" clip-rule="evenodd"/></svg>',
|
|
24
32
|
close: '<svg viewBox="0 0 20 20" fill="currentColor" class="size-3" aria-hidden="true"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"/></svg>',
|
|
25
33
|
check: '<svg viewBox="0 0 20 20" fill="currentColor" class="size-4" aria-hidden="true"><path fill-rule="evenodd" d="M16.7 5.3a1 1 0 010 1.4l-8 8a1 1 0 01-1.4 0l-4-4a1 1 0 011.4-1.4L8 12.6l7.3-7.3a1 1 0 011.4 0z" clip-rule="evenodd"/></svg>'
|
|
@@ -61,6 +69,7 @@ export class TamSelect {
|
|
|
61
69
|
minQueryLength: 0,
|
|
62
70
|
valueField: "value",
|
|
63
71
|
labelField: "label",
|
|
72
|
+
imageField: "image",
|
|
64
73
|
itemsPath: "items",
|
|
65
74
|
paginationPath: "pagination",
|
|
66
75
|
classes: {},
|
|
@@ -107,6 +116,9 @@ export class TamSelect {
|
|
|
107
116
|
|
|
108
117
|
this.values = document.createElement("div")
|
|
109
118
|
this.values.className = "contents"
|
|
119
|
+
this.searchIcon = document.createElement("span")
|
|
120
|
+
this.searchIcon.className = this.classes.searchIcon
|
|
121
|
+
this.searchIcon.innerHTML = ICONS.search
|
|
110
122
|
this.input = document.createElement("input")
|
|
111
123
|
this.input.type = "text"
|
|
112
124
|
this.input.className = this.classes.input
|
|
@@ -132,7 +144,7 @@ export class TamSelect {
|
|
|
132
144
|
this.dropdown.setAttribute("role", "listbox")
|
|
133
145
|
if (this.multiple) this.dropdown.setAttribute("aria-multiselectable", "true")
|
|
134
146
|
|
|
135
|
-
this.control.append(this.values, this.input, this.clearButton, this.chevron)
|
|
147
|
+
this.control.append(this.values, this.searchIcon, this.input, this.clearButton, this.chevron)
|
|
136
148
|
this.wrapper.append(this.control, this.dropdown)
|
|
137
149
|
this.select.after(this.wrapper)
|
|
138
150
|
this.applyDisabled()
|
|
@@ -171,7 +183,16 @@ export class TamSelect {
|
|
|
171
183
|
readNativeOptions() {
|
|
172
184
|
this.items = Array.from(this.select.options)
|
|
173
185
|
.filter(option => option.value !== "" || option.selected)
|
|
174
|
-
.map(option => ({
|
|
186
|
+
.map(option => ({
|
|
187
|
+
value: option.value,
|
|
188
|
+
label: option.textContent.trim(),
|
|
189
|
+
detail: option.dataset.detail || null,
|
|
190
|
+
meta: option.dataset.meta || null,
|
|
191
|
+
image: option.dataset.image || null,
|
|
192
|
+
disabled: option.disabled,
|
|
193
|
+
selected: option.selected,
|
|
194
|
+
option
|
|
195
|
+
}))
|
|
175
196
|
this.filterLocal(false)
|
|
176
197
|
}
|
|
177
198
|
|
|
@@ -179,7 +200,7 @@ export class TamSelect {
|
|
|
179
200
|
|
|
180
201
|
filterLocal(render = true) {
|
|
181
202
|
const needle = normalize(this.query)
|
|
182
|
-
this.filtered = this.items.filter(item => !needle ||
|
|
203
|
+
this.filtered = this.items.filter(item => !needle || [item.label, item.detail, item.meta].some(value => normalize(value).includes(needle)))
|
|
183
204
|
this.activeIndex = this.filtered.findIndex(item => !item.disabled)
|
|
184
205
|
if (render) this.renderDropdown()
|
|
185
206
|
}
|
|
@@ -189,12 +210,10 @@ export class TamSelect {
|
|
|
189
210
|
this.values.replaceChildren()
|
|
190
211
|
if (this.multiple) selected.forEach(item => this.values.append(this.makeTag(item)))
|
|
191
212
|
else if (selected.length) {
|
|
192
|
-
|
|
193
|
-
label.className = "min-w-0 flex-1 truncate"
|
|
194
|
-
label.textContent = selected[0].label
|
|
195
|
-
this.values.append(label)
|
|
213
|
+
this.values.append(this.makeItemContent(selected[0], true))
|
|
196
214
|
}
|
|
197
215
|
const hasValue = selected.length > 0
|
|
216
|
+
this.searchIcon.classList.toggle("hidden", !this.options.searchable || (hasValue && !this.opened))
|
|
198
217
|
this.input.classList.toggle("hidden", !this.multiple && hasValue && !this.opened)
|
|
199
218
|
this.input.placeholder = hasValue ? "" : this.options.placeholder
|
|
200
219
|
this.clearButton.classList.toggle("hidden", !this.options.clearable || !hasValue || this.select.disabled)
|
|
@@ -247,10 +266,14 @@ export class TamSelect {
|
|
|
247
266
|
option.setAttribute("role", "option")
|
|
248
267
|
option.setAttribute("aria-selected", String(Boolean(item.selected)))
|
|
249
268
|
option.setAttribute("aria-disabled", String(Boolean(item.disabled)))
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
const content = this.makeItemContent(item)
|
|
270
|
+
option.append(content)
|
|
271
|
+
if (item.meta) {
|
|
272
|
+
const meta = document.createElement("span")
|
|
273
|
+
meta.className = this.classes.optionMeta
|
|
274
|
+
meta.textContent = item.meta
|
|
275
|
+
option.append(meta)
|
|
276
|
+
}
|
|
254
277
|
if (item.selected) {
|
|
255
278
|
const check = document.createElement("span")
|
|
256
279
|
check.innerHTML = ICONS.check
|
|
@@ -261,6 +284,34 @@ export class TamSelect {
|
|
|
261
284
|
return option
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
makeItemContent(item, selected = false) {
|
|
288
|
+
const content = document.createElement("span")
|
|
289
|
+
content.className = this.classes.optionContent
|
|
290
|
+
if (selected) content.classList.add("min-w-0", "flex-1")
|
|
291
|
+
if (item.image) {
|
|
292
|
+
const image = document.createElement("img")
|
|
293
|
+
image.className = this.classes.optionImage
|
|
294
|
+
image.src = item.image
|
|
295
|
+
image.alt = ""
|
|
296
|
+
image.loading = "lazy"
|
|
297
|
+
content.append(image)
|
|
298
|
+
}
|
|
299
|
+
const text = document.createElement("span")
|
|
300
|
+
text.className = this.classes.optionText
|
|
301
|
+
const label = document.createElement("span")
|
|
302
|
+
label.className = `${this.classes.optionLabel} truncate`
|
|
303
|
+
label.textContent = item.label
|
|
304
|
+
text.append(label)
|
|
305
|
+
if (item.detail) {
|
|
306
|
+
const detail = document.createElement("span")
|
|
307
|
+
detail.className = `${this.classes.optionDetail} truncate`
|
|
308
|
+
detail.textContent = item.detail
|
|
309
|
+
text.append(detail)
|
|
310
|
+
}
|
|
311
|
+
content.append(text)
|
|
312
|
+
return content
|
|
313
|
+
}
|
|
314
|
+
|
|
264
315
|
makeCreateOption() {
|
|
265
316
|
const option = document.createElement("div")
|
|
266
317
|
option.className = this.classes.option
|
|
@@ -361,12 +412,29 @@ export class TamSelect {
|
|
|
361
412
|
addItem(raw) {
|
|
362
413
|
const value = String(raw.value ?? raw[this.options.valueField])
|
|
363
414
|
const existing = this.items.find(item => String(item.value) === value)
|
|
364
|
-
if (existing)
|
|
415
|
+
if (existing) {
|
|
416
|
+
const image = raw.image ?? raw[this.options.imageField]
|
|
417
|
+
Object.assign(existing, raw, {
|
|
418
|
+
detail: raw.detail ?? existing.detail,
|
|
419
|
+
meta: raw.meta ?? existing.meta,
|
|
420
|
+
image: image ?? existing.image
|
|
421
|
+
})
|
|
422
|
+
if (existing.option) {
|
|
423
|
+
if (existing.detail != null) existing.option.dataset.detail = String(existing.detail)
|
|
424
|
+
if (existing.meta != null) existing.option.dataset.meta = String(existing.meta)
|
|
425
|
+
if (existing.image != null) existing.option.dataset.image = String(existing.image)
|
|
426
|
+
}
|
|
427
|
+
return existing
|
|
428
|
+
}
|
|
365
429
|
const label = String(raw.label ?? raw[this.options.labelField] ?? value)
|
|
366
430
|
const option = new Option(label, value, Boolean(raw.selected), Boolean(raw.selected))
|
|
367
431
|
option.dataset.tamSelectGenerated = ""
|
|
432
|
+
if (raw.detail != null) option.dataset.detail = String(raw.detail)
|
|
433
|
+
if (raw.meta != null) option.dataset.meta = String(raw.meta)
|
|
434
|
+
const image = raw.image ?? raw[this.options.imageField]
|
|
435
|
+
if (image != null) option.dataset.image = String(image)
|
|
368
436
|
this.select.add(option)
|
|
369
|
-
const item = { ...raw, value, label, option, selected: option.selected, disabled: Boolean(raw.disabled) }
|
|
437
|
+
const item = { ...raw, value, label, image: image == null ? null : String(image), option, selected: option.selected, disabled: Boolean(raw.disabled) }
|
|
370
438
|
option.disabled = item.disabled
|
|
371
439
|
this.items.push(item)
|
|
372
440
|
this.filterLocal(false)
|